Public Access
Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d75154fa9b | ||
|
|
9ccec18c18 | ||
|
|
d9464fad76 | ||
|
|
d6e9a58a24 | ||
|
|
fa294be685 | ||
|
|
cb31156584 |
No files matched your search
@@ -32,6 +32,10 @@ bin/
|
|||||||
### Mac OS ###
|
### Mac OS ###
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|
||||||
|
### Python tools ###
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
|
||||||
### Minecraft Modding ###
|
### Minecraft Modding ###
|
||||||
run/
|
run/
|
||||||
logs/
|
logs/
|
||||||
|
|||||||
@@ -480,6 +480,48 @@ 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
|
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.
|
Link Panel's 16×16 item icon while preserving its red pulse and tooltip.
|
||||||
|
|
||||||
|
The local `1.0.1.33-beta.1` slice adds Flat and Void terrain to authored Binder
|
||||||
|
books. Flat uses vanilla superflat layers and can use one authored Biome; Void
|
||||||
|
generates no terrain but can use one authored Biome for environmental behavior.
|
||||||
|
Without a Biome page, Void uses `minecraft:the_void`. Books with multiple Biome
|
||||||
|
pages and Flat/Void terrain remain unassembled until their custom distribution
|
||||||
|
generator is ported. The dedicated-server smoke creates, samples, and reloads
|
||||||
|
both terrain families in a disposable world.
|
||||||
|
|
||||||
|
The local `1.0.1.34-beta.1` follow-up moves Flat ground to Y=0 and targets its
|
||||||
|
surface for travel. `1.0.1.35-beta.1` enables the selected biome's decoration
|
||||||
|
on Flat. `1.0.1.36-beta.1` ports a multi-biome Flat/Void chunk generator and
|
||||||
|
accepts the existing authored distribution pages for those terrains. New Flat
|
||||||
|
chunks can vary vegetation by biome; Void keeps biome climate without adding
|
||||||
|
a floor or decorations. These are beta-port slices, not full 1.21.1 parity.
|
||||||
|
|
||||||
|
The local `1.0.1.37-beta.1` slice ports Cave terrain authoring using the
|
||||||
|
26.3 caves noise preset. Cave and Nether Biome pages can be distributed
|
||||||
|
through an Age, and travel searches for footing from Y=64 downward rather
|
||||||
|
than mistaking the cave roof for the surface. Nether-style cave material
|
||||||
|
replacement from 1.21.1 remains a separate port task. The local
|
||||||
|
`1.0.1.38-beta.1` follow-up
|
||||||
|
keeps Cave Ages dry by default: the vanilla 26.3 Cave preset otherwise fills
|
||||||
|
low openings with water. This changes newly generated chunks only; a future
|
||||||
|
Flooded page will deliberately set an authored water level in both maintained
|
||||||
|
versions.
|
||||||
|
The local `1.0.1.39-beta.1` follow-up makes the 26.3 landing search reject
|
||||||
|
fluid at feet and head height and prevents a submerged fallback platform.
|
||||||
|
|
||||||
|
The local `1.0.1.40-beta.1` slice gives pageless Descriptive Books a
|
||||||
|
seed-selected terrain and biome plan. It follows the 1.21.1 rank weights for
|
||||||
|
terrain and biome distribution, selects registered Cave/Nether biomes for Cave
|
||||||
|
Ages and overworld-source biomes for other non-Void terrains, and persists the
|
||||||
|
resolved choices. Other random Age traits remain a later port task.
|
||||||
|
|
||||||
|
The local `1.0.1.41-beta.1` slice adds the first Age-scoped feature wrapper to
|
||||||
|
the 26.3 generator. A Block page followed by Rocks now places clusters of the
|
||||||
|
chosen block in new Standard, Amplified, Island, or Flat terrain chunks. The
|
||||||
|
Age index persists that selection, and the wrapper delegates all vanilla
|
||||||
|
terrain, biome, and structure generation. Cave and Void are intentionally
|
||||||
|
excluded until their feature placement rules are ported. This is not yet the
|
||||||
|
full 1.21.1 feature grammar or instability system.
|
||||||
|
|
||||||
## Verification of this first slice
|
## Verification of this first slice
|
||||||
|
|
||||||
The 1.21.1 `compileJava` and `build` tasks passed offline after the backend
|
The 1.21.1 `compileJava` and `build` tasks passed offline after the backend
|
||||||
|
|||||||
@@ -0,0 +1,82 @@
|
|||||||
|
# Publishing Rivencraft with the Gitea, Modrinth, and CurseForge APIs
|
||||||
|
|
||||||
|
The local helper is [`scripts/publish_mod_sites.py`](../scripts/publish_mod_sites.py).
|
||||||
|
It previews a release unless `--publish` is explicitly supplied. The current
|
||||||
|
configuration is for the Minecraft **26.3** artifact only. The 1.21.1 line needs
|
||||||
|
its required external dependencies mapped on both sites before API publishing
|
||||||
|
is enabled for it.
|
||||||
|
|
||||||
|
## One-time token setup
|
||||||
|
|
||||||
|
For Gitea, an existing personal access token is sufficient if it has
|
||||||
|
`write:repository` scope. That scope covers release creation and attachment
|
||||||
|
upload; do not grant `all` if a narrower token is available. Create or inspect
|
||||||
|
tokens under [Gitea user settings → Applications](https://git.paddedcell.org/user/settings/applications).
|
||||||
|
The helper checks token identity and scope without displaying the secret.
|
||||||
|
|
||||||
|
Create a [Modrinth personal access token](https://docs.modrinth.com/api/#authentication)
|
||||||
|
in account settings with `VERSION_CREATE` (upload) and `USER_READ` (read-only
|
||||||
|
credential check) scopes. Create a
|
||||||
|
[CurseForge Upload API token](https://support.curseforge.com/support/solutions/articles/9000197321)
|
||||||
|
from the author's API Tokens page. Treat both as passwords: never put them in
|
||||||
|
git, chat, a release note, or a command-line argument.
|
||||||
|
|
||||||
|
In a terminal at the repository root, run these one at a time and paste the
|
||||||
|
corresponding token into the hidden prompt:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
python3 scripts/publish_mod_sites.py store-token modrinth
|
||||||
|
python3 scripts/publish_mod_sites.py store-token curseforge
|
||||||
|
python3 scripts/publish_mod_sites.py store-token gitea
|
||||||
|
```
|
||||||
|
|
||||||
|
The tokens are stored in the desktop Secret Service keyring under
|
||||||
|
`application=rivencraft-publishing`, separately by site. The script also accepts
|
||||||
|
`RIVENCRAFT_MODRINTH_TOKEN` or `RIVENCRAFT_CURSEFORGE_TOKEN` environment variables,
|
||||||
|
but these take precedence over keyring entries and should not be put in shell
|
||||||
|
history or startup files.
|
||||||
|
|
||||||
|
Verify the credentials without uploading anything:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
python3 scripts/publish_mod_sites.py check-token modrinth
|
||||||
|
python3 scripts/publish_mod_sites.py check-token curseforge
|
||||||
|
python3 scripts/publish_mod_sites.py check-token gitea
|
||||||
|
```
|
||||||
|
|
||||||
|
## Release workflow
|
||||||
|
|
||||||
|
Build the JAR, write a Markdown changelog, and preview the metadata:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
python3 scripts/publish_mod_sites.py release \
|
||||||
|
--jar ports/mc263/build/libs/rivencraft-26.3-1.0.1.32-beta.1.jar \
|
||||||
|
--changelog ports/mc263/RELEASE_NOTES_1.0.1.32-beta.1.md \
|
||||||
|
--version 1.0.1.32-beta.1 \
|
||||||
|
--name 'Rivencraft 1.0.1.32-beta.1 — NeoForge 26.3' \
|
||||||
|
--minecraft 26.3 --java 25 --channel beta
|
||||||
|
```
|
||||||
|
|
||||||
|
For a *new* version only, append `--publish` to upload to both sites. Use
|
||||||
|
`--site modrinth` or `--site curseforge` to target one site. Modrinth rejects an
|
||||||
|
existing version number; CurseForge may not, so check its file list before any
|
||||||
|
retry after a partial failure. CurseForge approval may take additional time.
|
||||||
|
|
||||||
|
The default destination is now all three sites. The Gitea release step requires
|
||||||
|
the annotated version tag to have been pushed first. Gitea's `Vx.y.z.b-mc26.3-beta.1`
|
||||||
|
tag convention is derived from the version and channel; an existing release
|
||||||
|
with different title or notes is never overwritten. If release creation
|
||||||
|
succeeds but its JAR upload fails, rerun with `--site gitea --publish` to resume
|
||||||
|
only the missing attachment. `--site both` retains the earlier Modrinth and
|
||||||
|
CurseForge-only behavior.
|
||||||
|
|
||||||
|
The helper uses Gitea's documented
|
||||||
|
[`POST /repos/{owner}/{repo}/releases`](https://docs.gitea.com/api/operations/repo-create-release/)
|
||||||
|
and [release attachment endpoint](https://docs.gitea.com/api/operations/repo-create-release-attachment/),
|
||||||
|
plus Modrinth's documented multipart
|
||||||
|
[`POST /version`](https://docs.modrinth.com/api/operations/createversion/) and
|
||||||
|
CurseForge's documented
|
||||||
|
[`POST /api/projects/{projectId}/upload-file`](https://support.curseforge.com/support/solutions/articles/9000197321)
|
||||||
|
with the JAR, changelog, version channel, NeoForge, Minecraft version, Java
|
||||||
|
version, and client/server environment. It sets no external dependencies for
|
||||||
|
26.3, which uses Rivencraft's first-party Age backend.
|
||||||
+60
-2
@@ -3,8 +3,9 @@
|
|||||||
This is an **experimental beta port** for issues #97–#104, not gameplay parity
|
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
|
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
|
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
|
author Standard, Amplified, Island, Flat, Void, or Cave terrain with supported
|
||||||
pages. Other terrain families, feature/material effects, portals, and much of
|
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
|
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.
|
substitute this JAR for the 1.21.1 mod or import 1.21.1 saves.
|
||||||
|
|
||||||
@@ -153,6 +154,63 @@ 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
|
beside another block no longer hides that block's faces. Unsupported authoring
|
||||||
still stays in the Binder rather than silently producing an incorrect Age.
|
still stays in the Binder rather than silently producing an incorrect Age.
|
||||||
|
|
||||||
|
The local `1.0.1.33-beta.1` slice adds Flat and Void terrain to Binder
|
||||||
|
authoring. Flat uses vanilla's superflat layers; Void has no generated floor
|
||||||
|
and uses the selected Biome for its environmental rules. Each accepts zero or
|
||||||
|
one authored Biome page. Unsupported multi-biome Flat/Void books remain in the
|
||||||
|
Binder until their custom generator is ported. The operator probe can create
|
||||||
|
test Ages with `rivencraft_port age create <name> <seed> flat|void`.
|
||||||
|
|
||||||
|
The local `1.0.1.34-beta.1` follow-up starts Flat's solid layers at Y=0
|
||||||
|
(bedrock at 0, dirt at 1–2, grass at 3) and lands players at the ground surface
|
||||||
|
instead of creating an elevated fallback platform. This affects newly
|
||||||
|
generated Flat chunks; previously generated Age chunks are not rewritten.
|
||||||
|
|
||||||
|
The local `1.0.1.35-beta.1` follow-up enables biome decoration on newly
|
||||||
|
generated Flat chunks, allowing the selected biome's trees, vegetation, and
|
||||||
|
other placed features to appear on the Flat surface. Void remains undecorated.
|
||||||
|
|
||||||
|
The local `1.0.1.36-beta.1` slice accepts multiple Biome pages with Flat and
|
||||||
|
Void terrain. It ports the 1.21.1 authored-biome Flat generator to 26.3, so
|
||||||
|
Tiled, Grid, and scaled regions affect climate in both terrain types and
|
||||||
|
surface decoration in Flat. Void continues to generate no floor or biome
|
||||||
|
decoration. The operator-only `age create_multi` probe creates two-biome test
|
||||||
|
Ages for the dedicated-server smoke test.
|
||||||
|
|
||||||
|
The local `1.0.1.37-beta.1` slice adds the Cave terrain page to Binder
|
||||||
|
authoring. It uses Minecraft's caves noise settings, accepts Cave and Nether
|
||||||
|
biome pages (including multi-biome distribution), and searches below Y=64 for
|
||||||
|
a safe arrival instead of landing on the roof. The 26.3 vanilla Cave preset
|
||||||
|
defaults to water below Y=32, so this port derives a dry Cave setting with
|
||||||
|
air as its default fluid and the sea level below the world floor. This applies
|
||||||
|
to newly generated Cave chunks; it does not drain existing chunks. Cave-specific Nether surface
|
||||||
|
materials from 1.21.1 are not ported yet. The dedicated-server smoke now
|
||||||
|
creates and reloads Cave Ages alongside the other terrain families.
|
||||||
|
|
||||||
|
The dry-Cave follow-up is packaged as `1.0.1.38-beta.1`; no 26.3 release is
|
||||||
|
published until the in-game check passes and publication is explicitly requested.
|
||||||
|
|
||||||
|
The local `1.0.1.39-beta.1` follow-up rejects fluid at both feet and head
|
||||||
|
height during safe-landing searches. It also refuses to carve the fallback
|
||||||
|
platform into submerged space if no dry footing can be found.
|
||||||
|
|
||||||
|
The `1.0.1.40-beta.1` port slice seeds terrain and biome selection for
|
||||||
|
Descriptive Books with no authored symbol pages. It uses the same terrain and
|
||||||
|
biome-controller rank weights as the 1.21.1 line, freezes the resulting plan
|
||||||
|
in the Age index, and accepts the result after restart. The operator-only
|
||||||
|
`age create_random <name> <seed>` probe exercises this path. Random feature,
|
||||||
|
sky, weather, and instability choices are not yet ported; this slice covers
|
||||||
|
terrain and biomes only.
|
||||||
|
|
||||||
|
The `1.0.1.41-beta.1` slice establishes an Age-scoped decoration wrapper
|
||||||
|
for 26.3 and accepts one Block material page immediately before a Rocks feature
|
||||||
|
page. Newly generated surface chunks place small rock clusters made from that
|
||||||
|
block without modifying the shared biome registry. The selected block and
|
||||||
|
generator survive a restart. Cave and Void terrain remain unsupported for this
|
||||||
|
first surface-feature path; unsupported combinations stay in the Binder.
|
||||||
|
Random feature selection, unpaired materials, duplicate feature pages, and
|
||||||
|
instability scoring remain future port steps.
|
||||||
|
|
||||||
From the repository root:
|
From the repository root:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
# Rivencraft 26.3 beta 1.0.1.36-beta.1
|
||||||
|
|
||||||
|
This beta adds Flat and Void terrain to authored Ages on Minecraft 26.3.
|
||||||
|
|
||||||
|
- Bind Descriptive Books with Flat or Void terrain, including multiple Biome pages and supported biome-distribution pages.
|
||||||
|
- Flat ground starts at Y=0; travel lands on its surface rather than on an elevated fallback platform.
|
||||||
|
- Flat Ages generate the selected biomes' surface decorations, including trees and vegetation. Void Ages remain empty while retaining authored biome climate.
|
||||||
|
- Expanded dedicated-server checks for Flat and Void terrain, biome distribution, and persistence after restart.
|
||||||
|
|
||||||
|
This 26.3 port is still experimental and does not yet include all features of the separate Minecraft 1.21.1 version. 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.
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
# Rivencraft 26.3 beta 1.0.1.39-beta.1
|
||||||
|
|
||||||
|
This beta adds Cave terrain to authored Ages on Minecraft 26.3.
|
||||||
|
|
||||||
|
- Bind Descriptive Books with the Cave terrain page and Cave or Nether biome pages, including supported multi-biome distributions.
|
||||||
|
- Cave Ages use the cave noise shape but remain dry by default. Newly generated chunks no longer flood below the vanilla cave sea level.
|
||||||
|
- Cave travel searches from Y=64 downward for dry, properly footed space instead of landing on the cave roof or underwater.
|
||||||
|
- Expanded dedicated-server checks for Cave creation, biome distribution, and persistence after restart.
|
||||||
|
|
||||||
|
The player verified the Cave and safe-landing changes in game. Existing generated chunks are not drained or regenerated. A future Flooded page will provide an authored water level; see [issue #107](https://git.paddedcell.org/dnorberg/rivencraft/issues/107).
|
||||||
|
|
||||||
|
This 26.3 port is still experimental and does not yet include all features of the separate Minecraft 1.21.1 version. 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.
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
# Rivencraft 26.3 beta 1.0.1.41-beta.1
|
||||||
|
|
||||||
|
This beta expands Age generation on Minecraft 26.3.
|
||||||
|
|
||||||
|
- Descriptive Books with no authored symbol pages now choose a seeded random terrain and biome distribution. The chosen plan is saved with the Age and restored after restart.
|
||||||
|
- A Block material page followed by a Rocks feature page now generates small clusters of that block in newly generated surface chunks. This first feature path supports Standard, Amplified, Island, and Flat terrain; Cave and Void combinations remain unsupported.
|
||||||
|
- Expanded dedicated-server checks cover random Age plans and authored Rocks generation across restart.
|
||||||
|
|
||||||
|
The player verified the Rocks feature in game. Random feature selection, duplicate feature pages, unpaired materials, and instability scoring are not yet ported. Existing chunks are not regenerated.
|
||||||
|
|
||||||
|
This 26.3 port is still experimental and does not yet include all features of the separate Minecraft 1.21.1 version. 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.
|
||||||
@@ -7,4 +7,4 @@ neo_version=26.3.0.99
|
|||||||
mod_id=rivencraft
|
mod_id=rivencraft
|
||||||
mod_name=Rivencraft
|
mod_name=Rivencraft
|
||||||
mod_license=LGPL-3.0-or-later
|
mod_license=LGPL-3.0-or-later
|
||||||
mod_version=1.0.1.32-beta.1
|
mod_version=1.0.1.41-beta.1
|
||||||
@@ -21,6 +21,7 @@ import net.dnorberg.rivencraft.item.PageItem;
|
|||||||
import net.dnorberg.rivencraft.network.PortBookNetworking;
|
import net.dnorberg.rivencraft.network.PortBookNetworking;
|
||||||
import net.dnorberg.rivencraft.registry.ModCreativeTabs;
|
import net.dnorberg.rivencraft.registry.ModCreativeTabs;
|
||||||
import net.dnorberg.rivencraft.registry.PortBiomeSources;
|
import net.dnorberg.rivencraft.registry.PortBiomeSources;
|
||||||
|
import net.dnorberg.rivencraft.registry.PortChunkGenerators;
|
||||||
import net.dnorberg.rivencraft.writing.LinkingBookRecipe;
|
import net.dnorberg.rivencraft.writing.LinkingBookRecipe;
|
||||||
import net.minecraft.core.component.DataComponentType;
|
import net.minecraft.core.component.DataComponentType;
|
||||||
import net.minecraft.core.registries.Registries;
|
import net.minecraft.core.registries.Registries;
|
||||||
@@ -152,6 +153,7 @@ public final class Rivencraft {
|
|||||||
RECIPE_SERIALIZERS.register(modEventBus);
|
RECIPE_SERIALIZERS.register(modEventBus);
|
||||||
ModCreativeTabs.register(modEventBus);
|
ModCreativeTabs.register(modEventBus);
|
||||||
PortBiomeSources.register(modEventBus);
|
PortBiomeSources.register(modEventBus);
|
||||||
|
PortChunkGenerators.register(modEventBus);
|
||||||
NeoForge.EVENT_BUS.addListener(RuntimeAgeBackend::onLevelLoad);
|
NeoForge.EVENT_BUS.addListener(RuntimeAgeBackend::onLevelLoad);
|
||||||
NeoForge.EVENT_BUS.addListener(PortAgeCommands::register);
|
NeoForge.EVENT_BUS.addListener(PortAgeCommands::register);
|
||||||
NeoForge.EVENT_BUS.addListener(AgeCatalogSync::onLogin);
|
NeoForge.EVENT_BUS.addListener(AgeCatalogSync::onLogin);
|
||||||
|
|||||||
@@ -24,7 +24,9 @@ final class AgeLevelIndex extends SavedData {
|
|||||||
.forGetter(index -> index.biomePlans),
|
.forGetter(index -> index.biomePlans),
|
||||||
Codec.unboundedMap(Identifier.CODEC,
|
Codec.unboundedMap(Identifier.CODEC,
|
||||||
Codec.STRING.xmap(PortAgeTerrain::valueOf, PortAgeTerrain::name))
|
Codec.STRING.xmap(PortAgeTerrain::valueOf, PortAgeTerrain::name))
|
||||||
.optionalFieldOf("terrain", Map.of()).forGetter(index -> index.terrain)
|
.optionalFieldOf("terrain", Map.of()).forGetter(index -> index.terrain),
|
||||||
|
Codec.unboundedMap(Identifier.CODEC, PortRockPlan.CODEC).optionalFieldOf("rocks", Map.of())
|
||||||
|
.forGetter(index -> index.rocks)
|
||||||
).apply(instance, AgeLevelIndex::new));
|
).apply(instance, AgeLevelIndex::new));
|
||||||
|
|
||||||
static final SavedDataType<AgeLevelIndex> TYPE = new SavedDataType<>(
|
static final SavedDataType<AgeLevelIndex> TYPE = new SavedDataType<>(
|
||||||
@@ -38,6 +40,7 @@ final class AgeLevelIndex extends SavedData {
|
|||||||
private final Map<Identifier, Identifier> biomeOverrides;
|
private final Map<Identifier, Identifier> biomeOverrides;
|
||||||
private final Map<Identifier, PortBiomePlan> biomePlans;
|
private final Map<Identifier, PortBiomePlan> biomePlans;
|
||||||
private final Map<Identifier, PortAgeTerrain> terrain;
|
private final Map<Identifier, PortAgeTerrain> terrain;
|
||||||
|
private final Map<Identifier, PortRockPlan> rocks;
|
||||||
|
|
||||||
private AgeLevelIndex() {
|
private AgeLevelIndex() {
|
||||||
this.ages = new LinkedHashMap<>();
|
this.ages = new LinkedHashMap<>();
|
||||||
@@ -45,17 +48,20 @@ final class AgeLevelIndex extends SavedData {
|
|||||||
this.biomeOverrides = new LinkedHashMap<>();
|
this.biomeOverrides = new LinkedHashMap<>();
|
||||||
this.biomePlans = new LinkedHashMap<>();
|
this.biomePlans = new LinkedHashMap<>();
|
||||||
this.terrain = new LinkedHashMap<>();
|
this.terrain = new LinkedHashMap<>();
|
||||||
|
this.rocks = 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, Identifier> biomeOverrides,
|
||||||
Map<Identifier, PortBiomePlan> biomePlans,
|
Map<Identifier, PortBiomePlan> biomePlans,
|
||||||
Map<Identifier, PortAgeTerrain> terrain) {
|
Map<Identifier, PortAgeTerrain> terrain,
|
||||||
|
Map<Identifier, PortRockPlan> rocks) {
|
||||||
this.ages = new LinkedHashMap<>(ages);
|
this.ages = new LinkedHashMap<>(ages);
|
||||||
this.retired = new LinkedHashSet<>(retired);
|
this.retired = new LinkedHashSet<>(retired);
|
||||||
this.biomeOverrides = new LinkedHashMap<>(biomeOverrides);
|
this.biomeOverrides = new LinkedHashMap<>(biomeOverrides);
|
||||||
this.biomePlans = new LinkedHashMap<>(biomePlans);
|
this.biomePlans = new LinkedHashMap<>(biomePlans);
|
||||||
this.terrain = new LinkedHashMap<>(terrain);
|
this.terrain = new LinkedHashMap<>(terrain);
|
||||||
|
this.rocks = new LinkedHashMap<>(rocks);
|
||||||
}
|
}
|
||||||
|
|
||||||
Map<Identifier, Long> entries() {
|
Map<Identifier, Long> entries() {
|
||||||
@@ -81,13 +87,17 @@ final class AgeLevelIndex extends SavedData {
|
|||||||
return terrain.getOrDefault(id, PortAgeTerrain.STANDARD);
|
return terrain.getOrDefault(id, PortAgeTerrain.STANDARD);
|
||||||
}
|
}
|
||||||
|
|
||||||
void add(Identifier id, long seed, PortBiomePlan biomePlan, PortAgeTerrain ageTerrain) {
|
PortRockPlan rocks(Identifier id) { return rocks.get(id); }
|
||||||
|
|
||||||
|
void add(Identifier id, long seed, PortBiomePlan biomePlan, PortAgeTerrain ageTerrain,
|
||||||
|
PortRockPlan rockPlan) {
|
||||||
if (contains(id)) {
|
if (contains(id)) {
|
||||||
throw new IllegalArgumentException("Age already exists: " + id);
|
throw new IllegalArgumentException("Age already exists: " + id);
|
||||||
}
|
}
|
||||||
ages.put(id, seed);
|
ages.put(id, seed);
|
||||||
if (biomePlan != null) biomePlans.put(id, biomePlan);
|
if (biomePlan != null) biomePlans.put(id, biomePlan);
|
||||||
if (ageTerrain != PortAgeTerrain.STANDARD) terrain.put(id, ageTerrain);
|
if (ageTerrain != PortAgeTerrain.STANDARD) terrain.put(id, ageTerrain);
|
||||||
|
if (rockPlan != null) rocks.put(id, rockPlan);
|
||||||
setDirty();
|
setDirty();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -96,16 +106,18 @@ final class AgeLevelIndex extends SavedData {
|
|||||||
biomeOverrides.remove(id);
|
biomeOverrides.remove(id);
|
||||||
biomePlans.remove(id);
|
biomePlans.remove(id);
|
||||||
terrain.remove(id);
|
terrain.remove(id);
|
||||||
|
rocks.remove(id);
|
||||||
retired.add(id);
|
retired.add(id);
|
||||||
setDirty();
|
setDirty();
|
||||||
}
|
}
|
||||||
|
|
||||||
void restoreAfterFailedRetirement(Identifier id, long seed, PortBiomePlan biomePlan,
|
void restoreAfterFailedRetirement(Identifier id, long seed, PortBiomePlan biomePlan,
|
||||||
PortAgeTerrain ageTerrain) {
|
PortAgeTerrain ageTerrain, PortRockPlan rockPlan) {
|
||||||
if (!retired.remove(id)) throw new IllegalStateException("Age is not retired: " + id);
|
if (!retired.remove(id)) throw new IllegalStateException("Age is not retired: " + id);
|
||||||
ages.put(id, seed);
|
ages.put(id, seed);
|
||||||
if (biomePlan != null) biomePlans.put(id, biomePlan);
|
if (biomePlan != null) biomePlans.put(id, biomePlan);
|
||||||
if (ageTerrain != PortAgeTerrain.STANDARD) terrain.put(id, ageTerrain);
|
if (ageTerrain != PortAgeTerrain.STANDARD) terrain.put(id, ageTerrain);
|
||||||
|
if (rockPlan != null) rocks.put(id, rockPlan);
|
||||||
setDirty();
|
setDirty();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,9 +3,11 @@ package net.dnorberg.rivencraft.age;
|
|||||||
import com.mojang.brigadier.arguments.LongArgumentType;
|
import com.mojang.brigadier.arguments.LongArgumentType;
|
||||||
import com.mojang.brigadier.arguments.IntegerArgumentType;
|
import com.mojang.brigadier.arguments.IntegerArgumentType;
|
||||||
import com.mojang.brigadier.arguments.StringArgumentType;
|
import com.mojang.brigadier.arguments.StringArgumentType;
|
||||||
|
import com.mojang.brigadier.context.CommandContext;
|
||||||
import net.dnorberg.rivencraft.Rivencraft;
|
import net.dnorberg.rivencraft.Rivencraft;
|
||||||
import net.dnorberg.rivencraft.item.LinkBookTarget;
|
import net.dnorberg.rivencraft.item.LinkBookTarget;
|
||||||
import net.minecraft.commands.Commands;
|
import net.minecraft.commands.Commands;
|
||||||
|
import net.minecraft.commands.CommandSourceStack;
|
||||||
import net.minecraft.core.registries.Registries;
|
import net.minecraft.core.registries.Registries;
|
||||||
import net.minecraft.core.component.DataComponents;
|
import net.minecraft.core.component.DataComponents;
|
||||||
import net.minecraft.network.chat.Component;
|
import net.minecraft.network.chat.Component;
|
||||||
@@ -20,6 +22,7 @@ import net.neoforged.neoforge.event.RegisterCommandsEvent;
|
|||||||
import net.neoforged.fml.ModList;
|
import net.neoforged.fml.ModList;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.TreeMap;
|
import java.util.TreeMap;
|
||||||
|
import java.util.Locale;
|
||||||
|
|
||||||
/** Operator-only backend and synchronized-travel probes, not normal book behavior. */
|
/** Operator-only backend and synchronized-travel probes, not normal book behavior. */
|
||||||
public final class PortAgeCommands {
|
public final class PortAgeCommands {
|
||||||
@@ -64,25 +67,69 @@ public final class PortAgeCommands {
|
|||||||
})))
|
})))
|
||||||
.then(Commands.literal("age")
|
.then(Commands.literal("age")
|
||||||
.then(Commands.literal("create")
|
.then(Commands.literal("create")
|
||||||
|
.then(Commands.argument("name", StringArgumentType.word())
|
||||||
|
.then(Commands.argument("seed", LongArgumentType.longArg())
|
||||||
|
.executes(context -> createAge(context, PortAgeTerrain.STANDARD))
|
||||||
|
.then(Commands.argument("terrain", StringArgumentType.word())
|
||||||
|
.executes(context -> {
|
||||||
|
PortAgeTerrain terrain;
|
||||||
|
try {
|
||||||
|
terrain = PortAgeTerrain.valueOf(StringArgumentType.getString(
|
||||||
|
context, "terrain").toUpperCase(Locale.ROOT));
|
||||||
|
} catch (IllegalArgumentException exception) {
|
||||||
|
context.getSource().sendFailure(Component.literal(
|
||||||
|
"Unknown terrain; use standard, amplified, island, cave, flat, or void"));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return createAge(context, terrain);
|
||||||
|
})))))
|
||||||
|
.then(Commands.literal("create_multi")
|
||||||
|
.then(Commands.argument("name", StringArgumentType.word())
|
||||||
|
.then(Commands.argument("seed", LongArgumentType.longArg())
|
||||||
|
.then(Commands.argument("terrain", StringArgumentType.word())
|
||||||
|
.executes(context -> {
|
||||||
|
PortAgeTerrain terrain;
|
||||||
|
try {
|
||||||
|
terrain = PortAgeTerrain.valueOf(StringArgumentType.getString(
|
||||||
|
context, "terrain").toUpperCase(Locale.ROOT));
|
||||||
|
} catch (IllegalArgumentException exception) {
|
||||||
|
context.getSource().sendFailure(Component.literal(
|
||||||
|
"Unknown terrain; use cave, flat, or void"));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if (terrain != PortAgeTerrain.CAVE
|
||||||
|
&& terrain != PortAgeTerrain.FLAT
|
||||||
|
&& terrain != PortAgeTerrain.VOID) {
|
||||||
|
context.getSource().sendFailure(Component.literal(
|
||||||
|
"Multi-biome probe supports cave, flat, or void only"));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return createAge(context, terrain, new PortBiomePlan(
|
||||||
|
PortBiomePlan.Distribution.TILED,
|
||||||
|
terrain == PortAgeTerrain.CAVE
|
||||||
|
? java.util.List.of(Identifier.withDefaultNamespace("lush_caves"),
|
||||||
|
Identifier.withDefaultNamespace("dripstone_caves"))
|
||||||
|
: java.util.List.of(Identifier.withDefaultNamespace("plains"),
|
||||||
|
Identifier.withDefaultNamespace("forest"))));
|
||||||
|
})))))
|
||||||
|
.then(Commands.literal("create_random")
|
||||||
.then(Commands.argument("name", StringArgumentType.word())
|
.then(Commands.argument("name", StringArgumentType.word())
|
||||||
.then(Commands.argument("seed", LongArgumentType.longArg())
|
.then(Commands.argument("seed", LongArgumentType.longArg())
|
||||||
.executes(context -> {
|
.executes(context -> {
|
||||||
String name = StringArgumentType.getString(context, "name");
|
|
||||||
Identifier id = Identifier.fromNamespaceAndPath(
|
|
||||||
Rivencraft.MOD_ID, "age/" + name);
|
|
||||||
long seed = LongArgumentType.getLong(context, "seed");
|
long seed = LongArgumentType.getLong(context, "seed");
|
||||||
ServerLevel level;
|
PortRandomAgeSelection.Selection selected = PortRandomAgeSelection.select(
|
||||||
try {
|
context.getSource().getServer(), seed);
|
||||||
level = RuntimeAgeBackend.create(
|
int created = createAge(context, selected.terrain(), selected.biomes());
|
||||||
context.getSource().getServer(), id, seed);
|
if (created != 0) context.getSource().sendSuccess(() -> Component.literal(
|
||||||
} catch (IllegalArgumentException | IllegalStateException exception) {
|
"Random Age plan: terrain=" + selected.terrain()
|
||||||
context.getSource().sendFailure(Component.literal(exception.getMessage()));
|
+ " biomes=" + selected.biomes()), false);
|
||||||
return 0;
|
return created;
|
||||||
}
|
|
||||||
context.getSource().sendSuccess(() -> Component.literal(
|
|
||||||
"Created " + id + " with seed " + level.getSeed()), true);
|
|
||||||
return 1;
|
|
||||||
}))))
|
}))))
|
||||||
|
.then(Commands.literal("create_rocks")
|
||||||
|
.then(Commands.argument("name", StringArgumentType.word())
|
||||||
|
.then(Commands.argument("seed", LongArgumentType.longArg())
|
||||||
|
.executes(context -> createAge(context, PortAgeTerrain.FLAT,
|
||||||
|
null, new PortRockPlan(Identifier.withDefaultNamespace("gold_block")))))))
|
||||||
.then(Commands.literal("list")
|
.then(Commands.literal("list")
|
||||||
.executes(context -> {
|
.executes(context -> {
|
||||||
var ages = RuntimeAgeBackend.savedAges(context.getSource().getServer());
|
var ages = RuntimeAgeBackend.savedAges(context.getSource().getServer());
|
||||||
@@ -185,7 +232,60 @@ public final class PortAgeCommands {
|
|||||||
.executes(context -> sampleAge(context, 0))
|
.executes(context -> sampleAge(context, 0))
|
||||||
.then(Commands.argument("chunkOffset", IntegerArgumentType.integer(0))
|
.then(Commands.argument("chunkOffset", IntegerArgumentType.integer(0))
|
||||||
.executes(context -> sampleAge(context,
|
.executes(context -> sampleAge(context,
|
||||||
IntegerArgumentType.getInteger(context, "chunkOffset"))))))));
|
IntegerArgumentType.getInteger(context, "chunkOffset"))))))
|
||||||
|
.then(Commands.literal("rocks_probe")
|
||||||
|
.then(Commands.argument("name", StringArgumentType.word())
|
||||||
|
.executes(PortAgeCommands::probeRocks)))
|
||||||
|
.then(Commands.literal("biome")
|
||||||
|
.then(Commands.argument("name", StringArgumentType.word())
|
||||||
|
.then(Commands.argument("x", IntegerArgumentType.integer())
|
||||||
|
.then(Commands.argument("z", IntegerArgumentType.integer())
|
||||||
|
.executes(PortAgeCommands::sampleBiome)))))));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int createAge(CommandContext<CommandSourceStack> context, PortAgeTerrain terrain) {
|
||||||
|
return createAge(context, terrain, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int createAge(CommandContext<CommandSourceStack> context,
|
||||||
|
PortAgeTerrain terrain, PortBiomePlan biomePlan) {
|
||||||
|
return createAge(context, terrain, biomePlan, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int probeRocks(CommandContext<CommandSourceStack> context) {
|
||||||
|
Identifier id = ageId(context);
|
||||||
|
ServerLevel level = context.getSource().getServer().getLevel(ResourceKey.create(Registries.DIMENSION, id));
|
||||||
|
if (level == null || !(level.getChunkSource().getGenerator() instanceof PortRockChunkGenerator rocks)) {
|
||||||
|
context.getSource().sendFailure(Component.literal("Age has no loaded Rocks generator: " + id));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
var material = net.minecraft.core.registries.BuiltInRegistries.BLOCK.getValue(rocks.rocks().material());
|
||||||
|
int count = 0;
|
||||||
|
for (int x = 0; x < 64; x++) for (int z = 0; z < 64; z++) {
|
||||||
|
int y = level.getHeight(Heightmap.Types.MOTION_BLOCKING, x, z) - 1;
|
||||||
|
if (level.getBlockState(new net.minecraft.core.BlockPos(x, y, z)).is(material)) count++;
|
||||||
|
}
|
||||||
|
int placed = count;
|
||||||
|
context.getSource().sendSuccess(() -> Component.literal(
|
||||||
|
"Rocks probe " + id + " material=" + rocks.rocks().material() + " count=" + placed), false);
|
||||||
|
return placed;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int createAge(CommandContext<CommandSourceStack> context,
|
||||||
|
PortAgeTerrain terrain, PortBiomePlan biomePlan, PortRockPlan rockPlan) {
|
||||||
|
String name = StringArgumentType.getString(context, "name");
|
||||||
|
Identifier id = Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "age/" + name);
|
||||||
|
long seed = LongArgumentType.getLong(context, "seed");
|
||||||
|
ServerLevel level;
|
||||||
|
try {
|
||||||
|
level = RuntimeAgeBackend.create(context.getSource().getServer(), id, seed, biomePlan, terrain, rockPlan);
|
||||||
|
} catch (IllegalArgumentException | IllegalStateException exception) {
|
||||||
|
context.getSource().sendFailure(Component.literal(exception.getMessage()));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
context.getSource().sendSuccess(() -> Component.literal(
|
||||||
|
"Created " + id + " with seed " + level.getSeed() + " terrain=" + terrain), true);
|
||||||
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static int sampleAge(com.mojang.brigadier.context.CommandContext<net.minecraft.commands.CommandSourceStack> context,
|
private static int sampleAge(com.mojang.brigadier.context.CommandContext<net.minecraft.commands.CommandSourceStack> context,
|
||||||
@@ -213,6 +313,23 @@ public final class PortAgeCommands {
|
|||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static int sampleBiome(CommandContext<CommandSourceStack> context) {
|
||||||
|
Identifier id = ageId(context);
|
||||||
|
ServerLevel level = context.getSource().getServer().getLevel(ResourceKey.create(Registries.DIMENSION, id));
|
||||||
|
if (level == null) {
|
||||||
|
context.getSource().sendFailure(Component.literal("Age not loaded: " + id));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
int x = IntegerArgumentType.getInteger(context, "x");
|
||||||
|
int z = IntegerArgumentType.getInteger(context, "z");
|
||||||
|
level.getChunk(x >> 4, z >> 4);
|
||||||
|
Identifier biome = level.getBiome(new net.minecraft.core.BlockPos(x, 4, z))
|
||||||
|
.unwrapKey().orElseThrow().identifier();
|
||||||
|
context.getSource().sendSuccess(() -> Component.literal(
|
||||||
|
id + " biome at " + x + "," + z + "=" + biome), false);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
private static Identifier ageId(com.mojang.brigadier.context.CommandContext<net.minecraft.commands.CommandSourceStack> context) {
|
private static Identifier ageId(com.mojang.brigadier.context.CommandContext<net.minecraft.commands.CommandSourceStack> context) {
|
||||||
return Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID,
|
return Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID,
|
||||||
"age/" + StringArgumentType.getString(context, "name"));
|
"age/" + StringArgumentType.getString(context, "name"));
|
||||||
|
|||||||
@@ -4,5 +4,8 @@ package net.dnorberg.rivencraft.age;
|
|||||||
public enum PortAgeTerrain {
|
public enum PortAgeTerrain {
|
||||||
STANDARD,
|
STANDARD,
|
||||||
AMPLIFIED,
|
AMPLIFIED,
|
||||||
ISLAND
|
ISLAND,
|
||||||
|
CAVE,
|
||||||
|
FLAT,
|
||||||
|
VOID
|
||||||
}
|
}
|
||||||
@@ -11,6 +11,8 @@ import net.minecraft.server.level.ServerLevel;
|
|||||||
import net.minecraft.server.level.ServerPlayer;
|
import net.minecraft.server.level.ServerPlayer;
|
||||||
import net.minecraft.world.level.block.Blocks;
|
import net.minecraft.world.level.block.Blocks;
|
||||||
import net.minecraft.world.level.levelgen.Heightmap;
|
import net.minecraft.world.level.levelgen.Heightmap;
|
||||||
|
import net.minecraft.world.level.levelgen.FlatLevelSource;
|
||||||
|
import net.minecraft.world.level.levelgen.flat.FlatLevelGeneratorSettings;
|
||||||
|
|
||||||
/** Shared, server-authoritative landing for the 26.3 book and operator probe. */
|
/** Shared, server-authoritative landing for the 26.3 book and operator probe. */
|
||||||
public final class PortAgeTravel {
|
public final class PortAgeTravel {
|
||||||
@@ -31,16 +33,49 @@ public final class PortAgeTravel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static BlockPos findLanding(ServerLevel target) {
|
private static BlockPos findLanding(ServerLevel target) {
|
||||||
|
net.minecraft.world.level.chunk.ChunkGenerator generator = target.getChunkSource().getGenerator();
|
||||||
|
if (generator instanceof PortRockChunkGenerator rocks) generator = rocks.base();
|
||||||
|
if (generator instanceof net.minecraft.world.level.levelgen.NoiseBasedChunkGenerator noise
|
||||||
|
&& noise.generatorSettings().value().noiseSettings().equals(target.registryAccess()
|
||||||
|
.lookupOrThrow(Registries.NOISE_SETTINGS)
|
||||||
|
.getOrThrow(net.minecraft.world.level.levelgen.NoiseGeneratorSettings.CAVES)
|
||||||
|
.value().noiseSettings())) {
|
||||||
|
// The cave roof is not a valid arrival surface. Search down from
|
||||||
|
// Legacy's Y=64 start, then construct the normal safety platform
|
||||||
|
// at that height only when no footed opening can be found.
|
||||||
|
for (int radius = 0; radius <= 16; radius++) {
|
||||||
|
for (int x = -radius; x <= radius; x++) {
|
||||||
|
for (int z = -radius; z <= radius; z++) {
|
||||||
|
if (Math.max(Math.abs(x), Math.abs(z)) != radius) continue;
|
||||||
|
for (int y = Math.min(64, target.getMaxY() - 2); y > target.getMinY(); y--) {
|
||||||
|
BlockPos foot = new BlockPos(x, y, z);
|
||||||
|
if (hasFooting(target, foot)) return foot;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return makePlatform(target, new BlockPos(0, 64, 0));
|
||||||
|
}
|
||||||
|
// A Flat Age has a known surface. Use it directly before a heightmap
|
||||||
|
// fallback can place a platform in the air above a low superflat world.
|
||||||
|
FlatLevelGeneratorSettings flatSettings = switch (generator) {
|
||||||
|
case FlatLevelSource flat -> flat.settings();
|
||||||
|
case PortFlatChunkGenerator flat -> flat.settings();
|
||||||
|
default -> null;
|
||||||
|
};
|
||||||
|
if (flatSettings != null && !flatSettings.getLayersInfo().isEmpty()) {
|
||||||
|
int surfaceY = target.getMinY() + flatSettings.getLayersInfo().stream()
|
||||||
|
.mapToInt(layer -> layer.getHeight()).sum();
|
||||||
|
BlockPos surface = new BlockPos(0, surfaceY, 0);
|
||||||
|
if (hasFooting(target, surface)) return surface;
|
||||||
|
}
|
||||||
for (int radius = 0; radius <= 16; radius++) {
|
for (int radius = 0; radius <= 16; radius++) {
|
||||||
for (int x = -radius; x <= radius; x++) {
|
for (int x = -radius; x <= radius; x++) {
|
||||||
for (int z = -radius; z <= radius; z++) {
|
for (int z = -radius; z <= radius; z++) {
|
||||||
if (Math.max(Math.abs(x), Math.abs(z)) != radius) continue;
|
if (Math.max(Math.abs(x), Math.abs(z)) != radius) continue;
|
||||||
int y = target.getHeight(Heightmap.Types.MOTION_BLOCKING, x, z);
|
int y = target.getHeight(Heightmap.Types.MOTION_BLOCKING, x, z);
|
||||||
BlockPos foot = new BlockPos(x, y, z);
|
BlockPos foot = new BlockPos(x, y, z);
|
||||||
BlockPos floor = foot.below();
|
if (hasFooting(target, foot)) {
|
||||||
if (target.getBlockState(floor).isFaceSturdy(target, floor, Direction.UP)
|
|
||||||
&& target.getBlockState(foot).getCollisionShape(target, foot).isEmpty()
|
|
||||||
&& target.getBlockState(foot.above()).getCollisionShape(target, foot.above()).isEmpty()) {
|
|
||||||
return foot;
|
return foot;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -50,14 +85,34 @@ public final class PortAgeTravel {
|
|||||||
// origin is ocean or foliage. Final book landing rules are ported later.
|
// origin is ocean or foliage. Final book landing rules are ported later.
|
||||||
int y = Math.max(64, target.getHeight(Heightmap.Types.MOTION_BLOCKING, 0, 0) + 2);
|
int y = Math.max(64, target.getHeight(Heightmap.Types.MOTION_BLOCKING, 0, 0) + 2);
|
||||||
if (y + 2 >= target.getMaxY()) return null;
|
if (y + 2 >= target.getMaxY()) return null;
|
||||||
BlockPos foot = new BlockPos(0, y, 0);
|
return makePlatform(target, new BlockPos(0, y, 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static BlockPos makePlatform(ServerLevel target, BlockPos foot) {
|
||||||
|
if (foot.getY() <= target.getMinY() || foot.getY() + 2 >= target.getMaxY()) return null;
|
||||||
|
// A carved pocket inside a flooded cave immediately refills. Refuse
|
||||||
|
// that landing instead of placing the player in submerged spawn space.
|
||||||
|
if (!target.getFluidState(foot).isEmpty() || !target.getFluidState(foot.above()).isEmpty()) return null;
|
||||||
|
for (Direction direction : Direction.Plane.HORIZONTAL) {
|
||||||
|
if (!target.getFluidState(foot.relative(direction)).isEmpty()
|
||||||
|
|| !target.getFluidState(foot.above().relative(direction)).isEmpty()) return null;
|
||||||
|
}
|
||||||
for (int x = -1; x <= 1; x++) {
|
for (int x = -1; x <= 1; x++) {
|
||||||
for (int z = -1; z <= 1; z++) {
|
for (int z = -1; z <= 1; z++) {
|
||||||
target.setBlock(new BlockPos(x, y - 1, z), Rivencraft.CRYSTAL_BLOCK.get().defaultBlockState(), 3);
|
target.setBlock(foot.offset(x, -1, z), Rivencraft.CRYSTAL_BLOCK.get().defaultBlockState(), 3);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
target.setBlock(foot, Blocks.AIR.defaultBlockState(), 3);
|
target.setBlock(foot, Blocks.AIR.defaultBlockState(), 3);
|
||||||
target.setBlock(foot.above(), Blocks.AIR.defaultBlockState(), 3);
|
target.setBlock(foot.above(), Blocks.AIR.defaultBlockState(), 3);
|
||||||
return foot;
|
return foot;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static boolean hasFooting(ServerLevel target, BlockPos foot) {
|
||||||
|
BlockPos floor = foot.below();
|
||||||
|
return target.getBlockState(floor).isFaceSturdy(target, floor, Direction.UP)
|
||||||
|
&& target.getFluidState(foot).isEmpty()
|
||||||
|
&& target.getFluidState(foot.above()).isEmpty()
|
||||||
|
&& target.getBlockState(foot).getCollisionShape(target, foot).isEmpty()
|
||||||
|
&& target.getBlockState(foot.above()).getCollisionShape(target, foot.above()).isEmpty();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,142 @@
|
|||||||
|
package net.dnorberg.rivencraft.age;
|
||||||
|
|
||||||
|
import com.mojang.serialization.MapCodec;
|
||||||
|
import com.mojang.serialization.codecs.RecordCodecBuilder;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
import net.minecraft.core.BlockPos;
|
||||||
|
import net.minecraft.core.Holder;
|
||||||
|
import net.minecraft.core.HolderLookup;
|
||||||
|
import net.minecraft.core.HolderSet;
|
||||||
|
import net.minecraft.server.level.WorldGenRegion;
|
||||||
|
import net.minecraft.world.level.LevelHeightAccessor;
|
||||||
|
import net.minecraft.world.level.NoiseColumn;
|
||||||
|
import net.minecraft.world.level.StructureManager;
|
||||||
|
import net.minecraft.world.level.biome.Biome;
|
||||||
|
import net.minecraft.world.level.biome.BiomeGenerationSettings;
|
||||||
|
import net.minecraft.world.level.biome.BiomeManager;
|
||||||
|
import net.minecraft.world.level.biome.BiomeSource;
|
||||||
|
import net.minecraft.world.level.block.Blocks;
|
||||||
|
import net.minecraft.world.level.block.state.BlockState;
|
||||||
|
import net.minecraft.world.level.chunk.ChunkAccess;
|
||||||
|
import net.minecraft.world.level.chunk.ChunkGenerator;
|
||||||
|
import net.minecraft.world.level.chunk.ChunkGeneratorStructureState;
|
||||||
|
import net.minecraft.world.level.levelgen.GenerationStep;
|
||||||
|
import net.minecraft.world.level.levelgen.Heightmap;
|
||||||
|
import net.minecraft.world.level.levelgen.RandomState;
|
||||||
|
import net.minecraft.world.level.levelgen.blending.Blender;
|
||||||
|
import net.minecraft.world.level.levelgen.densityfunction.SamplerContext;
|
||||||
|
import net.minecraft.world.level.levelgen.flat.FlatLevelGeneratorSettings;
|
||||||
|
import net.minecraft.world.level.levelgen.placement.PlacedFeature;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.StructureSet;
|
||||||
|
import net.neoforged.neoforge.common.util.MemoizedFunction;
|
||||||
|
import org.jspecify.annotations.Nullable;
|
||||||
|
|
||||||
|
/** Flat layers with the authored biome distribution used by 1.21.1 Ages. */
|
||||||
|
public final class PortFlatChunkGenerator extends ChunkGenerator {
|
||||||
|
public static final MapCodec<PortFlatChunkGenerator> CODEC = RecordCodecBuilder.mapCodec(instance -> instance.group(
|
||||||
|
BiomeSource.CODEC.fieldOf("biome_source").forGetter(generator -> generator.biomeSource),
|
||||||
|
FlatLevelGeneratorSettings.CODEC.fieldOf("settings").forGetter(PortFlatChunkGenerator::settings)
|
||||||
|
).apply(instance, PortFlatChunkGenerator::new));
|
||||||
|
|
||||||
|
private final FlatLevelGeneratorSettings settings;
|
||||||
|
private final MemoizedFunction<Holder<Biome>, BiomeGenerationSettings> generationSettingsGetter;
|
||||||
|
|
||||||
|
public PortFlatChunkGenerator(BiomeSource biomeSource, FlatLevelGeneratorSettings settings) {
|
||||||
|
super(biomeSource, this.generationSettingsGetter = MemoizedFunction.of(
|
||||||
|
biome -> generationSettings(settings, biome)));
|
||||||
|
this.settings = settings;
|
||||||
|
}
|
||||||
|
|
||||||
|
public FlatLevelGeneratorSettings settings() {
|
||||||
|
return settings;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static BiomeGenerationSettings generationSettings(FlatLevelGeneratorSettings settings, Holder<Biome> biome) {
|
||||||
|
// Void biomes still control climate, but must not place terrain features.
|
||||||
|
if (settings.getLayersInfo().isEmpty()) return BiomeGenerationSettings.EMPTY;
|
||||||
|
if (biome.equals(settings.getBiome())) return settings.adjustGenerationSettings(biome);
|
||||||
|
BiomeGenerationSettings.PlainBuilder builder = new BiomeGenerationSettings.PlainBuilder();
|
||||||
|
List<HolderSet<PlacedFeature>> features = biome.value().getGenerationSettings().features();
|
||||||
|
for (int step = 0; step < features.size(); step++) {
|
||||||
|
if (step == GenerationStep.Decoration.UNDERGROUND_STRUCTURES.ordinal()
|
||||||
|
|| step == GenerationStep.Decoration.SURFACE_STRUCTURES.ordinal()) continue;
|
||||||
|
for (Holder<PlacedFeature> feature : features.get(step)) builder.addFeature(step, feature);
|
||||||
|
}
|
||||||
|
return builder.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected MapCodec<? extends ChunkGenerator> codec() {
|
||||||
|
return CODEC;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ChunkGeneratorStructureState createState(HolderLookup<StructureSet> structures,
|
||||||
|
RandomState random, long seed) {
|
||||||
|
Stream<Holder<StructureSet>> entries = settings.structureOverrides()
|
||||||
|
.map(HolderSet::stream)
|
||||||
|
.orElseGet(() -> structures.listElements().map(holder -> (Holder<StructureSet>) holder));
|
||||||
|
return ChunkGeneratorStructureState.createForFlat(random, seed, getOrigin(random), biomeSource, entries);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getSpawnHeight(LevelHeightAccessor height) {
|
||||||
|
return height.getMinY() + Math.min(height.getHeight(), settings.getLayers().size());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public CompletableFuture<ChunkAccess> buildTerrain(ChunkAccess chunk, Blender blender,
|
||||||
|
RandomState random, StructureManager structures, BiomeManager biomes,
|
||||||
|
@Nullable WorldGenRegion carverBiomeRegion, Set<Holder<Biome>> possibleBiomes) {
|
||||||
|
List<BlockState> layers = settings.getLayers();
|
||||||
|
BlockPos.MutableBlockPos pos = new BlockPos.MutableBlockPos();
|
||||||
|
Heightmap floor = chunk.getOrCreateHeightmapUnprimed(Heightmap.Types.OCEAN_FLOOR_WG);
|
||||||
|
Heightmap surface = chunk.getOrCreateHeightmapUnprimed(Heightmap.Types.WORLD_SURFACE_WG);
|
||||||
|
for (int layer = 0; layer < Math.min(chunk.getHeight(), layers.size()); layer++) {
|
||||||
|
BlockState state = layers.get(layer);
|
||||||
|
if (state == null) continue;
|
||||||
|
int y = chunk.getMinY() + layer;
|
||||||
|
for (int x = 0; x < 16; x++) for (int z = 0; z < 16; z++) {
|
||||||
|
chunk.setBlockState(pos.set(x, y, z), state);
|
||||||
|
floor.update(x, y, z, state);
|
||||||
|
surface.update(x, y, z, state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return CompletableFuture.completedFuture(chunk);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getBaseHeight(int x, int z, Heightmap.Types type,
|
||||||
|
LevelHeightAccessor height, RandomState random) {
|
||||||
|
List<BlockState> layers = settings.getLayers();
|
||||||
|
for (int layer = Math.min(layers.size() - 1, height.getHeight() - 1); layer >= 0; layer--) {
|
||||||
|
BlockState state = layers.get(layer);
|
||||||
|
if (state != null && type.isOpaque().test(state)) return height.getMinY() + layer + 1;
|
||||||
|
}
|
||||||
|
return height.getMinY();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public NoiseColumn getBaseColumn(int x, int z, LevelHeightAccessor height, RandomState random) {
|
||||||
|
return new NoiseColumn(height.getMinY(), settings.getLayers().stream().limit(height.getHeight())
|
||||||
|
.map(state -> state == null ? Blocks.AIR.defaultBlockState() : state)
|
||||||
|
.toArray(BlockState[]::new));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override public void addDebugScreenInfo(List<String> info, RandomState random,
|
||||||
|
BlockPos pos, SamplerContext context) { }
|
||||||
|
@Override public void spawnOriginalMobs(WorldGenRegion level) { }
|
||||||
|
@Override public int getMinY() { return 0; }
|
||||||
|
@Override public int getGenDepth() { return 384; }
|
||||||
|
@Override public int getSeaLevel() { return -63; }
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void refreshFeaturesPerStep() {
|
||||||
|
super.refreshFeaturesPerStep();
|
||||||
|
generationSettingsGetter.clear();
|
||||||
|
settings.updateLayers();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
package net.dnorberg.rivencraft.age;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Random;
|
||||||
|
import net.dnorberg.rivencraft.writing.PortRegistryPages;
|
||||||
|
import net.minecraft.core.registries.Registries;
|
||||||
|
import net.minecraft.resources.Identifier;
|
||||||
|
import net.minecraft.server.MinecraftServer;
|
||||||
|
|
||||||
|
/** Seed-frozen terrain and biome choices for a book with no authored symbols. */
|
||||||
|
public final class PortRandomAgeSelection {
|
||||||
|
private PortRandomAgeSelection() {}
|
||||||
|
|
||||||
|
public record Selection(PortAgeTerrain terrain, PortBiomePlan biomes) {}
|
||||||
|
|
||||||
|
public static Selection select(MinecraftServer server, long seed) {
|
||||||
|
// Match the 1.21.1 Legacy-rank choices: Standard and Flat are common;
|
||||||
|
// the other terrain families are rarer. The sorted order is deliberate.
|
||||||
|
PortAgeTerrain terrain = weighted(new Random(seed ^ 0x5445525241494E4CL),
|
||||||
|
List.of(PortAgeTerrain.AMPLIFIED, PortAgeTerrain.CAVE, PortAgeTerrain.FLAT,
|
||||||
|
PortAgeTerrain.ISLAND, PortAgeTerrain.STANDARD, PortAgeTerrain.VOID),
|
||||||
|
new int[]{1, 1, 5, 1, 6, 1});
|
||||||
|
if (terrain == PortAgeTerrain.VOID)
|
||||||
|
return new Selection(terrain, new PortBiomePlan(PortBiomePlan.Distribution.SINGLE,
|
||||||
|
List.of(Identifier.withDefaultNamespace("the_void"))));
|
||||||
|
|
||||||
|
PortBiomePlan.Distribution distribution = weighted(new Random(seed ^ 0x524956454E435241L),
|
||||||
|
List.of(PortBiomePlan.Distribution.NATIVE, PortBiomePlan.Distribution.SINGLE,
|
||||||
|
PortBiomePlan.Distribution.MEDIUM, PortBiomePlan.Distribution.TILED,
|
||||||
|
PortBiomePlan.Distribution.GRID, PortBiomePlan.Distribution.TINY,
|
||||||
|
PortBiomePlan.Distribution.SMALL, PortBiomePlan.Distribution.LARGE,
|
||||||
|
PortBiomePlan.Distribution.HUGE),
|
||||||
|
new int[]{3, 3, 3, 1, 1, 1, 1, 1, 1});
|
||||||
|
List<Identifier> eligible = eligibleBiomes(server, terrain);
|
||||||
|
if (eligible.size() < 2 && distribution != PortBiomePlan.Distribution.NATIVE)
|
||||||
|
distribution = PortBiomePlan.Distribution.SINGLE;
|
||||||
|
// 26.3 has no native multi-cave biome source; choose a seeded Cave
|
||||||
|
// biome rather than silently using Lush Caves for every random Age.
|
||||||
|
if (terrain == PortAgeTerrain.CAVE && distribution == PortBiomePlan.Distribution.NATIVE)
|
||||||
|
distribution = PortBiomePlan.Distribution.SINGLE;
|
||||||
|
if (distribution == PortBiomePlan.Distribution.NATIVE)
|
||||||
|
return new Selection(terrain, new PortBiomePlan(distribution, List.of()));
|
||||||
|
|
||||||
|
int count = distribution == PortBiomePlan.Distribution.SINGLE ? 1 : 2;
|
||||||
|
if (count > 1) {
|
||||||
|
Random countRandom = new Random(seed ^ 0x42494F4D45434F55L);
|
||||||
|
while (count < eligible.size() && countRandom.nextInt(3) != 0) count++;
|
||||||
|
}
|
||||||
|
List<Identifier> shuffled = new ArrayList<>(eligible);
|
||||||
|
Collections.shuffle(shuffled, new Random(seed ^ 0x42494F4D45504F4FL));
|
||||||
|
return new Selection(terrain, new PortBiomePlan(distribution, shuffled.subList(0, count)));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<Identifier> eligibleBiomes(MinecraftServer server, PortAgeTerrain terrain) {
|
||||||
|
if (terrain == PortAgeTerrain.CAVE) {
|
||||||
|
List<Identifier> caves = server.registryAccess().lookupOrThrow(Registries.BIOME).listElements()
|
||||||
|
.filter(PortRegistryPages::isCaveBiome)
|
||||||
|
.map(holder -> holder.key().identifier())
|
||||||
|
.sorted().toList();
|
||||||
|
return caves.isEmpty() ? List.of(Identifier.withDefaultNamespace("lush_caves")) : caves;
|
||||||
|
}
|
||||||
|
List<Identifier> surface = server.overworld().getChunkSource().getGenerator().getBiomeSource()
|
||||||
|
.possibleBiomes().stream().flatMap(holder -> holder.unwrapKey().stream())
|
||||||
|
.map(key -> key.identifier()).distinct().sorted(Comparator.naturalOrder()).toList();
|
||||||
|
return surface.isEmpty() ? List.of(Identifier.withDefaultNamespace("plains")) : surface;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static <T> T weighted(Random random, List<T> choices, int[] weights) {
|
||||||
|
int total = 0;
|
||||||
|
for (int weight : weights) total += weight;
|
||||||
|
int ticket = random.nextInt(total);
|
||||||
|
for (int index = 0; index < choices.size(); index++) {
|
||||||
|
ticket -= weights[index];
|
||||||
|
if (ticket < 0) return choices.get(index);
|
||||||
|
}
|
||||||
|
throw new IllegalStateException("Random Age ticket was not consumed");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,134 @@
|
|||||||
|
package net.dnorberg.rivencraft.age;
|
||||||
|
|
||||||
|
import com.mojang.serialization.MapCodec;
|
||||||
|
import com.mojang.serialization.codecs.RecordCodecBuilder;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
import net.minecraft.core.BlockPos;
|
||||||
|
import net.minecraft.core.Holder;
|
||||||
|
import net.minecraft.core.HolderLookup;
|
||||||
|
import net.minecraft.core.RegistryAccess;
|
||||||
|
import net.minecraft.core.registries.BuiltInRegistries;
|
||||||
|
import net.minecraft.resources.Identifier;
|
||||||
|
import net.minecraft.resources.ResourceKey;
|
||||||
|
import net.minecraft.server.level.ServerLevel;
|
||||||
|
import net.minecraft.server.level.WorldGenRegion;
|
||||||
|
import net.minecraft.util.RandomSource;
|
||||||
|
import net.minecraft.world.level.ChunkPos;
|
||||||
|
import net.minecraft.world.level.LevelHeightAccessor;
|
||||||
|
import net.minecraft.world.level.NoiseColumn;
|
||||||
|
import net.minecraft.world.level.StructureManager;
|
||||||
|
import net.minecraft.world.level.WorldGenLevel;
|
||||||
|
import net.minecraft.world.level.biome.Biome;
|
||||||
|
import net.minecraft.world.level.biome.BiomeManager;
|
||||||
|
import net.minecraft.world.level.chunk.ChunkAccess;
|
||||||
|
import net.minecraft.world.level.chunk.ChunkGenerator;
|
||||||
|
import net.minecraft.world.level.chunk.ChunkGeneratorStructureState;
|
||||||
|
import net.minecraft.world.level.levelgen.Heightmap;
|
||||||
|
import net.minecraft.world.level.levelgen.RandomState;
|
||||||
|
import net.minecraft.world.level.levelgen.blending.Blender;
|
||||||
|
import net.minecraft.world.level.levelgen.densityfunction.SamplerContext;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.StructureSet;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.Structure;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplateManager;
|
||||||
|
import com.mojang.datafixers.util.Pair;
|
||||||
|
import net.minecraft.core.HolderSet;
|
||||||
|
import net.minecraft.world.level.Level;
|
||||||
|
import net.minecraft.world.level.block.state.BlockState;
|
||||||
|
|
||||||
|
/** Delegates 26.3 terrain generation and adds one Age-scoped Rocks decoration. */
|
||||||
|
public final class PortRockChunkGenerator extends ChunkGenerator {
|
||||||
|
public static final MapCodec<PortRockChunkGenerator> CODEC = RecordCodecBuilder.mapCodec(instance -> instance.group(
|
||||||
|
ChunkGenerator.CODEC.fieldOf("base").forGetter(generator -> generator.base),
|
||||||
|
PortRockPlan.CODEC.fieldOf("rocks").forGetter(generator -> generator.rocks),
|
||||||
|
com.mojang.serialization.Codec.LONG.fieldOf("age_seed").forGetter(generator -> generator.ageSeed)
|
||||||
|
).apply(instance, PortRockChunkGenerator::new));
|
||||||
|
|
||||||
|
private final ChunkGenerator base;
|
||||||
|
private final PortRockPlan rocks;
|
||||||
|
private final long ageSeed;
|
||||||
|
|
||||||
|
public PortRockChunkGenerator(ChunkGenerator base, PortRockPlan rocks, long ageSeed) {
|
||||||
|
super(base.getBiomeSource(), base::getBiomeGenerationSettings);
|
||||||
|
this.base = base;
|
||||||
|
this.rocks = rocks;
|
||||||
|
this.ageSeed = ageSeed;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ChunkGenerator base() { return base; }
|
||||||
|
public PortRockPlan rocks() { return rocks; }
|
||||||
|
|
||||||
|
@Override protected MapCodec<? extends ChunkGenerator> codec() { return CODEC; }
|
||||||
|
@Override public void validate() { base.validate(); }
|
||||||
|
@Override public ChunkPos getOrigin(RandomState random) { return base.getOrigin(random); }
|
||||||
|
@Override public java.util.Optional<Identifier> getTypeNameForDataFixer() { return base.getTypeNameForDataFixer(); }
|
||||||
|
@Override public CompletableFuture<ChunkAccess> createBiomes(RandomState random, Blender blender,
|
||||||
|
StructureManager structures, ChunkAccess chunk) {
|
||||||
|
return base.createBiomes(random, blender, structures, chunk);
|
||||||
|
}
|
||||||
|
@Override public ChunkGeneratorStructureState createState(HolderLookup<StructureSet> sets,
|
||||||
|
RandomState random, long seed) {
|
||||||
|
return base.createState(sets, random, seed);
|
||||||
|
}
|
||||||
|
@Override public void createStructures(RegistryAccess registries, ChunkGeneratorStructureState state,
|
||||||
|
StructureManager structures, ChunkAccess chunk,
|
||||||
|
StructureTemplateManager templates, ResourceKey<Level> dimension) {
|
||||||
|
base.createStructures(registries, state, structures, chunk, templates, dimension);
|
||||||
|
}
|
||||||
|
@Override public void createReferences(WorldGenLevel level, StructureManager structures, ChunkAccess chunk) {
|
||||||
|
base.createReferences(level, structures, chunk);
|
||||||
|
}
|
||||||
|
@Override public Pair<BlockPos, Holder<Structure>> findNearestMapStructure(ServerLevel level,
|
||||||
|
HolderSet<Structure> targets, BlockPos origin, int radius, boolean skipKnown) {
|
||||||
|
return base.findNearestMapStructure(level, targets, origin, radius, skipKnown);
|
||||||
|
}
|
||||||
|
@Override public void spawnOriginalMobs(WorldGenRegion level) { base.spawnOriginalMobs(level); }
|
||||||
|
@Override public int getSpawnHeight(LevelHeightAccessor height) { return base.getSpawnHeight(height); }
|
||||||
|
@Override public int getGenDepth() { return base.getGenDepth(); }
|
||||||
|
@Override public int getSeaLevel() { return base.getSeaLevel(); }
|
||||||
|
@Override public int getMinY() { return base.getMinY(); }
|
||||||
|
@Override public CompletableFuture<ChunkAccess> buildTerrain(ChunkAccess chunk, Blender blender,
|
||||||
|
RandomState random, StructureManager structures, BiomeManager biomes,
|
||||||
|
WorldGenRegion carverBiomeRegion, Set<Holder<Biome>> possibleBiomes) {
|
||||||
|
return base.buildTerrain(chunk, blender, random, structures, biomes, carverBiomeRegion, possibleBiomes);
|
||||||
|
}
|
||||||
|
@Override public int getBaseHeight(int x, int z, Heightmap.Types type,
|
||||||
|
LevelHeightAccessor height, RandomState random) {
|
||||||
|
return base.getBaseHeight(x, z, type, height, random);
|
||||||
|
}
|
||||||
|
@Override public NoiseColumn getBaseColumn(int x, int z, LevelHeightAccessor height, RandomState random) {
|
||||||
|
return base.getBaseColumn(x, z, height, random);
|
||||||
|
}
|
||||||
|
@Override public void addDebugScreenInfo(List<String> info, RandomState random,
|
||||||
|
BlockPos pos, SamplerContext context) {
|
||||||
|
base.addDebugScreenInfo(info, random, pos, context);
|
||||||
|
}
|
||||||
|
@Override public void refreshFeaturesPerStep() {
|
||||||
|
base.refreshFeaturesPerStep();
|
||||||
|
super.refreshFeaturesPerStep();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override public void applyBiomeDecoration(WorldGenLevel level, ChunkAccess chunk,
|
||||||
|
StructureManager structures) {
|
||||||
|
base.applyBiomeDecoration(level, chunk, structures);
|
||||||
|
ChunkPos chunkPos = chunk.getPos();
|
||||||
|
long seed = ageSeed ^ (long) chunkPos.x() * 341873128712L ^ (long) chunkPos.z() * 132897987541L;
|
||||||
|
RandomSource random = RandomSource.create(seed);
|
||||||
|
if (random.nextInt(5) != 0) return;
|
||||||
|
BlockState material = BuiltInRegistries.BLOCK.getValue(rocks.material()).defaultBlockState();
|
||||||
|
int centerX = chunkPos.getMinBlockX() + 2 + random.nextInt(12);
|
||||||
|
int centerZ = chunkPos.getMinBlockZ() + 2 + random.nextInt(12);
|
||||||
|
for (int dx = -1; dx <= 1; dx++) for (int dz = -1; dz <= 1; dz++) {
|
||||||
|
if (random.nextInt(4) == 0) continue;
|
||||||
|
int x = centerX + dx;
|
||||||
|
int z = centerZ + dz;
|
||||||
|
int y = level.getHeight(Heightmap.Types.WORLD_SURFACE_WG, x, z);
|
||||||
|
BlockPos place = new BlockPos(x, y, z);
|
||||||
|
if (level.getBlockState(place).isAir()
|
||||||
|
&& level.getFluidState(place).isEmpty()
|
||||||
|
&& level.getBlockState(place.below()).isFaceSturdy(level, place.below(), net.minecraft.core.Direction.UP))
|
||||||
|
level.setBlock(place, material, 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
package net.dnorberg.rivencraft.age;
|
||||||
|
|
||||||
|
import com.mojang.serialization.Codec;
|
||||||
|
import com.mojang.serialization.codecs.RecordCodecBuilder;
|
||||||
|
import net.minecraft.resources.Identifier;
|
||||||
|
|
||||||
|
/** First frozen authored feature: one Rocks page with an explicit block material. */
|
||||||
|
public record PortRockPlan(Identifier material) {
|
||||||
|
public static final Codec<PortRockPlan> CODEC = RecordCodecBuilder.create(instance -> instance.group(
|
||||||
|
Identifier.CODEC.fieldOf("material").forGetter(PortRockPlan::material)
|
||||||
|
).apply(instance, PortRockPlan::new));
|
||||||
|
}
|
||||||
@@ -8,8 +8,10 @@ import java.nio.file.Path;
|
|||||||
import java.nio.file.StandardCopyOption;
|
import java.nio.file.StandardCopyOption;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
import java.util.OptionalLong;
|
import java.util.OptionalLong;
|
||||||
import net.dnorberg.rivencraft.Rivencraft;
|
import net.dnorberg.rivencraft.Rivencraft;
|
||||||
|
import net.dnorberg.rivencraft.writing.PortRegistryPages;
|
||||||
import net.minecraft.core.registries.Registries;
|
import net.minecraft.core.registries.Registries;
|
||||||
import net.minecraft.core.Holder;
|
import net.minecraft.core.Holder;
|
||||||
import net.minecraft.core.HolderSet;
|
import net.minecraft.core.HolderSet;
|
||||||
@@ -20,12 +22,18 @@ import net.minecraft.server.level.ServerLevel;
|
|||||||
import net.minecraft.world.level.Level;
|
import net.minecraft.world.level.Level;
|
||||||
import net.minecraft.world.level.TicketStorage;
|
import net.minecraft.world.level.TicketStorage;
|
||||||
import net.minecraft.world.level.biome.BiomeManager;
|
import net.minecraft.world.level.biome.BiomeManager;
|
||||||
|
import net.minecraft.world.level.biome.BiomeSource;
|
||||||
import net.minecraft.world.level.biome.FixedBiomeSource;
|
import net.minecraft.world.level.biome.FixedBiomeSource;
|
||||||
import net.minecraft.world.level.biome.Biome;
|
import net.minecraft.world.level.biome.Biome;
|
||||||
|
import net.minecraft.world.level.biome.Biomes;
|
||||||
import net.minecraft.world.level.biome.CheckerboardColumnBiomeSource;
|
import net.minecraft.world.level.biome.CheckerboardColumnBiomeSource;
|
||||||
import net.minecraft.world.level.dimension.LevelStem;
|
import net.minecraft.world.level.dimension.LevelStem;
|
||||||
|
import net.minecraft.world.level.levelgen.FlatLevelSource;
|
||||||
import net.minecraft.world.level.levelgen.NoiseBasedChunkGenerator;
|
import net.minecraft.world.level.levelgen.NoiseBasedChunkGenerator;
|
||||||
import net.minecraft.world.level.levelgen.NoiseGeneratorSettings;
|
import net.minecraft.world.level.levelgen.NoiseGeneratorSettings;
|
||||||
|
import net.minecraft.world.level.levelgen.flat.FlatLevelGeneratorSettings;
|
||||||
|
import net.minecraft.world.level.levelgen.flat.FlatLayerInfo;
|
||||||
|
import net.minecraft.world.level.block.Blocks;
|
||||||
import net.minecraft.world.level.storage.DerivedLevelData;
|
import net.minecraft.world.level.storage.DerivedLevelData;
|
||||||
import net.minecraft.world.level.storage.LevelResource;
|
import net.minecraft.world.level.storage.LevelResource;
|
||||||
import net.neoforged.neoforge.common.NeoForge;
|
import net.neoforged.neoforge.common.NeoForge;
|
||||||
@@ -49,7 +57,7 @@ public final class RuntimeAgeBackend {
|
|||||||
for (Map.Entry<Identifier, Long> age : index.entries().entrySet()) {
|
for (Map.Entry<Identifier, Long> age : index.entries().entrySet()) {
|
||||||
try {
|
try {
|
||||||
load(server, age.getKey(), age.getValue(), index.biomePlan(age.getKey()),
|
load(server, age.getKey(), age.getValue(), index.biomePlan(age.getKey()),
|
||||||
index.terrain(age.getKey()));
|
index.terrain(age.getKey()), index.rocks(age.getKey()));
|
||||||
} catch (RuntimeException exception) {
|
} catch (RuntimeException exception) {
|
||||||
Rivencraft.LOGGER.error("Could not restore Age {}. Its saved index remains intact.", age.getKey(), exception);
|
Rivencraft.LOGGER.error("Could not restore Age {}. Its saved index remains intact.", age.getKey(), exception);
|
||||||
}
|
}
|
||||||
@@ -67,12 +75,17 @@ public final class RuntimeAgeBackend {
|
|||||||
|
|
||||||
public static ServerLevel create(MinecraftServer server, Identifier id, long seed,
|
public static ServerLevel create(MinecraftServer server, Identifier id, long seed,
|
||||||
PortBiomePlan biomePlan, PortAgeTerrain terrain) {
|
PortBiomePlan biomePlan, PortAgeTerrain terrain) {
|
||||||
|
return create(server, id, seed, biomePlan, terrain, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ServerLevel create(MinecraftServer server, Identifier id, long seed,
|
||||||
|
PortBiomePlan biomePlan, PortAgeTerrain terrain, PortRockPlan rocks) {
|
||||||
checkServerThread(server);
|
checkServerThread(server);
|
||||||
ageName(id);
|
ageName(id);
|
||||||
AgeLevelIndex index = index(server);
|
AgeLevelIndex index = index(server);
|
||||||
if (index.contains(id)) throw new IllegalArgumentException("Age already exists: " + id);
|
if (index.contains(id)) throw new IllegalArgumentException("Age already exists: " + id);
|
||||||
ServerLevel level = load(server, id, seed, biomePlan, terrain);
|
ServerLevel level = load(server, id, seed, biomePlan, terrain, rocks);
|
||||||
index.add(id, seed, biomePlan, terrain);
|
index.add(id, seed, biomePlan, terrain, rocks);
|
||||||
// A newly bound book may be used immediately. Persist its Age identity
|
// A newly bound book may be used immediately. Persist its Age identity
|
||||||
// before publishing the catalog so a later crash cannot leave that
|
// before publishing the catalog so a later crash cannot leave that
|
||||||
// book pointing at a world omitted from the on-disk index.
|
// book pointing at a world omitted from the on-disk index.
|
||||||
@@ -92,7 +105,8 @@ public final class RuntimeAgeBackend {
|
|||||||
public static ServerLevel reload(MinecraftServer server, Identifier id) {
|
public static ServerLevel reload(MinecraftServer server, Identifier id) {
|
||||||
checkServerThread(server);
|
checkServerThread(server);
|
||||||
long seed = requireSeed(server, id);
|
long seed = requireSeed(server, id);
|
||||||
ServerLevel level = load(server, id, seed, index(server).biomePlan(id), index(server).terrain(id));
|
ServerLevel level = load(server, id, seed, index(server).biomePlan(id), index(server).terrain(id),
|
||||||
|
index(server).rocks(id));
|
||||||
AgeCatalogSync.onCatalogChanged(server);
|
AgeCatalogSync.onCatalogChanged(server);
|
||||||
return level;
|
return level;
|
||||||
}
|
}
|
||||||
@@ -104,18 +118,19 @@ public final class RuntimeAgeBackend {
|
|||||||
long previousSeed = requireSeed(server, id);
|
long previousSeed = requireSeed(server, id);
|
||||||
PortBiomePlan biomePlan = index.biomePlan(id);
|
PortBiomePlan biomePlan = index.biomePlan(id);
|
||||||
PortAgeTerrain terrain = index.terrain(id);
|
PortAgeTerrain terrain = index.terrain(id);
|
||||||
|
PortRockPlan rocks = index.rocks(id);
|
||||||
if (previousSeed == seed) return;
|
if (previousSeed == seed) return;
|
||||||
boolean loaded = server.getLevel(dimensionKey(id)) != null;
|
boolean loaded = server.getLevel(dimensionKey(id)) != null;
|
||||||
if (loaded) unloadLoaded(server, id);
|
if (loaded) unloadLoaded(server, id);
|
||||||
try {
|
try {
|
||||||
if (loaded) load(server, id, seed, biomePlan, terrain);
|
if (loaded) load(server, id, seed, biomePlan, terrain, rocks);
|
||||||
index.reseed(id, seed);
|
index.reseed(id, seed);
|
||||||
server.overworld().getDataStorage().saveAndJoin();
|
server.overworld().getDataStorage().saveAndJoin();
|
||||||
if (loaded) AgeCatalogSync.onCatalogChanged(server);
|
if (loaded) AgeCatalogSync.onCatalogChanged(server);
|
||||||
} catch (RuntimeException exception) {
|
} catch (RuntimeException exception) {
|
||||||
if (loaded) {
|
if (loaded) {
|
||||||
try {
|
try {
|
||||||
load(server, id, previousSeed, biomePlan, terrain);
|
load(server, id, previousSeed, biomePlan, terrain, rocks);
|
||||||
AgeCatalogSync.onCatalogChanged(server);
|
AgeCatalogSync.onCatalogChanged(server);
|
||||||
} catch (RuntimeException rollbackFailure) {
|
} catch (RuntimeException rollbackFailure) {
|
||||||
exception.addSuppressed(rollbackFailure);
|
exception.addSuppressed(rollbackFailure);
|
||||||
@@ -133,6 +148,7 @@ public final class RuntimeAgeBackend {
|
|||||||
long seed = requireSeed(server, id);
|
long seed = requireSeed(server, id);
|
||||||
PortBiomePlan biomePlan = index.biomePlan(id);
|
PortBiomePlan biomePlan = index.biomePlan(id);
|
||||||
PortAgeTerrain terrain = index.terrain(id);
|
PortAgeTerrain terrain = index.terrain(id);
|
||||||
|
PortRockPlan rocks = index.rocks(id);
|
||||||
boolean loaded = server.getLevel(dimensionKey(id)) != null;
|
boolean loaded = server.getLevel(dimensionKey(id)) != null;
|
||||||
ArchivePaths paths = archivePaths(server, name);
|
ArchivePaths paths = archivePaths(server, name);
|
||||||
if (loaded) unloadLoaded(server, id);
|
if (loaded) unloadLoaded(server, id);
|
||||||
@@ -151,14 +167,14 @@ public final class RuntimeAgeBackend {
|
|||||||
}
|
}
|
||||||
} catch (IOException | RuntimeException exception) {
|
} catch (IOException | RuntimeException exception) {
|
||||||
try {
|
try {
|
||||||
index.restoreAfterFailedRetirement(id, seed, biomePlan, terrain);
|
index.restoreAfterFailedRetirement(id, seed, biomePlan, terrain, rocks);
|
||||||
server.overworld().getDataStorage().saveAndJoin();
|
server.overworld().getDataStorage().saveAndJoin();
|
||||||
} catch (RuntimeException rollbackFailure) {
|
} catch (RuntimeException rollbackFailure) {
|
||||||
exception.addSuppressed(rollbackFailure);
|
exception.addSuppressed(rollbackFailure);
|
||||||
}
|
}
|
||||||
if (loaded) {
|
if (loaded) {
|
||||||
try {
|
try {
|
||||||
load(server, id, seed, biomePlan, terrain);
|
load(server, id, seed, biomePlan, terrain, rocks);
|
||||||
} catch (RuntimeException rollbackFailure) {
|
} catch (RuntimeException rollbackFailure) {
|
||||||
exception.addSuppressed(rollbackFailure);
|
exception.addSuppressed(rollbackFailure);
|
||||||
}
|
}
|
||||||
@@ -254,8 +270,27 @@ public final class RuntimeAgeBackend {
|
|||||||
private record ArchivePaths(Path live, Path archive) {
|
private record ArchivePaths(Path live, Path archive) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static BiomeSource authoredBiomeSource(MinecraftServer server, PortBiomePlan plan, long seed) {
|
||||||
|
if (plan.distribution() == PortBiomePlan.Distribution.NATIVE)
|
||||||
|
throw new IllegalArgumentException("Native biome distribution has no authored source");
|
||||||
|
var registry = server.registryAccess().lookupOrThrow(Registries.BIOME);
|
||||||
|
List<Holder<Biome>> biomes = plan.biomes().stream().map(id ->
|
||||||
|
(Holder<Biome>) registry.get(ResourceKey.create(Registries.BIOME, id))
|
||||||
|
.orElseThrow(() -> new IllegalArgumentException("Unknown authored biome: " + id)))
|
||||||
|
.toList();
|
||||||
|
return switch (plan.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),
|
||||||
|
plan.distribution() == PortBiomePlan.Distribution.GRID ? 0 : 4);
|
||||||
|
case TINY, SMALL, MEDIUM, LARGE, HUGE -> new PortScaledBiomeSource(
|
||||||
|
HolderSet.direct(biomes), seed, plan.distribution().ordinal() -
|
||||||
|
PortBiomePlan.Distribution.TINY.ordinal());
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
private static ServerLevel load(MinecraftServer server, Identifier id, long seed,
|
private static ServerLevel load(MinecraftServer server, Identifier id, long seed,
|
||||||
PortBiomePlan biomePlan, PortAgeTerrain terrain) {
|
PortBiomePlan biomePlan, PortAgeTerrain terrain, PortRockPlan rocks) {
|
||||||
ResourceKey<Level> key = ResourceKey.create(Registries.DIMENSION, id);
|
ResourceKey<Level> key = ResourceKey.create(Registries.DIMENSION, id);
|
||||||
if (server.getLevel(key) != null) throw new IllegalArgumentException("Level already loaded: " + id);
|
if (server.getLevel(key) != null) throw new IllegalArgumentException("Level already loaded: " + id);
|
||||||
|
|
||||||
@@ -266,26 +301,54 @@ public final class RuntimeAgeBackend {
|
|||||||
.getValue(LevelStem.OVERWORLD);
|
.getValue(LevelStem.OVERWORLD);
|
||||||
if (overworldStem == null) throw new IllegalStateException("Missing Overworld LevelStem");
|
if (overworldStem == null) throw new IllegalStateException("Missing Overworld LevelStem");
|
||||||
var generator = overworldStem.generator();
|
var generator = overworldStem.generator();
|
||||||
if ((biomePlan != null && biomePlan.distribution() != PortBiomePlan.Distribution.NATIVE)
|
if (terrain == PortAgeTerrain.FLAT || terrain == PortAgeTerrain.VOID) {
|
||||||
|
var biomes = server.registryAccess().lookupOrThrow(Registries.BIOME);
|
||||||
|
Holder<Biome> biome = biomes.getOrThrow(terrain == PortAgeTerrain.VOID
|
||||||
|
? Biomes.THE_VOID : Biomes.PLAINS);
|
||||||
|
if (biomePlan != null && !biomePlan.biomes().isEmpty()) {
|
||||||
|
Identifier biomeId = biomePlan.biomes().getFirst();
|
||||||
|
biome = biomes.get(ResourceKey.create(Registries.BIOME, biomeId))
|
||||||
|
.orElseThrow(() -> new IllegalArgumentException("Unknown authored biome: " + biomeId));
|
||||||
|
}
|
||||||
|
FlatLevelGeneratorSettings settings;
|
||||||
|
if (terrain == PortAgeTerrain.FLAT) {
|
||||||
|
settings = FlatLevelGeneratorSettings.getDefault(biomes,
|
||||||
|
server.registryAccess().lookupOrThrow(Registries.STRUCTURE_SET),
|
||||||
|
server.registryAccess().lookupOrThrow(Registries.PLACED_FEATURE));
|
||||||
|
// Vanilla superflat begins at the modern minimum build height.
|
||||||
|
// Pad the column with air so the first solid layer begins at Y=0.
|
||||||
|
int airBelowZero = Math.max(0, -server.overworld().getMinY());
|
||||||
|
if (airBelowZero > 0)
|
||||||
|
settings.getLayersInfo().addFirst(new FlatLayerInfo(airBelowZero, Blocks.AIR));
|
||||||
|
settings = settings.withBiomeAndLayers(settings.getLayersInfo(),
|
||||||
|
settings.structureOverrides(), biome);
|
||||||
|
settings.setDecoration();
|
||||||
|
} else {
|
||||||
|
settings = new FlatLevelGeneratorSettings(Optional.empty(), biome, List.of());
|
||||||
|
settings.updateLayers();
|
||||||
|
}
|
||||||
|
generator = biomePlan != null && biomePlan.biomes().size() > 1
|
||||||
|
? new PortFlatChunkGenerator(authoredBiomeSource(server, biomePlan, seed), settings)
|
||||||
|
: new FlatLevelSource(settings);
|
||||||
|
} else if ((biomePlan != null && biomePlan.distribution() != PortBiomePlan.Distribution.NATIVE)
|
||||||
|| terrain != PortAgeTerrain.STANDARD) {
|
|| terrain != PortAgeTerrain.STANDARD) {
|
||||||
if (!(generator instanceof NoiseBasedChunkGenerator noise))
|
if (!(generator instanceof NoiseBasedChunkGenerator noise))
|
||||||
throw new IllegalStateException("Overworld generator cannot accept authored noise settings");
|
throw new IllegalStateException("Overworld generator cannot accept authored noise settings");
|
||||||
var source = generator.getBiomeSource();
|
var source = terrain == PortAgeTerrain.CAVE
|
||||||
|
? new FixedBiomeSource(server.registryAccess().lookupOrThrow(Registries.BIOME)
|
||||||
|
.getOrThrow(Biomes.LUSH_CAVES))
|
||||||
|
: generator.getBiomeSource();
|
||||||
|
if (terrain == PortAgeTerrain.CAVE && biomePlan != null) {
|
||||||
|
var biomes = server.registryAccess().lookupOrThrow(Registries.BIOME);
|
||||||
|
for (Identifier biomeId : biomePlan.biomes()) {
|
||||||
|
var holder = biomes.get(ResourceKey.create(Registries.BIOME, biomeId))
|
||||||
|
.orElseThrow(() -> new IllegalArgumentException("Unknown authored biome: " + biomeId));
|
||||||
|
if (!PortRegistryPages.isCaveBiome(holder))
|
||||||
|
throw new IllegalArgumentException("Cave terrain requires a Cave or Nether biome: " + biomeId);
|
||||||
|
}
|
||||||
|
}
|
||||||
if (biomePlan != null && biomePlan.distribution() != PortBiomePlan.Distribution.NATIVE) {
|
if (biomePlan != null && biomePlan.distribution() != PortBiomePlan.Distribution.NATIVE) {
|
||||||
var registry = server.registryAccess().lookupOrThrow(Registries.BIOME);
|
source = authoredBiomeSource(server, biomePlan, seed);
|
||||||
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) {
|
var settings = switch (terrain) {
|
||||||
case STANDARD -> noise.generatorSettings();
|
case STANDARD -> noise.generatorSettings();
|
||||||
@@ -293,9 +356,12 @@ public final class RuntimeAgeBackend {
|
|||||||
.getOrThrow(NoiseGeneratorSettings.AMPLIFIED);
|
.getOrThrow(NoiseGeneratorSettings.AMPLIFIED);
|
||||||
case ISLAND -> server.registryAccess().lookupOrThrow(Registries.NOISE_SETTINGS)
|
case ISLAND -> server.registryAccess().lookupOrThrow(Registries.NOISE_SETTINGS)
|
||||||
.getOrThrow(NoiseGeneratorSettings.FLOATING_ISLANDS);
|
.getOrThrow(NoiseGeneratorSettings.FLOATING_ISLANDS);
|
||||||
|
case CAVE -> dryCaveSettings(server);
|
||||||
|
case FLAT, VOID -> throw new IllegalStateException("Flat terrain should use FlatLevelSource");
|
||||||
};
|
};
|
||||||
generator = new NoiseBasedChunkGenerator(source, settings);
|
generator = new NoiseBasedChunkGenerator(source, settings);
|
||||||
}
|
}
|
||||||
|
if (rocks != null) generator = new PortRockChunkGenerator(generator, rocks, seed);
|
||||||
LevelStem stem = new LevelStem(overworldStem.type(), generator, OptionalLong.of(seed));
|
LevelStem stem = new LevelStem(overworldStem.type(), generator, OptionalLong.of(seed));
|
||||||
long biomeZoomSeed = BiomeManager.obfuscateSeed(server.getWorldGenSettings().options().seed());
|
long biomeZoomSeed = BiomeManager.obfuscateSeed(server.getWorldGenSettings().options().seed());
|
||||||
ServerLevel level = new ServerLevel(server, server.executor, server.storageSource,
|
ServerLevel level = new ServerLevel(server, server.executor, server.storageSource,
|
||||||
@@ -326,4 +392,14 @@ public final class RuntimeAgeBackend {
|
|||||||
throw exception;
|
throw exception;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static Holder<NoiseGeneratorSettings> dryCaveSettings(MinecraftServer server) {
|
||||||
|
NoiseGeneratorSettings caves = server.registryAccess().lookupOrThrow(Registries.NOISE_SETTINGS)
|
||||||
|
.getOrThrow(NoiseGeneratorSettings.CAVES).value();
|
||||||
|
return Holder.direct(new NoiseGeneratorSettings(
|
||||||
|
caves.noiseSettings(), caves.defaultBlock(), Blocks.AIR.defaultBlockState(),
|
||||||
|
caves.noiseRouter(), caves.materialRule(), caves.spawnTarget(),
|
||||||
|
caves.noiseSettings().minY() - 1, caves.disableMobGeneration(), Optional.empty(),
|
||||||
|
caves.useLegacyRandomSource(), caves.debugFunctions()));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -2,7 +2,9 @@ package net.dnorberg.rivencraft.network;
|
|||||||
|
|
||||||
import net.dnorberg.rivencraft.Rivencraft;
|
import net.dnorberg.rivencraft.Rivencraft;
|
||||||
import net.dnorberg.rivencraft.age.PortAgeTravel;
|
import net.dnorberg.rivencraft.age.PortAgeTravel;
|
||||||
|
import net.dnorberg.rivencraft.age.PortAgeTerrain;
|
||||||
import net.dnorberg.rivencraft.age.RuntimeAgeBackend;
|
import net.dnorberg.rivencraft.age.RuntimeAgeBackend;
|
||||||
|
import net.dnorberg.rivencraft.age.PortRandomAgeSelection;
|
||||||
import net.dnorberg.rivencraft.item.LinkBookTarget;
|
import net.dnorberg.rivencraft.item.LinkBookTarget;
|
||||||
import net.dnorberg.rivencraft.item.LinkProperties;
|
import net.dnorberg.rivencraft.item.LinkProperties;
|
||||||
import net.dnorberg.rivencraft.item.LinkingBookItem;
|
import net.dnorberg.rivencraft.item.LinkingBookItem;
|
||||||
@@ -125,8 +127,15 @@ public final class PortBookNetworking {
|
|||||||
id = Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "age/" + name);
|
id = Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "age/" + name);
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
RuntimeAgeBackend.create(server, id, player.getRandom().nextLong(), biomePlan,
|
long seed = player.getRandom().nextLong();
|
||||||
PortAuthoredWriting.terrain(pages));
|
PortAgeTerrain terrain = PortAuthoredWriting.terrain(pages);
|
||||||
|
if (pages.isEmpty()) {
|
||||||
|
PortRandomAgeSelection.Selection random = PortRandomAgeSelection.select(server, seed);
|
||||||
|
terrain = random.terrain();
|
||||||
|
biomePlan = random.biomes();
|
||||||
|
}
|
||||||
|
RuntimeAgeBackend.create(server, id, seed, biomePlan, terrain,
|
||||||
|
PortAuthoredWriting.rockPlan(pages));
|
||||||
} catch (IllegalArgumentException | IllegalStateException exception) {
|
} catch (IllegalArgumentException | IllegalStateException exception) {
|
||||||
player.sendOverlayMessage(Component.literal("Age creation failed: " + exception.getMessage()));
|
player.sendOverlayMessage(Component.literal("Age creation failed: " + exception.getMessage()));
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
package net.dnorberg.rivencraft.registry;
|
||||||
|
|
||||||
|
import com.mojang.serialization.MapCodec;
|
||||||
|
import net.dnorberg.rivencraft.Rivencraft;
|
||||||
|
import net.dnorberg.rivencraft.age.PortFlatChunkGenerator;
|
||||||
|
import net.dnorberg.rivencraft.age.PortRockChunkGenerator;
|
||||||
|
import net.minecraft.core.registries.Registries;
|
||||||
|
import net.minecraft.world.level.chunk.ChunkGenerator;
|
||||||
|
import net.neoforged.bus.api.IEventBus;
|
||||||
|
import net.neoforged.neoforge.registries.DeferredRegister;
|
||||||
|
|
||||||
|
/** Codec for the authored Flat/Void biome source and layer combination. */
|
||||||
|
public final class PortChunkGenerators {
|
||||||
|
private static final DeferredRegister<MapCodec<? extends ChunkGenerator>> GENERATORS =
|
||||||
|
DeferredRegister.create(Registries.CHUNK_GENERATOR, Rivencraft.MOD_ID);
|
||||||
|
|
||||||
|
static {
|
||||||
|
GENERATORS.register("age_flat", () -> PortFlatChunkGenerator.CODEC);
|
||||||
|
GENERATORS.register("age_rocks", () -> PortRockChunkGenerator.CODEC);
|
||||||
|
}
|
||||||
|
|
||||||
|
private PortChunkGenerators() {}
|
||||||
|
|
||||||
|
public static void register(IEventBus bus) {
|
||||||
|
GENERATORS.register(bus);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ import java.util.List;
|
|||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import net.dnorberg.rivencraft.age.PortBiomePlan;
|
import net.dnorberg.rivencraft.age.PortBiomePlan;
|
||||||
import net.dnorberg.rivencraft.age.PortAgeTerrain;
|
import net.dnorberg.rivencraft.age.PortAgeTerrain;
|
||||||
|
import net.dnorberg.rivencraft.age.PortRockPlan;
|
||||||
import net.dnorberg.rivencraft.Rivencraft;
|
import net.dnorberg.rivencraft.Rivencraft;
|
||||||
import net.minecraft.core.HolderLookup;
|
import net.minecraft.core.HolderLookup;
|
||||||
import net.minecraft.core.registries.Registries;
|
import net.minecraft.core.registries.Registries;
|
||||||
@@ -19,6 +20,14 @@ public final class PortAuthoredWriting {
|
|||||||
Rivencraft.MOD_ID, "terrain/amplified");
|
Rivencraft.MOD_ID, "terrain/amplified");
|
||||||
private static final Identifier ISLAND_TERRAIN = Identifier.fromNamespaceAndPath(
|
private static final Identifier ISLAND_TERRAIN = Identifier.fromNamespaceAndPath(
|
||||||
Rivencraft.MOD_ID, "terrain/island");
|
Rivencraft.MOD_ID, "terrain/island");
|
||||||
|
private static final Identifier FLAT_TERRAIN = Identifier.fromNamespaceAndPath(
|
||||||
|
Rivencraft.MOD_ID, "terrain/flat");
|
||||||
|
private static final Identifier VOID_TERRAIN = Identifier.fromNamespaceAndPath(
|
||||||
|
Rivencraft.MOD_ID, "terrain/void");
|
||||||
|
private static final Identifier CAVE_TERRAIN = Identifier.fromNamespaceAndPath(
|
||||||
|
Rivencraft.MOD_ID, "terrain/cave");
|
||||||
|
private static final Identifier ROCKS = Identifier.fromNamespaceAndPath(
|
||||||
|
Rivencraft.MOD_ID, "feature/rocks");
|
||||||
|
|
||||||
private PortAuthoredWriting() {}
|
private PortAuthoredWriting() {}
|
||||||
|
|
||||||
@@ -27,12 +36,19 @@ public final class PortAuthoredWriting {
|
|||||||
boolean terrain = false;
|
boolean terrain = false;
|
||||||
int biomes = 0;
|
int biomes = 0;
|
||||||
int controllers = 0;
|
int controllers = 0;
|
||||||
|
boolean material = false;
|
||||||
|
boolean rocks = false;
|
||||||
for (Identifier page : pages) {
|
for (Identifier page : pages) {
|
||||||
if ((page.equals(STANDARD_TERRAIN) || page.equals(AMPLIFIED_TERRAIN)
|
if (isTerrain(page) && !terrain) {
|
||||||
|| page.equals(ISLAND_TERRAIN)) && !terrain) {
|
|
||||||
terrain = true;
|
terrain = true;
|
||||||
} else if (controller(page) != null && controllers++ == 0) {
|
} else if (controller(page) != null && controllers++ == 0) {
|
||||||
// An explicit controller needs an authored pool.
|
// An explicit controller needs an authored pool.
|
||||||
|
} else if (isBlockPage(page) && !material && !rocks
|
||||||
|
&& PortSymbolPages.find(page) != null
|
||||||
|
&& PortSymbolPages.find(page).category().equals("rivencraft:block")) {
|
||||||
|
material = true;
|
||||||
|
} else if (page.equals(ROCKS) && material && !rocks) {
|
||||||
|
rocks = true;
|
||||||
} else if (page.getNamespace().equals(Rivencraft.MOD_ID)
|
} else if (page.getNamespace().equals(Rivencraft.MOD_ID)
|
||||||
&& page.getPath().startsWith("biome/") && biomes < MAX_AUTHORED_BIOMES
|
&& page.getPath().startsWith("biome/") && biomes < MAX_AUTHORED_BIOMES
|
||||||
&& PortSymbolPages.find(page) != null
|
&& PortSymbolPages.find(page) != null
|
||||||
@@ -41,18 +57,30 @@ public final class PortAuthoredWriting {
|
|||||||
biomes++;
|
biomes++;
|
||||||
} else return false;
|
} else return false;
|
||||||
}
|
}
|
||||||
|
if (material != rocks || rocks && (pages.contains(CAVE_TERRAIN) || pages.contains(VOID_TERRAIN))) return false;
|
||||||
if (controllers != 0) {
|
if (controllers != 0) {
|
||||||
PortBiomePlan.Distribution distribution = pages.stream().map(PortAuthoredWriting::controller)
|
PortBiomePlan.Distribution distribution = pages.stream().map(PortAuthoredWriting::controller)
|
||||||
.filter(value -> value != null).findFirst().orElseThrow();
|
.filter(value -> value != null).findFirst().orElseThrow();
|
||||||
return switch (distribution) {
|
boolean valid = switch (distribution) {
|
||||||
case NATIVE -> biomes == 0;
|
case NATIVE -> biomes == 0;
|
||||||
case SINGLE -> biomes == 1;
|
case SINGLE -> biomes == 1;
|
||||||
default -> biomes >= 2;
|
default -> biomes >= 2;
|
||||||
};
|
};
|
||||||
|
return valid;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static boolean isTerrain(Identifier page) {
|
||||||
|
return page.equals(STANDARD_TERRAIN) || page.equals(AMPLIFIED_TERRAIN)
|
||||||
|
|| page.equals(ISLAND_TERRAIN) || page.equals(FLAT_TERRAIN)
|
||||||
|
|| page.equals(VOID_TERRAIN) || page.equals(CAVE_TERRAIN);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isBlockPage(Identifier page) {
|
||||||
|
return page.getNamespace().equals(Rivencraft.MOD_ID) && page.getPath().startsWith("block/");
|
||||||
|
}
|
||||||
|
|
||||||
private static PortBiomePlan.Distribution controller(Identifier page) {
|
private static PortBiomePlan.Distribution controller(Identifier page) {
|
||||||
if (!page.getNamespace().equals(Rivencraft.MOD_ID)) return null;
|
if (!page.getNamespace().equals(Rivencraft.MOD_ID)) return null;
|
||||||
return switch (page.getPath()) {
|
return switch (page.getPath()) {
|
||||||
@@ -73,24 +101,36 @@ public final class PortAuthoredWriting {
|
|||||||
if (!supports(pages)) throw new IllegalArgumentException("Unsupported authored Age pages");
|
if (!supports(pages)) throw new IllegalArgumentException("Unsupported authored Age pages");
|
||||||
if (pages.contains(AMPLIFIED_TERRAIN)) return PortAgeTerrain.AMPLIFIED;
|
if (pages.contains(AMPLIFIED_TERRAIN)) return PortAgeTerrain.AMPLIFIED;
|
||||||
if (pages.contains(ISLAND_TERRAIN)) return PortAgeTerrain.ISLAND;
|
if (pages.contains(ISLAND_TERRAIN)) return PortAgeTerrain.ISLAND;
|
||||||
|
if (pages.contains(FLAT_TERRAIN)) return PortAgeTerrain.FLAT;
|
||||||
|
if (pages.contains(VOID_TERRAIN)) return PortAgeTerrain.VOID;
|
||||||
|
if (pages.contains(CAVE_TERRAIN)) return PortAgeTerrain.CAVE;
|
||||||
return PortAgeTerrain.STANDARD;
|
return PortAgeTerrain.STANDARD;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static PortRockPlan rockPlan(List<Identifier> pages) {
|
||||||
|
if (!supports(pages)) throw new IllegalArgumentException("Unsupported authored Age pages");
|
||||||
|
return pages.contains(ROCKS) ? new PortRockPlan(pages.stream()
|
||||||
|
.filter(PortAuthoredWriting::isBlockPage).map(PortRegistryPages::blockId)
|
||||||
|
.findFirst().orElseThrow()) : null;
|
||||||
|
}
|
||||||
|
|
||||||
/** Resolves all authored biomes from the active registry before creating a level. */
|
/** Resolves all authored biomes from the active registry before creating a level. */
|
||||||
public static PortBiomePlan biomePlan(List<Identifier> pages, HolderLookup.Provider registries) {
|
public static PortBiomePlan biomePlan(List<Identifier> pages, HolderLookup.Provider registries) {
|
||||||
if (!supports(pages)) throw new IllegalArgumentException(
|
if (!supports(pages)) throw new IllegalArgumentException(
|
||||||
"Only Standard/Amplified/Island terrain with supported Biome pages is available in this port slice");
|
"Unsupported terrain or biome-page combination in this port slice");
|
||||||
List<Identifier> biomes = new ArrayList<>();
|
List<Identifier> biomes = new ArrayList<>();
|
||||||
PortBiomePlan.Distribution distribution = null;
|
PortBiomePlan.Distribution distribution = null;
|
||||||
|
boolean caveTerrain = pages.contains(CAVE_TERRAIN);
|
||||||
for (Identifier page : pages) {
|
for (Identifier page : pages) {
|
||||||
if (page.equals(STANDARD_TERRAIN) || page.equals(AMPLIFIED_TERRAIN)
|
if (isTerrain(page) || page.equals(ROCKS) || isBlockPage(page)) continue;
|
||||||
|| page.equals(ISLAND_TERRAIN)) continue;
|
|
||||||
PortBiomePlan.Distribution selected = controller(page);
|
PortBiomePlan.Distribution selected = controller(page);
|
||||||
if (selected != null) { distribution = selected; continue; }
|
if (selected != null) { distribution = selected; continue; }
|
||||||
Identifier biome = PortRegistryPages.biomeId(page);
|
Identifier biome = PortRegistryPages.biomeId(page);
|
||||||
var registry = registries.lookupOrThrow(Registries.BIOME);
|
var registry = registries.lookupOrThrow(Registries.BIOME);
|
||||||
if (registry.get(ResourceKey.create(Registries.BIOME, biome)).isEmpty())
|
var holder = registry.get(ResourceKey.create(Registries.BIOME, biome))
|
||||||
throw new IllegalArgumentException("Unknown authored biome: " + biome);
|
.orElseThrow(() -> new IllegalArgumentException("Unknown authored biome: " + biome));
|
||||||
|
if (caveTerrain && !PortRegistryPages.isCaveBiome(holder))
|
||||||
|
throw new IllegalArgumentException("Cave terrain requires a Cave or Nether biome: " + biome);
|
||||||
biomes.add(biome);
|
biomes.add(biome);
|
||||||
}
|
}
|
||||||
if (distribution == PortBiomePlan.Distribution.NATIVE)
|
if (distribution == PortBiomePlan.Distribution.NATIVE)
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ public final class PortRegistryPages {
|
|||||||
public static List<ItemStack> biomePages(HolderLookup.Provider registries, boolean caves) {
|
public static List<ItemStack> biomePages(HolderLookup.Provider registries, boolean caves) {
|
||||||
return registries.lookupOrThrow(Registries.BIOME).listElements()
|
return registries.lookupOrThrow(Registries.BIOME).listElements()
|
||||||
.filter(holder -> !holder.key().identifier().equals(DEFERRED_PLACEHOLDER))
|
.filter(holder -> !holder.key().identifier().equals(DEFERRED_PLACEHOLDER))
|
||||||
.filter(holder -> isCave(holder) == caves)
|
.filter(holder -> isCaveBiome(holder) == caves)
|
||||||
.map(holder -> PageItem.createSymbolPage(biomePageId(holder.key().identifier())))
|
.map(holder -> PageItem.createSymbolPage(biomePageId(holder.key().identifier())))
|
||||||
.sorted((left, right) -> left.get(Rivencraft.SYMBOL_PAGE_ID.get()).toString()
|
.sorted((left, right) -> left.get(Rivencraft.SYMBOL_PAGE_ID.get()).toString()
|
||||||
.compareTo(right.get(Rivencraft.SYMBOL_PAGE_ID.get()).toString()))
|
.compareTo(right.get(Rivencraft.SYMBOL_PAGE_ID.get()).toString()))
|
||||||
@@ -81,7 +81,7 @@ public final class PortRegistryPages {
|
|||||||
.map(id -> PageItem.createSymbolPage(blockPageId(id))).toList();
|
.map(id -> PageItem.createSymbolPage(blockPageId(id))).toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static boolean isCave(net.minecraft.core.Holder.Reference<net.minecraft.world.level.biome.Biome> holder) {
|
public static boolean isCaveBiome(net.minecraft.core.Holder.Reference<net.minecraft.world.level.biome.Biome> holder) {
|
||||||
String path = holder.key().identifier().getPath();
|
String path = holder.key().identifier().getPath();
|
||||||
return holder.is(CAVE_BIOMES) || holder.is(BiomeTags.IS_NETHER)
|
return holder.is(CAVE_BIOMES) || holder.is(BiomeTags.IS_NETHER)
|
||||||
|| path.equals("deep_dark") || path.endsWith("_caves");
|
|| path.equals("deep_dark") || path.endsWith("_caves");
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"replace": false,
|
||||||
|
"values": [
|
||||||
|
"minecraft:lush_caves",
|
||||||
|
"minecraft:dripstone_caves",
|
||||||
|
"minecraft:deep_dark"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,283 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Publish a Rivencraft JAR to Gitea, Modrinth, and/or CurseForge.
|
||||||
|
|
||||||
|
Preview is the default. Tokens live in the Secret Service keyring, never in git.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import getpass
|
||||||
|
import json
|
||||||
|
import mimetypes
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
import secrets
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import urllib.error
|
||||||
|
import urllib.request
|
||||||
|
|
||||||
|
|
||||||
|
MODRINTH_PROJECT = "GH2xfZJ8"
|
||||||
|
CURSEFORGE_PROJECT = "1766326"
|
||||||
|
GITEA_API = "https://git.paddedcell.org/api/v1/repos/dnorberg/rivencraft"
|
||||||
|
USER_AGENT = "RivencraftReleaseTool/1.0 (https://git.paddedcell.org/dnorberg/rivencraft)"
|
||||||
|
SITES = ("gitea", "modrinth", "curseforge")
|
||||||
|
|
||||||
|
|
||||||
|
def token_for(site: str) -> str:
|
||||||
|
environment = os.environ.get(f"RIVENCRAFT_{site.upper()}_TOKEN", "").strip()
|
||||||
|
if environment:
|
||||||
|
return environment
|
||||||
|
result = subprocess.run(
|
||||||
|
["secret-tool", "lookup", "application", "rivencraft-publishing", "site", site],
|
||||||
|
text=True,
|
||||||
|
capture_output=True,
|
||||||
|
check=False,
|
||||||
|
)
|
||||||
|
if result.returncode:
|
||||||
|
raise RuntimeError(f"Could not read {site} token from the system keyring")
|
||||||
|
token = result.stdout.strip()
|
||||||
|
if not token:
|
||||||
|
raise RuntimeError(f"No {site} token found; run `store-token {site}` first")
|
||||||
|
return token
|
||||||
|
|
||||||
|
|
||||||
|
def store_token(site: str) -> None:
|
||||||
|
token = getpass.getpass(f"Paste the {site} API token (input hidden): ").strip()
|
||||||
|
if not token:
|
||||||
|
raise RuntimeError("Empty token; nothing was stored")
|
||||||
|
subprocess.run(
|
||||||
|
[
|
||||||
|
"secret-tool", "store", "--label", f"Rivencraft {site} publishing token",
|
||||||
|
"application", "rivencraft-publishing", "site", site,
|
||||||
|
],
|
||||||
|
input=token + "\n",
|
||||||
|
text=True,
|
||||||
|
check=True,
|
||||||
|
)
|
||||||
|
print(f"Stored {site} token in the system keyring")
|
||||||
|
|
||||||
|
|
||||||
|
def check_token(site: str) -> None:
|
||||||
|
token = token_for(site)
|
||||||
|
if site == "gitea":
|
||||||
|
account = request_json("https://git.paddedcell.org/api/v1/token", token=f"token {token}")
|
||||||
|
scopes = account.get("scopes", [])
|
||||||
|
if "write:repository" not in scopes and "all" not in scopes:
|
||||||
|
raise RuntimeError("Gitea token is valid but lacks write:repository scope")
|
||||||
|
print(f"Gitea API access works for {account['user']['login']} (write:repository)")
|
||||||
|
elif site == "modrinth":
|
||||||
|
try:
|
||||||
|
account = request_json("https://api.modrinth.com/v2/user", token=token)
|
||||||
|
except RuntimeError as error:
|
||||||
|
raise RuntimeError(
|
||||||
|
"Modrinth check failed. The token needs USER_READ for this "
|
||||||
|
"read-only check, plus VERSION_CREATE for publishing. "
|
||||||
|
f"API response: {error}"
|
||||||
|
) from None
|
||||||
|
print(f"Modrinth API access works for {account['username']}")
|
||||||
|
else:
|
||||||
|
versions = request_json(
|
||||||
|
"https://www.curseforge.com/api/game/versions",
|
||||||
|
token=token, token_header="X-Api-Token",
|
||||||
|
)
|
||||||
|
if not isinstance(versions, list):
|
||||||
|
raise RuntimeError("CurseForge returned an unexpected response")
|
||||||
|
print("CurseForge API token accepted")
|
||||||
|
|
||||||
|
|
||||||
|
def request_json(url: str, *, token: str | None = None, token_header: str = "Authorization",
|
||||||
|
payload: bytes | None = None, content_type: str | None = None,
|
||||||
|
method: str = "GET", not_found_ok: bool = False) -> dict | list | None:
|
||||||
|
headers = {"User-Agent": USER_AGENT, "Accept": "application/json"}
|
||||||
|
if token:
|
||||||
|
headers[token_header] = token
|
||||||
|
if content_type:
|
||||||
|
headers["Content-Type"] = content_type
|
||||||
|
request = urllib.request.Request(url, data=payload, headers=headers, method=method)
|
||||||
|
try:
|
||||||
|
with urllib.request.urlopen(request, timeout=120) as response:
|
||||||
|
return json.load(response)
|
||||||
|
except urllib.error.HTTPError as error:
|
||||||
|
if error.code == 404 and not_found_ok:
|
||||||
|
return None
|
||||||
|
# Error bodies can contain server details, but never echo tokens.
|
||||||
|
detail = error.read(600).decode("utf-8", "replace")
|
||||||
|
raise RuntimeError(f"{url}: HTTP {error.code}: {detail}") from None
|
||||||
|
|
||||||
|
|
||||||
|
def multipart(fields: dict[str, str], jar: Path, *, file_field: str = "file") -> tuple[bytes, str]:
|
||||||
|
boundary = "rivencraft-" + secrets.token_hex(16)
|
||||||
|
chunks = []
|
||||||
|
for name, value in fields.items():
|
||||||
|
chunks.extend((
|
||||||
|
f"--{boundary}\r\nContent-Disposition: form-data; name=\"{name}\"\r\n\r\n".encode(),
|
||||||
|
value.encode(), b"\r\n",
|
||||||
|
))
|
||||||
|
filename = jar.name.replace('"', "")
|
||||||
|
mime = mimetypes.guess_type(filename)[0] or "application/java-archive"
|
||||||
|
chunks.extend((
|
||||||
|
f"--{boundary}\r\nContent-Disposition: form-data; name=\"{file_field}\"; filename=\"{filename}\"\r\nContent-Type: {mime}\r\n\r\n".encode(),
|
||||||
|
jar.read_bytes(), b"\r\n", f"--{boundary}--\r\n".encode(),
|
||||||
|
))
|
||||||
|
return b"".join(chunks), f"multipart/form-data; boundary={boundary}"
|
||||||
|
|
||||||
|
|
||||||
|
def release_metadata(args: argparse.Namespace) -> tuple[dict, dict]:
|
||||||
|
changelog = args.changelog.read_text(encoding="utf-8")
|
||||||
|
if not changelog.strip():
|
||||||
|
raise RuntimeError("Changelog is empty")
|
||||||
|
modrinth = {
|
||||||
|
"name": args.name,
|
||||||
|
"version_number": args.version,
|
||||||
|
"changelog": changelog,
|
||||||
|
"dependencies": [],
|
||||||
|
"game_versions": [args.minecraft],
|
||||||
|
"version_type": args.channel,
|
||||||
|
"loaders": ["neoforge"],
|
||||||
|
"featured": False,
|
||||||
|
"status": "listed",
|
||||||
|
"project_id": MODRINTH_PROJECT,
|
||||||
|
"file_parts": ["file"],
|
||||||
|
"primary_file": "file",
|
||||||
|
"environment": "client_and_server",
|
||||||
|
}
|
||||||
|
curseforge = {
|
||||||
|
"changelog": changelog,
|
||||||
|
"changelogType": "markdown",
|
||||||
|
"displayName": args.name,
|
||||||
|
"gameVersionNames": [args.minecraft, "NeoForge", "Client", "Server", f"Java {args.java}"],
|
||||||
|
"releaseType": args.channel,
|
||||||
|
"isMarkedForManualRelease": False,
|
||||||
|
}
|
||||||
|
return modrinth, curseforge
|
||||||
|
|
||||||
|
|
||||||
|
def publish_modrinth(metadata: dict, jar: Path) -> None:
|
||||||
|
token = token_for("modrinth")
|
||||||
|
versions = request_json(
|
||||||
|
f"https://api.modrinth.com/v2/project/{MODRINTH_PROJECT}/version",
|
||||||
|
token=token,
|
||||||
|
)
|
||||||
|
if any(version.get("version_number") == metadata["version_number"] for version in versions):
|
||||||
|
raise RuntimeError(f"Modrinth version {metadata['version_number']} already exists")
|
||||||
|
body, content_type = multipart({"data": json.dumps(metadata)}, jar)
|
||||||
|
result = request_json(
|
||||||
|
"https://api.modrinth.com/v2/version", token=token, payload=body,
|
||||||
|
content_type=content_type, method="POST",
|
||||||
|
)
|
||||||
|
print(f"Modrinth: https://modrinth.com/mod/rivencraft/version/{result['version_number']}")
|
||||||
|
|
||||||
|
|
||||||
|
def publish_curseforge(metadata: dict, jar: Path) -> None:
|
||||||
|
token = token_for("curseforge")
|
||||||
|
body, content_type = multipart({"metadata": json.dumps(metadata)}, jar)
|
||||||
|
result = request_json(
|
||||||
|
f"https://www.curseforge.com/api/projects/{CURSEFORGE_PROJECT}/upload-file",
|
||||||
|
token=token, token_header="X-Api-Token", payload=body,
|
||||||
|
content_type=content_type, method="POST",
|
||||||
|
)
|
||||||
|
print(f"CurseForge: https://www.curseforge.com/minecraft/mc-mods/rivencraft/files/{result['id']} (pending approval)")
|
||||||
|
|
||||||
|
|
||||||
|
def publish_gitea(args: argparse.Namespace) -> None:
|
||||||
|
base_version, suffix = args.version.split("-", 1)
|
||||||
|
if suffix != f"{args.channel}.1":
|
||||||
|
raise RuntimeError("Gitea tag convention requires a version ending in the selected channel and .1")
|
||||||
|
tag = f"V{base_version}-mc{args.minecraft}-{suffix}"
|
||||||
|
# The release must refer to a tag already pushed by the normal Git workflow.
|
||||||
|
remote_tag = subprocess.run(
|
||||||
|
["git", "ls-remote", "origin", f"refs/tags/{tag}"],
|
||||||
|
text=True, capture_output=True, check=True,
|
||||||
|
)
|
||||||
|
if not remote_tag.stdout.strip():
|
||||||
|
raise RuntimeError(f"Push the Gitea tag {tag} before publishing")
|
||||||
|
|
||||||
|
token = f"token {token_for('gitea')}"
|
||||||
|
release_url = f"{GITEA_API}/releases/tags/{tag}"
|
||||||
|
release = request_json(release_url, token=token, not_found_ok=True)
|
||||||
|
notes = args.changelog.read_text(encoding="utf-8")
|
||||||
|
title = f"Rivencraft {args.minecraft} {args.channel} {args.version}"
|
||||||
|
if release is None:
|
||||||
|
metadata = {
|
||||||
|
"tag_name": tag,
|
||||||
|
"target_commitish": "main",
|
||||||
|
"name": title,
|
||||||
|
"body": notes,
|
||||||
|
"draft": False,
|
||||||
|
"prerelease": args.channel != "release",
|
||||||
|
}
|
||||||
|
release = request_json(
|
||||||
|
f"{GITEA_API}/releases", token=token,
|
||||||
|
payload=json.dumps(metadata).encode(), content_type="application/json", method="POST",
|
||||||
|
)
|
||||||
|
elif release.get("name") != title or release.get("body", "").strip() != notes.strip():
|
||||||
|
raise RuntimeError(f"Gitea release {tag} exists with different notes or title; inspect it before retrying")
|
||||||
|
|
||||||
|
assets = release.get("assets", [])
|
||||||
|
if any(asset.get("name") == args.jar.name for asset in assets):
|
||||||
|
raise RuntimeError(f"Gitea release {tag} already has {args.jar.name}")
|
||||||
|
body, content_type = multipart({}, args.jar, file_field="attachment")
|
||||||
|
attachment = request_json(
|
||||||
|
f"{GITEA_API}/releases/{release['id']}/assets", token=token,
|
||||||
|
payload=body, content_type=content_type, method="POST",
|
||||||
|
)
|
||||||
|
if attachment.get("name") != args.jar.name or attachment.get("size") != args.jar.stat().st_size:
|
||||||
|
raise RuntimeError("Gitea returned unexpected attachment metadata; inspect the release")
|
||||||
|
print(f"Gitea: {release['html_url']}")
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
parser = argparse.ArgumentParser(description=__doc__)
|
||||||
|
sub = parser.add_subparsers(dest="command", required=True)
|
||||||
|
store = sub.add_parser("store-token", help="Securely save a token in the system keyring")
|
||||||
|
store.add_argument("site", choices=SITES)
|
||||||
|
check = sub.add_parser("check-token", help="Verify stored token with a read-only API call")
|
||||||
|
check.add_argument("site", choices=SITES)
|
||||||
|
publish = sub.add_parser("release", help="Preview or publish a JAR release")
|
||||||
|
publish.add_argument("--jar", type=Path, required=True)
|
||||||
|
publish.add_argument("--changelog", type=Path, required=True)
|
||||||
|
publish.add_argument("--version", required=True)
|
||||||
|
publish.add_argument("--name", required=True)
|
||||||
|
publish.add_argument("--minecraft", required=True)
|
||||||
|
publish.add_argument("--java", required=True)
|
||||||
|
publish.add_argument("--channel", choices=("alpha", "beta", "release"), required=True)
|
||||||
|
publish.add_argument("--site", choices=(*SITES, "both", "all"), default="all")
|
||||||
|
publish.add_argument("--publish", action="store_true", help="Actually upload; omitted by default")
|
||||||
|
args = parser.parse_args()
|
||||||
|
if args.command == "store-token":
|
||||||
|
store_token(args.site)
|
||||||
|
return
|
||||||
|
if args.command == "check-token":
|
||||||
|
check_token(args.site)
|
||||||
|
return
|
||||||
|
if not args.jar.is_file() or args.jar.suffix != ".jar":
|
||||||
|
raise RuntimeError(f"JAR does not exist: {args.jar}")
|
||||||
|
if not args.changelog.is_file():
|
||||||
|
raise RuntimeError(f"Changelog does not exist: {args.changelog}")
|
||||||
|
if args.minecraft != "26.3":
|
||||||
|
raise RuntimeError("Only the 26.3 line is configured; 1.21.1 needs required dependency relations")
|
||||||
|
if args.jar.name != f"rivencraft-{args.minecraft}-{args.version}.jar":
|
||||||
|
raise RuntimeError("JAR filename does not match Minecraft and release versions")
|
||||||
|
modrinth, curseforge = release_metadata(args)
|
||||||
|
sites = SITES if args.site == "all" else (("modrinth", "curseforge") if args.site == "both" else (args.site,))
|
||||||
|
print(f"JAR: {args.jar.resolve()} ({args.jar.stat().st_size} bytes)")
|
||||||
|
print(f"Release: {args.version} | {args.channel} | Minecraft {args.minecraft} | NeoForge | Java {args.java}")
|
||||||
|
print(f"Destinations: {', '.join(sites)}")
|
||||||
|
if not args.publish:
|
||||||
|
print("PREVIEW ONLY: add --publish to upload")
|
||||||
|
return
|
||||||
|
if "gitea" in sites:
|
||||||
|
publish_gitea(args)
|
||||||
|
if "modrinth" in sites:
|
||||||
|
publish_modrinth(modrinth, args.jar)
|
||||||
|
if "curseforge" in sites:
|
||||||
|
publish_curseforge(curseforge, args.jar)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
try:
|
||||||
|
main()
|
||||||
|
except (OSError, ValueError, RuntimeError, subprocess.CalledProcessError) as error:
|
||||||
|
print(f"Publishing failed: {error}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
@@ -69,6 +69,14 @@ start_server first
|
|||||||
send_and_wait 'rivencraft_port compat' 'Registered biomes by namespace:'
|
send_and_wait 'rivencraft_port compat' 'Registered biomes by namespace:'
|
||||||
send_and_wait 'rivencraft_port age create smoke_alpha 123456789' 'Created rivencraft:age/smoke_alpha with seed 123456789'
|
send_and_wait 'rivencraft_port age create smoke_alpha 123456789' 'Created rivencraft:age/smoke_alpha with seed 123456789'
|
||||||
send_and_wait 'rivencraft_port age create smoke_beta 987654321' 'Created rivencraft:age/smoke_beta with seed 987654321'
|
send_and_wait 'rivencraft_port age create smoke_beta 987654321' 'Created rivencraft:age/smoke_beta with seed 987654321'
|
||||||
|
send_and_wait 'rivencraft_port age create smoke_flat 24680 flat' 'Created rivencraft:age/smoke_flat with seed 24680 terrain=FLAT'
|
||||||
|
send_and_wait 'rivencraft_port age create smoke_void 13579 void' 'Created rivencraft:age/smoke_void with seed 13579 terrain=VOID'
|
||||||
|
send_and_wait 'rivencraft_port age create smoke_cave 112233 cave' 'Created rivencraft:age/smoke_cave with seed 112233 terrain=CAVE'
|
||||||
|
send_and_wait 'rivencraft_port age create_multi smoke_flat_multi 24681 flat' 'Created rivencraft:age/smoke_flat_multi with seed 24681 terrain=FLAT'
|
||||||
|
send_and_wait 'rivencraft_port age create_multi smoke_void_multi 13580 void' 'Created rivencraft:age/smoke_void_multi with seed 13580 terrain=VOID'
|
||||||
|
send_and_wait 'rivencraft_port age create_multi smoke_cave_multi 112234 cave' 'Created rivencraft:age/smoke_cave_multi with seed 112234 terrain=CAVE'
|
||||||
|
send_and_wait 'rivencraft_port age create_random smoke_random 889900' 'Random Age plan: terrain='
|
||||||
|
send_and_wait 'rivencraft_port age create_rocks smoke_rocks 445566' 'Created rivencraft:age/smoke_rocks with seed 445566 terrain=FLAT'
|
||||||
# The index must reach disk while the server is still running. A graceful
|
# The index must reach disk while the server is still running. A graceful
|
||||||
# shutdown would hide the interrupted-save regression this checks.
|
# shutdown would hide the interrupted-save regression this checks.
|
||||||
index_file="$server_dir/world/dimensions/minecraft/overworld/data/rivencraft/runtime_ages.dat"
|
index_file="$server_dir/world/dimensions/minecraft/overworld/data/rivencraft/runtime_ages.dat"
|
||||||
@@ -78,26 +86,104 @@ index_file="$server_dir/world/dimensions/minecraft/overworld/data/rivencraft/run
|
|||||||
}
|
}
|
||||||
gzip -dc "$index_file" | strings | grep -Fq 'rivencraft:age/smoke_alpha'
|
gzip -dc "$index_file" | strings | grep -Fq 'rivencraft:age/smoke_alpha'
|
||||||
gzip -dc "$index_file" | strings | grep -Fq 'rivencraft:age/smoke_beta'
|
gzip -dc "$index_file" | strings | grep -Fq 'rivencraft:age/smoke_beta'
|
||||||
|
gzip -dc "$index_file" | strings | grep -Fq 'rivencraft:age/smoke_flat'
|
||||||
|
gzip -dc "$index_file" | strings | grep -Fq 'rivencraft:age/smoke_void'
|
||||||
|
gzip -dc "$index_file" | strings | grep -Fq 'rivencraft:age/smoke_cave'
|
||||||
|
gzip -dc "$index_file" | strings | grep -Fq 'rivencraft:age/smoke_flat_multi'
|
||||||
|
gzip -dc "$index_file" | strings | grep -Fq 'rivencraft:age/smoke_void_multi'
|
||||||
|
gzip -dc "$index_file" | strings | grep -Fq 'rivencraft:age/smoke_cave_multi'
|
||||||
|
gzip -dc "$index_file" | strings | grep -Fq 'rivencraft:age/smoke_random'
|
||||||
|
gzip -dc "$index_file" | strings | grep -Fq 'rivencraft:age/smoke_rocks'
|
||||||
send_and_wait 'rivencraft_port age sample smoke_alpha' 'rivencraft:age/smoke_alpha seed=123456789 chunkOffset=0 terrain='
|
send_and_wait 'rivencraft_port age sample smoke_alpha' 'rivencraft:age/smoke_alpha seed=123456789 chunkOffset=0 terrain='
|
||||||
send_and_wait 'rivencraft_port age sample smoke_beta' 'rivencraft:age/smoke_beta seed=987654321 chunkOffset=0 terrain='
|
send_and_wait 'rivencraft_port age sample smoke_beta' 'rivencraft:age/smoke_beta seed=987654321 chunkOffset=0 terrain='
|
||||||
|
send_and_wait 'rivencraft_port age sample smoke_flat' 'rivencraft:age/smoke_flat seed=24680 chunkOffset=0 terrain='
|
||||||
|
send_and_wait 'rivencraft_port age sample smoke_void' 'rivencraft:age/smoke_void seed=13579 chunkOffset=0 terrain='
|
||||||
|
send_and_wait 'rivencraft_port age sample smoke_cave' 'rivencraft:age/smoke_cave seed=112233 chunkOffset=0 terrain='
|
||||||
|
send_and_wait 'rivencraft_port age sample smoke_flat_multi' 'rivencraft:age/smoke_flat_multi seed=24681 chunkOffset=0 terrain='
|
||||||
|
send_and_wait 'rivencraft_port age sample smoke_void_multi' 'rivencraft:age/smoke_void_multi seed=13580 chunkOffset=0 terrain='
|
||||||
|
send_and_wait 'rivencraft_port age sample smoke_cave_multi' 'rivencraft:age/smoke_cave_multi seed=112234 chunkOffset=0 terrain='
|
||||||
|
send_and_wait 'rivencraft_port age sample smoke_random' 'rivencraft:age/smoke_random seed=889900 chunkOffset=0 terrain='
|
||||||
|
send_and_wait 'rivencraft_port age sample smoke_rocks' 'rivencraft:age/smoke_rocks seed=445566 chunkOffset=0 terrain='
|
||||||
|
send_and_wait 'execute in rivencraft:age/smoke_flat run forceload add 0 0' 'Marked chunk [0, 0] in rivencraft:age/smoke_flat to be force loaded'
|
||||||
|
send_and_wait 'execute in rivencraft:age/smoke_void run forceload add 0 0' 'Marked chunk [0, 0] in rivencraft:age/smoke_void to be force loaded'
|
||||||
|
send_and_wait 'execute in rivencraft:age/smoke_flat_multi run forceload add 0 0' 'Marked chunk [0, 0] in rivencraft:age/smoke_flat_multi to be force loaded'
|
||||||
|
send_and_wait 'execute in rivencraft:age/smoke_void_multi run forceload add 0 0' 'Marked chunk [0, 0] in rivencraft:age/smoke_void_multi to be force loaded'
|
||||||
|
send_and_wait 'execute in rivencraft:age/smoke_cave run forceload add 0 0' 'Marked chunk [0, 0] in rivencraft:age/smoke_cave to be force loaded'
|
||||||
|
send_and_wait 'execute in rivencraft:age/smoke_cave_multi run forceload add 0 0' 'Marked chunk [0, 0] in rivencraft:age/smoke_cave_multi to be force loaded'
|
||||||
|
send_and_wait 'execute in rivencraft:age/smoke_rocks run forceload add 0 0' 'Marked chunk [0, 0] in rivencraft:age/smoke_rocks to be force loaded'
|
||||||
|
send_and_wait 'rivencraft_port age rocks_probe smoke_rocks' 'Rocks probe rivencraft:age/smoke_rocks material=minecraft:gold_block count='
|
||||||
|
rocks_before=$(grep -F 'Rocks probe rivencraft:age/smoke_rocks material=minecraft:gold_block count=' "$log_file" | tail -1 | sed 's/.*count=//')
|
||||||
|
[[ "$rocks_before" =~ ^[1-9][0-9]*$ ]] || {
|
||||||
|
printf 'Authored Rocks placed no gold blocks: %s\n' "$rocks_before" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
send_and_wait 'execute in rivencraft:age/smoke_flat if block 0 0 0 minecraft:bedrock run say Flat bedrock starts at Y0' 'Flat bedrock starts at Y0'
|
||||||
|
send_and_wait 'execute in rivencraft:age/smoke_flat if block 0 3 0 minecraft:grass_block run say Flat surface is Y3' 'Flat surface is Y3'
|
||||||
|
send_and_wait 'execute in rivencraft:age/smoke_void if block 0 0 0 minecraft:air run say Void remains empty' 'Void remains empty'
|
||||||
|
send_and_wait 'execute in rivencraft:age/smoke_flat_multi if block 0 0 0 minecraft:bedrock run say Multi Flat bedrock starts at Y0' 'Multi Flat bedrock starts at Y0'
|
||||||
|
send_and_wait 'execute in rivencraft:age/smoke_void_multi if block 0 0 0 minecraft:air run say Multi Void remains empty' 'Multi Void remains empty'
|
||||||
|
# Check region interiors after loading the sampled chunks.
|
||||||
|
send_and_wait 'rivencraft_port age biome smoke_flat_multi 128 128' 'rivencraft:age/smoke_flat_multi biome at 128,128=minecraft:plains'
|
||||||
|
send_and_wait 'rivencraft_port age biome smoke_flat_multi 384 128' 'rivencraft:age/smoke_flat_multi biome at 384,128=minecraft:forest'
|
||||||
|
send_and_wait 'rivencraft_port age biome smoke_void_multi 384 128' 'rivencraft:age/smoke_void_multi biome at 384,128=minecraft:forest'
|
||||||
|
send_and_wait 'rivencraft_port age biome smoke_cave_multi 128 128' 'rivencraft:age/smoke_cave_multi biome at 128,128=minecraft:lush_caves'
|
||||||
|
send_and_wait 'rivencraft_port age biome smoke_cave_multi 384 128' 'rivencraft:age/smoke_cave_multi biome at 384,128=minecraft:dripstone_caves'
|
||||||
alpha_before=$(grep -F 'rivencraft:age/smoke_alpha seed=123456789 chunkOffset=0 terrain=' "$log_file" | tail -1 | sed 's/.*terrain=//')
|
alpha_before=$(grep -F 'rivencraft:age/smoke_alpha seed=123456789 chunkOffset=0 terrain=' "$log_file" | tail -1 | sed 's/.*terrain=//')
|
||||||
beta_before=$(grep -F 'rivencraft:age/smoke_beta seed=987654321 chunkOffset=0 terrain=' "$log_file" | tail -1 | sed 's/.*terrain=//')
|
beta_before=$(grep -F 'rivencraft:age/smoke_beta seed=987654321 chunkOffset=0 terrain=' "$log_file" | tail -1 | sed 's/.*terrain=//')
|
||||||
|
flat_before=$(grep -F 'rivencraft:age/smoke_flat seed=24680 chunkOffset=0 terrain=' "$log_file" | tail -1 | sed 's/.*terrain=//')
|
||||||
|
void_before=$(grep -F 'rivencraft:age/smoke_void seed=13579 chunkOffset=0 terrain=' "$log_file" | tail -1 | sed 's/.*terrain=//')
|
||||||
|
random_before=$(grep -F 'rivencraft:age/smoke_random seed=889900 chunkOffset=0 terrain=' "$log_file" | tail -1 | sed 's/.*terrain=//')
|
||||||
if [[ "$alpha_before" == "$beta_before" ]]; then
|
if [[ "$alpha_before" == "$beta_before" ]]; then
|
||||||
printf 'Different Age seeds produced the same terrain probe: %s\n' "$alpha_before" >&2
|
printf 'Different Age seeds produced the same terrain probe: %s\n' "$alpha_before" >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
if [[ "$flat_before" == "$void_before" ]]; then
|
||||||
|
printf 'Flat and Void Ages produced the same height probe: %s\n' "$flat_before" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
stop_server
|
stop_server
|
||||||
|
|
||||||
start_server restart
|
start_server restart
|
||||||
send_and_wait 'rivencraft_port age list' 'smoke_alpha'
|
send_and_wait 'rivencraft_port age list' 'smoke_alpha'
|
||||||
send_and_wait 'rivencraft_port age sample smoke_alpha' 'rivencraft:age/smoke_alpha seed=123456789 chunkOffset=0 terrain='
|
send_and_wait 'rivencraft_port age sample smoke_alpha' 'rivencraft:age/smoke_alpha seed=123456789 chunkOffset=0 terrain='
|
||||||
send_and_wait 'rivencraft_port age sample smoke_beta' 'rivencraft:age/smoke_beta seed=987654321 chunkOffset=0 terrain='
|
send_and_wait 'rivencraft_port age sample smoke_beta' 'rivencraft:age/smoke_beta seed=987654321 chunkOffset=0 terrain='
|
||||||
|
send_and_wait 'rivencraft_port age sample smoke_flat' 'rivencraft:age/smoke_flat seed=24680 chunkOffset=0 terrain='
|
||||||
|
send_and_wait 'rivencraft_port age sample smoke_void' 'rivencraft:age/smoke_void seed=13579 chunkOffset=0 terrain='
|
||||||
|
send_and_wait 'rivencraft_port age sample smoke_cave' 'rivencraft:age/smoke_cave seed=112233 chunkOffset=0 terrain='
|
||||||
|
send_and_wait 'rivencraft_port age sample smoke_flat_multi' 'rivencraft:age/smoke_flat_multi seed=24681 chunkOffset=0 terrain='
|
||||||
|
send_and_wait 'rivencraft_port age sample smoke_void_multi' 'rivencraft:age/smoke_void_multi seed=13580 chunkOffset=0 terrain='
|
||||||
|
send_and_wait 'rivencraft_port age sample smoke_cave_multi' 'rivencraft:age/smoke_cave_multi seed=112234 chunkOffset=0 terrain='
|
||||||
|
send_and_wait 'rivencraft_port age sample smoke_random' 'rivencraft:age/smoke_random seed=889900 chunkOffset=0 terrain='
|
||||||
|
send_and_wait 'rivencraft_port age sample smoke_rocks' 'rivencraft:age/smoke_rocks seed=445566 chunkOffset=0 terrain='
|
||||||
|
send_and_wait 'rivencraft_port age rocks_probe smoke_rocks' 'Rocks probe rivencraft:age/smoke_rocks material=minecraft:gold_block count='
|
||||||
|
rocks_after=$(grep -F 'Rocks probe rivencraft:age/smoke_rocks material=minecraft:gold_block count=' "$log_file" | tail -1 | sed 's/.*count=//')
|
||||||
|
send_and_wait 'rivencraft_port age biome smoke_flat_multi 384 128' 'rivencraft:age/smoke_flat_multi biome at 384,128=minecraft:forest'
|
||||||
|
send_and_wait 'rivencraft_port age biome smoke_void_multi 384 128' 'rivencraft:age/smoke_void_multi biome at 384,128=minecraft:forest'
|
||||||
|
send_and_wait 'rivencraft_port age biome smoke_cave_multi 384 128' 'rivencraft:age/smoke_cave_multi biome at 384,128=minecraft:dripstone_caves'
|
||||||
alpha_after=$(grep -F 'rivencraft:age/smoke_alpha seed=123456789 chunkOffset=0 terrain=' "$log_file" | tail -1 | sed 's/.*terrain=//')
|
alpha_after=$(grep -F 'rivencraft:age/smoke_alpha seed=123456789 chunkOffset=0 terrain=' "$log_file" | tail -1 | sed 's/.*terrain=//')
|
||||||
beta_after=$(grep -F 'rivencraft:age/smoke_beta seed=987654321 chunkOffset=0 terrain=' "$log_file" | tail -1 | sed 's/.*terrain=//')
|
beta_after=$(grep -F 'rivencraft:age/smoke_beta seed=987654321 chunkOffset=0 terrain=' "$log_file" | tail -1 | sed 's/.*terrain=//')
|
||||||
|
flat_after=$(grep -F 'rivencraft:age/smoke_flat seed=24680 chunkOffset=0 terrain=' "$log_file" | tail -1 | sed 's/.*terrain=//')
|
||||||
|
void_after=$(grep -F 'rivencraft:age/smoke_void seed=13579 chunkOffset=0 terrain=' "$log_file" | tail -1 | sed 's/.*terrain=//')
|
||||||
|
random_after=$(grep -F 'rivencraft:age/smoke_random seed=889900 chunkOffset=0 terrain=' "$log_file" | tail -1 | sed 's/.*terrain=//')
|
||||||
[[ "$alpha_before" == "$alpha_after" && "$beta_before" == "$beta_after" ]] || {
|
[[ "$alpha_before" == "$alpha_after" && "$beta_before" == "$beta_after" ]] || {
|
||||||
printf 'Age terrain changed across restart: %s/%s -> %s/%s\n' \
|
printf 'Age terrain changed across restart: %s/%s -> %s/%s\n' \
|
||||||
"$alpha_before" "$beta_before" "$alpha_after" "$beta_after" >&2
|
"$alpha_before" "$beta_before" "$alpha_after" "$beta_after" >&2
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
[[ "$flat_before" == "$flat_after" && "$void_before" == "$void_after" ]] || {
|
||||||
|
printf 'Flat/Void terrain changed across restart: %s/%s -> %s/%s\n' \
|
||||||
|
"$flat_before" "$void_before" "$flat_after" "$void_after" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
[[ "$random_before" == "$random_after" ]] || {
|
||||||
|
printf 'Random Age terrain changed across restart: %s -> %s\n' \
|
||||||
|
"$random_before" "$random_after" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
[[ "$rocks_before" == "$rocks_after" ]] || {
|
||||||
|
printf 'Authored Rocks changed across restart: %s -> %s\n' \
|
||||||
|
"$rocks_before" "$rocks_after" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
stop_server
|
stop_server
|
||||||
printf '26.3 dedicated-server smoke passed: two distinct Ages persisted across restart.\n'
|
printf '26.3 dedicated-server smoke passed: Standard, Flat, Void, Cave, random and authored Rocks Ages persisted across restart.\n'
|
||||||
Reference in new issue
Block a user