Public Access
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4a59fb5d02 |
No files matched your search
@@ -0,0 +1,64 @@
|
||||
name: Build and smoke
|
||||
|
||||
on: [push, pull_request]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
changes:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
matrix: ${{ steps.targets.outputs.matrix }}
|
||||
any: ${{ steps.targets.outputs.any }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Check release workflow safeguards
|
||||
run: python3 scripts/test_release_workflow.py -v
|
||||
- id: targets
|
||||
env:
|
||||
BASE: ${{ github.event.pull_request.base.sha || github.event.before }}
|
||||
run: |
|
||||
base="$BASE"
|
||||
if [ -z "$base" ] || ! git cat-file -e "$base^{commit}" 2>/dev/null; then
|
||||
base=$(git rev-parse HEAD^)
|
||||
fi
|
||||
matrix=$(python3 scripts/changed_targets.py --base "$base")
|
||||
echo "matrix=$matrix" >> "$GITHUB_OUTPUT"
|
||||
if [ "$matrix" = '{"include":[]}' ]; then
|
||||
echo "any=false" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "any=true" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
build:
|
||||
needs: changes
|
||||
if: needs.changes.outputs.any == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 20
|
||||
strategy:
|
||||
fail-fast: false
|
||||
max-parallel: 1
|
||||
matrix: ${{ fromJSON(needs.changes.outputs.matrix) }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
fetch-tags: true
|
||||
- uses: actions/setup-java@v4
|
||||
with:
|
||||
java-version: ${{ matrix.java }}
|
||||
distribution: temurin
|
||||
- uses: gradle/actions/setup-gradle@v4
|
||||
- name: Build Minecraft target
|
||||
run: ./gradlew ${{ matrix.gradle_args }} --console=plain
|
||||
- name: Disposable dedicated-server smoke
|
||||
run: python3 scripts/smoke_server.py --target ${{ matrix.target }}
|
||||
- name: Keep smoke logs
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: smoke-${{ matrix.target }}
|
||||
path: build/verification/${{ matrix.target }}/*.log
|
||||
if-no-files-found: ignore
|
||||
+54
-15
@@ -1,25 +1,64 @@
|
||||
name: Build
|
||||
name: Build and smoke
|
||||
|
||||
on: [push, pull_request]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
build:
|
||||
changes:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
matrix: ${{ steps.targets.outputs.matrix }}
|
||||
any: ${{ steps.targets.outputs.any }}
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Check release workflow safeguards
|
||||
run: python3 scripts/test_release_workflow.py -v
|
||||
- id: targets
|
||||
env:
|
||||
BASE: ${{ github.event.pull_request.base.sha || github.event.before }}
|
||||
run: |
|
||||
base="$BASE"
|
||||
if [ -z "$base" ] || ! git cat-file -e "$base^{commit}" 2>/dev/null; then
|
||||
base=$(git rev-parse HEAD^)
|
||||
fi
|
||||
matrix=$(python3 scripts/changed_targets.py --base "$base")
|
||||
echo "matrix=$matrix" >> "$GITHUB_OUTPUT"
|
||||
if [ "$matrix" = '{"include":[]}' ]; then
|
||||
echo "any=false" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "any=true" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
build:
|
||||
needs: changes
|
||||
if: needs.changes.outputs.any == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 20
|
||||
strategy:
|
||||
fail-fast: false
|
||||
max-parallel: 1
|
||||
matrix: ${{ fromJSON(needs.changes.outputs.matrix) }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
fetch-tags: true
|
||||
|
||||
- name: Setup JDK 21
|
||||
uses: actions/setup-java@v4
|
||||
- uses: actions/setup-java@v4
|
||||
with:
|
||||
java-version: '21'
|
||||
distribution: 'temurin'
|
||||
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@v4
|
||||
|
||||
- name: Build with Gradle
|
||||
run: ./gradlew build
|
||||
java-version: ${{ matrix.java }}
|
||||
distribution: temurin
|
||||
- uses: gradle/actions/setup-gradle@v4
|
||||
- name: Build Minecraft target
|
||||
run: ./gradlew ${{ matrix.gradle_args }} --console=plain
|
||||
- name: Disposable dedicated-server smoke
|
||||
run: python3 scripts/smoke_server.py --target ${{ matrix.target }}
|
||||
- name: Keep smoke logs
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: smoke-${{ matrix.target }}
|
||||
path: build/verification/${{ matrix.target }}/*.log
|
||||
if-no-files-found: ignore
|
||||
@@ -25,6 +25,13 @@ See the [26.3 guide](ports/mc263/README.md) and
|
||||
[release notes](ports/mc263/RELEASE_NOTES_26.3.0.1.md).
|
||||
Keep the builds and their saves separate, and back up worlds before updating.
|
||||
|
||||
Minecraft **26.3** is also supported through a separate **Java 25 / NeoForge
|
||||
26.3.0.99** release with a first-party Age backend. See the
|
||||
[26.3 guide](ports/mc263/README.md) and
|
||||
[parity review](docs/MC263_PARITY_REVIEW.md). Keep the game-version JARs and saves
|
||||
separate; 1.21.1 remains supported with its existing dependencies. The guide below
|
||||
describes 1.21.1 unless stated otherwise.
|
||||
|
||||
## Getting started
|
||||
|
||||
1. Make an Ink Vial from two black dyes, a glass bottle, and a water bucket.
|
||||
|
||||
@@ -43,11 +43,62 @@ JARs; use new worlds and remove them regardless of pass/fail. Stop test processe
|
||||
If generation stalls, hand the test to the user.
|
||||
|
||||
Before closing a port umbrella issue, compare its acceptance criteria and comments
|
||||
to actual evidence. Current expanded-catalog two-client create/travel/reconnect/
|
||||
restart/stale-ack regression remains in #100. #103 retains dual-line release
|
||||
automation/regression completion and #104 retains the full parity gate.
|
||||
to actual evidence. The expanded-catalog matrix and manual restart/return handoff completed in #100.
|
||||
See [the final parity review](MC263_PARITY_REVIEW.md) for evidence and limits.
|
||||
Shared issues need both versions resolved before closure.
|
||||
|
||||
For each release, document tested optional versions and no-optional fallbacks,
|
||||
match client/server JARs, preserve the independent 1.21.1 support line, update wiki
|
||||
and platform descriptions, and keep known bugs linked in the release notes.
|
||||
|
||||
## Changed-target validation policy
|
||||
|
||||
Only test a Minecraft target when its code/resources changed. Select targets with
|
||||
`python3 scripts/changed_targets.py --base <commit>`. Shared assets can select
|
||||
both; temporary servers must run sequentially. Earlier gameplay sign-off remains
|
||||
valid for unchanged 1.21.1 code.
|
||||
|
||||
Repeatable local checks:
|
||||
|
||||
- Java 21 / 1.21.1: `./gradlew build --console=plain`.
|
||||
- Java 25 / 26.3: `./gradlew -p ports/mc263 build --console=plain`.
|
||||
- Smoke one changed target: `python3 scripts/smoke_server.py --target mc1211`
|
||||
or `--target mc263`.
|
||||
- Two-client 26.3 acceptance: `python3 scripts/regression_multiplayer_263.py`.
|
||||
Fixture sources remain outside the shipping JAR. If a test stalls, stop it,
|
||||
preserve logs, delete temporary worlds and hand the remaining check to the user.
|
||||
Do not automatically retry.
|
||||
|
||||
GitHub and Gitea workflow definitions select changed targets and use
|
||||
`max-parallel: 1`. Hosted execution is not yet verified. Repository runner
|
||||
inventory alone does not establish availability of instance-wide runners.
|
||||
The publisher checks embedded version/Minecraft metadata, Java selection,
|
||||
filename identity, absence of test fixtures and required 1.21.1 dependencies.
|
||||
|
||||
Apply relevant fixes to both supported lines, with separate implementation and
|
||||
validation per target. Keep version-specific fixes on the affected line. Advancing
|
||||
the newer Minecraft/NeoForge target requires a compatibility audit and must not
|
||||
silently replace 1.21.1 support or widen an existing JAR dependency range.
|
||||
|
||||
## Current acceptance evidence (2026-11-14)
|
||||
|
||||
26.3.0.3 builds and the user passed book-window mouse-look, right-click return
|
||||
and re-entry. Beta branding is removed. The release collects these changes,
|
||||
the workflow tools and the final parity review.
|
||||
|
||||
The expanded-catalog dedicated matrix ran with two real clients. Both received
|
||||
runtime-created Ages and authored environment data, traveled using normal books,
|
||||
and passed stale-ack rejection/recovery, occupied-unload refusal, empty-Age
|
||||
reload/reseed and retirement checks. After full server restart both reconnected in
|
||||
the surviving Age with its saved seed and presentation, without the retired Age.
|
||||
The run stalled at the final post-restart return Linking Book step (phase 22).
|
||||
It was stopped, temporary worlds deleted, and the last check handed off rather
|
||||
than retried. Singleplayer preview sign-off does not replace that dedicated check.
|
||||
|
||||
The player completed the manual dedicated-server restart/return check on
|
||||
2026-11-14 and explicitly accepted #100. This was one player-operated client,
|
||||
complementing the automated two-client evidence; it is not described as a new
|
||||
automated two-client pass. The test server was stopped, logs retained under
|
||||
build/verification/issue100-manual, and its temporary world removed.
|
||||
See [the final parity review](MC263_PARITY_REVIEW.md). Renderer bug #116 remains
|
||||
separately tracked. #103/#104 and parent #96 close after release verification.
|
||||
@@ -0,0 +1,79 @@
|
||||
# 26.3 port parity and acceptance review
|
||||
|
||||
Reviewed 2026-11-14 for 26.3.0.3 against the independently maintained 1.21.1
|
||||
implementation. This closes the port feature checklist, not every open bug or
|
||||
future enhancement. The dated development entries in MINECRAFT_26_3_PORT.md
|
||||
retain their original evidence and are superseded by later slices where stated.
|
||||
|
||||
## Feature comparison
|
||||
|
||||
| Area | 1.21.1 implementation | 26.3 counterpart / acceptance |
|
||||
| --- | --- | --- |
|
||||
| Page language | Ordered writing, registry-backed pages, modifiers, recovery and corrupted pages | PortAuthoredWriting, PortWritingRecovery, PortMaterialCompletion, PortRegistryPages and the shared symbol resources retain source writing, material pairing and diagnostics. Port slices received player sign-off. |
|
||||
| Ink Mixer | Paper/ink, additive modifiers, vial/bottle exchange and output reset | InkMixerMenu/Screen, InkModifierIngredients and InkVialExchange port the interaction and assets. One ink and paper per panel; additives reset on removal. The separate 1.21.1 correction remains a shared follow-up. |
|
||||
| Page storage | Notebook, folders, portfolios, sealed notebooks | NotebookMenu/Screen and the corresponding item components port sparse storage, selection/moving, held-slot protection and bulk import. Legacy glyph/page assets reused. |
|
||||
| Desk / Binder | Tab catalog, ink/paper copy transactions, collection input, named books and OP diagnostics | WritingDeskMenu/Screen, WritingDeskCatalog and PortBookBinderMenu/Screen with authoritative networking and existing textures/models. Input, output pickup and naming fixes passed in game. |
|
||||
| Link Modifier | Name, properties, seed and retirement | LinkModifierMenu/Screen and explicit edit payloads provide six property controls, title/seed edits and guarded retirement. Player-approved before this audit. |
|
||||
| Terrain and biomes | Standard/amplified/flat/cave/void/sky-island profiles and biome distributions | PortAgeTerrain, PortBiomePlan, native registry biome pools and first-party generators. Authored and random plans persist their own seeds. Version-specific terrain/biome follow-ups remain separate. |
|
||||
| Decorations and structures | Material features, vegetation, lakes, carvers, vanilla structures, libraries, crystals and fissures | Port terrain/vegetation/lake/carver/structure plans and generation passes implement these families, with modern structures and registry-backed materials. Family-specific terrain restrictions are retained; this audit does not claim every possible combination can generate. |
|
||||
| Time, sky and celestials | Authored/random bodies, number/color modifiers, time modes, eclipses and ambient lighting | PortDayCycle, PortAgeColors, PortCelestialDiscs and sky/light mixins; colors, multiple bodies, sizes, positions, frozen time and eclipse rendering received player checks. Static Sky with Reborn has known bug #116. |
|
||||
| Weather and seasons | Per-Age weather and optional Serene policies | PortWeatherMode, PortSeasonPolicies and Serene adapters preserve independent Age policy. Serene is optional; Ecliptic integration remains deferred on both lines. |
|
||||
| Instability | Writing and live material scores, provider plans, effects and operator controls | PortInstabilityData/Plan/Runtime, block profiling, status/decay/hazard/meteor/environment providers and operator commands. Scores remain OP-only. Accelerated ticks use bounded loaded-world budgets; machine parity work remains tracked separately. |
|
||||
| Travel | Handheld books, stands, receptacles, portals, footing/platform, modifiers and return links | Shared port destination/travel services, PortalTravelQueue and CrystalPortalNetwork keep book properties authoritative. Return links default disabled. Unsafe or unsynchronized travel leaves the player at source. |
|
||||
| Presentation | Legacy books/pages/models/sounds, dropped open books and preview renderers | Existing assets reused through 26.3 extracted render states and modern input. Live Reborn book/portal views and look-only exploration are implemented; right-click return/re-entry passed on 26.3.0.3. |
|
||||
| Discovery / guides | Libraries, loot, crafting, advancements, creative pages and JEI | Libraries/stands, sealed notebook discovery, progression and JEI guides ported. Random eligibility controls apply to new random choices/loot while existing authored pages remain usable. |
|
||||
| Backend lifecycle | Dynamic Dimensions AR plus catalog/travel behavior | Rivencraft-owned backend with atomic creation, seed persistence, catalog acknowledgement, unload/reseed/retire. #99/#101 and now #100 accepted. |
|
||||
|
||||
## Deliberate platform differences
|
||||
|
||||
- Java 21 / NeoForge 21.1.264 and Dynamic Dimensions AR + Bad Packets stay on
|
||||
1.21.1. The tested 26.3 target uses Java 25 / NeoForge 26.3.0.99 and a first-party
|
||||
Age backend. No optional integration is bundled or made mandatory.
|
||||
- 26.3 starts fresh saves. Importing 1.21.1 dimension/save internals is unsupported.
|
||||
New chunk algorithms and random streams need not produce identical terrain.
|
||||
- Create/Ponder has no ported 26.3 integration. Built-in animation is always
|
||||
available; Reborn 7.0.12 provides optional live book/crystal views and look-only
|
||||
exploration. Seamless has the dimension bridge, not this Reborn rendering path.
|
||||
- Optional libraries, registry contents and vanilla generation APIs differ across
|
||||
game versions. Changes to shared symbols/assets must be considered for both;
|
||||
version-specific implementations and dependency requirements stay separate.
|
||||
- Shared requested enhancements in #105–#115 and renderer bug #116 are separate
|
||||
work. Port closure does not close those issues or describe their work as done.
|
||||
|
||||
## Regression evidence and limits
|
||||
|
||||
Earlier recorded 1.21.1 client/dedicated/optional-mod sign-offs remain valid:
|
||||
its game code did not change during this work, so it was not retested merely
|
||||
because the 26.3 target changed. Both build targets were built during the workflow
|
||||
audit; only changed-target gameplay checks are required going forward.
|
||||
|
||||
The expanded-catalog 26.3 test used two actual clients on one dedicated server.
|
||||
Runtime Age creation, catalog/environment delivery, normal-book travel,
|
||||
stale-ack rejection/recovery, occupied unload refusal, empty reload/reseed and
|
||||
retirement passed. On full restart both reconnected in the surviving Age with
|
||||
the saved seed/presentation; retired data stayed absent. The final return-book
|
||||
step stalled and was handed off without retry, per the user's instruction.
|
||||
|
||||
The player then created and entered an Age on the current local dedicated server
|
||||
with the Dev optional stack, requested a full restart, reconnected, and returned
|
||||
to the Overworld by Linking Book. Server logs confirm restored Age
|
||||
rivencraft:age/random, seed -1985136355166262436, and the Overworld return.
|
||||
The player explicitly accepted #100. This final handoff used one player-operated
|
||||
client, not a new two-client automated pass. Logs are retained locally under
|
||||
build/verification; temporary worlds and server processes were removed.
|
||||
|
||||
Production 26.3.0.3 builds and its book-window look/exit/re-entry received an
|
||||
in-game pass. Earlier slices record no-optional startup, generation, portal
|
||||
geometry/lifecycle and optional integration checks. This is cumulative evidence,
|
||||
not a claim that every historical test was rerun on the final JAR.
|
||||
|
||||
## Release gate
|
||||
|
||||
[DUAL_VERSION_RELEASE.md](DUAL_VERSION_RELEASE.md) defines changed-target builds,
|
||||
sequential temporary servers, stalled-test handoff, maintenance policy and upload
|
||||
identity checks. Offline tooling checks cover track selection, release naming,
|
||||
required dependencies, wrong-artifact/fixture rejection and server exclusion.
|
||||
Hosted workflow execution remains unverified; repeatable local commands provide
|
||||
the supported fallback. Release acceptance is recorded in Gitea after publication
|
||||
and artifact verification. The existing published 1.21.1 release is retained;
|
||||
unchanged code is not republished merely to adopt the new filename scheme.
|
||||
@@ -8,6 +8,18 @@ The user wants both Minecraft lines maintained after the port, not a one-time
|
||||
26.3 release. Gitea milestone "Dual-version support: 1.21.1 and 26.3"
|
||||
tracks issues #97–#103 under parent #96.
|
||||
|
||||
## Final port acceptance — 2026-11-14
|
||||
|
||||
The 26.3 port is playable on the regular release channel. The complete writing,
|
||||
generation, instability, travel, discovery and presentation review is in
|
||||
[MC263_PARITY_REVIEW.md](MC263_PARITY_REVIEW.md). #100 is complete: the
|
||||
expanded-catalog two-client checks and the player-operated full-restart return
|
||||
handoff passed. The look-only book-window overlay and its right-click exit fix
|
||||
also passed on 26.3.0.3. #103/#104 closure follows the workflow/release audit.
|
||||
The original introductory audit and slice entries below are historical, not a
|
||||
list of currently missing features. 1.21.1 remains maintained independently.
|
||||
Known issues and shared enhancements remain tracked separately.
|
||||
|
||||
## Published status — 2026-11-11
|
||||
|
||||
[26.3.0.1](../ports/mc263/RELEASE_NOTES_26.3.0.1.md) collects the player-validated
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Minecraft 26.3 beta port workspace
|
||||
# Minecraft 26.3
|
||||
|
||||
The current **Minecraft 26.3 release is 26.3.0.1**, for NeoForge 26.3.0.99 and
|
||||
The current **Minecraft 26.3 release is 26.3.0.3**, for NeoForge 26.3.0.99 and
|
||||
Java 25. It uses Rivencraft's first-party Age backend, with no Dynamic Dimensions
|
||||
or Bad Packets dependency. The writing loop, authored/seeded Ages, generation
|
||||
features, weather/skies/celestials, libraries, crystal portals, live instability,
|
||||
@@ -16,11 +16,13 @@ with `book_previews.reborn_live_views` in `rivencraft-local.toml`.
|
||||
Reborn's Static Sky rendering issue remains tracked in
|
||||
[#116](https://git.paddedcell.org/dnorberg/rivencraft/issues/116).
|
||||
|
||||
This regular release is player-validated but does not claim complete 1.21.1 parity.
|
||||
Issues #100, #103 and #104 retain the final multiplayer/parity/release-workflow
|
||||
checks. Preview exploration controls and Create/Ponder scenes remain unported.
|
||||
Use matching client/server builds, keep saves separate from 1.21.1 and back up
|
||||
before updating. See [release notes](RELEASE_NOTES_26.3.0.1.md).
|
||||
The port feature and acceptance review is recorded in
|
||||
[the parity review](../../docs/MC263_PARITY_REVIEW.md). Book exploration matches
|
||||
1.21.1: use the eye for mouse look inside the book panel and right-click to return;
|
||||
movement is disabled. The final dedicated-server restart/return check passed.
|
||||
Create/Ponder scenes are not ported to this target. Use matching client/server
|
||||
builds, keep saves separate from 1.21.1 and back up before updating.
|
||||
See [release notes](RELEASE_NOTES_26.3.0.3.md).
|
||||
|
||||
The entries below record how the port was built, including earlier alpha-only
|
||||
limitations that were addressed by later slices.
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
# Rivencraft 26.3.0.3
|
||||
|
||||
For **Minecraft 26.3**, **NeoForge 26.3.0.99** and **Java 25**.
|
||||
|
||||
## Book preview exploration
|
||||
|
||||
- The eye button now matches 1.21.1: it grabs the mouse for look-only exploration
|
||||
while the destination view remains inside the book panel. Movement is disabled.
|
||||
- Right-click returns to the ordinary book controls without releasing the preview.
|
||||
Books on stands retain their disabled exploration button.
|
||||
- Fixed the exit crash caused by a forced render and duplicate Reborn render-target
|
||||
submission. Pending target submissions are reused until the renderer drains them.
|
||||
- Removed remaining Beta branding from the in-game mod name/description.
|
||||
|
||||
## Port acceptance and maintenance
|
||||
|
||||
- Completed dedicated-server restart/return-link acceptance for #100, following
|
||||
the two-client matrix and its manual handoff. The player confirmed return travel.
|
||||
- Added changed-target build/smoke workflows, sequential test-server exclusion and
|
||||
repeatable dual-version validation commands.
|
||||
- Hardened release uploads against mismatched filename, embedded version,
|
||||
Minecraft/Java target and accidental test-fixture packaging. The 1.21.1 metadata
|
||||
declares its required Dynamic Dimensions AR and Bad Packets dependencies.
|
||||
- Documented the feature parity review and ongoing independent support of 1.21.1.
|
||||
Existing 1.21.1 code and published artifacts are unchanged.
|
||||
|
||||
## Validation and known issues
|
||||
|
||||
The player passed the live-view exploration/right-click/re-entry check and the
|
||||
dedicated-server Age restart/Linking Book return check. The expanded-catalog
|
||||
two-client matrix passed creation/catalog/travel/stale-ack/lifecycle checks;
|
||||
its final return step stalled and was completed by the player, not retried.
|
||||
Release-tooling checks pass. No optional mod or test fixture is bundled.
|
||||
|
||||
**Known issue #116:** Static Sky can leak outside book previews with Reborn.
|
||||
It remains separately tracked. Other shared follow-ups remain open where needed.
|
||||
Create/Ponder is not ported to 26.3. Without Reborn, built-in animated previews
|
||||
and normal book/portal travel remain available. 1.21.1 saves are not imported.
|
||||
@@ -7,7 +7,7 @@ neo_version=26.3.0.99
|
||||
mod_id=rivencraft
|
||||
mod_name=Rivencraft
|
||||
mod_license=LGPL-3.0-or-later
|
||||
mod_version=26.3.0.1
|
||||
mod_version=26.3.0.3
|
||||
|
||||
jei_version=31.17.0.109
|
||||
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
package net.dnorberg.rivencraft.client;
|
||||
|
||||
import net.dnorberg.rivencraft.Rivencraft;
|
||||
import net.minecraft.client.KeyMapping;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.gui.components.Button;
|
||||
import net.minecraft.client.gui.components.Tooltip;
|
||||
import net.minecraft.client.gui.screens.Screen;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
import net.neoforged.api.distmarker.Dist;
|
||||
import net.neoforged.bus.api.SubscribeEvent;
|
||||
import net.neoforged.fml.common.EventBusSubscriber;
|
||||
import net.neoforged.neoforge.client.event.ClientTickEvent;
|
||||
import net.neoforged.neoforge.client.event.InputEvent;
|
||||
import net.neoforged.neoforge.client.event.RenderGuiEvent;
|
||||
import com.mojang.blaze3d.platform.InputConstants;
|
||||
|
||||
/** Matches 1.21.1: grabbed mouse look with the book drawn over the source world. */
|
||||
@EventBusSubscriber(modid = Rivencraft.MOD_ID, value = Dist.CLIENT)
|
||||
public final class PortBookExploration {
|
||||
private static PortBookExploration current;
|
||||
private final PortLegacyLinkPanel panel;
|
||||
private Screen owner;
|
||||
private Button eye;
|
||||
private Vec3 sourcePosition;
|
||||
private net.minecraft.client.multiplayer.ClientLevel sourceLevel;
|
||||
private int guiWidth, guiHeight;
|
||||
private boolean active;
|
||||
|
||||
PortBookExploration(PortLegacyLinkPanel panel) { this.panel = panel; }
|
||||
|
||||
void attach(Screen screen, java.util.function.Consumer<Button> add, int left, int top, boolean stand) {
|
||||
owner = screen;
|
||||
eye = Button.builder(Component.literal("👁"), button -> {
|
||||
var minecraft = Minecraft.getInstance();
|
||||
if (stand || !panel.canExplore() || minecraft.player == null) return;
|
||||
sourcePosition = minecraft.player.position();
|
||||
sourceLevel = minecraft.level;
|
||||
guiWidth = minecraft.getWindow().getGuiScaledWidth();
|
||||
guiHeight = minecraft.getWindow().getGuiScaledHeight();
|
||||
active = true;
|
||||
current = this;
|
||||
eye.visible = false;
|
||||
panel.exploring(true);
|
||||
KeyMapping.releaseAll();
|
||||
// Removing the screen grabs the mouse normally. removed() preserves this session.
|
||||
minecraft.gui.setScreen(null);
|
||||
}).bounds(left + 229, top + 145, 20, 20).build();
|
||||
eye.active = !stand;
|
||||
eye.setTooltip(Tooltip.create(Component.literal("Look around destination (right-click to return)")));
|
||||
add.accept(eye);
|
||||
}
|
||||
|
||||
void tick(boolean frontPage) {
|
||||
if (eye != null) eye.visible = !active && frontPage && panel.canExplore();
|
||||
}
|
||||
|
||||
boolean active() { return active; }
|
||||
|
||||
void stop() {
|
||||
active = false;
|
||||
if (current == this) current = null;
|
||||
sourcePosition = null;
|
||||
sourceLevel = null;
|
||||
panel.exploring(false);
|
||||
}
|
||||
|
||||
@SubscribeEvent
|
||||
public static void beforeTick(ClientTickEvent.Pre event) {
|
||||
if (current != null) KeyMapping.releaseAll();
|
||||
}
|
||||
|
||||
@SubscribeEvent
|
||||
public static void afterTick(ClientTickEvent.Post event) {
|
||||
var overlay = current;
|
||||
if (overlay == null) return;
|
||||
var minecraft = Minecraft.getInstance();
|
||||
if (minecraft.player == null || minecraft.level != overlay.sourceLevel || minecraft.gui.screen() != null) {
|
||||
overlay.stop();
|
||||
overlay.panel.close();
|
||||
return;
|
||||
}
|
||||
minecraft.player.setPos(overlay.sourcePosition);
|
||||
minecraft.player.setDeltaMovement(Vec3.ZERO);
|
||||
overlay.panel.tick();
|
||||
}
|
||||
|
||||
@SubscribeEvent
|
||||
public static void drawBook(RenderGuiEvent.Post event) {
|
||||
var overlay = current;
|
||||
var minecraft = Minecraft.getInstance();
|
||||
if (overlay == null || minecraft.gui.screen() != null) return;
|
||||
int width = minecraft.getWindow().getGuiScaledWidth();
|
||||
int height = minecraft.getWindow().getGuiScaledHeight();
|
||||
if (width != overlay.guiWidth || height != overlay.guiHeight) {
|
||||
overlay.guiWidth = width;
|
||||
overlay.guiHeight = height;
|
||||
overlay.owner.resize(width, height);
|
||||
overlay.eye.visible = false;
|
||||
}
|
||||
overlay.owner.extractRenderState(event.getGuiGraphics(), -1, -1,
|
||||
event.getPartialTick().getGameTimeDeltaPartialTick(false));
|
||||
}
|
||||
|
||||
@SubscribeEvent
|
||||
public static void mouseButton(InputEvent.MouseButton.Pre event) {
|
||||
var overlay = current;
|
||||
if (overlay == null || Minecraft.getInstance().gui.screen() != null) return;
|
||||
// Neither mouse button may use the held book or attack in the source world.
|
||||
event.setCanceled(true);
|
||||
if (event.getButton() == InputConstants.MOUSE_BUTTON_RIGHT && event.getAction() == InputConstants.PRESS) {
|
||||
overlay.stop();
|
||||
KeyMapping.releaseAll();
|
||||
// Unlike 1.21.1 setScreen, setScreenAndShow forces a render immediately.
|
||||
// Resume through the regular GUI lifecycle without nesting a new frame.
|
||||
Minecraft.getInstance().gui.setScreen(overlay.owner);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,8 @@ public final class PortBookPreview {
|
||||
void accept(net.dnorberg.rivencraft.item.LinkBookTarget destination);
|
||||
boolean render(GuiGraphicsExtractor graphics, int x, int y, int width, int height, float time);
|
||||
void close();
|
||||
default void exploring(boolean enabled) {}
|
||||
default void tick() {}
|
||||
}
|
||||
private static PortBookPreview active;
|
||||
private final UUID session = UUID.randomUUID();
|
||||
@@ -46,12 +48,16 @@ public final class PortBookPreview {
|
||||
}
|
||||
void tick() {
|
||||
ticks++;
|
||||
if (live != null) live.tick();
|
||||
if (ticks % (received ? 200 : 40) == 0) sendRequest();
|
||||
}
|
||||
boolean render(GuiGraphicsExtractor graphics, int x, int y, int width, int height, float partialTick) {
|
||||
if (closed || !received || live == null) return false;
|
||||
return live.render(graphics, x, y, width, height, ticks + partialTick);
|
||||
}
|
||||
boolean canExplore() { return live != null && received && !closed; }
|
||||
void exploring(boolean enabled) { if (live != null) live.exploring(enabled); }
|
||||
|
||||
public static void accept(BookPreviewPayload payload) {
|
||||
if (active == null || active.closed || active.live == null || !active.session.equals(payload.session())
|
||||
|| !payload.destination().valid()) return;
|
||||
|
||||
+8
-2
@@ -27,6 +27,7 @@ public final class PortDescriptiveBookScreen extends Screen {
|
||||
private final boolean bound;
|
||||
private final net.minecraft.resources.Identifier ageId;
|
||||
private final PortLegacyLinkPanel linkPanel = new PortLegacyLinkPanel();
|
||||
private final PortBookExploration exploration = new PortBookExploration(linkPanel);
|
||||
private final net.dnorberg.rivencraft.writing.DescriptiveBookPages pages;
|
||||
private int pageIndex;
|
||||
private net.dnorberg.rivencraft.network.UseBookStandPayload standSource;
|
||||
@@ -48,7 +49,11 @@ public final class PortDescriptiveBookScreen extends Screen {
|
||||
|
||||
@Override
|
||||
protected void init() {
|
||||
if (bound) { linkPanel.open(hand, book, standSource); return; }
|
||||
if (bound) {
|
||||
linkPanel.open(hand, book, standSource);
|
||||
exploration.attach(this, this::addRenderableWidget, (width - WIDTH) / 2, (height - HEIGHT) / 2, standSource != null);
|
||||
return;
|
||||
}
|
||||
if (standSource != null) return;
|
||||
int left = (width - WIDTH) / 2;
|
||||
int top = (height - HEIGHT) / 2;
|
||||
@@ -183,9 +188,10 @@ public final class PortDescriptiveBookScreen extends Screen {
|
||||
|
||||
@Override public void tick() {
|
||||
if (bound) linkPanel.tick();
|
||||
exploration.tick(pageIndex == 0);
|
||||
}
|
||||
|
||||
@Override public void removed() { linkPanel.close(); super.removed(); }
|
||||
@Override public void removed() { if (!exploration.active()) { exploration.stop(); linkPanel.close(); } super.removed(); }
|
||||
|
||||
@Override
|
||||
public boolean isPauseScreen() {
|
||||
|
||||
@@ -19,7 +19,9 @@ final class PortLegacyLinkPanel {
|
||||
net.dnorberg.rivencraft.network.UseBookStandPayload stand) {
|
||||
if (preview == null) preview = new PortBookPreview(hand, book, stand);
|
||||
}
|
||||
void close() { if (preview != null) preview.close(); }
|
||||
void close() { if (preview != null) { preview.close(); preview = null; } }
|
||||
boolean canExplore() { return preview != null && preview.canExplore(); }
|
||||
void exploring(boolean value) { if (preview != null) preview.exploring(value); }
|
||||
|
||||
private static final int LINK_COLOR = 0xFF006666;
|
||||
|
||||
|
||||
@@ -30,6 +30,7 @@ public final class PortLinkingBookScreen extends Screen {
|
||||
private final Component destination;
|
||||
private int pageIndex;
|
||||
private final PortLegacyLinkPanel linkPanel = new PortLegacyLinkPanel();
|
||||
private final PortBookExploration exploration = new PortBookExploration(linkPanel);
|
||||
private net.dnorberg.rivencraft.network.UseBookStandPayload standSource;
|
||||
private EditBox titleInput;
|
||||
|
||||
@@ -53,7 +54,11 @@ public final class PortLinkingBookScreen extends Screen {
|
||||
|
||||
@Override
|
||||
protected void init() {
|
||||
if (bound) { linkPanel.open(hand, book, standSource); return; }
|
||||
if (bound) {
|
||||
linkPanel.open(hand, book, standSource);
|
||||
exploration.attach(this, this::addRenderableWidget, (width - WIDTH) / 2, (height - HEIGHT) / 2, standSource != null);
|
||||
return;
|
||||
}
|
||||
if (standSource != null) return;
|
||||
int left = (width - WIDTH) / 2;
|
||||
int top = (height - HEIGHT) / 2;
|
||||
@@ -148,9 +153,10 @@ public final class PortLinkingBookScreen extends Screen {
|
||||
|
||||
@Override public void tick() {
|
||||
if (bound) linkPanel.tick();
|
||||
exploration.tick(pageIndex == 0);
|
||||
}
|
||||
|
||||
@Override public void removed() { linkPanel.close(); super.removed(); }
|
||||
@Override public void removed() { if (!exploration.active()) { exploration.stop(); linkPanel.close(); } super.removed(); }
|
||||
|
||||
@Override
|
||||
public boolean isPauseScreen() {
|
||||
|
||||
@@ -26,7 +26,28 @@ public final class RebornLiveBookPanel implements PortBookPreview.LivePanel {
|
||||
private static RebornLiveBookPanel queued;
|
||||
private LinkBookTarget destination;
|
||||
private boolean closed;
|
||||
private boolean exploring;
|
||||
private Vec3 explorePosition;
|
||||
private float yaw, pitch, playerYaw, playerPitch;
|
||||
|
||||
@Override public void exploring(boolean enabled) {
|
||||
exploring = enabled;
|
||||
if (enabled && destination != null) {
|
||||
explorePosition = new Vec3(destination.x(), destination.y() + 1.62, destination.z());
|
||||
yaw = destination.yaw(); pitch = destination.pitch();
|
||||
var player = Minecraft.getInstance().player;
|
||||
if (player != null) {
|
||||
explorePosition = new Vec3(destination.x(), destination.y() + player.getEyeHeight(), destination.z());
|
||||
playerYaw = player.getYRot(); playerPitch = player.getXRot();
|
||||
}
|
||||
}
|
||||
}
|
||||
@Override public void tick() {
|
||||
var player = Minecraft.getInstance().player;
|
||||
if (!exploring || destination == null || player == null) return;
|
||||
yaw = destination.yaw() + net.minecraft.util.Mth.wrapDegrees(player.getYRot() - playerYaw);
|
||||
pitch = Math.clamp(destination.pitch() + player.getXRot() - playerPitch, -89, 89);
|
||||
}
|
||||
@Override public void accept(LinkBookTarget value) { destination = value; }
|
||||
@Override public boolean render(GuiGraphicsExtractor graphics, int x, int y, int width, int height, float time) {
|
||||
if (closed || destination == null || !ClientWorldLoader.getIsInitialized()) return false;
|
||||
@@ -40,16 +61,25 @@ public final class RebornLiveBookPanel implements PortBookPreview.LivePanel {
|
||||
Vec3 center = new Vec3(destination.x(), destination.y() + 2.5, destination.z());
|
||||
double angle = time * 0.012;
|
||||
Vec3 camera = center.add(Math.sin(angle) * 11, 5, Math.cos(angle) * 11);
|
||||
Matrix4f transform = new Matrix4f().rotateX((float) Math.toRadians(20))
|
||||
.rotateY((float) angle);
|
||||
Matrix4f transform;
|
||||
if (exploring && explorePosition != null) {
|
||||
camera = explorePosition;
|
||||
transform = new Matrix4f().rotateX((float) Math.toRadians(pitch))
|
||||
.rotateY((float) Math.toRadians(yaw + 180));
|
||||
} else transform = new Matrix4f().rotateX((float) Math.toRadians(20)).rotateY((float) angle);
|
||||
var info = new WorldRenderInfo.Builder().setWorld(world).setCameraPos(camera)
|
||||
.setCameraTransformation(transform).setOverwriteCameraTransformation(true)
|
||||
.setRenderDistance(Math.min(8, minecraft.options.getEffectiveRenderDistance()))
|
||||
.setDoRenderHand(false).setEnableViewBobbing(false).setDoRenderSky(true).setHasFog(true).build();
|
||||
// Submit first: Reborn may resize the target, invalidating its old texture view.
|
||||
// GPU clearing happens in beginRender(), after extraction but before remote rendering.
|
||||
GuiPortalRendering.submitNextFrameRendering(info, target);
|
||||
queued = this;
|
||||
// Overlay-to-screen transitions can extract this panel twice before Reborn
|
||||
// drains its queue. Its API accepts only one task per target until then.
|
||||
// Reuse that pending view for subsequent draws instead of submitting twice.
|
||||
if (queued == null) {
|
||||
// Submit first: Reborn may resize the target and invalidate its texture view.
|
||||
// GPU clearing happens in beginRender(), before remote rendering.
|
||||
GuiPortalRendering.submitNextFrameRendering(info, target);
|
||||
queued = this;
|
||||
}
|
||||
graphics.blit(target.getColorTextureView(), RenderSystem.getSamplerCache().getClampToEdge(FilterMode.LINEAR),
|
||||
x, y, x + width, y + height, 0, 1, 1, 0);
|
||||
graphics.outline(x, y, width, height, 0xFF7B6AA8);
|
||||
@@ -75,5 +105,5 @@ public final class RebornLiveBookPanel implements PortBookPreview.LivePanel {
|
||||
camera.update(minecraft.getDeltaTracker());
|
||||
}
|
||||
}
|
||||
@Override public void close() { closed = true; destination = null; }
|
||||
@Override public void close() { exploring(false); closed = true; destination = null; }
|
||||
}
|
||||
@@ -4,11 +4,11 @@ issueTrackerURL="https://git.paddedcell.org/dnorberg/rivencraft/issues"
|
||||
[[mods]]
|
||||
modId="${mod_id}"
|
||||
version="${mod_version}"
|
||||
displayName="${mod_name} 26.3 Beta"
|
||||
displayName="${mod_name}"
|
||||
displayURL="https://git.paddedcell.org/dnorberg/rivencraft"
|
||||
authors="dnorberg and contributors"
|
||||
description='''
|
||||
Experimental Minecraft 26.3 beta with books, writing stations and selected
|
||||
Minecraft 26.3 release with books, writing stations and selected
|
||||
authored Age generation. Full 1.21.1 gameplay parity is still in progress.
|
||||
Use a test world and keep backups; 1.21.1 worlds are not compatible.
|
||||
'''
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Select affected game targets; docs and release tooling do not trigger game tests."""
|
||||
import argparse
|
||||
import json
|
||||
import subprocess
|
||||
|
||||
|
||||
def affected(paths):
|
||||
targets = set()
|
||||
for path in paths:
|
||||
if path.startswith("src/main/java/") or path in {"build.gradle", "settings.gradle", "gradle.properties"}:
|
||||
targets.add("mc1211")
|
||||
if path.startswith("ports/mc263/") and not path.endswith(".md"):
|
||||
targets.add("mc263")
|
||||
if path.startswith(("src/main/resources/", "src/generated/", "buildSrc/", "gradle/")) or path in {"gradlew", "gradlew.bat"}:
|
||||
targets.update(("mc1211", "mc263"))
|
||||
return sorted(targets)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--base", required=True)
|
||||
args = parser.parse_args()
|
||||
paths = subprocess.check_output(["git", "diff", "--name-only", args.base, "HEAD"], text=True).splitlines()
|
||||
values = {"mc1211": ("21", "build"), "mc263": ("25", "-p ports/mc263 build")}
|
||||
matrix = {"include": [{"target": target, "java": values[target][0], "gradle_args": values[target][1]}
|
||||
for target in affected(paths)]}
|
||||
print(json.dumps(matrix, separators=(",", ":")))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
package net.dnorberg.rivencraft.regression;
|
||||
|
||||
import net.dnorberg.rivencraft.Rivencraft;
|
||||
import net.dnorberg.rivencraft.age.*;
|
||||
import net.dnorberg.rivencraft.network.*;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.neoforged.api.distmarker.Dist;
|
||||
import net.neoforged.bus.api.SubscribeEvent;
|
||||
import net.neoforged.fml.common.EventBusSubscriber;
|
||||
import net.neoforged.neoforge.client.network.ClientPacketDistributor;
|
||||
|
||||
@EventBusSubscriber(modid = "rivencraft", value = Dist.CLIENT)
|
||||
public final class MatrixClient {
|
||||
private static MatrixPayloads.Step pending;
|
||||
private static int delay;
|
||||
@SubscribeEvent public static void register(net.neoforged.neoforge.client.network.event.RegisterClientPayloadHandlersEvent event) {
|
||||
event.register(MatrixPayloads.Step.TYPE, (payload, context) -> { pending = payload; delay = 0; });
|
||||
}
|
||||
@SubscribeEvent public static void tick(net.neoforged.neoforge.client.event.ClientTickEvent.Post event) {
|
||||
var minecraft = Minecraft.getInstance();
|
||||
// A player object/catalog can arrive before the receiving-level screen
|
||||
// finishes. Real book input is only possible once normal play resumes.
|
||||
if (pending == null || minecraft.player == null || minecraft.gui.screen() != null || ++delay < 8) return;
|
||||
var step = pending; pending = null;
|
||||
String result = "ok";
|
||||
try {
|
||||
switch (step.action()) {
|
||||
case "bind" -> ClientPacketDistributor.sendToServer(new BindDescriptiveBookPayload(false, "Matrix Live A"));
|
||||
case "enter" -> ClientPacketDistributor.sendToServer(new UseDescriptiveBookPayload(false));
|
||||
case "return" -> ClientPacketDistributor.sendToServer(new UseLinkingBookPayload(false));
|
||||
case "stale" -> {
|
||||
ClientPacketDistributor.sendToServer(new AgeCatalogAckPayload(step.version()));
|
||||
ClientPacketDistributor.sendToServer(new UseDescriptiveBookPayload(false));
|
||||
}
|
||||
case "verify" -> {
|
||||
var listener = minecraft.getConnection();
|
||||
if (listener == null || !listener.levels.contains(ResourceKey.create(Registries.DIMENSION, step.age())))
|
||||
throw new AssertionError("missing live dimension");
|
||||
String actual = PortAgeDescription.forClient(step.age()) + "|" + PortDayCycle.forClient(step.age());
|
||||
if (!actual.equals(step.expected())) throw new AssertionError("environment differs: " + actual);
|
||||
result = "verified";
|
||||
}
|
||||
case "verify_removed" -> {
|
||||
var listener = minecraft.getConnection();
|
||||
if (listener == null || listener.levels.contains(ResourceKey.create(Registries.DIMENSION, step.age())))
|
||||
throw new AssertionError("retired dimension retained");
|
||||
if (PortAgeDescription.forClient(step.age()) != null) throw new AssertionError("stale description retained");
|
||||
result = "removed";
|
||||
}
|
||||
case "exit" -> { minecraft.stop(); return; }
|
||||
default -> throw new AssertionError("unknown action");
|
||||
}
|
||||
} catch (Throwable error) { result = "FAIL " + error; }
|
||||
ClientPacketDistributor.sendToServer(new MatrixPayloads.Report(result));
|
||||
}
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
package net.dnorberg.rivencraft.regression;
|
||||
|
||||
import net.minecraft.network.RegistryFriendlyByteBuf;
|
||||
import net.minecraft.network.codec.ByteBufCodecs;
|
||||
import net.minecraft.network.codec.StreamCodec;
|
||||
import net.minecraft.network.protocol.common.custom.CustomPacketPayload;
|
||||
import net.minecraft.resources.Identifier;
|
||||
|
||||
/** Test-only channels; these sources are never part of either shipping source set. */
|
||||
public final class MatrixPayloads {
|
||||
public record Step(String action, Identifier age, String expected, int version) implements CustomPacketPayload {
|
||||
public static final Type<Step> TYPE = new Type<>(Identifier.fromNamespaceAndPath("rivencraft", "regression_step"));
|
||||
public static final StreamCodec<RegistryFriendlyByteBuf, Step> CODEC = StreamCodec.composite(
|
||||
ByteBufCodecs.STRING_UTF8, Step::action, Identifier.STREAM_CODEC, Step::age,
|
||||
ByteBufCodecs.STRING_UTF8, Step::expected, ByteBufCodecs.VAR_INT, Step::version, Step::new);
|
||||
@Override public Type<Step> type() { return TYPE; }
|
||||
}
|
||||
public record Report(String result) implements CustomPacketPayload {
|
||||
public static final Type<Report> TYPE = new Type<>(Identifier.fromNamespaceAndPath("rivencraft", "regression_report"));
|
||||
public static final StreamCodec<RegistryFriendlyByteBuf, Report> CODEC =
|
||||
StreamCodec.composite(ByteBufCodecs.STRING_UTF8, Report::result, Report::new);
|
||||
@Override public Type<Report> type() { return TYPE; }
|
||||
}
|
||||
private MatrixPayloads() {}
|
||||
}
|
||||
+208
@@ -0,0 +1,208 @@
|
||||
package net.dnorberg.rivencraft.regression;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.*;
|
||||
import net.dnorberg.rivencraft.Rivencraft;
|
||||
import net.dnorberg.rivencraft.PortConfig;
|
||||
import net.dnorberg.rivencraft.age.*;
|
||||
import net.dnorberg.rivencraft.item.LinkBookTarget;
|
||||
import net.dnorberg.rivencraft.network.PortBookNetworking;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.world.InteractionHand;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.level.Level;
|
||||
import net.minecraft.world.level.storage.LevelResource;
|
||||
import net.neoforged.bus.api.SubscribeEvent;
|
||||
import net.neoforged.fml.common.EventBusSubscriber;
|
||||
import net.neoforged.neoforge.network.PacketDistributor;
|
||||
|
||||
@EventBusSubscriber(modid = "rivencraft")
|
||||
public final class MatrixServer {
|
||||
private static final Map<String, String> reports = new HashMap<>();
|
||||
private static int phase, ticks, checks;
|
||||
private static boolean initialized, restart, finished;
|
||||
private static Identifier a, b;
|
||||
private static long seed;
|
||||
private static final long RESEED = 102030405060L;
|
||||
|
||||
@SubscribeEvent public static void payloads(net.neoforged.neoforge.network.event.RegisterPayloadHandlersEvent event) {
|
||||
var registrar = event.registrar("matrix-1");
|
||||
registrar.playToClient(MatrixPayloads.Step.TYPE, MatrixPayloads.Step.CODEC);
|
||||
registrar.playToServer(MatrixPayloads.Report.TYPE, MatrixPayloads.Report.CODEC, (payload, context) -> {
|
||||
if (context.player() instanceof ServerPlayer player) {
|
||||
if (payload.result().startsWith("FAIL")) {
|
||||
finished = true; System.out.println("MC263_MATRIX FAIL " + player.getName().getString() + ": " + payload.result()); return;
|
||||
}
|
||||
reports.put(player.getName().getString(), payload.result());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static void check(boolean ok, String message) {
|
||||
if (!ok) throw new AssertionError(message);
|
||||
checks++;
|
||||
}
|
||||
private static Identifier id(String path) { return Identifier.fromNamespaceAndPath("rivencraft", path); }
|
||||
private static Path marker(MinecraftServer server) { return server.getWorldPath(LevelResource.ROOT).resolve("matrix.properties"); }
|
||||
private static void phase(int next, String label) {
|
||||
phase = next; ticks = 0; reports.clear();
|
||||
System.out.println("MC263_MATRIX PHASE " + next + " " + label);
|
||||
}
|
||||
private static List<ServerPlayer> players(MinecraftServer server) {
|
||||
return server.getPlayerList().getPlayers().stream().sorted(Comparator.comparing(p -> p.getName().getString())).toList();
|
||||
}
|
||||
private static void send(ServerPlayer player, String action, Identifier age, String expected, int version) {
|
||||
PacketDistributor.sendToPlayer(player, new MatrixPayloads.Step(action, age, expected, version));
|
||||
}
|
||||
private static void both(MinecraftServer server, String action, Identifier age, String expected) {
|
||||
for (var player : players(server)) send(player, action, age, expected, 0);
|
||||
}
|
||||
private static void give(ServerPlayer player, ItemStack book) {
|
||||
player.getInventory().setSelectedSlot(0);
|
||||
player.getInventory().setItem(0, book.copy());
|
||||
player.containerMenu.broadcastChanges();
|
||||
}
|
||||
private static ItemStack book(Identifier age) {
|
||||
var stack = new ItemStack(Rivencraft.DESCRIPTIVE_BOOK.get());
|
||||
stack.set(Rivencraft.AGE_BOOK_ID.get(), age);
|
||||
return stack;
|
||||
}
|
||||
private static boolean ready(MinecraftServer server, Identifier age) {
|
||||
return players(server).size() == 2 && players(server).stream().allMatch(p -> AgeCatalogSync.ready(p, age));
|
||||
}
|
||||
private static boolean arrived(MinecraftServer server, Identifier age) {
|
||||
return players(server).size() == 2 && players(server).stream().allMatch(p -> p.level().dimension().identifier().equals(age));
|
||||
}
|
||||
private static boolean reported(String expected) {
|
||||
return reports.size() == 2 && reports.values().stream().allMatch(expected::equals);
|
||||
}
|
||||
private static String presentation(MinecraftServer server, Identifier age) {
|
||||
return RuntimeAgeBackend.loadedDescriptions(server).get(age) + "|" + RuntimeAgeBackend.savedDayCycles(server).get(age);
|
||||
}
|
||||
private static void enter(MinecraftServer server, Identifier age) {
|
||||
for (var player : players(server)) { give(player, book(age)); send(player, "enter", age, "", 0); }
|
||||
}
|
||||
private static void leave(MinecraftServer server) {
|
||||
var source = server.overworld();
|
||||
for (var player : players(server)) {
|
||||
int x = player.getName().getString().endsWith("One") ? -4 : 4;
|
||||
int y = source.getHeight(net.minecraft.world.level.levelgen.Heightmap.Types.MOTION_BLOCKING, x, 0);
|
||||
var stack = new ItemStack(Rivencraft.LINKING_BOOK.get());
|
||||
stack.set(Rivencraft.LINK_BOOK_TARGET.get(), new LinkBookTarget(Level.OVERWORLD.identifier(), x + .5, y, .5, 0, 0));
|
||||
give(player, stack); send(player, "return", Level.OVERWORLD.identifier(), "", 0);
|
||||
}
|
||||
}
|
||||
|
||||
@SubscribeEvent public static void tick(net.neoforged.neoforge.event.tick.ServerTickEvent.Post event) {
|
||||
if (finished) return;
|
||||
var server = event.getServer();
|
||||
try {
|
||||
if (!initialized) {
|
||||
initialized = true;
|
||||
restart = Files.exists(marker(server));
|
||||
if (restart) {
|
||||
var p = new Properties();
|
||||
try (var input = Files.newInputStream(marker(server))) { p.load(input); }
|
||||
a = Identifier.parse(p.getProperty("a")); b = Identifier.parse(p.getProperty("b"));
|
||||
seed = Long.parseLong(p.getProperty("seed"));
|
||||
}
|
||||
phase(0, restart ? "await restarted clients" : "await two connected clients");
|
||||
}
|
||||
ticks++;
|
||||
var ps = players(server);
|
||||
if (phase == 0) {
|
||||
if (ps.size() != 2 || !ready(server, restart ? a : Level.OVERWORLD.identifier())) return;
|
||||
for (var player : ps) player.setGameMode(net.minecraft.world.level.GameType.CREATIVE);
|
||||
if (restart) {
|
||||
check(arrived(server, a), "both saved players restored in Age A");
|
||||
check(RuntimeAgeBackend.savedAges(server).get(a) == seed, "reseed survives full restart");
|
||||
check(!RuntimeAgeBackend.savedAges(server).containsKey(b), "retired Age not restored");
|
||||
both(server, "verify", a, presentation(server, a)); phase(20, "restarted catalog and presentation");
|
||||
} else {
|
||||
PortConfig.RANDOM_PAGE_ALLOWLIST.set(List.of("terrain/flat", "biome/single", "biome/minecraft/plains",
|
||||
"sky/static", "weather/storm", "lighting/bright"));
|
||||
give(ps.getFirst(), new ItemStack(Rivencraft.DESCRIPTIVE_BOOK.get()));
|
||||
send(ps.getFirst(), "bind", Level.OVERWORLD.identifier(), "", 0);
|
||||
phase(1, "normal handheld binding with both clients online");
|
||||
}
|
||||
} else if (phase == 1) {
|
||||
var age = ps.getFirst().getMainHandItem().get(Rivencraft.AGE_BOOK_ID.get());
|
||||
if (age == null || !ready(server, age)) return;
|
||||
a = age;
|
||||
check(ps.stream().allMatch(p -> p.level().dimension() == Level.OVERWORLD), "creation does not teleport either player");
|
||||
both(server, "verify", a, presentation(server, a)); phase(2, "new catalog reached both clients without reconnect");
|
||||
} else if (phase == 2 && reported("verified")) {
|
||||
b = PortBookNetworking.createAge(ps.getLast(), "Matrix Authored B", List.of(
|
||||
id("terrain/flat"), id("biome/single"), id("biome/minecraft/plains"),
|
||||
id("sky/end"), id("lighting/dark"), id("weather/off")));
|
||||
check(b != null && !a.equals(b), "distinct authored Age from normal Binder backend");
|
||||
phase(3, "expanded authored catalog");
|
||||
} else if (phase == 3 && ready(server, b)) {
|
||||
both(server, "verify", b, presentation(server, b)); phase(4, "authored environment arrived at both clients");
|
||||
} else if (phase == 4 && reported("verified")) {
|
||||
enter(server, a); phase(5, "normal books enter live-created Age A");
|
||||
} else if (phase == 5 && arrived(server, a)) {
|
||||
check(true, "both clients entered live-created A");
|
||||
leave(server); phase(6, "normal Linking Books return");
|
||||
} else if (phase == 6 && arrived(server, Level.OVERWORLD.identifier())) {
|
||||
// Deliberately withhold readiness; the stale acknowledgement itself is a real client payload.
|
||||
var field = AgeCatalogSync.class.getDeclaredField("STATES"); field.setAccessible(true);
|
||||
var state = ((Map<?, ?>) field.get(null)).get(server);
|
||||
var ack = state.getClass().getDeclaredField("acknowledged"); ack.setAccessible(true);
|
||||
((Map<?, ?>) ack.get(state)).remove(ps.getLast().getUUID());
|
||||
var version = state.getClass().getDeclaredField("version"); version.setAccessible(true);
|
||||
give(ps.getLast(), book(a));
|
||||
send(ps.getLast(), "stale", a, "", version.getInt(state) - 1);
|
||||
phase(7, "stale acknowledgement and failed travel");
|
||||
} else if (phase == 7 && ticks >= 30 && reports.containsKey(ps.getLast().getName().getString())) {
|
||||
check(ps.getLast().level().dimension() == Level.OVERWORLD, "unacknowledged travel stays at source");
|
||||
check(!AgeCatalogSync.ready(ps.getLast(), a), "stale payload did not acknowledge current catalog");
|
||||
AgeCatalogSync.onCatalogChanged(server); phase(8, "fresh catalog recovery");
|
||||
} else if (phase == 8 && ready(server, b)) {
|
||||
enter(server, b); phase(9, "normal books enter authored Age B");
|
||||
} else if (phase == 9 && arrived(server, b)) {
|
||||
boolean refused = false;
|
||||
try { RuntimeAgeBackend.unload(server, b); } catch (RuntimeException expected) { refused = true; }
|
||||
check(refused, "occupied Age unload refused");
|
||||
leave(server); phase(10, "vacate before lifecycle operations");
|
||||
} else if (phase == 10 && arrived(server, Level.OVERWORLD.identifier())) {
|
||||
RuntimeAgeBackend.unload(server, a);
|
||||
check(server.getLevel(ResourceKey.create(Registries.DIMENSION, a)) == null, "unload removed live level");
|
||||
RuntimeAgeBackend.reload(server, a);
|
||||
RuntimeAgeBackend.reseed(server, a, RESEED);
|
||||
check(RuntimeAgeBackend.savedAges(server).get(a) == RESEED, "new seed persisted");
|
||||
RuntimeAgeBackend.retire(server, b);
|
||||
check(!RuntimeAgeBackend.savedAges(server).containsKey(b), "retired B removed from catalog");
|
||||
phase(11, "unload reload reseed retire catalog propagation");
|
||||
} else if (phase == 11 && ready(server, a)) {
|
||||
both(server, "verify_removed", b, ""); phase(12, "both clients removed retired dimension");
|
||||
} else if (phase == 12 && reported("removed")) {
|
||||
enter(server, b); phase(13, "stale retired book cannot recreate B");
|
||||
} else if (phase == 13 && ticks >= 30 && reported("ok")) {
|
||||
check(arrived(server, Level.OVERWORLD.identifier()), "retired books leave players at source");
|
||||
enter(server, a); phase(14, "save both players inside surviving Age");
|
||||
} else if (phase == 14 && arrived(server, a)) {
|
||||
var p = new Properties(); p.setProperty("a", a.toString()); p.setProperty("b", b.toString());
|
||||
p.setProperty("seed", Long.toString(RESEED));
|
||||
try (var output = Files.newOutputStream(marker(server))) { p.store(output, "Disposable multiplayer restart evidence"); }
|
||||
both(server, "exit", a, ""); finished = true;
|
||||
System.out.println("MC263_MATRIX FIRST_PASS checks=" + checks);
|
||||
} else if (phase == 20 && reported("verified")) {
|
||||
both(server, "verify_removed", b, ""); phase(21, "retirement persisted through restart");
|
||||
} else if (phase == 21 && reported("removed")) {
|
||||
leave(server); phase(22, "return linking works after reconnect and restart");
|
||||
} else if (phase == 22 && arrived(server, Level.OVERWORLD.identifier())) {
|
||||
check(true, "both players returned after full restart");
|
||||
both(server, "exit", a, ""); finished = true;
|
||||
System.out.println("MC263_MATRIX RESTART_PASS checks=" + checks);
|
||||
}
|
||||
} catch (Throwable error) {
|
||||
finished = true; System.out.println("MC263_MATRIX FAIL phase=" + phase); error.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,8 @@ from pathlib import Path
|
||||
import secrets
|
||||
import subprocess
|
||||
import sys
|
||||
import tomllib
|
||||
import zipfile
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
|
||||
@@ -97,7 +99,8 @@ def request_json(url: str, *, token: str | None = None, token_header: str = "Aut
|
||||
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)
|
||||
body = response.read()
|
||||
return json.loads(body) if body else None
|
||||
except urllib.error.HTTPError as error:
|
||||
if error.code == 404 and not_found_ok:
|
||||
return None
|
||||
@@ -150,6 +153,15 @@ def release_metadata(args: argparse.Namespace) -> tuple[dict, dict]:
|
||||
"releaseType": args.channel,
|
||||
"isMarkedForManualRelease": False,
|
||||
}
|
||||
if args.minecraft == "1.21.1":
|
||||
modrinth["dependencies"] = [
|
||||
{"project_id": "ftdbN0KK", "dependency_type": "required"},
|
||||
{"project_id": "qbUzidyB", "dependency_type": "required"},
|
||||
]
|
||||
curseforge["relations"] = {"projects": [
|
||||
{"slug": "badpackets", "type": "requiredDependency"},
|
||||
{"slug": "dynamicdimension-ar-edition", "projectID": "1469680", "type": "requiredDependency"},
|
||||
]}
|
||||
return modrinth, curseforge
|
||||
|
||||
|
||||
@@ -180,6 +192,27 @@ def publish_curseforge(metadata: dict, jar: Path) -> None:
|
||||
print(f"CurseForge: https://www.curseforge.com/minecraft/mc-mods/rivencraft/files/{result['id']} (pending approval)")
|
||||
|
||||
|
||||
def verify_artifact(jar: Path, version: str, minecraft: str, java: str, channel: str) -> None:
|
||||
"""Refuse renamed/wrong-track artifacts before obtaining any upload credential."""
|
||||
expected_java = {"1.21.1": "21", "26.3": "25"}
|
||||
if minecraft not in expected_java or java != expected_java[minecraft]:
|
||||
raise RuntimeError("Unsupported Minecraft/Java target pairing")
|
||||
_, filename = release_identity(version, minecraft, channel)
|
||||
if jar.name != filename:
|
||||
raise RuntimeError("JAR filename does not match Minecraft and release versions")
|
||||
with zipfile.ZipFile(jar) as archive:
|
||||
metadata = tomllib.loads(archive.read("META-INF/neoforge.mods.toml").decode())
|
||||
mod = next((entry for entry in metadata.get("mods", []) if entry.get("modId") == "rivencraft"), None)
|
||||
if mod is None or mod.get("version") != version:
|
||||
raise RuntimeError("Embedded mod version does not match the requested release")
|
||||
dependencies = metadata.get("dependencies", {}).get("rivencraft", [])
|
||||
game = next((entry for entry in dependencies if entry.get("modId") == "minecraft"), None)
|
||||
if game is None or game.get("versionRange") != f"[{minecraft}]":
|
||||
raise RuntimeError("Embedded Minecraft range does not match the requested target")
|
||||
if any(name.startswith("net/dnorberg/rivencraft/regression/") for name in archive.namelist()):
|
||||
raise RuntimeError("Refusing an artifact containing disposable multiplayer test fixtures")
|
||||
|
||||
|
||||
def release_identity(version: str, minecraft: str, channel: str) -> tuple[str, str]:
|
||||
"""New releases prefix the version with Minecraft; retain old beta upload support."""
|
||||
if version.startswith(minecraft + "."):
|
||||
@@ -271,11 +304,7 @@ def main() -> None:
|
||||
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")
|
||||
_, expected_filename = release_identity(args.version, args.minecraft, args.channel)
|
||||
if args.jar.name != expected_filename:
|
||||
raise RuntimeError("JAR filename does not match Minecraft and release versions")
|
||||
verify_artifact(args.jar, args.version, args.minecraft, args.java, args.channel)
|
||||
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)")
|
||||
|
||||
@@ -0,0 +1,166 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Two real 26.3 clients, one sequential server, normal book travel and full restart."""
|
||||
from server_test_lock import server_test_lock
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import signal
|
||||
import socket
|
||||
import subprocess
|
||||
import tempfile
|
||||
import time
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
LOGS = ROOT / "build/verification/multiplayer263"
|
||||
|
||||
|
||||
class Handoff(RuntimeError):
|
||||
pass
|
||||
|
||||
|
||||
def wait_for(processes, log, marker, timeout=60):
|
||||
last_progress = time.monotonic()
|
||||
last_stage = ""
|
||||
while True:
|
||||
text = log.read_text(errors="replace") if log.exists() else ""
|
||||
if marker in text:
|
||||
return
|
||||
if "MC263_MATRIX FAIL" in text:
|
||||
raise RuntimeError(f"Multiplayer assertion failed; inspect {log}")
|
||||
stages = [line for line in text.splitlines() if "MC263_MATRIX PHASE" in line]
|
||||
if stages and stages[-1] != last_stage:
|
||||
last_stage = stages[-1]
|
||||
last_progress = time.monotonic()
|
||||
print(last_stage, flush=True)
|
||||
if any(process.poll() is not None for process in processes):
|
||||
raise RuntimeError(f"Process exited before {marker}; inspect {LOGS}")
|
||||
if time.monotonic() - last_progress >= timeout:
|
||||
raise Handoff(f"Stalled at {last_stage or marker}; stop and hand off, do not retry")
|
||||
time.sleep(.25)
|
||||
|
||||
|
||||
def stop(process, server=False):
|
||||
if process is None or process.poll() is not None:
|
||||
return
|
||||
if server:
|
||||
try:
|
||||
process.stdin.write("stop\n")
|
||||
process.stdin.flush()
|
||||
process.wait(timeout=25)
|
||||
return
|
||||
except (BrokenPipeError, subprocess.TimeoutExpired):
|
||||
pass
|
||||
os.killpg(process.pid, signal.SIGTERM)
|
||||
try:
|
||||
process.wait(timeout=10)
|
||||
except subprocess.TimeoutExpired:
|
||||
os.killpg(process.pid, signal.SIGKILL)
|
||||
process.wait()
|
||||
|
||||
|
||||
def run():
|
||||
if not os.environ.get("DISPLAY") and not os.environ.get("WAYLAND_DISPLAY"):
|
||||
raise SystemExit("This real-client matrix needs a graphics session. Use the manual checklist.")
|
||||
LOGS.mkdir(parents=True, exist_ok=True)
|
||||
work = Path(tempfile.mkdtemp(prefix="rivencraft-multiplayer263-"))
|
||||
server = work / "server"
|
||||
server.mkdir()
|
||||
with socket.socket() as reservation:
|
||||
reservation.bind(("127.0.0.1", 0))
|
||||
port = reservation.getsockname()[1]
|
||||
settings = {"layers": [{"height": 1, "block": "minecraft:bedrock"},
|
||||
{"height": 2, "block": "minecraft:dirt"},
|
||||
{"height": 1, "block": "minecraft:grass_block"}],
|
||||
"biome": "minecraft:plains"}
|
||||
(server / "eula.txt").write_text("eula=true\n")
|
||||
(server / "server.properties").write_text(
|
||||
f"server-ip=127.0.0.1\nserver-port={port}\nonline-mode=false\nwhite-list=false\n"
|
||||
"view-distance=2\nsimulation-distance=2\ngenerate-structures=false\n"
|
||||
"level-type=minecraft:flat\nlevel-seed=263100\ngenerator-settings=" + json.dumps(settings) + "\n")
|
||||
for name in ("One", "Two"):
|
||||
client = work / f"client-{name}"
|
||||
(client / "config").mkdir(parents=True)
|
||||
(client / "config/neoforge-client.toml").write_text("showLoadWarnings=false\n")
|
||||
(client / "options.txt").write_text("renderDistance:2\nsimulationDistance:5\nmaxFps:25\npauseOnLostFocus:false\n")
|
||||
init = work / "fixture.gradle"
|
||||
init.write_text("""
|
||||
gradle.beforeProject { p ->
|
||||
p.layout.buildDirectory.set(p.file(System.getenv('RIVENCRAFT_MATRIX_WORK') + '/build'))
|
||||
p.afterEvaluate {
|
||||
p.sourceSets.main.java.srcDir(p.file('""" + str(ROOT / "scripts/fixtures/mc263_multiplayer") + """'))
|
||||
p.neoForge.runs.client.gameDirectory = p.file(System.getenv('RIVENCRAFT_MATRIX_WORK') + '/client-' + p.findProperty('matrixClient'))
|
||||
p.neoForge.runs.client.programArgument('--width')
|
||||
p.neoForge.runs.client.programArgument('640')
|
||||
p.neoForge.runs.client.programArgument('--height')
|
||||
p.neoForge.runs.client.programArgument('480')
|
||||
}
|
||||
}
|
||||
""")
|
||||
env = dict(os.environ, RIVENCRAFT_MATRIX_WORK=str(work))
|
||||
base = [str(ROOT / "gradlew"), "-p", str(ROOT / "ports/mc263"),
|
||||
"-I", str(init), "--no-daemon", "--console=plain"]
|
||||
server_process = None
|
||||
clients = []
|
||||
handles = []
|
||||
try:
|
||||
# Build fixture classes once in an isolated output tree. Shipping outputs
|
||||
# are never overwritten or packaged with these extra test channels.
|
||||
with (LOGS / "compile.log").open("w") as output:
|
||||
subprocess.run(base + ["classes"], cwd=ROOT, env=env, stdout=output,
|
||||
stderr=subprocess.STDOUT, check=True, timeout=300)
|
||||
for phase in ("first", "restart"):
|
||||
log = LOGS / f"server-{phase}.log"
|
||||
output = log.open("w")
|
||||
handles.append(output)
|
||||
server_process = subprocess.Popen(base + ["runServer", f"-PserverRunDir={server}"],
|
||||
cwd=ROOT, env=env, stdin=subprocess.PIPE, stdout=output,
|
||||
stderr=subprocess.STDOUT, text=True, start_new_session=True)
|
||||
wait_for([server_process], log, "Done (")
|
||||
clients = []
|
||||
for name in ("One", "Two"):
|
||||
output = (LOGS / f"client-{name}-{phase}.log").open("w")
|
||||
handles.append(output)
|
||||
clients.append(subprocess.Popen(base + ["runClient", f"-PmatrixClient={name}",
|
||||
f"-PclientName=Matrix{name}", f"-PconnectAddress=127.0.0.1:{port}"],
|
||||
cwd=ROOT, env=env, stdout=output, stderr=subprocess.STDOUT,
|
||||
text=True, start_new_session=True))
|
||||
wait_for([server_process], log, "MC263_MATRIX " + ("FIRST_PASS" if phase == "first" else "RESTART_PASS"))
|
||||
for process in clients:
|
||||
process.wait(timeout=30)
|
||||
if process.returncode:
|
||||
raise RuntimeError("Client exited unsuccessfully; inspect client log")
|
||||
clients.clear()
|
||||
stop(server_process, server=True)
|
||||
if server_process.returncode:
|
||||
raise RuntimeError("Server exited unsuccessfully")
|
||||
server_process = None
|
||||
print(f"PASS two-client {phase}", flush=True)
|
||||
except Handoff as error:
|
||||
directions = (
|
||||
f"{error}\n\nManual test: two clients on one 26.3 server. Create a new Age while both "
|
||||
"are online; both visit via Descriptive Books and return via Linking Books. "
|
||||
"Create a second Age with different sky/lighting/weather. Restart and reconnect both, "
|
||||
"confirm names, seeds and return links. Refuse occupied unload, then vacate and "
|
||||
"unload/reload/reseed/retire an empty Age. A retired book must leave the player at source.\n"
|
||||
"Temporary worlds were removed; do not rerun this stalled test automatically.\n")
|
||||
(LOGS / "HANDOFF.md").write_text(directions)
|
||||
print("HANDOFF: " + directions, flush=True)
|
||||
raise SystemExit(2)
|
||||
finally:
|
||||
for process in clients:
|
||||
stop(process)
|
||||
stop(server_process, server=True)
|
||||
for handle in handles:
|
||||
handle.close()
|
||||
shutil.rmtree(work)
|
||||
print(f"Removed all temporary client/server worlds and fixture outputs; logs: {LOGS}", flush=True)
|
||||
|
||||
|
||||
def main():
|
||||
with server_test_lock():
|
||||
run()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,18 @@
|
||||
"""One managed temporary Minecraft server at a time on this host."""
|
||||
from contextlib import contextmanager
|
||||
import fcntl
|
||||
from pathlib import Path
|
||||
import tempfile
|
||||
|
||||
|
||||
@contextmanager
|
||||
def server_test_lock():
|
||||
with (Path(tempfile.gettempdir()) / "rivencraft-server-tests.lock").open("a") as lock:
|
||||
try:
|
||||
fcntl.flock(lock, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
||||
except BlockingIOError:
|
||||
raise SystemExit("Another Rivencraft server test is active; finish it before starting this one")
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
fcntl.flock(lock, fcntl.LOCK_UN)
|
||||
@@ -0,0 +1,121 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Small dual-target dedicated smoke. Worlds are disposable; stalled checks hand off."""
|
||||
import argparse
|
||||
from server_test_lock import server_test_lock
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import signal
|
||||
import subprocess
|
||||
import tempfile
|
||||
import time
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
class StalledTest(RuntimeError):
|
||||
pass
|
||||
|
||||
|
||||
def wait_for(process, log, marker, timeout=60):
|
||||
started = time.monotonic()
|
||||
while time.monotonic() - started < timeout:
|
||||
text = log.read_text(errors="replace") if log.exists() else ""
|
||||
if marker in text:
|
||||
return text
|
||||
if process.poll() is not None:
|
||||
raise RuntimeError(f"Server exited before {marker!r}; inspect {log}")
|
||||
time.sleep(0.25)
|
||||
raise StalledTest(f"No result for {marker!r} within {timeout}s; stop and hand off, do not retry")
|
||||
|
||||
|
||||
def stop(process):
|
||||
if process is None or process.poll() is not None:
|
||||
return
|
||||
try:
|
||||
process.stdin.write("stop\n")
|
||||
process.stdin.flush()
|
||||
process.wait(timeout=25)
|
||||
except (BrokenPipeError, subprocess.TimeoutExpired):
|
||||
os.killpg(process.pid, signal.SIGTERM)
|
||||
try:
|
||||
process.wait(timeout=10)
|
||||
except subprocess.TimeoutExpired:
|
||||
os.killpg(process.pid, signal.SIGKILL)
|
||||
process.wait()
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--target", choices=("mc1211", "mc263"), required=True)
|
||||
args = parser.parse_args()
|
||||
logs = ROOT / "build/verification" / args.target
|
||||
logs.mkdir(parents=True, exist_ok=True)
|
||||
work = Path(tempfile.mkdtemp(prefix=f"rivencraft-{args.target}-smoke-"))
|
||||
settings = {"layers": [{"height": 1, "block": "minecraft:bedrock"},
|
||||
{"height": 2, "block": "minecraft:dirt"},
|
||||
{"height": 1, "block": "minecraft:grass_block"}],
|
||||
"biome": "minecraft:plains"}
|
||||
(work / "eula.txt").write_text("eula=true\n")
|
||||
(work / "server.properties").write_text(
|
||||
"server-ip=127.0.0.1\nserver-port=0\nonline-mode=false\nwhite-list=false\n"
|
||||
"view-distance=2\nsimulation-distance=2\ngenerate-structures=false\n"
|
||||
"level-type=minecraft:flat\nlevel-seed=263100\ngenerator-settings=" + json.dumps(settings) + "\n")
|
||||
command = [str(ROOT / "gradlew")]
|
||||
if args.target == "mc263":
|
||||
command += ["-p", str(ROOT / "ports/mc263")]
|
||||
command += ["--no-daemon", "runServer", f"-PserverRunDir={work}", "--console=plain"]
|
||||
process = None
|
||||
try:
|
||||
for phase in ("first", "restart"):
|
||||
log = logs / f"{phase}.log"
|
||||
with log.open("w") as output:
|
||||
process = subprocess.Popen(command, cwd=ROOT, stdin=subprocess.PIPE,
|
||||
stdout=output, stderr=subprocess.STDOUT, text=True, start_new_session=True)
|
||||
wait_for(process, log, "Done (")
|
||||
if args.target == "mc263":
|
||||
if phase == "first":
|
||||
for name, seed in (("smoke_a", 123456789), ("smoke_b", 987654321)):
|
||||
process.stdin.write(f"rivencraft_port age create {name} {seed} flat\n")
|
||||
process.stdin.flush()
|
||||
wait_for(process, log, f"Created rivencraft:age/{name} with seed {seed}")
|
||||
else:
|
||||
text = log.read_text()
|
||||
for name, seed in (("smoke_a", 123456789), ("smoke_b", 987654321)):
|
||||
if f"Loaded 26.3 prototype Age rivencraft:age/{name} with seed {seed}" not in text:
|
||||
raise RuntimeError("Age identity/seed failed full restart")
|
||||
process.stdin.write("rivencraft_port age list\n")
|
||||
process.stdin.flush()
|
||||
process.stdin.write("save-all flush\n")
|
||||
process.stdin.flush()
|
||||
wait_for(process, log, "Saved the game")
|
||||
else:
|
||||
# 1.21.1's Gradle launcher does not forward this pipe. Its
|
||||
# unchanged gameplay uses the recorded/manual regression;
|
||||
# this smoke checks startup and graceful process shutdown.
|
||||
os.killpg(process.pid, signal.SIGINT)
|
||||
if args.target == "mc1211":
|
||||
try:
|
||||
process.wait(timeout=25)
|
||||
except subprocess.TimeoutExpired:
|
||||
stop(process)
|
||||
else:
|
||||
stop(process)
|
||||
if process.returncode not in (0, 130, -signal.SIGINT):
|
||||
raise RuntimeError(f"Server exited with {process.returncode}; inspect {log}")
|
||||
process = None
|
||||
print(f"PASS {args.target} {phase}", flush=True)
|
||||
except StalledTest as error:
|
||||
print(f"HANDOFF: {error}. Manual check: start {args.target}, create two Ages, travel, "
|
||||
"restart and revisit both; confirm names/seeds and return travel.", flush=True)
|
||||
raise SystemExit(2)
|
||||
finally:
|
||||
stop(process)
|
||||
shutil.rmtree(work)
|
||||
print(f"Removed temporary world; logs: {logs}", flush=True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
with server_test_lock():
|
||||
main()
|
||||
+2
-323
@@ -1,326 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
# Disposable, headless smoke check for the incomplete 26.3 runtime Age backend.
|
||||
# Compatibility entry point; keep smoke narrow instead of rerunning every feature probe.
|
||||
set -euo pipefail
|
||||
|
||||
repo_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
|
||||
smoke_dir=$(mktemp -d "${TMPDIR:-/tmp}/rivencraft-263-smoke.XXXXXX")
|
||||
server_dir="$smoke_dir/server"
|
||||
server_pid=
|
||||
input_fd=
|
||||
|
||||
cleanup() {
|
||||
if [[ -n "$server_pid" ]] && kill -0 "$server_pid" 2>/dev/null; then
|
||||
if [[ -n "$input_fd" ]]; then printf 'stop\n' >&"$input_fd" || true; fi
|
||||
wait "$server_pid" 2>/dev/null || true
|
||||
fi
|
||||
if [[ -n "$input_fd" ]]; then exec {input_fd}>&-; fi
|
||||
rm -rf -- "$smoke_dir"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
mkdir -p "$server_dir"
|
||||
printf 'eula=true\n' > "$server_dir/eula.txt"
|
||||
printf 'server-port=0\nonline-mode=false\nwhite-list=false\n' > "$server_dir/server.properties"
|
||||
|
||||
wait_for_log() {
|
||||
local log_file=$1 pattern=$2 seconds=$3
|
||||
local tries=$((seconds * 4))
|
||||
for ((i = 0; i < tries; i++)); do
|
||||
if grep -Fq -- "$pattern" "$log_file" 2>/dev/null; then return 0; fi
|
||||
if ! kill -0 "$server_pid" 2>/dev/null; then
|
||||
printf 'Server exited before: %s\n' "$pattern" >&2
|
||||
tail -80 "$log_file" >&2
|
||||
return 1
|
||||
fi
|
||||
sleep 0.25
|
||||
done
|
||||
printf 'Timed out waiting for: %s\n' "$pattern" >&2
|
||||
tail -80 "$log_file" >&2
|
||||
return 1
|
||||
}
|
||||
|
||||
start_server() {
|
||||
local phase=$1
|
||||
log_file="$smoke_dir/$phase.log"
|
||||
fifo="$smoke_dir/$phase.stdin"
|
||||
mkfifo "$fifo"
|
||||
exec {input_fd}<>"$fifo"
|
||||
"$repo_dir/gradlew" -p "$repo_dir/ports/mc263" runServer \
|
||||
-PserverRunDir="$server_dir" --no-daemon < "$fifo" > "$log_file" 2>&1 &
|
||||
server_pid=$!
|
||||
wait_for_log "$log_file" 'Done (' 120
|
||||
}
|
||||
|
||||
stop_server() {
|
||||
printf 'stop\n' >&"$input_fd"
|
||||
wait "$server_pid"
|
||||
server_pid=
|
||||
exec {input_fd}>&-
|
||||
rm -- "$fifo"
|
||||
}
|
||||
|
||||
send_and_wait() {
|
||||
local command=$1 result=$2
|
||||
printf '%s\n' "$command" >&"$input_fd"
|
||||
wait_for_log "$log_file" "$result" 60
|
||||
}
|
||||
|
||||
start_server first
|
||||
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_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'
|
||||
send_and_wait 'rivencraft_port age create_rocks_multi smoke_rocks_multi 445566' 'Created rivencraft:age/smoke_rocks_multi with seed 445566 terrain=FLAT'
|
||||
send_and_wait 'rivencraft_port age create_rocks_random smoke_rocks_random 445566' 'Created rivencraft:age/smoke_rocks_random with seed 445566 terrain=FLAT'
|
||||
send_and_wait 'rivencraft_port age create_vegetation smoke_vegetation 24680' 'Created rivencraft:age/smoke_vegetation with seed 24680 terrain=FLAT'
|
||||
send_and_wait 'rivencraft_port age create_dungeons smoke_dungeons 135791' 'Created rivencraft:age/smoke_dungeons with seed 135791 terrain=STANDARD'
|
||||
send_and_wait 'rivencraft_port age create smoke_lake_control 667788' 'Created rivencraft:age/smoke_lake_control with seed 667788 terrain=STANDARD'
|
||||
send_and_wait 'rivencraft_port age create_lakes smoke_lakes 667788' 'Created rivencraft:age/smoke_lakes with seed 667788 terrain=STANDARD'
|
||||
send_and_wait 'rivencraft_port age create_lakes_random smoke_lakes_random 667788' 'Created rivencraft:age/smoke_lakes_random with seed 667788 terrain=STANDARD'
|
||||
# This fixture includes a rare island candidate near the origin at normal rarity.
|
||||
send_and_wait 'rivencraft_port age create_islands smoke_islands 887767 void' 'Created rivencraft:age/smoke_islands with seed 887767 terrain=VOID'
|
||||
send_and_wait 'rivencraft_port age create_islands smoke_islands_flat 887767 flat' 'Created rivencraft:age/smoke_islands_flat with seed 887767 terrain=FLAT'
|
||||
send_and_wait 'rivencraft_port age create_islands smoke_islands_noise 887767 standard' 'Created rivencraft:age/smoke_islands_noise with seed 887767 terrain=STANDARD'
|
||||
send_and_wait 'rivencraft_port age create_islands_random smoke_islands_random 887767 void' 'Created rivencraft:age/smoke_islands_random with seed 887767 terrain=VOID'
|
||||
# The index must reach disk while the server is still running. A graceful
|
||||
# shutdown would hide the interrupted-save regression this checks.
|
||||
index_file="$server_dir/world/dimensions/minecraft/overworld/data/rivencraft/runtime_ages.dat"
|
||||
[[ -s "$index_file" ]] || {
|
||||
printf 'Age index was not persisted before shutdown: %s\n' "$index_file" >&2
|
||||
exit 1
|
||||
}
|
||||
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_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'
|
||||
gzip -dc "$index_file" | strings | grep -Fq 'rivencraft:age/smoke_rocks_multi'
|
||||
gzip -dc "$index_file" | strings | grep -Fq 'rivencraft:age/smoke_rocks_random'
|
||||
gzip -dc "$index_file" | strings | grep -Fq 'rivencraft:age/smoke_vegetation'
|
||||
gzip -dc "$index_file" | strings | grep -Fq 'rivencraft:age/smoke_dungeons'
|
||||
gzip -dc "$index_file" | strings | grep -Fq 'rivencraft:age/smoke_lakes'
|
||||
gzip -dc "$index_file" | strings | grep -Fq 'rivencraft:age/smoke_lakes_random'
|
||||
for island_age in smoke_islands smoke_islands_flat smoke_islands_noise smoke_islands_random; do
|
||||
gzip -dc "$index_file" | strings | grep -Fq "rivencraft:age/$island_age"
|
||||
done
|
||||
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_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 sample smoke_rocks_multi' 'rivencraft:age/smoke_rocks_multi seed=445566 chunkOffset=0 terrain='
|
||||
send_and_wait 'rivencraft_port age sample smoke_rocks_random' 'rivencraft:age/smoke_rocks_random seed=445566 chunkOffset=0 terrain='
|
||||
send_and_wait 'rivencraft_port age sample smoke_vegetation' 'rivencraft:age/smoke_vegetation seed=24680 chunkOffset=0 terrain='
|
||||
send_and_wait 'rivencraft_port age sample smoke_dungeons' 'rivencraft:age/smoke_dungeons seed=135791 chunkOffset=0 terrain='
|
||||
send_and_wait 'rivencraft_port age dungeons_probe smoke_dungeons' 'Dungeons probe rivencraft:age/smoke_dungeons pages=8 spawners='
|
||||
dungeons_before=$(grep -F 'Dungeons probe rivencraft:age/smoke_dungeons pages=8 spawners=' "$log_file" | tail -1 | sed 's/.*spawners=//')
|
||||
[[ "$dungeons_before" =~ ^[1-9][0-9]*$ ]] || {
|
||||
printf 'Authored Dungeons placed no monster spawners: %s\n' "$dungeons_before" >&2
|
||||
exit 1
|
||||
}
|
||||
send_and_wait 'rivencraft_port age lakes_probe smoke_lake_control' 'Lakes probe rivencraft:age/smoke_lake_control pages=0'
|
||||
lake_control=$(grep -F 'Lakes probe rivencraft:age/smoke_lake_control pages=0' "$log_file" | tail -1 | sed 's/.* water=//; s/ lava=.*//')
|
||||
send_and_wait 'rivencraft_port age lakes_probe smoke_lakes' 'Lakes probe rivencraft:age/smoke_lakes pages=24 fluids=[minecraft:water]'
|
||||
lakes_before=$(grep -F 'Lakes probe rivencraft:age/smoke_lakes pages=24' "$log_file" | tail -1 | sed 's/.*Lakes probe /Lakes probe /')
|
||||
lake_water=$(printf '%s' "$lakes_before" | sed 's/.* water=//; s/ lava=.*//')
|
||||
[[ "$lake_water" -gt "$lake_control" ]] || {
|
||||
printf 'Water lake pages did not add water: %s -> %s\n%s\n' "$lake_control" "$lake_water" "$lakes_before" >&2
|
||||
exit 1
|
||||
}
|
||||
send_and_wait 'rivencraft_port age lakes_probe smoke_lakes_random' 'Lakes probe rivencraft:age/smoke_lakes_random pages=24'
|
||||
lakes_random_before=$(grep -F 'Lakes probe rivencraft:age/smoke_lakes_random pages=24' "$log_file" | tail -1 | sed 's/.*Lakes probe /Lakes probe /')
|
||||
[[ "$lakes_random_before" == *minecraft:water* && "$lakes_random_before" == *minecraft:lava* ]] || {
|
||||
printf 'Unmodified lake pages did not resolve both vanilla fluids: %s\n' "$lakes_random_before" >&2
|
||||
exit 1
|
||||
}
|
||||
declare -A islands_before
|
||||
for island_age in smoke_islands smoke_islands_flat smoke_islands_noise smoke_islands_random; do
|
||||
send_and_wait "rivencraft_port age islands_probe $island_age" "Islands probe rivencraft:age/$island_age pages=4"
|
||||
island_result=$(grep -F "Islands probe rivencraft:age/$island_age pages=4" "$log_file" | tail -1 | sed 's/.*Islands probe /Islands probe /')
|
||||
island_blocks=$(printf '%s' "$island_result" | sed 's/.* count=//; s/ fingerprint=.*//')
|
||||
[[ "$island_blocks" =~ ^[1-9][0-9]*$ ]] || {
|
||||
printf 'Floating Islands placed no selected material: %s\n' "$island_result" >&2
|
||||
exit 1
|
||||
}
|
||||
islands_before[$island_age]=$island_result
|
||||
done
|
||||
island_materials=${islands_before[smoke_islands_random]#*materials=[}
|
||||
island_materials=${island_materials%%]*}
|
||||
IFS=',' read -r -a island_material_list <<< "$island_materials"
|
||||
[[ ${#island_material_list[@]} -eq 4 ]] || {
|
||||
printf 'Repeated unmodified islands did not choose independent materials: %s\n' "$island_materials" >&2
|
||||
exit 1
|
||||
}
|
||||
send_and_wait 'execute in rivencraft:age/smoke_islands if block 0 0 0 minecraft:air run say Island Void remains empty below' 'Island Void remains empty below'
|
||||
send_and_wait 'execute in rivencraft:age/smoke_islands_flat if block 0 0 0 minecraft:bedrock run say Island Flat retains its floor' 'Island Flat retains its floor'
|
||||
send_and_wait 'rivencraft_port age biome smoke_islands 128 128' 'rivencraft:age/smoke_islands biome at 128,128=minecraft:forest'
|
||||
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 'execute in rivencraft:age/smoke_rocks_multi run forceload add 0 0' 'Marked chunk [0, 0] in rivencraft:age/smoke_rocks_multi to be force loaded'
|
||||
send_and_wait 'execute in rivencraft:age/smoke_rocks_random run forceload add 0 0' 'Marked chunk [0, 0] in rivencraft:age/smoke_rocks_random to be force loaded'
|
||||
send_and_wait 'execute in rivencraft:age/smoke_vegetation run forceload add 0 0' 'Marked chunk [0, 0] in rivencraft:age/smoke_vegetation to be force loaded'
|
||||
send_and_wait 'rivencraft_port age vegetation_probe smoke_flat' 'Vegetation probe rivencraft:age/smoke_flat flowers='
|
||||
flat_vegetation=$(grep -F 'Vegetation probe rivencraft:age/smoke_flat flowers=' "$log_file" | tail -1 | sed 's/.*flowers=//')
|
||||
send_and_wait 'rivencraft_port age vegetation_probe smoke_vegetation' 'Vegetation probe rivencraft:age/smoke_vegetation flowers='
|
||||
authored_vegetation=$(grep -F 'Vegetation probe rivencraft:age/smoke_vegetation flowers=' "$log_file" | tail -1 | sed 's/.*flowers=//')
|
||||
flat_flowers=${flat_vegetation%% logs=*}
|
||||
flat_logs=${flat_vegetation##* logs=}
|
||||
authored_flowers=${authored_vegetation%% logs=*}
|
||||
authored_logs=${authored_vegetation##* logs=}
|
||||
[[ "$authored_flowers" -gt "$flat_flowers" && "$authored_logs" -gt "$flat_logs" ]] || {
|
||||
printf 'Authored vegetation did not increase flowers and logs: %s -> %s\n' \
|
||||
"$flat_vegetation" "$authored_vegetation" >&2
|
||||
exit 1
|
||||
}
|
||||
send_and_wait 'rivencraft_port age rocks_probe smoke_rocks' 'Rocks probe rivencraft:age/smoke_rocks materials=[minecraft:gold_block] count='
|
||||
rocks_before=$(grep -F 'Rocks probe rivencraft:age/smoke_rocks materials=[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 'rivencraft_port age rocks_probe smoke_rocks_multi' 'Rocks probe rivencraft:age/smoke_rocks_multi materials=[minecraft:gold_block, minecraft:gold_block, minecraft:gold_block, minecraft:gold_block] count='
|
||||
rocks_multi_before=$(grep -F 'Rocks probe rivencraft:age/smoke_rocks_multi materials=' "$log_file" | tail -1 | sed 's/.*count=//')
|
||||
[[ "$rocks_multi_before" =~ ^[1-9][0-9]*$ && "$rocks_multi_before" -gt "$rocks_before" ]] || {
|
||||
printf 'Repeated Rocks pages did not increase placement: %s -> %s\n' "$rocks_before" "$rocks_multi_before" >&2
|
||||
exit 1
|
||||
}
|
||||
send_and_wait 'rivencraft_port age rocks_probe smoke_rocks_random' 'Rocks probe rivencraft:age/smoke_rocks_random materials=['
|
||||
rocks_random_before=$(grep -F 'Rocks probe rivencraft:age/smoke_rocks_random materials=' "$log_file" | tail -1 | sed 's/.*Rocks probe /Rocks probe /')
|
||||
[[ "$rocks_random_before" =~ count=[1-9][0-9]*$ ]] || {
|
||||
printf 'Unmodified Rocks page placed no material: %s\n' "$rocks_random_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=//')
|
||||
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
|
||||
printf 'Different Age seeds produced the same terrain probe: %s\n' "$alpha_before" >&2
|
||||
exit 1
|
||||
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
|
||||
|
||||
start_server restart
|
||||
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_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 sample smoke_rocks_multi' 'rivencraft:age/smoke_rocks_multi seed=445566 chunkOffset=0 terrain='
|
||||
send_and_wait 'rivencraft_port age sample smoke_rocks_random' 'rivencraft:age/smoke_rocks_random seed=445566 chunkOffset=0 terrain='
|
||||
send_and_wait 'rivencraft_port age sample smoke_vegetation' 'rivencraft:age/smoke_vegetation seed=24680 chunkOffset=0 terrain='
|
||||
send_and_wait 'rivencraft_port age sample smoke_dungeons' 'rivencraft:age/smoke_dungeons seed=135791 chunkOffset=0 terrain='
|
||||
send_and_wait 'rivencraft_port age dungeons_probe smoke_dungeons' 'Dungeons probe rivencraft:age/smoke_dungeons pages=8 spawners='
|
||||
dungeons_after=$(grep -F 'Dungeons probe rivencraft:age/smoke_dungeons pages=8 spawners=' "$log_file" | tail -1 | sed 's/.*spawners=//')
|
||||
send_and_wait 'rivencraft_port age lakes_probe smoke_lakes' 'Lakes probe rivencraft:age/smoke_lakes pages=24 fluids=[minecraft:water]'
|
||||
lakes_after=$(grep -F 'Lakes probe rivencraft:age/smoke_lakes pages=24' "$log_file" | tail -1 | sed 's/.*Lakes probe /Lakes probe /')
|
||||
send_and_wait 'rivencraft_port age lakes_probe smoke_lakes_random' 'Lakes probe rivencraft:age/smoke_lakes_random pages=24'
|
||||
lakes_random_after=$(grep -F 'Lakes probe rivencraft:age/smoke_lakes_random pages=24' "$log_file" | tail -1 | sed 's/.*Lakes probe /Lakes probe /')
|
||||
for island_age in smoke_islands smoke_islands_flat smoke_islands_noise smoke_islands_random; do
|
||||
send_and_wait "rivencraft_port age islands_probe $island_age" "Islands probe rivencraft:age/$island_age pages=4"
|
||||
island_result=$(grep -F "Islands probe rivencraft:age/$island_age pages=4" "$log_file" | tail -1 | sed 's/.*Islands probe /Islands probe /')
|
||||
[[ "${islands_before[$island_age]}" == "$island_result" ]] || {
|
||||
printf 'Floating Islands changed across restart: %s\n%s\n' "${islands_before[$island_age]}" "$island_result" >&2
|
||||
exit 1
|
||||
}
|
||||
done
|
||||
send_and_wait 'rivencraft_port age rocks_probe smoke_rocks' 'Rocks probe rivencraft:age/smoke_rocks materials=[minecraft:gold_block] count='
|
||||
rocks_after=$(grep -F 'Rocks probe rivencraft:age/smoke_rocks materials=[minecraft:gold_block] count=' "$log_file" | tail -1 | sed 's/.*count=//')
|
||||
send_and_wait 'rivencraft_port age rocks_probe smoke_rocks_multi' 'Rocks probe rivencraft:age/smoke_rocks_multi materials=['
|
||||
rocks_multi_after=$(grep -F 'Rocks probe rivencraft:age/smoke_rocks_multi materials=' "$log_file" | tail -1 | sed 's/.*count=//')
|
||||
send_and_wait 'rivencraft_port age rocks_probe smoke_rocks_random' 'Rocks probe rivencraft:age/smoke_rocks_random materials=['
|
||||
rocks_random_after=$(grep -F 'Rocks probe rivencraft:age/smoke_rocks_random materials=' "$log_file" | tail -1 | sed 's/.*Rocks probe /Rocks probe /')
|
||||
send_and_wait 'rivencraft_port age vegetation_probe smoke_vegetation' 'Vegetation probe rivencraft:age/smoke_vegetation flowers='
|
||||
authored_vegetation_after=$(grep -F 'Vegetation probe rivencraft:age/smoke_vegetation flowers=' "$log_file" | tail -1 | sed 's/.*flowers=//')
|
||||
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=//')
|
||||
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" ]] || {
|
||||
printf 'Age terrain changed across restart: %s/%s -> %s/%s\n' \
|
||||
"$alpha_before" "$beta_before" "$alpha_after" "$beta_after" >&2
|
||||
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
|
||||
}
|
||||
[[ "$rocks_multi_before" == "$rocks_multi_after" && "$rocks_random_before" == "$rocks_random_after" ]] || {
|
||||
printf 'Repeated or unmodified Rocks changed across restart\n' >&2
|
||||
exit 1
|
||||
}
|
||||
[[ "$authored_vegetation" == "$authored_vegetation_after" ]] || {
|
||||
printf 'Authored vegetation changed across restart: %s -> %s\n' \
|
||||
"$authored_vegetation" "$authored_vegetation_after" >&2
|
||||
exit 1
|
||||
}
|
||||
[[ "$dungeons_before" == "$dungeons_after" ]] || {
|
||||
printf 'Authored Dungeons changed across restart: %s -> %s\n' \
|
||||
"$dungeons_before" "$dungeons_after" >&2
|
||||
exit 1
|
||||
}
|
||||
[[ "$lakes_before" == "$lakes_after" && "$lakes_random_before" == "$lakes_random_after" ]] || {
|
||||
printf 'Explicit or random lake plans/blocks changed across restart\n' >&2
|
||||
exit 1
|
||||
}
|
||||
stop_server
|
||||
printf '26.3 dedicated-server smoke passed: terrain, biomes, Rocks, vegetation, Dungeons, lakes, and Floating Islands persisted across restart.\n'
|
||||
exec python3 "$repo_dir/scripts/smoke_server.py" --target mc263
|
||||
@@ -0,0 +1,74 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Offline checks for track selection, release identity, dependency metadata and server exclusion."""
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
import tempfile
|
||||
import unittest
|
||||
import zipfile
|
||||
|
||||
from changed_targets import affected
|
||||
from publish_mod_sites import release_identity, release_metadata, verify_artifact
|
||||
from server_test_lock import server_test_lock
|
||||
|
||||
|
||||
class WorkflowChecks(unittest.TestCase):
|
||||
def test_changes_stay_on_their_track(self):
|
||||
self.assertEqual(affected(["src/main/java/net/dnorberg/rivencraft/Rivencraft.java"]), ["mc1211"])
|
||||
self.assertEqual(affected(["ports/mc263/src/main/java/Example.java"]), ["mc263"])
|
||||
self.assertEqual(affected(["src/main/resources/assets/rivencraft/sounds.json"]), ["mc1211", "mc263"])
|
||||
self.assertEqual(affected(["README.md", "scripts/publish_mod_sites.py"]), [])
|
||||
|
||||
def test_release_identity_rejects_mislabeled_channels(self):
|
||||
self.assertEqual(release_identity("26.3.0.3", "26.3", "release"),
|
||||
("V26.3.0.3", "rivencraft-26.3.0.3.jar"))
|
||||
self.assertEqual(release_identity("1.21.1.0.1", "1.21.1", "release"),
|
||||
("V1.21.1.0.1", "rivencraft-1.21.1.0.1.jar"))
|
||||
for version, channel in [("26.3.0.3-beta.1", "release"), ("26.3.0.3", "beta")]:
|
||||
with self.assertRaises(RuntimeError):
|
||||
release_identity(version, "26.3", channel)
|
||||
|
||||
def test_required_dependencies_follow_minecraft(self):
|
||||
with tempfile.TemporaryDirectory() as work:
|
||||
notes = Path(work) / "notes.md"
|
||||
notes.write_text("Release notes")
|
||||
args = argparse.Namespace(changelog=notes, name="Rivencraft", version="1.21.1.0.1",
|
||||
minecraft="1.21.1", java="21", channel="release")
|
||||
mr, cf = release_metadata(args)
|
||||
self.assertEqual(len(mr["dependencies"]), 2)
|
||||
self.assertTrue(all(d["dependency_type"] == "required" for d in mr["dependencies"]))
|
||||
self.assertEqual(len(cf["relations"]["projects"]), 2)
|
||||
args.minecraft, args.java, args.version = "26.3", "25", "26.3.0.3"
|
||||
mr, cf = release_metadata(args)
|
||||
self.assertEqual(mr["dependencies"], [])
|
||||
self.assertNotIn("relations", cf)
|
||||
|
||||
def test_wrong_track_and_fixture_jars_are_rejected(self):
|
||||
with tempfile.TemporaryDirectory() as work:
|
||||
jar = Path(work) / "rivencraft-26.3.0.3.jar"
|
||||
metadata = (
|
||||
'[[mods]]\nmodId="rivencraft"\nversion="26.3.0.3"\n'
|
||||
'[[dependencies.rivencraft]]\nmodId="minecraft"\nversionRange="[26.3]"\n'
|
||||
)
|
||||
with zipfile.ZipFile(jar, "w") as archive:
|
||||
archive.writestr("META-INF/neoforge.mods.toml", metadata)
|
||||
verify_artifact(jar, "26.3.0.3", "26.3", "25", "release")
|
||||
with self.assertRaises(RuntimeError):
|
||||
verify_artifact(jar, "26.3.0.3", "26.3", "21", "release")
|
||||
with self.assertRaises(RuntimeError):
|
||||
verify_artifact(jar, "1.21.1.0.1", "1.21.1", "21", "release")
|
||||
with zipfile.ZipFile(jar, "a") as archive:
|
||||
archive.writestr("net/dnorberg/rivencraft/regression/Fixture.class", b"test")
|
||||
with self.assertRaises(RuntimeError):
|
||||
verify_artifact(jar, "26.3.0.3", "26.3", "25", "release")
|
||||
|
||||
def test_server_lock_refuses_overlap_and_releases(self):
|
||||
with server_test_lock():
|
||||
with self.assertRaises(SystemExit):
|
||||
with server_test_lock():
|
||||
self.fail("overlapping server admitted")
|
||||
with server_test_lock():
|
||||
pass
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in new issue
Block a user