Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9f82577927 | ||
|
|
79d3185cba | ||
|
|
ad234a808f | ||
|
|
392ade0b7e |
@@ -15,3 +15,4 @@ screenshots/
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
__pycache__/
|
||||
|
||||
@@ -1,5 +1,23 @@
|
||||
# Changelog
|
||||
|
||||
## 0.1.0-alpha.28 — 2026-09-12
|
||||
|
||||
Spéléologie par bruits natifs, déformations volumiques 3D et failles obliques bornées ;
|
||||
bateaux rapprochés et îlots au-dessus de Sanctuary. Marais réduits. Mines,
|
||||
huttes et donjons vanilla adaptés aux souterrains ; filons natifs selon la
|
||||
profondeur dans la roche, dans l’île et les expansions. Nouveau monde de test.
|
||||
Voir [génération](docs/generation-alpha28.md) et [validation](docs/testing-alpha28.md).
|
||||
|
||||
## 0.1.0-alpha.27 — 2026-09-12
|
||||
|
||||
Retour à la base alpha.24 après rejet visuel de l’alpha.25. Le relief et les
|
||||
souterrains de la 24 sont conservés ; la montgolfière est retirée. Première
|
||||
passe limitée aux marais souterrains et à des épaves vanilla redressées
|
||||
et restaurées pour les navires marchands. Les nouvelles
|
||||
montagnes, cavités et garanties océaniques propres à la 25 ne sont pas reprises.
|
||||
Alpha.26 sautée ; essai sur nouveau monde, sans migration. Voir le
|
||||
[contrat alpha.27](docs/generation-alpha27.md) et les [vérifications](docs/testing-alpha27.md).
|
||||
|
||||
## 0.1.0-alpha.20 — 2026-09-10
|
||||
|
||||
- Passe architecturale sur les six bâtiments urbains, les quatorze anciennes
|
||||
|
||||
@@ -15,9 +15,42 @@ infrastructures qui permettront progressivement d'ouvrir de nouveaux continents.
|
||||
Ce dépôt démarre la nouvelle base Fabric. La [vision complète](docs/vision.md)
|
||||
conserve les intentions ; le [backlog](docs/backlog.md) suit les
|
||||
tickets. Les systèmes d’économie, de progression collective et les dimensions
|
||||
décrits dans la vision ne sont pas encore implémentés. **L’alpha.24 est
|
||||
publiée** : cinq lieux suspendus accompagnent l’île naturelle et ses secrets.
|
||||
Voir [les essais](docs/testing-alpha24.md) et [la distribution](docs/packwiz.md).
|
||||
décrits dans la vision ne sont pas encore implémentés. **L’alpha.27 repart de
|
||||
l’alpha.24**, après le rejet du rendu de l’alpha.25. Cette première passe conserve
|
||||
le relief et les souterrains de la 24, retire la montgolfière et travaille les
|
||||
marais souterrains et la restauration de vrais modèles d’épaves vanilla. Voir le
|
||||
[chantier et ses limites](docs/generation-alpha27.md).
|
||||
|
||||
## Alpha.28 — grottes et lieux proches
|
||||
|
||||
Cette passe prolonge la 27 validée en jeu : bruits de grottes vanilla dans
|
||||
la roche, failles moins traversantes, reliefs déplacés et affaissés par du bruit 3D,
|
||||
bateaux près des berges et deux îlots au-dessus de Sanctuary. Les marais
|
||||
occupent moins de place. Mines, huttes et donjons vanilla bénéficient de
|
||||
contrôles adaptés aux cavités ; les filons suivent la profondeur de la masse
|
||||
rocheuse, y compris dans les expansions.
|
||||
|
||||
Créer un **nouveau monde**. Voir le [périmètre](docs/generation-alpha28.md),
|
||||
les [résultats effectivement vérifiés](docs/testing-alpha28.md) et la
|
||||
[distribution](docs/packwiz.md).
|
||||
|
||||
## Alpha.27 — retour au terrain de la 24
|
||||
|
||||
La base est le commit `e7d1f29`, sans les massifs coniques, chambres creusées
|
||||
et couches de planification de l’alpha.25. Les changements de relief seront
|
||||
repris séparément après un essai visuel. L’alpha.26 est sautée à la demande
|
||||
du créateur.
|
||||
|
||||
Les deux navires et les deux îlots restent présents, avec un seul îlot à
|
||||
cascade. Les marais occupent des volumes de biomes dans les cavités existantes,
|
||||
sans produire une nouvelle géométrie de grotte. Ce retour ne prétend pas
|
||||
remplacer le creusement Sanctuary par l’ensemble du moteur de grottes vanilla.
|
||||
|
||||
Créer un **nouveau monde Sanctuary** ; tailles 512 / 724 / 1 024 et Moyen par
|
||||
défaut conservés. Le codec de terrain reste `sanctuary:island_v24` dans ce
|
||||
prototype alpha.27 ; la reprise des anciennes sauvegardes n’est pas un objectif.
|
||||
La distribution et les vérifications effectuées sont détaillées dans
|
||||
[les essais alpha.27](docs/testing-alpha27.md) et [packwiz](docs/packwiz.md).
|
||||
|
||||
## Alpha.24 — les lieux suspendus
|
||||
|
||||
@@ -552,7 +585,7 @@ sera une mise à jour explicite, avec vérification des API et des sauvegardes.
|
||||
| Fabric API | 0.160.0+26.3 |
|
||||
| Fabric Loom | 1.17.20 |
|
||||
| Gradle Wrapper | 9.5.1, distribution vérifiée par SHA-256 |
|
||||
| Sanctuary / pack | 0.1.0-alpha.23 |
|
||||
| Sanctuary / pack | 0.1.0-alpha.28 |
|
||||
|
||||
Java 25 et Python 3.11 ou plus récent sont nécessaires. Le script pack utilise
|
||||
uniquement la bibliothèque standard et repère aussi une installation Python
|
||||
@@ -581,7 +614,7 @@ décrits dans [Validation](docs/testing.md).
|
||||
|
||||
Résultats :
|
||||
|
||||
- `mods/sanctuary/build/libs/sanctuary-0.1.0-alpha.23.jar` : mod à installer avec
|
||||
- `mods/sanctuary/build/libs/sanctuary-0.1.0-alpha.28.jar` : mod à installer avec
|
||||
Fabric API sur la version Minecraft indiquée.
|
||||
- `build/packwiz/` : pack de développement complet, avec le mod construit et
|
||||
l'index vérifié. Voir [Installation packwiz](packwiz/README.md).
|
||||
|
||||
@@ -954,3 +954,27 @@ herbes et fleurs. [Validation](testing-alpha24.md) réussie sur Mac : 14 scénar
|
||||
Release alpha.24 publiée ; canal packwiz et instance Prism existante
|
||||
synchronisés deux fois. Les 925 fichiers personnels et réglages suivis sont
|
||||
conservés. Aucun essai Windows natif revendiqué.
|
||||
|
||||
### WG-29 — Reprendre la génération par petits incréments — alpha.27
|
||||
|
||||
Branche `codex/reprise-sanctuary-alpha27`, directement depuis `e7d1f29` (alpha.24).
|
||||
La 25 est rejetée pour son rendu artificiel : les validations fonctionnelles ne
|
||||
validaient pas l’expérience visuelle attendue. Alpha.26 sautée.
|
||||
|
||||
Première passe : retrouver le terrain 24, enlever la montgolfière, proposer des
|
||||
marais volumétriques dans les cavités existantes et affiner les coques. Voir le
|
||||
[contrat](generation-alpha27.md). Les étagements, l’érosion, les nouvelles grottes
|
||||
et les petites sorties d’eau seront traités un à un, avec retour en jeu entre
|
||||
les changements ; ils ne sont pas déclarés implémentés dans cette passe.
|
||||
|
||||
|
||||
### WG-30 — Spéléologie, reliefs et satellites proches — alpha.28
|
||||
|
||||
Branche `codex/caves-relief-alpha28`, depuis la 27 validée visuellement par le
|
||||
créateur. Ajouter les bruits de grottes vanilla dans les masses rocheuses,
|
||||
limiter et incliner les failles, développer des hauteurs avec ridge/erosion,
|
||||
rapprocher les bateaux des vraies berges et placer les îlots au-dessus du terrain.
|
||||
Moins de marais ; mines, huttes et donjons vanilla adaptés aux cavités réelles.
|
||||
Les filons natifs doivent aussi alimenter les continents ouverts ensuite.
|
||||
Aucun retour des salles ellipsoïdales ni des cônes de l’alpha.25.
|
||||
Résultats et limites : [génération](generation-alpha28.md), [essais](testing-alpha28.md).
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
# Alpha.27 — reprendre la 24 et regarder le résultat
|
||||
|
||||
Le retour du 12 septembre 2026 rejette l'alpha.25 : cônes creux, enveloppes
|
||||
rocheuses trop minces, cavités génériques et île trop perforée. Les tests
|
||||
fonctionnels passaient mais ne démontraient pas un rendu naturel. La branche
|
||||
`codex/reprise-sanctuary-alpha27` repart directement de `e7d1f29`, la base 24
|
||||
publiée ; elle ne désactive pas simplement quelques options de la 25.
|
||||
|
||||
## Cette première passe
|
||||
|
||||
- Retrouver le relief, les cavités et l'hydrologie de la 24. Aucun ajout de
|
||||
montagne, volume de cavité, lac ou plateforme pour faire tenir un biome.
|
||||
- Retirer la montgolfière, son placement et ses réservations ; conserver les
|
||||
deux navires marchands et les deux îlots minéraux, dont un seul à cascade.
|
||||
- Reprendre directement `minecraft:shipwreck/rightsideup_full`, une épave
|
||||
vanilla complète déjà à l’endroit, puis restaurer les trous, les mâts et
|
||||
l’accès. Conserver ses blocs, dalles, escaliers et sa cabine, au lieu de
|
||||
dessiner une nouvelle coque approximative. Garder les marchands et un
|
||||
coffre de cargaison par navire.
|
||||
- Répartir des secteurs de marais en trois dimensions dans les souterrains
|
||||
existants, remplaçant localement les autres biomes de grottes. Le sol et les
|
||||
végétaux changent, pas la densité rocheuse. Chêne et végétation humide plutôt
|
||||
que bosquets de chêne noir dans ces secteurs.
|
||||
|
||||
Minecraft reste en **26.3-pre-2**. Les tailles restent **512 / 724 / 1 024**, Moyen
|
||||
par défaut, avec le bouton natif Personnaliser. Les options existantes des
|
||||
structures gardent leur rôle. Les marais sont naturels, indépendants de ces
|
||||
options. Le Deep Dark n'est pas ajouté.
|
||||
|
||||
Le prototype alpha.27 conserve le codec `sanctuary:island_v24` et ses ressources
|
||||
de terrain. Le créateur teste des mondes jetables et a levé l'objectif de
|
||||
compatibilité ; on ne convertit ni ne régénère ses sauvegardes. **Nouveau monde
|
||||
nécessaire.** Les archives de la 25 restent sur `codex/terrain-sanctuary-alpha25`,
|
||||
commit de livraison `df51e17` et documentation `1bf3a3b`.
|
||||
|
||||
## Ce qui reste à travailler après l'essai
|
||||
|
||||
Les plateaux étagés, l'érosion, les galeries proches des bruits de grottes
|
||||
vanilla et les sorties d'eau libres ne sont pas réintroduits en bloc. Ils
|
||||
seront examinés séparément, à partir de ce que l'on voit en jeu. Des points
|
||||
d'eau peuvent retomber sur l'île ou dans le vide ; leur intérêt ne dépend pas
|
||||
d'une arrivée dans un bassin calculé.
|
||||
|
||||
Mangroves, grenouilles, slimes et lucioles restent les références de l'écologie
|
||||
des marais. La liste réellement présente et les restrictions natives sont
|
||||
consignées dans [les vérifications](testing-alpha27.md), sans promettre une
|
||||
population observée à partir d'une simple entrée dans un biome.
|
||||
|
||||
Les fonctions propres à la 25 — nouveau village souterrain, cabane du mineur,
|
||||
cadre du sanctuaire et garantie spécifique du monument — sont écartées avec
|
||||
sa base. La 24 conserve ses structures et expansions ; leur présence n'est
|
||||
pas présentée comme une reprise de tous les contrats de la 25.
|
||||
|
||||
## Méthode retenue
|
||||
|
||||
Une passe limitée, un pack jouable, puis un retour sur son apparence. Vérifier
|
||||
le build et les nouveaux points de comportement ne remplace pas ce retour.
|
||||
Pas de matrice exhaustive ni de benchmark supplémentaire avant cet essai.
|
||||
Les prochaines optimisations devront conserver les formes jugées bonnes,
|
||||
au lieu de fabriquer des espaces faciles à certifier.
|
||||
|
||||
Le benchmark archivé de la 25 mesurait 169,816 s contre 60,932 s pour un parcours
|
||||
de 369 chunks (générations 24/25 dans les mêmes sources 25). Il documente un coût
|
||||
sur ce témoin Mac ; il ne sauve pas le rendu refusé et ne mesure pas cette 27.
|
||||
@@ -0,0 +1,61 @@
|
||||
# Alpha.28 — roche à explorer
|
||||
|
||||
La 27 est la base validée par le créateur. Cette passe garde son île, les
|
||||
secrets minéraux et les réseaux, puis ajoute du creusement dans la roche.
|
||||
Les nouvelles cavités ne sont pas des salles de structure aux volumes prescrits.
|
||||
|
||||
## Terrain
|
||||
|
||||
Le champ commun compose les fonctions de bruit de Minecraft 26.3-pre-2 :
|
||||
cheese et cave_layer, entrées/spaghetti 3D, spaghetti 2D et roughness, piliers
|
||||
et noodle. Leur altitude est déplacée pour la masse flottante. Les coupes
|
||||
restent soustractives et s’atténuent vers les limites verticales ; il ne
|
||||
s’agit pas de recopier toute la génération Overworld ou ses aquifères.
|
||||
Les carvers historiques restent désactivés pour ne pas ajouter une seconde
|
||||
passe étrangère aux colonnes de certification.
|
||||
|
||||
Le relief supérieur déforme le volume rocheux dans les trois axes avec les
|
||||
bruits ridge/erosion natifs échantillonnés en 3D. Les déplacements sont signés :
|
||||
soulèvements, affaissements et cisaillements varient aussi avec la profondeur.
|
||||
Le déplacement est borné à ±16 blocs horizontalement et ±40 verticalement,
|
||||
avec une transition progressive de Y180 à Y228. L’enveloppe de l’île reste
|
||||
évaluée aux coordonnées réelles ; seul le champ rocheux interne est déplacé.
|
||||
Ce n’est pas une simulation physique de plaques tectoniques. Aucun cône ni
|
||||
salle géométrique ne sert de forme à ajouter au terrain.
|
||||
L’arrivée et les côtes gardent leur place. Les anciennes failles deviennent
|
||||
obliques, interrompues et limitées en hauteur, avec de la roche au-dessus et
|
||||
en dessous. Les expansions utilisent la même source de terrain ; leur climat
|
||||
et leur déformation propre s’appliquent ensuite.
|
||||
|
||||
Le seuil du champ de marais passe de 0,04 à 0,25 : les anciens secteurs se
|
||||
réduisent au profit des biomes de grottes précédents, dont les champignons.
|
||||
La hauteur du monde, les nuages et les trois diamètres sont conservés.
|
||||
|
||||
## Exploration et ressources
|
||||
|
||||
Les bateaux vanilla restaurés recherchent désormais les vraies berges proches.
|
||||
Les deux îlots sont au-dessus de Sanctuary, avec un espace libre sous leur
|
||||
roche et un seul exutoire d’eau. Leurs corps et leurs chutes d’eau ont des
|
||||
emprises distinctes ; rien ne réserve toute la colonne sous un îlot sec.
|
||||
|
||||
Les mines emploient les pièces vanilla, leurs supports, rails, coffres et
|
||||
spawners. Une partie exposée peut passer si l’ensemble reste ancré. Les huttes
|
||||
cherchent aussi des berges souterraines de marais : sol local, couverture de
|
||||
la cavité et quatre pilotis natifs. Les salles à spawner sont de vraies
|
||||
MonsterRoomFeature, posées sur des candidats réels et soumises aux refus
|
||||
natifs. Leur présence n’est pas garantie sur chaque île.
|
||||
|
||||
Une passe de filons native complète les distributions existantes dans les
|
||||
chunks de l’île et des expansions. Charbon, fer, cuivre, or, lapis, redstone,
|
||||
diamant et émeraude utilisent les formes OreFeature vanilla. La profondeur
|
||||
se mesure dans l’enveloppe rocheuse entière de la colonne, sans recommencer
|
||||
le calcul à chaque plafond de grotte. Les stocks restent finis. Les minerais
|
||||
du Nether restent dans le Nether ; le Deep Dark n’est pas ajouté à l’île.
|
||||
|
||||
## Livraison de test
|
||||
|
||||
Mod et pack `0.1.0-alpha.28`, Minecraft `26.3-pre-2`, codec de prototype
|
||||
`sanctuary:island_v24` conservé. Créer un nouveau monde. Aucune migration,
|
||||
aucune régénération ni intervention dans les sauvegardes personnelles.
|
||||
L’alpha.27 et ses artefacts restent disponibles. Les vérifications réelles
|
||||
et leurs limites sont dans [le compte rendu](testing-alpha28.md).
|
||||
@@ -13,6 +13,67 @@ JAR Sanctuary sont des pièces jointes de releases Gitea. Aucun JAR de mod ou
|
||||
dépendance téléchargée n'entre dans l'historique Git. Le canal Beta est distinct
|
||||
de l'ancien pack 26.2.
|
||||
|
||||
## Alpha.28 publiée — 12 septembre 2026
|
||||
|
||||
La [release alpha.28](https://git.botsu.net/koka/sanctuary-beta/releases/tag/v0.1.0-alpha.28)
|
||||
est publiée depuis `79d3185`. Elle prolonge la 27 avec les cavités natives,
|
||||
la déformation 3D signée du relief, des failles bornées, les mines/donjons et
|
||||
les filons, moins de marais, deux bateaux près des berges et deux îlots au-dessus
|
||||
de Sanctuary. Les huttes restent conditionnelles. Voir
|
||||
[la génération](generation-alpha28.md) et [les résultats](testing-alpha28.md).
|
||||
|
||||
`check build` passe sur Mac : **21/21 GameTests natifs**. `assemblePack`,
|
||||
JAR, MRpack et ZIP d’amorçage sont vérifiés localement puis par téléchargement
|
||||
public. Minecraft 26.3-pre-2 et les dépendances restent inchangés.
|
||||
|
||||
Le [MRpack autonome](https://git.botsu.net/koka/sanctuary-beta/releases/download/v0.1.0-alpha.28/Sanctuary-0.1.0-alpha.28.mrpack)
|
||||
est disponible. Le canal packwiz est au commit
|
||||
`6ec1c10c710870a2ccc591b524cc26d125ca03a2`. Deux synchronisations isolées,
|
||||
puis deux dans l’instance existante **Sanctuary Beta**, réussissent.
|
||||
Un seul JAR Sanctuary alpha.28 est actif ; les **925 fichiers personnels
|
||||
et réglages suivis** gardent leurs empreintes. Le jeu était fermé.
|
||||
Sauvegarde ciblée : `sanctuary-backups/before-0.1.0-alpha.28/`.
|
||||
|
||||
SHA-256 JAR : `7f95e9ebd50063c7ebfc649fa325e75263649e42c02e7159244953e09e27d24e`.
|
||||
SHA-256 MRpack : `12d4db8f06a13fb19feb08e61325361f7ab4d783d6f43b3d0c3582d316c54d0f`.
|
||||
Reçus : `build/alpha28-delivery-public.json`,
|
||||
`build/alpha28-isolated-validation.json`, `build/alpha28-prism-validation.json`.
|
||||
|
||||
Créer un **nouveau monde Sanctuary** ; témoin **Moyen 724, graine 0**.
|
||||
Les tests natifs concernent Mac, pas Windows. Le rendu client et les surplombs
|
||||
restent à juger en jeu. Aucun monde personnel n’a été régénéré.
|
||||
|
||||
## Alpha.27 publiée — 12 septembre 2026
|
||||
|
||||
La reprise repart directement de l’alpha.24 : relief et cavités conservés,
|
||||
marais souterrains localisés, montgolfière retirée et navires fondés sur le
|
||||
véritable template vanilla `minecraft:shipwreck/rightsideup_full`, restauré.
|
||||
Voir [le périmètre](generation-alpha27.md) et [les essais](testing-alpha27.md).
|
||||
Minecraft 26.3-pre-2 et les dépendances de la 24 sont conservés.
|
||||
|
||||
`check build` puis `assemblePack` passent sur Mac. Le JAR, le MRpack et le ZIP
|
||||
d’amorçage sont vérifiés localement puis par téléchargement public.
|
||||
|
||||
La [release alpha.27](https://git.botsu.net/koka/sanctuary-beta/releases/tag/v0.1.0-alpha.27)
|
||||
est publiée depuis `392ade0`. Le canal packwiz est au commit
|
||||
`9803b2279aa5242c9e3e7202b940bf47e1c3f404`. Le
|
||||
[MRpack](https://git.botsu.net/koka/sanctuary-beta/releases/download/v0.1.0-alpha.27/Sanctuary-0.1.0-alpha.27.mrpack)
|
||||
est disponible séparément.
|
||||
|
||||
Deux synchronisations isolées, puis deux synchronisations dans la même instance
|
||||
Prism **Sanctuary Beta** réussissent. Un seul JAR Sanctuary alpha.27 est actif.
|
||||
Les **925 fichiers personnels et réglages suivis** gardent leurs empreintes ;
|
||||
le jeu était fermé. Le dossier reste `Sanctuary-0.1.0-alpha.1`, avec sauvegarde
|
||||
ciblée dans `sanctuary-backups/before-0.1.0-alpha.27/`.
|
||||
Reçus locaux : `build/alpha27-delivery-public.json`,
|
||||
`build/alpha27-isolated-validation.json`, `build/alpha27-prism-validation.json`.
|
||||
|
||||
SHA-256 JAR : `56144b291407650e148aa3769596165a5e87398ea4a847cf67352ce2a2c625bf`.
|
||||
SHA-256 MRpack : `e13be90c6454bad3cdd3418fc54b827488700876f6e4ddb42e4a05f57acfecb6`.
|
||||
|
||||
Créer un **nouveau monde Sanctuary** pour cet essai. Les sauvegardes et réglages
|
||||
personnels sont conservés. Aucun essai Windows natif n’est revendiqué.
|
||||
|
||||
## Alpha.24 publiée — 11 septembre 2026
|
||||
|
||||
`check build assemblePack` et les essais natifs passent. Le JAR et
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
# Alpha.27 — vérifications de reprise
|
||||
|
||||
Base de travail : `e7d1f29` (alpha.24), branche
|
||||
`codex/reprise-sanctuary-alpha27`. Mod et pack : `0.1.0-alpha.27`, Minecraft
|
||||
`26.3-pre-2`. Nouveau monde Sanctuary, Moyen 724 par défaut.
|
||||
|
||||
Les fichiers de densité et les ressources de relief de la 24 sont conservés.
|
||||
Les deux changements localisés concernent les biomes de marais et les lieux
|
||||
aériens. Voir le [périmètre exact](generation-alpha27.md).
|
||||
|
||||
## Résultat local du 12 septembre 2026
|
||||
|
||||
`./gradlew check build` réussit en **3 min 28 s**. Le serveur de développement
|
||||
termine ses **19 tests obligatoires**, dont les témoins marais et aériens, sans
|
||||
échec. Le journal faisant foi est `build/alpha27-check-build-final.log` ; les
|
||||
premiers journaux de préparation ne décrivent pas ce résultat final.
|
||||
|
||||
Commande exacte :
|
||||
|
||||
```sh
|
||||
./gradlew check build -PsanctuaryDensityMaps=false -PsanctuaryAerialPreview -PsanctuaryAerial24Tests=true
|
||||
./gradlew assemblePack -x :sanctuary:check
|
||||
```
|
||||
|
||||
La première option désactive les images de densité facultatives. Le contrôle
|
||||
`check` vient de réussir et n’est pas répété pendant l’assemblage du pack.
|
||||
Les 56 fichiers de densité, relief et hydrologie recensés sont identiques à
|
||||
la base `e7d1f29` (`build/alpha27-evidence/terrain-base.json`).
|
||||
|
||||
Les deux reçus natifs ci-dessous proviennent du même processus `48406`, dans
|
||||
un **nouveau monde Moyen, diamètre 724, graine 0**, avec les structures et
|
||||
l'option expérimentale activées. Ils ont été écrits à 00:32:38 et 00:32:47,
|
||||
pendant le dernier passage réussi. Le générateur reste `sanctuary:island_v24`
|
||||
dans cette reprise ; la version du mod est bien alpha.27.
|
||||
|
||||
## Marais souterrain réel
|
||||
|
||||
`Swamp27WorldGameTests` réutilise le plan hydrologique central déjà en cache,
|
||||
puis inspecte au maximum huit chunks FULL demandés explicitement. Aucune
|
||||
région supplémentaire n'est planifiée pour sélectionner les eaux témoins ;
|
||||
aucun bloc ni point d'eau n'est posé par le test.
|
||||
|
||||
Le reçu `mods/sanctuary/build/run/gameTest/diagnostics/swamp27/native.json`
|
||||
confirme :
|
||||
|
||||
- **1 444 sols de cavités sous voûte**, dont 1 315 couverts d'herbe, mousse ou boue ;
|
||||
- **355 blocs d'eau** dans le biome de marais ;
|
||||
- **494 plantes** : 107 buissons à lucioles, 292 herbes courtes et 95 lianes ;
|
||||
- des eaux souterraines existantes à Y118, 120, 129 et 130 ; 289 colonnes d'eau
|
||||
recoupent le champ du marais dans la région inspectée.
|
||||
|
||||
Repère pour visiter ce témoin dans un nouveau monde de mêmes paramètres :
|
||||
|
||||
```text
|
||||
/tp @s -6.5 130 -92.5
|
||||
```
|
||||
|
||||
`diagnostics/swamp27/blocks.json` exporte une fenêtre des **blocs FULL réels**
|
||||
et une coupe destinée à l'aperçu. Le monde reste intact. Cette projection ne
|
||||
reproduit pas l'éclairage, les shaders ou le rendu exact du client Minecraft.
|
||||
|
||||
## Quatre lieux aériens
|
||||
|
||||
Le reçu `mods/sanctuary/build/run/gameTest/diagnostics/aerial24/create.json`
|
||||
porte `passed: true` et confirme **deux bateaux, deux îlots, deux marchands
|
||||
réels, quatre coffres et une source de cascade**. Les deux marchands sont
|
||||
observés après chargement effectif des entités. Les 56 chunks du témoin
|
||||
aérien incluent les structures et leurs points de rivage.
|
||||
|
||||
Les coques lisent le vrai template Minecraft `shipwreck/rightsideup_full`,
|
||||
palette 0 : 643 des 659 blocs physiques d’origine sont conservés lors de la
|
||||
restauration, 16 remplacés et 76 ajoutés avant le mobilier marchand. Les
|
||||
escaliers, dalles et la cabine native restent présents. Les aperçus de blocs
|
||||
simplifiés ont été inspectés ; ils ne sont pas des captures du client.
|
||||
|
||||
Le test contrôle les blocs posés, les accès, les coffres enterrés des îlots,
|
||||
les starts natifs et leur sérialisation. Il ouvre les quatre coffres,
|
||||
déplace les deux marchands et retire quelques blocs dans le monde jetable,
|
||||
puis sauvegarde. Les quatre sites restent enregistrés comme générés ; le
|
||||
journal d'expansion est inchangé.
|
||||
|
||||
Les tests purs couvrent également **neuf combinaisons** de graines 0/42/2026
|
||||
et diamètres 512/724/1024 : déterminisme des plans, orientations, limites et
|
||||
réservations. Ce sont des calculs de plans et de géométrie, **pas neuf mondes
|
||||
natifs intégralement générés**.
|
||||
|
||||
## Limites
|
||||
|
||||
Cette passe ne valide pas une nouvelle matrice de rechargements, toutes les
|
||||
graines ni toute l'île. Elle ne garantit pas des grenouilles ou des chênes
|
||||
dans chaque cavité. Le premier témoin sec a servi à corriger la sélection
|
||||
du test ; les chiffres ci-dessus proviennent du témoin humide final.
|
||||
|
||||
Aucun essai Windows natif, mesure de FPS ou validation visuelle par le joueur
|
||||
n'est revendiqué. La durée du build comprend les vérifications et ne mesure
|
||||
pas le temps de chargement d'une partie. La publication des artefacts et la
|
||||
synchronisation Prism font l'objet de reçus de livraison distincts.
|
||||
|
||||
## Livraison
|
||||
|
||||
`assemblePack` réussit. JAR, MRpack, dépendances et ZIP d’amorçage vérifiés,
|
||||
puis téléchargement anonyme des mêmes artefacts. Deux installations isolées
|
||||
et deux synchronisations de l’instance Prism existante réussissent ; les
|
||||
925 fichiers personnels et réglages suivis sont inchangés. Voir
|
||||
[la livraison](packwiz.md#alpha27-publiée--12-septembre-2026).
|
||||
@@ -0,0 +1,110 @@
|
||||
# Alpha.28 — vérifications
|
||||
|
||||
## Protocole
|
||||
|
||||
Tests sur macOS avec Java 25 et Minecraft 26.3-pre-2. Aucun essai Windows
|
||||
natif ni jugement de rendu client n’est revendiqué. Le créateur a validé
|
||||
visuellement la 27 ; les coupes de densité servent ici à contrôler cette
|
||||
nouvelle passe, sans représenter l’eau, la végétation ou les structures.
|
||||
|
||||
Commande de livraison :
|
||||
|
||||
```sh
|
||||
./gradlew check build -PsanctuaryDensityMaps=false -PsanctuaryAerial24Tests=true
|
||||
```
|
||||
|
||||
`sanctuaryDensityMaps=false` coupe la production des anciennes images,
|
||||
pas les assertions. Le monde GameTest est recréé dans le dossier de développement
|
||||
ignoré. Les sauvegardes personnelles ne sont pas utilisées par les tests.
|
||||
|
||||
Les smokes de placement aérien examinent 512 / 724 / 1 024, graines 0 / 42 /
|
||||
2026. Le témoin natif chargé en chunks FULL est Moyen 724, graine 0 ; ceci
|
||||
ne prétend pas être neuf îles entièrement explorées. Les comparaisons de
|
||||
terrain analysent deux coupes espacées de 2 blocs et une grille de hauteurs
|
||||
espacée de 32 blocs. Une expansion naturelle de 512 est contrôlée par son
|
||||
vrai champ de densité sans être réservée ni ouverte dans le serveur.
|
||||
|
||||
## Résultats
|
||||
|
||||
`check build` passe en **4 min 15 s**, dont **21/21 GameTests natifs**
|
||||
en 57,06 s. `assemblePack` passe ensuite sans recompilation des binaires vérifiés.
|
||||
Les smokes couvrent aussi le Blocodex, les formats de journal, les priorités
|
||||
et les réservations d’expansion, ainsi que les anciennes géométries pures.
|
||||
|
||||
Le terrain de référence conserve **6 098 des 6 860** anciens échantillons
|
||||
rocheux intérieurs (88,9 %), avec **762** désormais creusés. Cette comparaison
|
||||
inclut les raccords du nouveau cisaillement, pas seulement les grottes.
|
||||
L’expansion de 512 conserve 1 299 échantillons contre 136 creusés.
|
||||
|
||||
Sur la grille de hauteurs, 54 colonnes hautes montent et 84 s’abaissent.
|
||||
La hausse maximale relevée est de 32 blocs et le sommet échantillonné atteint
|
||||
Y280. La baisse maximale de sommet, 132 blocs en (0,160), correspond à un
|
||||
lobe supérieur qui quitte cette colonne : le sol observé passe de Y248 à
|
||||
Y116. Elle ne désigne pas un déplacement vertical de 132 blocs ; le déplacement
|
||||
3D est borné à ±40 en Y. Des surplombs et étranglements subsistent et restent
|
||||
à apprécier visuellement dans le jeu.
|
||||
|
||||
Les **9 chunks FULL** inspectés contiennent les huit minerais Overworld :
|
||||
|
||||
| Minerai | Blocs observés, variantes deepslate comprises |
|
||||
|---|---:|
|
||||
| Charbon | 1408 |
|
||||
| Fer | 325 |
|
||||
| Cuivre | 302 |
|
||||
| Or | 87 |
|
||||
| Lapis | 53 |
|
||||
| Redstone | 77 |
|
||||
| Diamant | 16 |
|
||||
| Émeraude | 4 |
|
||||
|
||||
Il s’agit d’un relevé après génération, pas de quotas imposés à chaque chunk.
|
||||
|
||||
La relecture finale des fichiers de région confirme **trois donjons vanilla**
|
||||
avec spawners zombies et coffres `minecraft:chests/simple_dungeon` :
|
||||
`-101 170 100`, `-92 175 -92` et `-85 178 11`. Une vraie mine est présente
|
||||
vers `152 176 -120`, avec **143 pièces sauvegardées**. Dans les 14 chunks FULL
|
||||
du contrôle des structures : 91 planches, 88 barrières, 13 rails, 29 toiles
|
||||
et un spawner ont été observés dans les pièces chargées. Ce relevé sur les
|
||||
fichiers MCA après fermeture du serveur ne génère aucun chunk supplémentaire.
|
||||
|
||||
Les quatre lieux aériens ont de vrais départs et pièces sauvegardées,
|
||||
quatre coffres et deux marchands adultes au total, puis passent les contrôles
|
||||
après ouverture, extraction et modification dans le protocole natif. Il y a
|
||||
exactement une source de cascade et deux îlots survolant le terrain. Aucun
|
||||
nouveau site ni habitant n’est recréé pour remplacer ce qui a été utilisé.
|
||||
|
||||
Preuves locales ignorées : `build/alpha28-check-build-final.log`,
|
||||
`build/alpha28-preview/geology-final.json`,
|
||||
`build/alpha28-preview/geology-final-comparison.png`,
|
||||
`build/alpha28-evidence/final/` et reçus de distribution `build/alpha28-*`.
|
||||
|
||||
|
||||
## Limites et essai du créateur
|
||||
|
||||
- La hutte de sorcière est conditionnelle aux berges 3D, à l’espace et aux quatre
|
||||
appuis natifs. Le premier petit échantillon n’en contient aucune : son audit
|
||||
de placement ne constitue pas une preuve de présence en jeu.
|
||||
- Les quatre départs et les deux UUID de marchands persistants sont relus
|
||||
dans les données sauvegardées. Aucun second lancement JVM avec ce monde
|
||||
alpha.28 n’est revendiqué.
|
||||
- La source de la cascade est contrôlée ; son écoulement complet jusqu’au vide
|
||||
n’est pas mesuré par ce protocole. La propagation reste celle de Minecraft.
|
||||
- Les témoins historiques purs restent vérifiés ; les comparaisons exactes de
|
||||
l’ancien champ utilisent explicitement `legacy27()`. Elles ne prétendent pas
|
||||
que le nouveau relief serait identique à celui de la 27.
|
||||
- Il ne s’agit pas d’un benchmark client ni d’une simulation physique tectonique.
|
||||
Les performances pourront être étudiées après ce retour visuel.
|
||||
|
||||
Créer un **nouveau monde Sanctuary**, taille **Moyen**, graine **0** pour
|
||||
retrouver le témoin. Garder ses autres sauvegardes ; aucune migration ni
|
||||
régénération n’est réalisée. Valider en priorité les masses soulevées et
|
||||
abaissées, les cavités à explorer, les îlots au-dessus du terrain et les
|
||||
navires proches des berges.
|
||||
|
||||
## Distribution
|
||||
|
||||
Le JAR, le MRpack et le ZIP d’amorçage ont les mêmes empreintes sur disque
|
||||
et après téléchargement anonyme de la release. Les deux synchronisations
|
||||
isolées sont identiques ; les deux synchronisations de l’instance Prism
|
||||
existante conservent les empreintes des 925 fichiers personnels et réglages
|
||||
suivis. Un seul JAR alpha.28 est actif. Voir [le reçu de livraison](packwiz.md).
|
||||
+2
-2
@@ -8,6 +8,6 @@ loader_version=0.19.5
|
||||
loom_version=1.17.20
|
||||
fabric_api_version=0.160.0+26.3
|
||||
|
||||
mod_version=0.1.0-alpha.24
|
||||
pack_version=0.1.0-alpha.24
|
||||
mod_version=0.1.0-alpha.28
|
||||
pack_version=0.1.0-alpha.28
|
||||
maven_group=fr.koka.sanctuary
|
||||
|
||||
@@ -603,6 +603,8 @@ tasks.register('aerial24Smoke', JavaExec) {
|
||||
dependsOn('testClasses')
|
||||
classpath = sourceSets.test.runtimeClasspath
|
||||
mainClass = 'fr.koka.sanctuary.worldgen.aerial.Aerial24Smoke'
|
||||
if (providers.gradleProperty('sanctuaryAerialPreview').isPresent())
|
||||
systemProperty('sanctuary.aerial.preview', rootProject.layout.buildDirectory.file('alpha27-preview/native-ships.json').get().asFile.absolutePath)
|
||||
}
|
||||
tasks.named('check') { dependsOn('aerial24Smoke') }
|
||||
|
||||
@@ -613,3 +615,19 @@ tasks.register('aerialPersistence24Smoke', JavaExec) {
|
||||
mainClass = 'fr.koka.sanctuary.worldgen.aerial.AerialPersistence24Smoke'
|
||||
}
|
||||
tasks.named('check') { dependsOn('aerialPersistence24Smoke') }
|
||||
|
||||
tasks.register('swamp27VolumesSmoke', JavaExec) {
|
||||
group = 'verification'
|
||||
dependsOn('testClasses')
|
||||
classpath = sourceSets.test.runtimeClasspath
|
||||
mainClass = 'fr.koka.sanctuary.worldgen.Swamp27VolumesSmoke'
|
||||
}
|
||||
tasks.named('check') { dependsOn('swamp27VolumesSmoke') }
|
||||
|
||||
tasks.register('cavesRelief28Smoke', JavaExec) {
|
||||
group = 'verification'
|
||||
dependsOn('testClasses')
|
||||
classpath = sourceSets.test.runtimeClasspath
|
||||
mainClass = 'fr.koka.sanctuary.worldgen.CavesRelief28Smoke'
|
||||
}
|
||||
tasks.named('check') { dependsOn('cavesRelief28Smoke') }
|
||||
|
||||
+13
-9
@@ -33,7 +33,7 @@ public final class Aerial24FlowAdmissionGameTests {
|
||||
&& level.structureManager().shouldGenerateStructures(), "Fresh generation24 with aerial sites and native structures enabled");
|
||||
var plan = generator.aerialPlan24();
|
||||
var session = Objects.requireNonNull(ExpansionRuntime.session(level));
|
||||
helper.assertTrue(session.islands().size() == 1 && plan.sites().size() == 5, "Use an untouched disposable world for admission witnesses");
|
||||
helper.assertTrue(session.islands().size() == 1 && plan.sites().size() == 4, "Use an untouched disposable world for admission witnesses");
|
||||
var evidence = StructureCensusService.snapshot(level.getServer()).evidence();
|
||||
String dimension = level.dimension().identifier().toString();
|
||||
for (var site : plan.sites()) for (var chunk : site.claimedChunks())
|
||||
@@ -44,7 +44,9 @@ public final class Aerial24FlowAdmissionGameTests {
|
||||
var admitted = new ArrayList<ExpansionIsland>();
|
||||
try {
|
||||
report.put("stage", "real admission toward an unseen satellite");
|
||||
admitted.add(admit(helper, level, generator, session, plan, true, report));
|
||||
if (!requests(level.getSeed(), session.islands(), plan, true).isEmpty())
|
||||
admitted.add(admit(helper, level, generator, session, plan, true, report));
|
||||
else report.put("collision_admission", "No raw expansion candidate reaches the close sites; initial island ownership already excludes them");
|
||||
report.put("stage", "real admission through an unclaimed interval");
|
||||
admitted.add(admit(helper, level, generator, session, plan, false, report));
|
||||
} catch (Exception error) { failure(report, error); throw error; }
|
||||
@@ -88,15 +90,17 @@ public final class Aerial24FlowAdmissionGameTests {
|
||||
if (!tickets[0]) {
|
||||
requested.forEach(chunk -> level.getChunkSource().addTicketWithRadius(TicketType.DRAGON, chunk, 2)); tickets[0] = true;
|
||||
}
|
||||
flowTicks[0] += 20; report.put("flow_wait_ticks", flowTicks[0]); report.put("stage", "native fluid ticks toward the void");
|
||||
flowTicks[0] += 20; report.put("flow_wait_ticks", flowTicks[0]); report.put("stage", "native fluid ticks beneath the suspended island");
|
||||
var source = AerialGeometry24.waterfallSource(waterfall);
|
||||
helper.assertTrue(level.getFluidState(source).is(FluidTags.WATER) && level.getFluidState(source).isSource(), "The authored source remains a single native water source");
|
||||
var below = waterAt(level, waterfall, waterfall.minY() - 16);
|
||||
var low = waterAt(level, waterfall, 8);
|
||||
// Alpha28 islets stand above real terrain: a cascade may land on Sanctuary.
|
||||
// Require native flow through the free air gap, not an impossible route through rock.
|
||||
var below = waterAt(level, waterfall, waterfall.minY() - 1);
|
||||
var low = waterAt(level, waterfall, waterfall.minY() - AerialPlanner24.MIN_ISLET_GAP);
|
||||
report.put("water_below_islet", below == null ? List.of() : coords(below));
|
||||
report.put("water_near_y0", low == null ? List.of() : coords(low));
|
||||
report.put("water_across_air_gap", low == null ? List.of() : coords(low));
|
||||
if (below == null || low == null) {
|
||||
helper.assertTrue(flowTicks[0] <= 4000, "Scheduled vanilla fluid ticks carry the waterfall below the island and down to y=8");
|
||||
helper.assertTrue(flowTicks[0] <= 4000, "Scheduled vanilla fluid ticks carry the waterfall through the sixteen-block air gap");
|
||||
helper.runAtTickTime(helper.getTick() + 20, () -> step[0].run()); return;
|
||||
}
|
||||
helper.assertTrue(!level.getFluidState(low).isSource(), "Low waterfall water is flowing, not a fabricated source column");
|
||||
@@ -128,7 +132,7 @@ public final class Aerial24FlowAdmissionGameTests {
|
||||
helper.assertTrue(elapsed <= 31_000, "Actual Session.create keeps its production thirty-second deadline");
|
||||
helper.assertTrue(!plan.conflicts(reserved), "The admitted expansion cannot claim any satellite chunk");
|
||||
helper.assertTrue(!ExpansionVoidReuse.requiresReload(level, reserved.id()), "Untouched candidate prepares in the same development session");
|
||||
if (!collision) helper.assertTrue(inAerialBand(reserved, plan), "The admitted free interval lies among the nearby satellites, not beyond a blanket exclusion ring");
|
||||
|
||||
helper.assertTrue(session.create(request.id(), "sanctuary", request.direction(), "temperate", request.diameter(), "natural").equals(reserved),
|
||||
"Reservation retry is idempotent");
|
||||
attempts.add(Map.of("request", request, "reserved", reserved, "elapsed_ms", elapsed));
|
||||
@@ -149,7 +153,7 @@ public final class Aerial24FlowAdmissionGameTests {
|
||||
var raw = ExpansionPlacement.candidates(seed, islands, id, "sanctuary", direction, "temperate", 128, "natural", 0);
|
||||
if (raw.isEmpty() || plan.conflicts(raw.getFirst()) != collision) continue;
|
||||
var filtered = raw.stream().filter(candidate -> !plan.conflicts(candidate)).findFirst().orElse(null);
|
||||
if (filtered == null || !collision && !inAerialBand(filtered, plan)) continue;
|
||||
if (filtered == null) continue;
|
||||
result.add(new Request(id, direction, 128, raw.getFirst(), filtered));
|
||||
}
|
||||
return result;
|
||||
|
||||
+62
-18
@@ -47,7 +47,7 @@ public final class Aerial24WorldGameTests {
|
||||
private static final com.google.gson.Gson JSON = new GsonBuilder().setPrettyPrinting().create();
|
||||
|
||||
@GameTest(maxTicks = 24000)
|
||||
public void fiveSitesPersistAfterUse(GameTestHelper helper) throws Exception {
|
||||
public void fourSitesPersistAfterUse(GameTestHelper helper) throws Exception {
|
||||
var report = new LinkedHashMap<String, Object>();
|
||||
try { begin(helper, report); }
|
||||
catch (Exception failure) { failure(report, failure); throw failure; }
|
||||
@@ -69,10 +69,10 @@ public final class Aerial24WorldGameTests {
|
||||
helper.assertTrue(StructureCensusService.aerialSites(level).isEmpty(), "Disabled aerial sites are not presented as planned");
|
||||
report.put("passed", true); write(Path.of("diagnostics/aerial24/off.json"), report); helper.succeed(); return;
|
||||
}
|
||||
helper.assertTrue(plan.sites().size() == 5, "All five initial aerial sites have a finite saved plan");
|
||||
helper.assertTrue(plan.sites().size() == 4, "All four initial aerial sites have a finite saved plan");
|
||||
helper.assertTrue(plan.sites().stream().filter(AerialPlan24.Site::merchant).count() == 2, "Two merchant ships");
|
||||
helper.assertTrue(plan.sites().stream().filter(AerialPlan24.Site::waterfall).count() == 1, "One waterfall site");
|
||||
if (generator.initialDiameter() == 724 && level.getSeed() == 0)
|
||||
if (generator.initialDiameter() == 724 && (level.getSeed() == 0 || level.getSeed() == 42))
|
||||
AerialRenderer24.writeDiagnostic(Path.of("diagnostics/aerial24/blocks.json"), plan.sites());
|
||||
report.put("geometry", JSON.toJsonTree(plan.sites())); report.put("claim_count", plan.claimsChunks().size());
|
||||
report.put("census_before", StructureCensusService.aerialSites(level));
|
||||
@@ -112,6 +112,12 @@ public final class Aerial24WorldGameTests {
|
||||
level.getChunk(chunk.x(), chunk.z(), ChunkStatus.FULL, true);
|
||||
helper.runAtTickTime(helper.getTick() + 1, () -> step[0].run()); return;
|
||||
}
|
||||
if (!report.containsKey("native_departures_ready")) {
|
||||
// Fail on a missing native start immediately instead of waiting for entities
|
||||
// which could never have been placed (for example, an obsolete void-only gate).
|
||||
report.put("stage", "verify saved aerial departures before entity loading");
|
||||
report.put("native_departures_ready", starts(helper, level, plan).size());
|
||||
}
|
||||
if (!tickets[0]) {
|
||||
entityChunks.forEach(pos -> level.getChunkSource().addTicketWithRadius(TicketType.DRAGON, pos, 2)); tickets[0] = true;
|
||||
}
|
||||
@@ -148,8 +154,8 @@ public final class Aerial24WorldGameTests {
|
||||
report.put("native_tags", tags); report.put("full_chunks", chunks.size());
|
||||
report.put("blocks_after", countSites(level, plan));
|
||||
var census = StructureCensusService.aerialSites(level);
|
||||
helper.assertTrue(census.size() == 5 && census.stream().allMatch(site -> site.status() == StructureCensusService.AerialStatus.GENERATED),
|
||||
"Five historical generated sites remain indexed after loot, mining and demolition");
|
||||
helper.assertTrue(census.size() == 4 && census.stream().allMatch(site -> site.status() == StructureCensusService.AerialStatus.GENERATED),
|
||||
"Four historical generated sites remain indexed after loot, mining and demolition");
|
||||
report.put("census_after", census);
|
||||
helper.assertTrue(equalFiles(journalBefore, files(world.resolve("data").resolve(generator.journalDirectory()))), "Aerial tests never reserve or activate an expansion");
|
||||
report.put("expansion_journal_unchanged", true);
|
||||
@@ -170,14 +176,25 @@ public final class Aerial24WorldGameTests {
|
||||
var root = generator.source().islands().stream().filter(ExpansionIsland::origin).findFirst().orElseThrow();
|
||||
var views = new LinkedHashMap<String, Object>();
|
||||
for (var site : plan.sites()) {
|
||||
helper.assertTrue(site.claimedChunks().stream().noneMatch(chunk -> root.ownsChunk(chunk.x(), chunk.z())), "No aerial claim replaces natural starter chunks");
|
||||
if (site.merchant()) {
|
||||
double gap = AerialPlanner24.shoreDistance(site);
|
||||
helper.assertTrue(gap >= 16 && gap <= 48, "Native ship is just off its verified shore");
|
||||
helper.assertTrue(Math.abs(AerialGeometry24.entry(site).getY() - site.shoreY()) <= 4, "Boarding deck follows the nearby shore height");
|
||||
} else {
|
||||
helper.assertTrue(root.ownsChunk(site.startChunkX(), site.startChunkZ()), "Mineral islet floats above Sanctuary terrain");
|
||||
var column = generator.aerialBaseColumn24(site.shoreX(), site.shoreZ());
|
||||
helper.assertTrue(column.getBlock(site.shoreY() - 1).isSolid(), "Overhead islet retains a real ground witness below it");
|
||||
int highest = 0;
|
||||
for (int y = 382; y > 0; y--) if (!column.getBlock(y).isAir()) { highest = y; break; }
|
||||
helper.assertTrue(site.minY() - highest - 1 >= AerialPlanner24.MIN_ISLET_GAP, "At least sixteen air blocks separate ground and suspended rock");
|
||||
}
|
||||
var overlap = new ExpansionIsland("aerial_overlap", root.id(), "east", "temperate", 64,
|
||||
site.startChunkX() * 16, site.startChunkZ() * 16, 42, "natural", "reserved", 24);
|
||||
helper.assertTrue(plan.conflicts(overlap), "An unseen site already prevents overlapping expansion ownership");
|
||||
double distance = Math.hypot(site.locator().getX() - site.shoreX(), site.locator().getZ() - site.shoreZ());
|
||||
helper.assertTrue(distance <= 512, "Aerial site remains within 512 blocks of a real shore witness");
|
||||
views.put(site.id(), Map.of("shore", List.of(site.shoreX(), site.shoreY() + 2, site.shoreZ()),
|
||||
"entry", coordinates(AerialGeometry24.entry(site)), "centre", coordinates(site.locator()), "shore_distance", distance,
|
||||
"entry", coordinates(AerialGeometry24.entry(site)), "centre", coordinates(site.locator()), "shore_distance", distance, "hull_to_shore", AerialPlanner24.shoreDistance(site),
|
||||
"exterior_view", List.of(site.minX() - 24, site.minY() + site.height() / 2, site.minZ() - 24)));
|
||||
}
|
||||
helper.assertTrue(!plan.conflicts(new ExpansionIsland("aerial_far", root.id(), "east", "temperate", 64, 4096, 4096, 42, "natural", "reserved", 24)),
|
||||
@@ -223,8 +240,17 @@ public final class Aerial24WorldGameTests {
|
||||
for (int dy = -2; dy <= 20; dy++) column.put(shore.getY() + dy, level.getBlockState(shore.above(dy)).toString());
|
||||
shores.put(site.id(), Map.of("position", coordinates(shore), "column", column));
|
||||
helper.assertTrue(level.getBlockState(shore.below()).isSolid(), "Saved shore witness has a real FULL terrain support: " + site.id() + " " + shore);
|
||||
helper.assertTrue(level.getBlockState(shore.above()).getCollisionShape(level, shore.above()).isEmpty(),
|
||||
"Shore viewpoint is not buried inside the island: " + site.id() + " " + shore.above() + " " + level.getBlockState(shore.above()));
|
||||
if (site.merchant()) {
|
||||
helper.assertTrue(level.getBlockState(shore.above()).getCollisionShape(level, shore.above()).isEmpty(),
|
||||
"Shore viewpoint is not buried inside the island: " + site.id() + " " + shore.above() + " " + level.getBlockState(shore.above()));
|
||||
} else {
|
||||
// Natural trees may occupy the mathematical ground reference below an islet.
|
||||
// Observe a real nearby walking position without removing that vegetation.
|
||||
var viewpoint = nearbyGroundView(level, shore);
|
||||
helper.assertTrue(viewpoint != null, "A walkable ground viewpoint exists within twelve horizontal blocks below the suspended islet: " + site.id());
|
||||
shores.put(site.id(), Map.of("position", coordinates(shore), "column", column,
|
||||
"walkable_viewpoint", coordinates(viewpoint), "reference_obstruction", level.getBlockState(shore.above()).toString()));
|
||||
}
|
||||
var chestAt = AerialGeometry24.chest(site);
|
||||
helper.assertTrue(level.getBlockEntity(chestAt) instanceof RandomizableContainerBlockEntity, "Real chest at " + site.id()); chestCount++;
|
||||
var chest = (RandomizableContainerBlockEntity) level.getBlockEntity(chestAt);
|
||||
@@ -246,19 +272,37 @@ public final class Aerial24WorldGameTests {
|
||||
var source = AerialGeometry24.waterfallSource(site);
|
||||
helper.assertTrue(level.getFluidState(source).isSource(), "The one real source drives the native waterfall"); waterSources++;
|
||||
}
|
||||
// These columns are beyond the authored box and well outside initial terrain ownership.
|
||||
for (var pos : List.of(new BlockPos(site.minX() - 2, site.minY(), site.minZ() - 2), new BlockPos(site.maxX() + 2, site.minY(), site.maxZ() + 2)))
|
||||
for (int y = site.minY(); y <= site.maxY() + 2; y++) {
|
||||
var state = level.getBlockState(pos.atY(y));
|
||||
helper.assertTrue(state.isAir() || site.waterfall() && !state.getFluidState().isEmpty(), "No manufactured structure leaks outside its authored footprint");
|
||||
}
|
||||
// Root terrain and its own trees may now exist outside these aerial envelopes.
|
||||
// Chunk clipping is checked against every authored position; do not confuse legal
|
||||
// surrounding terrain with a structure leak by requiring a whole exterior air column.
|
||||
helper.assertTrue(model.keySet().stream().allMatch(site.bounds()::isInside), "Every proposed structure block is inside its finite envelope");
|
||||
var exterior = new ArrayList<Map<String, Object>>();
|
||||
for (var at : List.of(new BlockPos(site.minX() - 2, site.minY(), site.minZ() - 2),
|
||||
new BlockPos(site.maxX() + 2, site.minY(), site.maxZ() + 2)))
|
||||
exterior.add(Map.of("position", coordinates(at), "observed_block", level.getBlockState(at).toString()));
|
||||
report.put(site.id() + "_exterior_observations", exterior);
|
||||
}
|
||||
helper.assertTrue(chestCount == 5 && waterSources == 1, "Five physical chests and exactly one planned source");
|
||||
helper.assertTrue(chestCount == 4 && waterSources == 1, "Four physical chests and exactly one planned source");
|
||||
report.put("chests", chestCount); report.put("waterfall_sources", waterSources); report.put("blocks_before", countSites(level, plan));
|
||||
report.put("vegetation", vegetation);
|
||||
return expected;
|
||||
}
|
||||
|
||||
private static BlockPos nearbyGroundView(ServerLevel level, BlockPos reference) {
|
||||
for (int radius = 0; radius <= 12; radius++) for (int dx = -radius; dx <= radius; dx++) for (int dz = -radius; dz <= radius; dz++) {
|
||||
if (Math.max(Math.abs(dx), Math.abs(dz)) != radius || Math.hypot(dx, dz) > 12) continue;
|
||||
for (int dy = 8; dy >= -8; dy--) {
|
||||
var at = reference.offset(dx, dy, dz);
|
||||
var floor = level.getBlockState(at.below());
|
||||
if (!floor.isSolid() || floor.is(net.minecraft.tags.BlockTags.LEAVES) || floor.is(net.minecraft.tags.BlockTags.LOGS)) continue;
|
||||
if (level.getBlockState(at).getCollisionShape(level, at).isEmpty()
|
||||
&& level.getBlockState(at.above()).getCollisionShape(level, at.above()).isEmpty()
|
||||
&& level.getFluidState(at).isEmpty() && level.getFluidState(at.above()).isEmpty()) return at;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static Map<String, Object> vegetation(GameTestHelper helper, ServerLevel level, AerialPlan24.Site site) {
|
||||
helper.assertTrue(site.rockHeight() >= 20 && site.rockHeight() <= 35
|
||||
&& site.height() == site.rockHeight() + AerialPlan24.VEGETATION_HEADROOM,
|
||||
@@ -305,8 +349,8 @@ public final class Aerial24WorldGameTests {
|
||||
for (var entry : ores) { level.setBlock(entry.getKey(), Blocks.AIR.defaultBlockState(), 2); removed.add(coordinates(entry.getKey())); }
|
||||
}
|
||||
}
|
||||
var balloon = plan.sites().stream().filter(site -> site.kind() == AerialPlan24.Kind.BALLOON).findFirst().orElseThrow();
|
||||
var demolition = AerialGeometry24.entry(balloon).below();
|
||||
var ship = plan.sites().stream().filter(AerialPlan24.Site::merchant).findFirst().orElseThrow();
|
||||
var demolition = AerialGeometry24.entry(ship).below();
|
||||
helper.assertTrue(level.getBlockState(demolition).isSolid(), "The demolition witness removes an existing floor");
|
||||
level.setBlock(demolition, Blocks.AIR.defaultBlockState(), 2); removed.add(coordinates(demolition));
|
||||
var extracted = new TreeMap<String, List<Double>>(); int i = 0;
|
||||
|
||||
+148
@@ -0,0 +1,148 @@
|
||||
package fr.koka.sanctuary.gametest;
|
||||
|
||||
import com.google.gson.GsonBuilder;
|
||||
import fr.koka.sanctuary.SanctuaryMod;
|
||||
import fr.koka.sanctuary.expansion.ExpansionStructures23;
|
||||
import fr.koka.sanctuary.expansion.SwampHutPlacement28;
|
||||
import fr.koka.sanctuary.knowledge.structures.StructureCensusMemory;
|
||||
import fr.koka.sanctuary.knowledge.structures.StructureCensusService;
|
||||
import fr.koka.sanctuary.worldgen.SanctuaryChunkGenerator;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.*;
|
||||
import net.fabricmc.fabric.api.gametest.v1.GameTest;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.registries.BuiltInRegistries;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.gametest.framework.GameTestHelper;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.world.level.ChunkPos;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.block.entity.SpawnerBlockEntity;
|
||||
import net.minecraft.world.level.chunk.status.ChunkStatus;
|
||||
import net.minecraft.world.level.levelgen.structure.StructureStart;
|
||||
import net.minecraft.world.level.levelgen.structure.pieces.StructurePieceSerializationContext;
|
||||
|
||||
/** Observes normal root generation only. No start injection, fake room, expansion or seed search. */
|
||||
public final class Alpha28NativeStructuresGameTests {
|
||||
private static final com.google.gson.Gson JSON = new GsonBuilder().setPrettyPrinting().create();
|
||||
private static final Path OUT = Path.of("diagnostics/native28");
|
||||
|
||||
@GameTest(maxTicks = 3600)
|
||||
public void actualNativeStructures(GameTestHelper helper) throws Exception {
|
||||
var report = new LinkedHashMap<String, Object>();
|
||||
try {
|
||||
var level = helper.getLevel(); var host = (SanctuaryChunkGenerator) level.getChunkSource().getGenerator();
|
||||
report.put("seed", level.getSeed()); report.put("diameter", host.initialDiameter());
|
||||
report.put("process_id", ProcessHandle.current().pid()); report.put("generate_structures", level.structureManager().shouldGenerateStructures());
|
||||
if (!level.structureManager().shouldGenerateStructures()) {
|
||||
report.put("scope", "Native structures globally disabled"); report.put("passed", true); write(report); helper.succeed(); return;
|
||||
}
|
||||
var root = host.source().islands().stream().filter(island -> island.origin()).findFirst().orElseThrow();
|
||||
var candidates = ExpansionStructures23.candidates(root).stream().limit(12).toList();
|
||||
var registry = level.registryAccess().lookupOrThrow(Registries.STRUCTURE);
|
||||
var found = new LinkedHashMap<String, StructureStart>(); var observed = new TreeMap<String, Integer>();
|
||||
for (var candidate : candidates) {
|
||||
var chunk = level.getChunk(candidate.chunk().x(), candidate.chunk().z(), ChunkStatus.STRUCTURE_STARTS, true);
|
||||
for (var start : chunk.getAllStarts().values()) {
|
||||
if (!start.isValid()) continue;
|
||||
String id = registry.getKey(start.getStructure()).toString(); observed.merge(id, 1, Integer::sum);
|
||||
if (List.of("minecraft:mineshaft", "minecraft:mineshaft_mesa", "minecraft:swamp_hut").contains(id)) found.putIfAbsent(id, start);
|
||||
}
|
||||
}
|
||||
report.put("candidate_chunks", candidates.size()); report.put("observed_starts", observed);
|
||||
var full = new LinkedHashSet<ChunkPos>();
|
||||
// Only a few actual pieces are visited: this is not full-map pregeneration.
|
||||
for (var entry : found.entrySet()) {
|
||||
full.add(entry.getValue().getChunkPos());
|
||||
int remaining = entry.getKey().equals("minecraft:swamp_hut") ? 4 : 4;
|
||||
for (var piece : entry.getValue().getPieces()) {
|
||||
if (remaining-- <= 0) break;
|
||||
var b = piece.getBoundingBox(); full.add(new ChunkPos(b.getCenter().getX() >> 4, b.getCenter().getZ() >> 4));
|
||||
if (entry.getKey().equals("minecraft:swamp_hut")) b.intersectingChunks().forEach(full::add);
|
||||
}
|
||||
}
|
||||
// A few additional real generation chunks can contain native monster_room attempts.
|
||||
for (var candidate : candidates) {
|
||||
if (full.size() >= 16) break;
|
||||
full.add(candidate.chunk());
|
||||
}
|
||||
helper.assertTrue(full.size() <= 24, "Bounded native structure witness requests at most24 FULL chunks");
|
||||
var iterator = full.iterator(); Runnable[] step = new Runnable[1];
|
||||
step[0] = () -> {
|
||||
try {
|
||||
if (iterator.hasNext()) {
|
||||
var at = iterator.next(); report.put("stage", "FULL " + at);
|
||||
level.getChunk(at.x(), at.z(), ChunkStatus.FULL, true);
|
||||
helper.runAtTickTime(helper.getTick() + 1, () -> step[0].run()); return;
|
||||
}
|
||||
inspect(helper, level, found, full, report);
|
||||
report.put("passed", true); write(report); helper.succeed();
|
||||
} catch (Exception error) { failure(report, error); throw new IllegalStateException("Native28 witness failed", error); }
|
||||
};
|
||||
helper.runAtTickTime(helper.getTick() + 1, () -> step[0].run());
|
||||
} catch (Exception error) { failure(report, error); throw error; }
|
||||
}
|
||||
|
||||
private static void inspect(GameTestHelper helper, ServerLevel level, Map<String, StructureStart> found,
|
||||
Set<ChunkPos> full, Map<String, Object> report) {
|
||||
var registry = level.registryAccess().lookupOrThrow(Registries.STRUCTURE);
|
||||
var context = StructurePieceSerializationContext.fromLevel(level);
|
||||
var proofs = new LinkedHashMap<String, Object>();
|
||||
for (var entry : found.entrySet()) {
|
||||
var expected = entry.getValue(); var cp = expected.getChunkPos();
|
||||
var actual = level.getChunk(cp.x(), cp.z(), ChunkStatus.FULL, true)
|
||||
.getStartForStructure(registry.getValue(Identifier.parse(entry.getKey())));
|
||||
helper.assertTrue(actual != null && actual.isValid(), "The real selected native start survives FULL: " + entry.getKey());
|
||||
var tag = actual.createTag(context, cp);
|
||||
var restored = StructureStart.loadStaticStart(context, tag, level.getSeed());
|
||||
helper.assertTrue(restored != null && tag.equals(restored.createTag(context, cp)), "Native piece NBT roundtrip: " + entry.getKey());
|
||||
var counts = new TreeMap<String, Integer>(); var positions = new HashSet<BlockPos>();
|
||||
for (var piece : actual.getPieces()) {
|
||||
var b = piece.getBoundingBox();
|
||||
for (int x = b.minX(); x <= b.maxX(); x++) for (int z = b.minZ(); z <= b.maxZ(); z++) {
|
||||
if (!full.contains(new ChunkPos(x >> 4, z >> 4))) continue;
|
||||
for (int y = b.minY(); y <= b.maxY(); y++) {
|
||||
BlockPos pos = new BlockPos(x, y, z); if (!positions.add(pos)) continue;
|
||||
var state = level.getBlockState(pos);
|
||||
if (!state.isAir()) counts.merge(BuiltInRegistries.BLOCK.getKey(state.getBlock()).toString(), 1, Integer::sum);
|
||||
}
|
||||
}
|
||||
if (entry.getKey().equals("minecraft:swamp_hut")) for (var pier : SwampHutPlacement28.piers(piece)) {
|
||||
var state = level.getBlockState(pier);
|
||||
helper.assertTrue(state.is(Blocks.OAK_LOG) || state.isSolid(), "A native hut pier has wood or a real solid foot: " + pier);
|
||||
}
|
||||
}
|
||||
if (entry.getKey().contains("mineshaft")) helper.assertTrue(counts.entrySet().stream()
|
||||
.filter(e -> e.getKey().endsWith("_planks") || e.getKey().endsWith("_fence") || e.getKey().equals("minecraft:rail"))
|
||||
.mapToInt(Map.Entry::getValue).sum() > 0, "A selected mine creates actual native timber/rail geometry");
|
||||
proofs.put(entry.getKey(), Map.of("start_chunk", List.of(cp.x(), cp.z()), "pieces", actual.getPieces().size(),
|
||||
"blocks_in_loaded_pieces", counts, "native_tag", tag.toString(), "visit", actual.getPieces().getFirst().getLocatorPosition().toShortString()));
|
||||
}
|
||||
var rooms = new ArrayList<Object>();
|
||||
for (var site : StructureCensusService.snapshot(level.getServer()).evidence().sites().values()) {
|
||||
var key = site.key();
|
||||
if (!key.id().equals(StructureCensusMemory.MONSTER_ROOM) || !key.dimension().equals(level.dimension().identifier().toString())) continue;
|
||||
var cp = new ChunkPos(key.x() >> 4, key.z() >> 4);
|
||||
if (!full.contains(cp)) continue;
|
||||
var at = new BlockPos(key.x(), key.y(), key.z());
|
||||
helper.assertTrue(level.getBlockEntity(at) instanceof SpawnerBlockEntity, "Successful vanilla monster_room has its real spawner at " + at);
|
||||
rooms.add(Map.of("position", List.of(key.x(), key.y(), key.z()), "spawner", true));
|
||||
}
|
||||
report.put("structures", proofs); report.put("monster_rooms", rooms); report.put("full_chunks", full.size());
|
||||
report.put("mineshaft_found", found.keySet().stream().anyMatch(id -> id.contains("mineshaft")));
|
||||
report.put("swamp_hut_found", found.containsKey("minecraft:swamp_hut"));
|
||||
report.put("monster_room_found", !rooms.isEmpty());
|
||||
report.put("scope", "At most12 normal start candidates and24 FULL chunks; absence is reported, never replaced by synthetic fixtures");
|
||||
}
|
||||
private static void write(Map<String, Object> report) throws Exception {
|
||||
Files.createDirectories(OUT); Files.writeString(OUT.resolve("native.json"), JSON.toJson(report) + "\n");
|
||||
}
|
||||
private static void failure(Map<String, Object> report, Exception error) {
|
||||
report.put("passed", false); report.put("error", error.toString());
|
||||
try { Files.createDirectories(OUT); Files.writeString(OUT.resolve("failure.json"), JSON.toJson(report) + "\n"); }
|
||||
catch (Exception io) { error.addSuppressed(io); }
|
||||
SanctuaryMod.LOGGER.error("Native28 witness failed", error);
|
||||
}
|
||||
}
|
||||
+117
@@ -0,0 +1,117 @@
|
||||
package fr.koka.sanctuary.gametest;
|
||||
|
||||
import com.google.gson.GsonBuilder;
|
||||
import fr.koka.sanctuary.SanctuaryMod;
|
||||
import fr.koka.sanctuary.worldgen.PopulationIslandDensity;
|
||||
import fr.koka.sanctuary.worldgen.SanctuaryChunkGenerator;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.*;
|
||||
import net.fabricmc.fabric.api.gametest.v1.GameTest;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.registries.BuiltInRegistries;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.gametest.framework.GameTestHelper;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.world.level.chunk.status.ChunkStatus;
|
||||
import net.minecraft.world.level.levelgen.densityfunction.SamplerContext;
|
||||
|
||||
/** Bounded comparison of the real registered density, followed by inspection of ordinary FULL chunks. */
|
||||
public final class Caves28WorldGameTests {
|
||||
@GameTest(maxTicks = 3600)
|
||||
public void geologyAndNativeOres(GameTestHelper helper) throws Exception {
|
||||
var level = helper.getLevel();
|
||||
var generator = (SanctuaryChunkGenerator) level.getChunkSource().getGenerator();
|
||||
var binding = generator.starterContext();
|
||||
var field = (PopulationIslandDensity) level.registryAccess().lookupOrThrow(Registries.DENSITY_FUNCTION)
|
||||
.getOrThrow(ResourceKey.create(Registries.DENSITY_FUNCTION,
|
||||
SanctuaryMod.id("population_" + generator.capacity().players()))).value();
|
||||
helper.assertTrue(field.geology28(), "Current world uses the native-cave adaptation");
|
||||
var current = binding.random().getSampler(field);
|
||||
var previous = binding.random().getSampler(field.legacy27());
|
||||
var context = SamplerContext.builder().enableCaches().build();
|
||||
var report = new LinkedHashMap<String, Object>();
|
||||
report.put("seed", level.getSeed()); report.put("diameter", generator.initialDiameter());
|
||||
report.put("scope", "Registered density comparison; no synthetic terrain, water, ore or room is placed by the test");
|
||||
var heights = new ArrayList<List<Integer>>();
|
||||
long oldSolid = 0, remainingSolid = 0, excavated = 0, restored = 0;
|
||||
int min = -Math.min(320, generator.initialDiameter() / 2), max = -min, raised = 0, lowered = 0, largestUplift = 0, largestSubsidence = 0;
|
||||
for (int z = min; z <= max; z += 32) for (int x = min; x <= max; x += 32) {
|
||||
int oldTop = 0, newTop = 0;
|
||||
for (int y = 2; y <= 382; y += 2) {
|
||||
boolean old = previous.sampleValue(context, x, y, z) > 0;
|
||||
boolean now = current.sampleValue(context, x, y, z) > 0;
|
||||
if (old) oldTop = y; if (now) newTop = y;
|
||||
if (y >= 48 && y <= 202) {
|
||||
if (old) { oldSolid++; if (now) remainingSolid++; else excavated++; }
|
||||
else if (now) restored++;
|
||||
}
|
||||
if (Math.hypot(x, z) < 40 && y >= 240)
|
||||
helper.assertTrue(old == now, "High arrival neighbourhood remains unchanged");
|
||||
}
|
||||
if (newTop > oldTop && oldTop > 208) { raised++; largestUplift = Math.max(largestUplift, newTop - oldTop); }
|
||||
if (newTop < oldTop && oldTop > 240) { lowered++; largestSubsidence = Math.max(largestSubsidence, oldTop - newTop); }
|
||||
heights.add(List.of(x, z, oldTop, newTop));
|
||||
}
|
||||
report.put("heights", heights); report.put("old_solid_samples", oldSolid);
|
||||
report.put("remaining_solid_samples", remainingSolid); report.put("excavated_samples", excavated);
|
||||
report.put("restored_rift_samples", restored); report.put("raised_columns", raised);
|
||||
report.put("largest_uplift", largestUplift);
|
||||
report.put("lowered_columns", lowered); report.put("largest_subsidence", largestSubsidence);
|
||||
helper.assertTrue(excavated > 0 && remainingSolid > excavated, "New passages exist while most sampled former rock remains");
|
||||
helper.assertTrue(raised > 0, "Native volumetric deformation raises some uplands");
|
||||
var childField = (PopulationIslandDensity) level.registryAccess().lookupOrThrow(Registries.DENSITY_FUNCTION)
|
||||
.getOrThrow(ResourceKey.create(Registries.DENSITY_FUNCTION, SanctuaryMod.id("population_5"))).value();
|
||||
var child = new fr.koka.sanctuary.expansion.ExpansionIsland("geology_witness", "sanctuary", "east", "temperate",
|
||||
512, 2048, 0, level.getSeed(), "natural", "ready", 24);
|
||||
var childSettings = level.registryAccess().lookupOrThrow(Registries.NOISE_SETTINGS)
|
||||
.getOrThrow(ResourceKey.create(Registries.NOISE_SETTINGS, SanctuaryMod.id("population_5"))).value();
|
||||
var childState = net.minecraft.world.level.levelgen.RandomState.create(level.registryAccess().lookupOrThrow(Registries.NOISE), child.seed(), childSettings);
|
||||
var childNew = childState.getSampler(new fr.koka.sanctuary.expansion.ExpansionDensity(childField, child));
|
||||
var childOld = childState.getSampler(new fr.koka.sanctuary.expansion.ExpansionDensity(childField.legacy27(), child));
|
||||
long childExcavated = 0, childRemaining = 0;
|
||||
for (int z = -160; z <= 160; z += 32) for (int dx = -160; dx <= 160; dx += 32)
|
||||
for (int y = 48; y <= 202; y += 4) {
|
||||
if (childOld.sampleValue(context, 2048 + dx, y, z) > 0) {
|
||||
if (childNew.sampleValue(context, 2048 + dx, y, z) <= 0) childExcavated++;
|
||||
else childRemaining++;
|
||||
}
|
||||
}
|
||||
report.put("expansion_density", Map.of("diameter", 512, "excavated", childExcavated,
|
||||
"remaining", childRemaining, "reserved_or_opened", false));
|
||||
helper.assertTrue(childExcavated > 0 && childRemaining > childExcavated, "The actual expansion wrapper uses the same finite cave field");
|
||||
var sections = new LinkedHashMap<String, Object>();
|
||||
for (int z : new int[]{0, 96}) {
|
||||
var before = new ArrayList<String>(); var after = new ArrayList<String>();
|
||||
for (int y = 382; y >= 0; y -= 2) {
|
||||
var a = new StringBuilder(); var b = new StringBuilder();
|
||||
for (int x = min; x <= max; x += 2) {
|
||||
a.append(previous.sampleValue(context, x, y, z) > 0 ? '#' : '.');
|
||||
b.append(current.sampleValue(context, x, y, z) > 0 ? '#' : '.');
|
||||
}
|
||||
before.add(a.toString()); after.add(b.toString());
|
||||
}
|
||||
sections.put("z" + z, Map.of("before", before, "after", after, "min_x", min, "step", 2));
|
||||
}
|
||||
report.put("sections", sections);
|
||||
var ores = new TreeMap<String, Integer>(); var oreDepths = new TreeMap<String, List<Integer>>();
|
||||
for (int cx : new int[]{-6, 0, 6}) for (int cz : new int[]{-6, 0, 6}) {
|
||||
var chunk = level.getChunk(cx, cz, ChunkStatus.FULL, true);
|
||||
for (int x = 0; x < 16; x++) for (int z = 0; z < 16; z++) for (int y = 1; y < 383; y++) {
|
||||
var state = chunk.getBlockState(new BlockPos(cx * 16 + x, y, cz * 16 + z));
|
||||
String id = BuiltInRegistries.BLOCK.getKey(state.getBlock()).toString();
|
||||
if (id.endsWith("_ore")) {
|
||||
ores.merge(id, 1, Integer::sum);
|
||||
var band = oreDepths.computeIfAbsent(id, ignored -> new ArrayList<>(List.of(384, 0)));
|
||||
band.set(0, Math.min(band.get(0), y)); band.set(1, Math.max(band.get(1), y));
|
||||
}
|
||||
}
|
||||
}
|
||||
report.put("full_chunks_inspected", 9); report.put("ores", ores); report.put("ore_y_ranges", oreDepths);
|
||||
helper.assertTrue(!ores.isEmpty(), "Actual generated rock contains native ore deposits");
|
||||
report.put("passed", true);
|
||||
Path path = Path.of("diagnostics/alpha28/geology.json"); Files.createDirectories(path.getParent());
|
||||
Files.writeString(path, new GsonBuilder().setPrettyPrinting().create().toJson(report));
|
||||
helper.succeed();
|
||||
}
|
||||
}
|
||||
+3
-3
@@ -23,7 +23,7 @@ import net.minecraft.world.level.levelgen.densityfunction.DensitySampler;
|
||||
import net.minecraft.world.level.levelgen.densityfunction.DensityVolume;
|
||||
import net.minecraft.world.level.levelgen.densityfunction.SamplerContext;
|
||||
|
||||
/** Compares production against the frozen alpha.10 implementation; no chunks are created. */
|
||||
/** Compares the explicit legacy27 mode against frozen alpha10, not the intentional alpha28 terrain changes. */
|
||||
final class PopulationDensityEquivalenceDiagnostics {
|
||||
private record Pair(int players, long seed, DensitySampler reference, DensitySampler actual) {}
|
||||
|
||||
@@ -39,7 +39,7 @@ final class PopulationDensityEquivalenceDiagnostics {
|
||||
var key = SanctuaryMod.id("population_" + players);
|
||||
var registered = densities.getOrThrow(ResourceKey.create(Registries.DENSITY_FUNCTION, key)).value();
|
||||
helper.assertTrue(registered instanceof PopulationIslandDensity, "Compare the real registered Population density");
|
||||
var density = (PopulationIslandDensity) registered;
|
||||
var density = ((PopulationIslandDensity) registered).legacy27();
|
||||
helper.assertTrue(ExactInterpolationSampler.supportsTerrain(density.terrain()),
|
||||
"The shipped coordinate-only terrain must actually use the exact cache, not the fallback");
|
||||
var reference = new Alpha10PopulationDensityReference(density.terrain(), density.distortion(), density.sculpt(),
|
||||
@@ -113,7 +113,7 @@ final class PopulationDensityEquivalenceDiagnostics {
|
||||
"reference_commit", "a26b72f", "reference_source_sha256", "fcc5a8a39b7c4d181d74ce4f9183a1ca93646137955ac64bab9d22e6db0752a1",
|
||||
"comparison", "Float.floatToRawIntBits; no tolerance", "profiles", reports, "adversarial", adversarial,
|
||||
"concurrent_comparisons", concurrentComparisons, "concurrent_workers", 3, "passed", true,
|
||||
"scope", "Real compiled scalar/bulk densities, 4 capacities x 3 seeds (5/20/100 compatibility and new10 using the same parameterized algorithm), signed coordinates, region/coast/height boundaries, context switches and concurrency; no block changes")) + "\n");
|
||||
"scope", "Explicit legacy27 mode versus frozen alpha10: real compiled scalar/bulk densities, 4 capacities x 3 seeds, signed coordinates, region/coast/height boundaries, context switches and concurrency. Geology28 is intentionally disabled only for this historical equivalence proof; no alpha28 generation claim.")) + "\n");
|
||||
}
|
||||
|
||||
private static List<DensityVolume> volumes(IslandCapacity capacity) {
|
||||
|
||||
+5
-1
@@ -87,6 +87,7 @@ public final class PopulationDiagnostics {
|
||||
ResourceKey.create(Registries.DENSITY_FUNCTION, SanctuaryMod.id("population_" + capacity.players()))).value();
|
||||
helper.assertTrue(density instanceof PopulationIslandDensity, "Inspect the actual compiled population density");
|
||||
var island = (PopulationIslandDensity) density;
|
||||
var legacyEnvelope = island.legacy27();
|
||||
helper.assertTrue(island.capacity() == capacity, "Noise settings and density must agree on saved capacity");
|
||||
var random = binding.random();
|
||||
var replay = RandomState.create(level.registryAccess().lookupOrThrow(Registries.NOISE), random.seed(), generator.generatorSettings().value());
|
||||
@@ -102,7 +103,10 @@ public final class PopulationDiagnostics {
|
||||
float value = random.sampleBlockValueUncached(density, x, y, z);
|
||||
float raw = random.sampleBlockValueUncached(island.terrain(), x, y - 64, z);
|
||||
helper.assertTrue(Float.isFinite(value), "Density remains finite at every sampled position");
|
||||
helper.assertTrue(value <= 0 || raw > 0, "Enlarging the envelope must never invent terrain inside natural holes");
|
||||
float historical = island.geology28()
|
||||
? random.sampleBlockValueUncached(legacyEnvelope, x, y, z) : value;
|
||||
helper.assertTrue(historical <= 0 || raw > 0,
|
||||
"The explicit legacy27 envelope never fills its raw natural holes; alpha28 uplands change sample Y");
|
||||
if (value > 0) { if (bottom < 0) bottom = y; top = y; }
|
||||
if (x % (step * 8) == 0 && z % (step * 8) == 0)
|
||||
helper.assertTrue(Float.floatToIntBits(value) == Float.floatToIntBits(replay.sampleBlockValueUncached(density, x, y, z)),
|
||||
|
||||
+247
@@ -0,0 +1,247 @@
|
||||
package fr.koka.sanctuary.gametest;
|
||||
|
||||
import com.google.gson.GsonBuilder;
|
||||
import fr.koka.sanctuary.SanctuaryMod;
|
||||
import fr.koka.sanctuary.worldgen.PopulationGenerationContext;
|
||||
import fr.koka.sanctuary.worldgen.PopulationHydrology;
|
||||
import fr.koka.sanctuary.worldgen.PopulationHydrologyRuntime;
|
||||
import fr.koka.sanctuary.worldgen.IslandCapacity;
|
||||
import fr.koka.sanctuary.worldgen.SanctuaryBiomeSource;
|
||||
import fr.koka.sanctuary.worldgen.SanctuaryChunkGenerator;
|
||||
import fr.koka.sanctuary.worldgen.Swamp27Volumes;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.*;
|
||||
import net.fabricmc.fabric.api.gametest.v1.GameTest;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.registries.BuiltInRegistries;
|
||||
import net.minecraft.gametest.framework.GameTestHelper;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.tags.FluidTags;
|
||||
import net.minecraft.world.level.ChunkPos;
|
||||
import net.minecraft.world.level.NoiseColumn;
|
||||
import net.minecraft.world.level.biome.BiomeResolver;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.chunk.status.ChunkStatus;
|
||||
|
||||
/** One bounded native observation. Never places blocks, entities, or synthetic water. */
|
||||
public final class Swamp27WorldGameTests {
|
||||
private static final com.google.gson.Gson JSON = new GsonBuilder().setPrettyPrinting().create();
|
||||
private static final Path OUT = Path.of("diagnostics/swamp27");
|
||||
private static final int MAX_COLUMNS = 256;
|
||||
private static final int MAX_FULL_CHUNKS = 8;
|
||||
|
||||
@GameTest(maxTicks = 2400)
|
||||
public void nativeUndergroundSwamp(GameTestHelper helper) throws Exception {
|
||||
var report = new LinkedHashMap<String, Object>();
|
||||
long started = System.nanoTime();
|
||||
try {
|
||||
ServerLevel level = helper.getLevel();
|
||||
var generator = (SanctuaryChunkGenerator) level.getChunkSource().getGenerator();
|
||||
report.put("seed", level.getSeed()); report.put("diameter", generator.initialDiameter());
|
||||
report.put("generator", generator.rootGeneration()); report.put("process_id", ProcessHandle.current().pid());
|
||||
report.put("generate_structures", level.structureManager().shouldGenerateStructures());
|
||||
report.put("experimental", generator.experimentalStructures());
|
||||
helper.assertTrue(generator.rootGeneration() == 24, "Alpha27 uses the restored24 terrain generator");
|
||||
var binding = Objects.requireNonNull(PopulationGenerationContext.of(level));
|
||||
var resolver = generator.source().createResolver(binding.random().createClimateSampler(net.minecraft.world.level.levelgen.densityfunction.SamplerContext.builder().enableCaches().build()));
|
||||
var candidates = waterCandidates(helper, binding, resolver, report);
|
||||
int columns = 0;
|
||||
// Deterministic rings visit the middle of distinct chunks, with a strict sampler budget.
|
||||
int radius = Math.min(8, generator.initialDiameter() / 48);
|
||||
search: for (int ring = 0; ring <= radius; ring++)
|
||||
for (int cx = -ring; cx <= ring; cx++) for (int cz = -ring; cz <= ring; cz++) {
|
||||
if (Math.max(Math.abs(cx), Math.abs(cz)) != ring) continue;
|
||||
if (columns++ >= MAX_COLUMNS) break search;
|
||||
int x = cx * 16 + 8, z = cz * 16 + 8;
|
||||
NoiseColumn column = binding.generator().getBaseColumn(x, z, level, binding.random());
|
||||
for (int y = Swamp27Volumes.MAX_Y - 2; y >= Swamp27Volumes.MIN_Y; y--) {
|
||||
if (!column.getBlock(y).isAir() || !column.getBlock(y + 1).isAir()
|
||||
|| !natural(column.getBlock(y - 1))) continue;
|
||||
if (!resolver.getNoiseBiome(x >> 2, y >> 2, z >> 2).is(SanctuaryBiomeSource.SWAMP27)) continue;
|
||||
int roof = roof(column, y);
|
||||
if (roof < 0) continue;
|
||||
candidates.putIfAbsent(new ChunkPos(cx, cz), new BlockPos(x, y, z));
|
||||
break;
|
||||
}
|
||||
if (candidates.size() == MAX_FULL_CHUNKS) break search;
|
||||
}
|
||||
report.put("sampled_columns", Math.min(columns, MAX_COLUMNS));
|
||||
report.put("candidate_positions", candidates.values().stream().map(Swamp27WorldGameTests::xyz).toList());
|
||||
helper.assertTrue(!candidates.isEmpty(), "The bounded biome/terrain scan finds a real cave floor below rock");
|
||||
var pending = candidates.keySet().iterator();
|
||||
var loaded = new ArrayList<ChunkPos>();
|
||||
Runnable[] step = new Runnable[1];
|
||||
step[0] = () -> {
|
||||
try {
|
||||
if (pending.hasNext()) {
|
||||
var chunk = pending.next(); report.put("stage", "FULL " + chunk);
|
||||
level.getChunk(chunk.x(), chunk.z(), ChunkStatus.FULL, true); loaded.add(chunk);
|
||||
helper.runAtTickTime(helper.getTick() + 1, () -> step[0].run()); return;
|
||||
}
|
||||
report.put("stage", "inspect actual cave floors and vegetation");
|
||||
inspect(helper, level, loaded, report);
|
||||
report.put("explicit_full_chunks", loaded.size());
|
||||
report.put("full_chunk_limit", MAX_FULL_CHUNKS);
|
||||
report.put("elapsed_seconds", (System.nanoTime() - started) / 1_000_000_000.0);
|
||||
report.put("passed", true); write(OUT.resolve("native.json"), report); helper.succeed();
|
||||
} catch (Exception error) {
|
||||
failure(report, error); throw new IllegalStateException("Swamp27 native witness failed", error);
|
||||
}
|
||||
};
|
||||
helper.runAtTickTime(helper.getTick() + 1, () -> step[0].run());
|
||||
} catch (Exception error) { failure(report, error); throw error; }
|
||||
}
|
||||
|
||||
private static LinkedHashMap<ChunkPos, BlockPos> waterCandidates(GameTestHelper helper,
|
||||
PopulationGenerationContext.Binding binding, BiomeResolver resolver, Map<String, Object> report) {
|
||||
int before = PopulationHydrologyRuntime.cachedRegionCount(binding.random());
|
||||
var plan = PopulationHydrologyRuntime.regionPlan(binding.generator(), binding.random(), new IslandCapacity.Region(0, 0)).water();
|
||||
int after = PopulationHydrologyRuntime.cachedRegionCount(binding.random());
|
||||
helper.assertTrue(before == after, "The water witness reuses the already cached central region");
|
||||
var undergroundIds = new HashSet<Long>();
|
||||
plan.features().stream().filter(feature -> feature.kind() == PopulationHydrology.Kind.TERRACE)
|
||||
.forEach(feature -> undergroundIds.add(feature.id()));
|
||||
var allY = new TreeMap<Integer, Integer>(); var lowerY = new TreeMap<Integer, Integer>();
|
||||
var byChunk = new HashMap<ChunkPos, Integer>(); var positions = new HashMap<ChunkPos, BlockPos>();
|
||||
int intersections = 0, lowerIntersections = 0, waterTopIntersections = 0;
|
||||
for (var cell : plan.cells()) {
|
||||
if (!cell.hasWater()) continue;
|
||||
allY.merge(cell.waterY(), 1, Integer::sum);
|
||||
boolean lower = undergroundIds.contains(cell.featureId());
|
||||
if (lower) lowerY.merge(cell.waterY(), 1, Integer::sum);
|
||||
boolean inWater = resolver.getNoiseBiome(cell.x() >> 2, cell.waterY() >> 2, cell.z() >> 2).is(SanctuaryBiomeSource.SWAMP27);
|
||||
boolean aboveWater = resolver.getNoiseBiome(cell.x() >> 2, (cell.waterY() + 1) >> 2, cell.z() >> 2).is(SanctuaryBiomeSource.SWAMP27);
|
||||
if (inWater) { intersections++; if (lower) lowerIntersections++; }
|
||||
if (aboveWater) waterTopIntersections++;
|
||||
if (!lower || !inWater && !aboveWater) continue;
|
||||
var chunk = new ChunkPos(cell.x() >> 4, cell.z() >> 4);
|
||||
byChunk.merge(chunk, 1, Integer::sum);
|
||||
positions.putIfAbsent(chunk, new BlockPos(cell.x(), cell.waterY() + 1, cell.z()));
|
||||
}
|
||||
var selected = new LinkedHashMap<ChunkPos, BlockPos>();
|
||||
byChunk.keySet().stream().sorted(Comparator.<ChunkPos>comparingInt(chunk -> byChunk.get(chunk)).reversed()
|
||||
.thenComparingInt(ChunkPos::x).thenComparingInt(ChunkPos::z)).limit(4)
|
||||
.forEach(chunk -> selected.put(chunk, positions.get(chunk)));
|
||||
var evidence = new LinkedHashMap<String, Object>();
|
||||
evidence.put("region", List.of(0, 0)); evidence.put("new_regions_planned", after - before);
|
||||
evidence.put("all_water_columns_by_surface_y", allY);
|
||||
evidence.put("underground_terrace_water_columns_by_surface_y", lowerY);
|
||||
evidence.put("swamp_band", List.of(Swamp27Volumes.MIN_Y, Swamp27Volumes.MAX_Y - 1));
|
||||
evidence.put("water_surface_biome_intersections", intersections);
|
||||
evidence.put("underground_water_surface_biome_intersections", lowerIntersections);
|
||||
evidence.put("above_water_biome_intersections", waterTopIntersections);
|
||||
evidence.put("prioritized_chunks", selected.keySet().stream().map(chunk -> List.of(chunk.x(), chunk.z())).toList());
|
||||
evidence.put("scope", "Existing central region only; zeros are observations, not a required water quota");
|
||||
report.put("cached_water_selection", evidence);
|
||||
return selected;
|
||||
}
|
||||
|
||||
private static void inspect(GameTestHelper helper, ServerLevel level, List<ChunkPos> chunks,
|
||||
Map<String, Object> report) throws Exception {
|
||||
int floors = 0, covered = 0, plants = 0, water = 0;
|
||||
BlockPos witness = null; int bestScore = -1, witnessRoof = -1;
|
||||
var counts = new TreeMap<String, Integer>();
|
||||
var byChunk = new ArrayList<Map<String, Object>>();
|
||||
for (var chunk : chunks) {
|
||||
int chunkFloors = 0, chunkPlants = 0, chunkWater = 0;
|
||||
for (int x = chunk.x() * 16; x < chunk.x() * 16 + 16; x++)
|
||||
for (int z = chunk.z() * 16; z < chunk.z() * 16 + 16; z++)
|
||||
for (int y = Swamp27Volumes.MIN_Y; y < Swamp27Volumes.MAX_Y; y++) {
|
||||
var at = new BlockPos(x, y, z);
|
||||
if (!level.getBiome(at).is(SanctuaryBiomeSource.SWAMP27)) continue;
|
||||
var state = level.getBlockState(at);
|
||||
if (state.getFluidState().is(FluidTags.WATER)) { water++; chunkWater++; }
|
||||
if (plant(state)) {
|
||||
// A feature must obey native placement; old vanilla vines elsewhere are irrelevant.
|
||||
if (!state.is(Blocks.VINE)) helper.assertTrue(state.canSurvive(level, at), "Native swamp plant retains its support: " + at + " " + state);
|
||||
plants++; chunkPlants++; counts.merge(id(state), 1, Integer::sum);
|
||||
}
|
||||
if ((!state.isAir() && !plant(state)) || !state.getFluidState().isEmpty()) continue;
|
||||
var ground = level.getBlockState(at.below());
|
||||
if (!natural(ground)) continue;
|
||||
int ceiling = roof(level, at);
|
||||
if (ceiling < 0 || !level.getBlockState(at.above()).getCollisionShape(level, at.above()).isEmpty()) continue;
|
||||
floors++; chunkFloors++;
|
||||
boolean soil = ground.is(Blocks.MOSS_BLOCK) || ground.is(Blocks.GRASS_BLOCK) || ground.is(Blocks.MUD);
|
||||
if (soil) covered++;
|
||||
int score = (waterNearby(level, at.below(), chunk) ? 40 : 0)
|
||||
+ (plant(state) ? 20 : 0) + (soil ? 5 : 0) - Math.max(0, ceiling - y - 24);
|
||||
// Keep the export within this already loaded chunk, without another request.
|
||||
if (score > bestScore && (x & 15) >= 3 && (x & 15) <= 12 && (z & 15) >= 3 && (z & 15) <= 12) {
|
||||
witness = at; bestScore = score; witnessRoof = ceiling;
|
||||
}
|
||||
}
|
||||
byChunk.add(Map.of("chunk", List.of(chunk.x(), chunk.z()), "cave_floors", chunkFloors,
|
||||
"plants", chunkPlants, "water_blocks", chunkWater));
|
||||
}
|
||||
report.put("native_cave_floors", floors); report.put("covered_floors", covered);
|
||||
report.put("plants", plants); report.put("plant_counts", counts); report.put("water_blocks", water);
|
||||
report.put("chunks", byChunk);
|
||||
helper.assertTrue(floors > 0 && covered > 0, "FULL contains the new swamp biome over actual grass/moss/mud cave floors with a rock ceiling");
|
||||
helper.assertTrue(plants > 0, "The bounded FULL witness contains naturally generated swamp vegetation");
|
||||
helper.assertTrue(witness != null, "A visitable cave witness exists inside the loaded footprint");
|
||||
report.put("visit", xyz(witness)); report.put("ceiling_y", witnessRoof);
|
||||
report.put("teleport", "/tp @s " + (witness.getX() + .5) + " " + witness.getY() + " " + (witness.getZ() + .5));
|
||||
export(level, witness, witnessRoof, report);
|
||||
}
|
||||
|
||||
private static void export(ServerLevel level, BlockPos witness, int ceiling, Map<String, Object> report) throws Exception {
|
||||
int minX = (witness.getX() >> 4) * 16, minZ = (witness.getZ() >> 4) * 16;
|
||||
int minY = Math.max(0, witness.getY() - 3), maxY = Math.min(ceiling + 1, witness.getY() + 32);
|
||||
var blocks = new ArrayList<List<Object>>(); var cutaway = new ArrayList<List<Object>>();
|
||||
for (int x = minX; x < minX + 16; x++) for (int z = minZ; z < minZ + 16; z++) for (int y = minY; y <= maxY; y++) {
|
||||
BlockState state = level.getBlockState(new BlockPos(x, y, z));
|
||||
if (state.isAir()) continue;
|
||||
List<Object> block = List.of(x - minX, y - minY, z - minZ, id(state)); blocks.add(block);
|
||||
// A documented display cut only; the actual world and the full export stay intact.
|
||||
if (y < witness.getY() || z >= witness.getZ() && y < ceiling) cutaway.add(block);
|
||||
}
|
||||
var origin = List.of(minX, minY, minZ); var size = List.of(16, maxY - minY + 1, 16);
|
||||
write(OUT.resolve("blocks.json"), List.of(
|
||||
Map.of("id", "swamp27_native_full", "size", size, "origin", origin, "blocks", blocks),
|
||||
Map.of("id", "swamp27_native_cutaway", "size", size, "origin", origin, "blocks", cutaway)));
|
||||
report.put("export", Map.of("path", "diagnostics/swamp27/blocks.json", "origin", origin, "size", size,
|
||||
"blocks", blocks.size(), "cutaway_blocks", cutaway.size(), "source", "actual FULL blocks; no test-authored terrain"));
|
||||
}
|
||||
|
||||
private static int roof(NoiseColumn column, int y) {
|
||||
for (int top = y + 3; top <= Math.min(280, y + 96); top++) if (natural(column.getBlock(top))) return top;
|
||||
return -1;
|
||||
}
|
||||
private static boolean waterNearby(ServerLevel level, BlockPos floor, ChunkPos chunk) {
|
||||
for (int dx = -2; dx <= 2; dx++) for (int dz = -2; dz <= 2; dz++) {
|
||||
var at = floor.offset(dx, 0, dz);
|
||||
if (at.getX() >> 4 != chunk.x() || at.getZ() >> 4 != chunk.z()) continue;
|
||||
if (level.getFluidState(at).is(FluidTags.WATER)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
private static int roof(ServerLevel level, BlockPos at) {
|
||||
for (int y = at.getY() + 3; y <= Math.min(280, at.getY() + 96); y++)
|
||||
if (natural(level.getBlockState(at.atY(y)))) return y;
|
||||
return -1;
|
||||
}
|
||||
private static boolean plant(BlockState state) {
|
||||
return state.is(Blocks.SHORT_GRASS) || state.is(Blocks.FIREFLY_BUSH) || state.is(Blocks.SUGAR_CANE)
|
||||
|| state.is(Blocks.LILY_PAD) || state.is(Blocks.VINE);
|
||||
}
|
||||
private static boolean natural(BlockState state) {
|
||||
return state.is(Blocks.STONE) || state.is(Blocks.DEEPSLATE) || state.is(Blocks.TUFF) || state.is(Blocks.CALCITE)
|
||||
|| state.is(Blocks.ANDESITE) || state.is(Blocks.GRANITE) || state.is(Blocks.DIORITE) || state.is(Blocks.DIRT)
|
||||
|| state.is(Blocks.COARSE_DIRT) || state.is(Blocks.GRASS_BLOCK) || state.is(Blocks.ROOTED_DIRT)
|
||||
|| state.is(Blocks.PODZOL) || state.is(Blocks.MYCELIUM) || state.is(Blocks.MOSS_BLOCK)
|
||||
|| state.is(Blocks.MUD) || state.is(Blocks.PACKED_MUD) || state.is(Blocks.CLAY);
|
||||
}
|
||||
private static List<Integer> xyz(BlockPos at) { return List.of(at.getX(), at.getY(), at.getZ()); }
|
||||
private static String id(BlockState state) { return BuiltInRegistries.BLOCK.getKey(state.getBlock()).toString(); }
|
||||
private static void write(Path path, Object value) throws Exception {
|
||||
Files.createDirectories(path.getParent()); Files.writeString(path, JSON.toJson(value) + "\n");
|
||||
}
|
||||
private static void failure(Map<String, Object> report, Exception error) {
|
||||
report.put("passed", false); report.put("error", error.toString());
|
||||
try { write(OUT.resolve("failure.json"), report); } catch (Exception io) { error.addSuppressed(io); }
|
||||
SanctuaryMod.LOGGER.error("Swamp27 native witness failed", error);
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,7 @@
|
||||
"environment": "*",
|
||||
"license": "GPL-3.0-or-later",
|
||||
"entrypoints": {
|
||||
"fabric-gametest": ["${gametest_entrypoint}", "fr.koka.sanctuary.gametest.BlockKnowledgeGameTests", "fr.koka.sanctuary.gametest.CensusGameTestsTerrain", "fr.koka.sanctuary.gametest.CensusStockGameTests", "fr.koka.sanctuary.gametest.MaterialActivityGameTests"],
|
||||
"fabric-gametest": ["${gametest_entrypoint}", "fr.koka.sanctuary.gametest.Swamp27WorldGameTests", "fr.koka.sanctuary.gametest.Caves28WorldGameTests", "fr.koka.sanctuary.gametest.Alpha28NativeStructuresGameTests", "fr.koka.sanctuary.gametest.BlockKnowledgeGameTests", "fr.koka.sanctuary.gametest.CensusGameTestsTerrain", "fr.koka.sanctuary.gametest.CensusStockGameTests", "fr.koka.sanctuary.gametest.MaterialActivityGameTests"],
|
||||
"fabric-client-gametest": ["fr.koka.sanctuary.gametest.SanctuaryClientRenderTests"]
|
||||
},
|
||||
"mixins": ["sanctuary-gametest.mixins.json"],
|
||||
|
||||
@@ -86,6 +86,7 @@ public final class SanctuaryMod implements ModInitializer {
|
||||
fr.koka.sanctuary.worldgen.ruins.RuinsStructures17.register();
|
||||
Registry.register(BuiltInRegistries.DENSITY_FUNCTION_TYPE, id("population_island"), PopulationIslandDensity.CODEC);
|
||||
Registry.register(BuiltInRegistries.BIOME_SOURCE, id("population_island"), PopulationIslandBiomeSource.CODEC);
|
||||
Registry.register(BuiltInRegistries.FEATURE_TYPE, id("swamp27_decoration"), fr.koka.sanctuary.worldgen.Swamp27Decoration.CODEC);
|
||||
Registry.register(BuiltInRegistries.FEATURE_TYPE, id("population_groves"), PopulationGrovesFeature.CODEC);
|
||||
Registry.register(BuiltInRegistries.FEATURE_TYPE, id("population_decorations"), PopulationDecorationsFeature.CODEC);
|
||||
Registry.register(BuiltInRegistries.FEATURE_TYPE, id("population_shore_sugar_cane"), PopulationShoreSugarCaneFeature.CODEC);
|
||||
|
||||
+10
-2
@@ -146,9 +146,17 @@ public class ExpansionChunkGenerator extends ChunkGenerator {
|
||||
}
|
||||
@Override public void applyBiomeDecoration(net.minecraft.world.level.WorldGenLevel level, ChunkAccess chunk, StructureManager structures) {
|
||||
var island = islandAt(chunk.getPos().getMiddleBlockX(), chunk.getPos().getMiddleBlockZ());
|
||||
if (!ExpansionOcean23.active(island)) { super.applyBiomeDecoration(level, chunk, structures); return; }
|
||||
fr.koka.sanctuary.worldgen.IslandOres28.place(level, this, chunk, island);
|
||||
if (!ExpansionOcean23.active(island)) {
|
||||
super.applyBiomeDecoration(level, chunk, structures);
|
||||
fr.koka.sanctuary.worldgen.CavityMonsterRoom28.decorate(level, this, chunk);
|
||||
return;
|
||||
}
|
||||
Integer previous = ExpansionOcean23.enterDecoration(ExpansionOcean23.seaLevel(island));
|
||||
try { super.applyBiomeDecoration(level, chunk, structures); }
|
||||
try {
|
||||
super.applyBiomeDecoration(level, chunk, structures);
|
||||
fr.koka.sanctuary.worldgen.CavityMonsterRoom28.decorate(level, this, chunk);
|
||||
}
|
||||
finally { ExpansionOcean23.restoreDecoration(previous); }
|
||||
}
|
||||
@Override public int getSpawnHeight(LevelHeightAccessor level) { return 220; }
|
||||
|
||||
@@ -49,7 +49,11 @@ public final class ExpansionRelief {
|
||||
public static float sculpt(ExpansionIsland island, Column column, double y, float source) {
|
||||
if (source <= 0) return source;
|
||||
double reference = referenceY(island, y);
|
||||
double canyon = 2.1 * column.canyonWeight() * smoothstep((reference - 120.0) / 34.0);
|
||||
double canyonHeight = island.generation() == 24
|
||||
? smoothstep((reference - 126 - column.uplift() * .35) / 16)
|
||||
* smoothstep((226 + column.uplift() * .35 - reference) / 18)
|
||||
: smoothstep((reference - 120.0) / 34.0);
|
||||
double canyon = 2.1 * column.canyonWeight() * canyonHeight;
|
||||
double crater = 2.1 * column.craterWeight() * smoothstep((reference - CRATER_FLOOR) / 14.0);
|
||||
return (float) Math.clamp(source - Math.max(canyon, crater), -1.0, 1.0);
|
||||
}
|
||||
|
||||
+61
-12
@@ -169,6 +169,15 @@ public final class ExpansionStructures23 {
|
||||
if (centre.top < 32) return null;
|
||||
var sampler = random.createClimateSampler(SamplerContext.builder().enableCaches().build());
|
||||
var resolver = view.getBiomeSource().createResolver(sampler);
|
||||
var hutHeights = new ArrayList<Integer>();
|
||||
// The biome is genuinely three dimensional: a swamp bank can be below another island floor.
|
||||
for (int y = Math.min(centre.top + 1, 280); y >= 48 && hutHeights.size() < 4; y--) {
|
||||
var floor = centre.column.getBlock(y - 1);
|
||||
if ((!floor.isSolid() && !floor.getFluidState().is(net.minecraft.tags.FluidTags.WATER))
|
||||
|| !centre.column.getBlock(y).isAir() || !centre.column.getBlock(y + 1).isAir()) continue;
|
||||
if (!host.structureBiome(resolver.getNoiseBiome(x >> 2, y >> 2, z >> 2)).is(net.minecraft.world.level.biome.Biomes.SWAMP)) continue;
|
||||
if (hutHeights.isEmpty() || hutHeights.getLast() - y >= 8) hutHeights.add(y);
|
||||
}
|
||||
int undergroundY = Math.clamp(centre.top - 60, 50, 210);
|
||||
Holder<Biome> surfaceBiome = host.structureBiome(resolver.getNoiseBiome(x >> 2, centre.top >> 2, z >> 2));
|
||||
Holder<Biome> undergroundBiome = host.structureBiome(resolver.getNoiseBiome(x >> 2, undergroundY >> 2, z >> 2));
|
||||
@@ -182,7 +191,8 @@ public final class ExpansionStructures23 {
|
||||
if (holder == null) continue;
|
||||
Holder<Biome> biome = entry.habitat() == Habitat.UNDERGROUND ? undergroundBiome
|
||||
: entry.habitat() == Habitat.OCEAN ? waterBiome : surfaceBiome;
|
||||
if (!holder.value().biomes().contains(biome)) continue;
|
||||
if (entry.family().equals("swamp_hut")) { if (hutHeights.isEmpty()) continue; }
|
||||
else if (!holder.value().biomes().contains(biome)) continue;
|
||||
if (entry.habitat() == Habitat.OCEAN && !ExpansionOcean23.column(island, x, z).water()) continue;
|
||||
long salt = mix(candidate.roll() ^ entry.id().hashCode() * 0x9e3779b97f4a7c15L);
|
||||
double unit = ((salt >>> 11) + 1.0) * 0x1.0p-53;
|
||||
@@ -196,10 +206,14 @@ public final class ExpansionStructures23 {
|
||||
checkDeadline(deadlineNanos);
|
||||
Structure original = choice.holder().value();
|
||||
Entry entry = choice.entry();
|
||||
int variants = entry.habitat() == Habitat.UNDERGROUND || entry.family().equals("ruined_portal") ? 3 : 1;
|
||||
boolean hut = entry.family().equals("swamp_hut");
|
||||
int variants = hut ? hutHeights.size() : entry.habitat() == Habitat.UNDERGROUND || entry.family().equals("ruined_portal") ? 3 : 1;
|
||||
for (int vertical = 0; vertical < variants; vertical++) {
|
||||
checkDeadline(deadlineNanos);
|
||||
int targetY = undergroundY - vertical * 20;
|
||||
int targetY = hut ? hutHeights.get(vertical) : undergroundY - vertical * 20;
|
||||
ExpansionChunkGenerator nativeView = hut && view instanceof NativeView local
|
||||
? local.atHeight(targetY + 1) : view;
|
||||
GroundSampler nativeGround = hut ? new GroundSampler(nativeView, chunk, random, deadlineNanos, targetY + 1) : ground;
|
||||
Structure generated = original;
|
||||
if (entry.family().equals("trial_chambers")) generated = withStartHeight(original, registries, targetY);
|
||||
Predicate<Holder<Biome>> valid = b -> original.biomes().contains(host.structureBiome(b));
|
||||
@@ -207,18 +221,18 @@ public final class ExpansionStructures23 {
|
||||
if (original instanceof StrongholdStructure) {
|
||||
// Vanilla stronghold piece weights are static mutable state.
|
||||
synchronized (StrongholdStructure.class) {
|
||||
start = generated.generate(choice.holder(), dimension, registries, view, view.getBiomeSource(), sampler,
|
||||
start = generated.generate(choice.holder(), dimension, registries, nativeView, view.getBiomeSource(), sampler,
|
||||
random, templates, island.seed(), candidate.chunk(), 0, chunk, valid);
|
||||
}
|
||||
} else {
|
||||
start = generated.generate(choice.holder(), dimension, registries, view, view.getBiomeSource(), sampler,
|
||||
start = generated.generate(choice.holder(), dimension, registries, nativeView, view.getBiomeSource(), sampler,
|
||||
random, templates, island.seed(), candidate.chunk(), 0, chunk, valid);
|
||||
}
|
||||
if (!start.isValid()) { diagnostic(island, candidate, "native-refused", entry.id()); continue; }
|
||||
// Only buried portal variants need the alternative underground elevations.
|
||||
if (vertical > 0 && entry.family().equals("ruined_portal") && !undergroundPortal(start)) break;
|
||||
start = adjustNativeStart(original, start, entry, island, targetY, ground);
|
||||
String rejection = supportRejection(host, island, entry, start, ground);
|
||||
start = adjustNativeStart(original, start, entry, island, targetY, nativeGround);
|
||||
String rejection = supportRejection(host, island, entry, start, nativeGround);
|
||||
if (rejection != null) { diagnostic(island, candidate, rejection, entry.id()); continue; }
|
||||
((NativeStructureMetadata23) (Object) start).sanctuary$native23(new NativeStructureMetadata23.Data(island.seed(), seaLevel));
|
||||
return new Accepted(choice.holder(), start);
|
||||
@@ -333,17 +347,35 @@ public final class ExpansionStructures23 {
|
||||
|| city.intersectsCityConstruction15(total.minX() - 2, total.minY() - 12, total.minZ() - 2, total.maxX() + 2, total.maxY() + 4, total.maxZ() + 2)
|
||||
|| city.intersectsCityConstruction16(total.minX() - 2, total.minY() - 12, total.minZ() - 2, total.maxX() + 2, total.maxY() + 4, total.maxZ() + 2)
|
||||
|| city.intersectsExperimentalStructures(total.minX() - 2, total.minY() - 12, total.minZ() - 2, total.maxX() + 2, total.maxY() + 4, total.maxZ() + 2)) return "reserved-secret";
|
||||
if (island.origin()) {
|
||||
if (island.origin() && !entry.family().equals("swamp_hut")) {
|
||||
var binding = city.starterContext();
|
||||
if (fr.koka.sanctuary.worldgen.PopulationHydrologyRuntime.protects(binding.generator(), binding.random(),
|
||||
total.getCenter().atY(total.minY()), Math.max(total.getXSpan(), total.getZSpan()) / 2 + 3, total.getYSpan() + 16, 26)) return "protected-hydrology";
|
||||
}
|
||||
}
|
||||
if (entry.family().equals("swamp_hut")) {
|
||||
if (host instanceof SanctuaryChunkGenerator sanctuary && island.origin()) {
|
||||
var binding = sanctuary.starterContext();
|
||||
var box = new BoundingBox(total.minX(), total.minY() - SwampHutPlacement28.MAX_PIER_DROP - 2,
|
||||
total.minZ(), total.maxX(), total.maxY(), total.maxZ());
|
||||
for (var region : fr.koka.sanctuary.worldgen.PopulationHydrologyRuntime.plansIntersecting(
|
||||
binding.generator(), binding.random(), total.minX() - 16, total.minZ() - 16, total.maxX() + 16, total.maxZ() + 16)) {
|
||||
String refusal = SwampHutPlacement28.hydrologyRejection(region, box);
|
||||
if (refusal != null) return refusal;
|
||||
}
|
||||
}
|
||||
return SwampHutPlacement28.rejection(start, (x, z) -> ground.at(x, z).column);
|
||||
}
|
||||
boolean underground = entry.habitat() == Habitat.UNDERGROUND || undergroundPortal(start);
|
||||
boolean aquatic = entry.habitat() == Habitat.OCEAN;
|
||||
int siteMin = Integer.MAX_VALUE, siteMax = Integer.MIN_VALUE;
|
||||
for (StructurePiece piece : start.getPieces()) {
|
||||
BoundingBox box = piece.getBoundingBox();
|
||||
if (entry.family().equals("mineshaft")
|
||||
&& piece instanceof net.minecraft.world.level.levelgen.structure.structures.MineshaftPieces.MineShaftCorridor) {
|
||||
if (!MineshaftSupport28.anchored(piece, (x, z) -> ground.at(x, z).column)) return "mineshaft-no-native-support";
|
||||
continue;
|
||||
}
|
||||
int low = Integer.MAX_VALUE, high = Integer.MIN_VALUE, supported = 0, sampled = 0;
|
||||
boolean buriedPiece = underground || !aquatic && box.maxY() < ground.at(box.getCenter().getX(), box.getCenter().getZ()).top - 6;
|
||||
int step = buriedPiece ? 8 : 4;
|
||||
@@ -474,7 +506,7 @@ public final class ExpansionStructures23 {
|
||||
}
|
||||
private static int weight(String family) {
|
||||
return switch (family) { case "camp" -> 6; case "ruined_portal", "mineshaft", "ocean_ruin" -> 3;
|
||||
case "village", "shipwreck", "trail_ruins", "buried_treasure" -> 2; default -> 1; };
|
||||
case "village", "shipwreck", "trail_ruins", "buried_treasure", "swamp_hut" -> 2; default -> 1; };
|
||||
}
|
||||
private static int[] samples(int low, int high, int step) {
|
||||
var values = new java.util.TreeSet<Integer>(); values.add(low); values.add(high); values.add((low + high) / 2);
|
||||
@@ -493,9 +525,11 @@ public final class ExpansionStructures23 {
|
||||
private record Choice(Entry entry, Holder<Structure> holder, double score) {}
|
||||
private static final class Ground {
|
||||
final NoiseColumn column; final int top; final int thickness;
|
||||
Ground(NoiseColumn column, int min, int max) {
|
||||
Ground(NoiseColumn column, int min, int max) { this(column, min, max, false); }
|
||||
Ground(NoiseColumn column, int min, int max, boolean waterSurface) {
|
||||
this.column = column; int y = max;
|
||||
while (y >= min && !column.getBlock(y).isSolid()) y--;
|
||||
while (y >= min && !column.getBlock(y).isSolid()
|
||||
&& !(waterSurface && column.getBlock(y).getFluidState().is(net.minecraft.tags.FluidTags.WATER))) y--;
|
||||
top = y;
|
||||
while (y >= min && top - y < 24 && solid(y)) y--;
|
||||
thickness = top - y;
|
||||
@@ -505,17 +539,23 @@ public final class ExpansionStructures23 {
|
||||
}
|
||||
private static final class GroundSampler {
|
||||
final ExpansionChunkGenerator generator; final LevelHeightAccessor level; final RandomState random; final long deadline;
|
||||
final Integer localCeiling;
|
||||
final Map<Long, Ground> columns = new HashMap<>();
|
||||
fr.koka.sanctuary.worldgen.city.LostCityProtection16 localCity;
|
||||
GroundSampler(ExpansionChunkGenerator generator, LevelHeightAccessor level, RandomState random, long deadline) {
|
||||
this(generator, level, random, deadline, null);
|
||||
}
|
||||
GroundSampler(ExpansionChunkGenerator generator, LevelHeightAccessor level, RandomState random, long deadline, Integer localCeiling) {
|
||||
this.generator = generator; this.level = level; this.random = random; this.deadline = deadline;
|
||||
this.localCeiling = localCeiling;
|
||||
}
|
||||
Ground at(int x, int z) {
|
||||
long key = ((long) x << 32) ^ (z & 0xffffffffL);
|
||||
Ground old = columns.get(key); if (old != null) return old;
|
||||
checkDeadline(deadline);
|
||||
if (columns.size() >= MAX_COLUMN_SAMPLES) throw new SampleBudgetExceeded();
|
||||
Ground result = new Ground(generator.getBaseColumn(x, z, level, random), level.getMinY(), level.getMaxY());
|
||||
Ground result = new Ground(generator.getBaseColumn(x, z, level, random), level.getMinY(),
|
||||
localCeiling == null ? level.getMaxY() : localCeiling, localCeiling != null);
|
||||
columns.put(key, result); return result;
|
||||
}
|
||||
}
|
||||
@@ -536,14 +576,23 @@ public final class ExpansionStructures23 {
|
||||
/** Isolated biome snapshot during preflight, per-island sea level during native generation. */
|
||||
private static final class NativeView extends ExpansionChunkGenerator {
|
||||
private final ExpansionChunkGenerator host; private final ExpansionIsland island;
|
||||
private Integer localCeiling;
|
||||
NativeView(ExpansionChunkGenerator host, ExpansionIsland island, ExpansionBiomeSource source, RegistryAccess registries) {
|
||||
super(source, host.initialDiameter(), true); this.host = host; this.island = island; this.registries = registries;
|
||||
}
|
||||
NativeView atHeight(int ceiling) {
|
||||
var result = new NativeView(host, island, source(), registries); result.localCeiling = ceiling; return result;
|
||||
}
|
||||
@Override public int rootGeneration() { return island.generation(); }
|
||||
@Override public Holder<Biome> structureBiome(Holder<Biome> biome) { return host.structureBiome(biome); }
|
||||
@Override public int getSeaLevel() { return ExpansionOcean23.active(island) ? ExpansionOcean23.seaLevel(island) : 192; }
|
||||
@Override public RandomState randomState(ExpansionIsland candidate) { return island.origin() ? host.randomState(candidate) : super.randomState(candidate); }
|
||||
@Override public int getBaseHeight(int x, int z, Heightmap.Types type, LevelHeightAccessor level, RandomState random) {
|
||||
if (localCeiling != null) {
|
||||
var column = getBaseColumn(x, z, level, random);
|
||||
for (int y = localCeiling; y >= level.getMinY(); y--) if (type.isOpaque().test(column.getBlock(y))) return y + 1;
|
||||
return level.getMinY();
|
||||
}
|
||||
return island.origin() ? host.getBaseHeight(x, z, type, level, random) : super.getBaseHeight(x, z, type, level, random);
|
||||
}
|
||||
@Override public NoiseColumn getBaseColumn(int x, int z, LevelHeightAccessor level, RandomState random) {
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
package fr.koka.sanctuary.expansion;
|
||||
|
||||
import java.util.function.BiFunction;
|
||||
import net.minecraft.world.level.NoiseColumn;
|
||||
import net.minecraft.world.level.levelgen.structure.StructurePiece;
|
||||
|
||||
/** Certifies the support positions actually used by vanilla MineShaftCorridor, not a percentage of nearby rock. */
|
||||
public final class MineshaftSupport28 {
|
||||
private MineshaftSupport28() {}
|
||||
public static boolean anchored(StructurePiece piece, BiFunction<Integer, Integer, NoiseColumn> columns) {
|
||||
var box = piece.getBoundingBox();
|
||||
int length = piece.getOrientation().getAxis() == net.minecraft.core.Direction.Axis.Z ? box.getZSpan() : box.getXSpan();
|
||||
for (int z : length > 5 ? new int[]{2, length - 3} : new int[]{2}) for (int x : new int[]{0, 2}) {
|
||||
var pier = SwampHutPlacement28.worldPosition(piece, x, -1, z);
|
||||
var column = columns.apply(pier.getX(), pier.getZ()); boolean anchor = false;
|
||||
// Native wooden support searches at most20 below its floor plank.
|
||||
for (int y = pier.getY() - 1; y >= pier.getY() - 20; y--) {
|
||||
var state = column.getBlock(y);
|
||||
if (!state.getFluidState().isEmpty()) break;
|
||||
if (state.isSolid()) { anchor = true; break; }
|
||||
}
|
||||
// Its alternative is a chain up to50 blocks, anchored above the excavated corridor.
|
||||
if (!anchor) for (int y = box.maxY() + 1; y <= pier.getY() + 50; y++) {
|
||||
var state = column.getBlock(y);
|
||||
if (!state.getFluidState().isEmpty()) break;
|
||||
if (state.isSolid()) { anchor = true; break; }
|
||||
}
|
||||
if (!anchor) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
+24
@@ -7,10 +7,34 @@ import net.minecraft.world.level.levelgen.structure.structures.OceanMonumentPiec
|
||||
/** Scoped native placement parameters; never change the sea level of another dimension or start. */
|
||||
public final class NativeStructurePlacement23 {
|
||||
private static final ThreadLocal<Integer> SEA_LEVEL = new ThreadLocal<>();
|
||||
public record HutScope(java.util.List<net.minecraft.core.BlockPos> piers,
|
||||
java.util.List<net.minecraft.world.level.levelgen.structure.BoundingBox> bodies) {}
|
||||
private static final ThreadLocal<HutScope> HUT = new ThreadLocal<>();
|
||||
private NativeStructurePlacement23() {}
|
||||
public static Integer seaLevel() { return SEA_LEVEL.get(); }
|
||||
public static Integer enter(int seaLevel) { Integer previous = SEA_LEVEL.get(); SEA_LEVEL.set(seaLevel); return previous; }
|
||||
public static void restore(Integer previous) { if (previous == null) SEA_LEVEL.remove(); else SEA_LEVEL.set(previous); }
|
||||
public static HutScope enterHut(net.minecraft.world.level.levelgen.structure.StructureStart start) {
|
||||
var previous = HUT.get();
|
||||
var pieces = start.getPieces().stream().filter(net.minecraft.world.level.levelgen.structure.structures.SwampHutPiece.class::isInstance).toList();
|
||||
HUT.set(new HutScope(pieces.stream().flatMap(piece -> SwampHutPlacement28.piers(piece).stream()).toList(),
|
||||
pieces.stream().map(StructurePiece::getBoundingBox).toList()));
|
||||
return previous;
|
||||
}
|
||||
public static void restoreHut(HutScope previous) {
|
||||
if (previous == null) HUT.remove(); else HUT.set(previous);
|
||||
}
|
||||
public static boolean allowsHutWrite(net.minecraft.core.BlockPos pos, net.minecraft.world.level.block.state.BlockState state,
|
||||
net.minecraft.world.level.block.state.BlockState replaced) {
|
||||
var scope = HUT.get(); if (scope == null) return false;
|
||||
// Native construction in already empty air above a certified bank does not cut its retaining bed.
|
||||
if (replaced.isAir() && scope.bodies().stream().anyMatch(box -> box.isInside(pos))) return true;
|
||||
if (!state.is(net.minecraft.world.level.block.Blocks.OAK_LOG)
|
||||
|| !(replaced.isAir() || replaced.getFluidState().isSource()
|
||||
&& replaced.getFluidState().is(net.minecraft.tags.FluidTags.WATER))) return false;
|
||||
return scope.piers().stream().anyMatch(pier -> pier.getX() == pos.getX() && pier.getZ() == pos.getZ()
|
||||
&& pos.getY() <= pier.getY() && pos.getY() > pier.getY() - SwampHutPlacement28.MAX_PIER_DROP);
|
||||
}
|
||||
public static void move(StructurePiece piece, int dy) {
|
||||
if (piece instanceof OceanMonumentPieces.MonumentBuilding monument) {
|
||||
for (StructurePiece child : ((MonumentChildren23Accessor) monument).sanctuary$children()) child.move(0, dy, 0);
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
package fr.koka.sanctuary.expansion;
|
||||
|
||||
import fr.koka.sanctuary.worldgen.PopulationHydrology;
|
||||
import fr.koka.sanctuary.worldgen.PopulationHydrologyRuntime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.function.BiFunction;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Direction;
|
||||
import net.minecraft.world.level.NoiseColumn;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.levelgen.structure.BoundingBox;
|
||||
import net.minecraft.world.level.levelgen.structure.StructurePiece;
|
||||
import net.minecraft.world.level.levelgen.structure.StructureStart;
|
||||
import net.minecraft.world.level.levelgen.structure.structures.SwampHutPiece;
|
||||
import net.minecraft.world.level.material.Fluids;
|
||||
|
||||
/** Native hut geometry on a real shallow bank, including banks below another island floor. */
|
||||
public final class SwampHutPlacement28 {
|
||||
public static final int MAX_PIER_DROP = 8;
|
||||
private SwampHutPlacement28() {}
|
||||
|
||||
public static String rejection(StructureStart start, BiFunction<Integer, Integer, NoiseColumn> columns) {
|
||||
for (var piece : start.getPieces()) {
|
||||
if (!(piece instanceof SwampHutPiece)) return "hut-unexpected-piece";
|
||||
var box = piece.getBoundingBox();
|
||||
for (int x = box.minX(); x <= box.maxX(); x++) for (int z = box.minZ(); z <= box.maxZ(); z++) {
|
||||
var column = columns.apply(x, z);
|
||||
int waterTop = Integer.MIN_VALUE, waterBottom = Integer.MAX_VALUE;
|
||||
for (int y = box.minY() - MAX_PIER_DROP; y <= box.maxY(); y++) {
|
||||
var state = column.getBlock(y); var fluid = state.getFluidState();
|
||||
if (y >= box.minY() + 2 && !state.isAir()) return "hut-natural-headroom";
|
||||
if (fluid.isEmpty()) continue;
|
||||
if (fluid.getType() != Fluids.WATER || !fluid.isSource()) return "hut-flowing-or-nonwater";
|
||||
waterTop = Math.max(waterTop, y); waterBottom = Math.min(waterBottom, y);
|
||||
}
|
||||
if (waterTop != Integer.MIN_VALUE) {
|
||||
if (waterTop > box.minY() || waterTop - waterBottom + 1 > 4) return "hut-deep-water";
|
||||
if (!solid(column.getBlock(waterBottom - 1)) || !solid(column.getBlock(waterBottom - 2))) return "hut-unsealed-bed";
|
||||
}
|
||||
}
|
||||
for (var pier : piers(piece)) {
|
||||
var column = columns.apply(pier.getX(), pier.getZ()); int y = pier.getY(), depth = 0;
|
||||
while (replaceable(column.getBlock(y)) && depth <= MAX_PIER_DROP) { y--; depth++; }
|
||||
if (depth > MAX_PIER_DROP || !solid(column.getBlock(y)) || !solid(column.getBlock(y - 1))) return "hut-unsupported-pier";
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Still water is compatible; springs, spills, channels and lava keep their protection. */
|
||||
public static String hydrologyRejection(PopulationHydrologyRuntime.RegionPlan region, BoundingBox box) {
|
||||
for (var spring : region.water().springs()) if (intersects(box, spring.flowBounds(), 3)) return "hut-protected-spring";
|
||||
for (var spill : region.water().spills()) if (intersects(box, spill.flowBounds(), 3)) return "hut-protected-spill";
|
||||
for (var flow : region.lava().flowBounds()) if (intersects(box, flow, 4)) return "hut-protected-lava";
|
||||
var rivers = region.water().features().stream().filter(f -> f.kind() == PopulationHydrology.Kind.RIVER)
|
||||
.map(PopulationHydrology.Feature::id).collect(java.util.stream.Collectors.toSet());
|
||||
for (int x = box.minX(); x <= box.maxX(); x++) for (int z = box.minZ(); z <= box.maxZ(); z++)
|
||||
for (var cell : region.water().cellsAt(x, z)) if (rivers.contains(cell.featureId())
|
||||
&& box.minY() <= cell.carveTop() && box.maxY() >= cell.bedY() - cell.sedimentDepth() - 1) return "hut-protected-river";
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Exact four support origins used by SwampHutPiece.postProcess. */
|
||||
public static List<BlockPos> piers(StructurePiece piece) {
|
||||
var result = new ArrayList<BlockPos>(4);
|
||||
for (int z : new int[]{2, 7}) for (int x : new int[]{1, 5}) result.add(worldPosition(piece, x, -1, z));
|
||||
return List.copyOf(result);
|
||||
}
|
||||
public static BlockPos worldPosition(StructurePiece piece, int x, int y, int z) {
|
||||
var b = piece.getBoundingBox(); Direction facing = piece.getOrientation();
|
||||
if (facing == null || facing.getAxis().isVertical()) throw new IllegalArgumentException("A hut requires horizontal orientation");
|
||||
int wx = switch (facing) { case WEST -> b.maxX() - z; case EAST -> b.minX() + z; default -> b.minX() + x; };
|
||||
int wz = switch (facing) { case NORTH -> b.maxZ() - z; case SOUTH -> b.minZ() + z; default -> b.minZ() + x; };
|
||||
return new BlockPos(wx, b.minY() + y, wz);
|
||||
}
|
||||
private static boolean solid(BlockState state) { return state.isSolid() && state.getFluidState().isEmpty(); }
|
||||
private static boolean replaceable(BlockState state) {
|
||||
return state.isAir() || state.liquid() || state.is(Blocks.GLOW_LICHEN) || state.is(Blocks.SEAGRASS) || state.is(Blocks.TALL_SEAGRASS);
|
||||
}
|
||||
private static boolean intersects(BoundingBox b, PopulationHydrology.FlowBounds flow, int margin) {
|
||||
return b.minX() <= flow.maxX() + margin && b.maxX() >= flow.minX() - margin
|
||||
&& b.minY() <= flow.maxY() + margin && b.maxY() >= flow.minY() - margin
|
||||
&& b.minZ() <= flow.maxZ() + margin && b.maxZ() >= flow.minZ() - margin;
|
||||
}
|
||||
}
|
||||
+2
-1
@@ -67,8 +67,9 @@ public abstract class NativeStructureStart23Mixin implements NativeStructureMeta
|
||||
RandomSource random, BoundingBox bounds, ChunkPos pos, Operation<Void> original) {
|
||||
if (sanctuary$native23 == null) { original.call(level, structures, generator, random, bounds, pos); return; }
|
||||
Integer previous = NativeStructurePlacement23.enter(sanctuary$native23.seaLevel());
|
||||
var previousHut = NativeStructurePlacement23.enterHut((StructureStart) (Object) this);
|
||||
try { original.call(level, structures, generator, random, bounds, pos); }
|
||||
finally { NativeStructurePlacement23.restore(previous); }
|
||||
finally { NativeStructurePlacement23.restoreHut(previousHut); NativeStructurePlacement23.restore(previous); }
|
||||
}
|
||||
|
||||
@Unique private static Data sanctuary$read(CompoundTag tag) {
|
||||
|
||||
+4
@@ -62,6 +62,10 @@ public abstract class UnifiedDecorationProtectionMixin {
|
||||
}
|
||||
// The dedicated shoreline feature still grows cane on its naturally valid dry banks.
|
||||
if (state.is(Blocks.SUGAR_CANE)) return;
|
||||
// Admitted native huts use existing air and four bounded wooden piers in still water.
|
||||
// The intact basin bed, all other water cells, island ownership and secrets remain protected.
|
||||
if (!generator.protectsCityDecoration(pos)
|
||||
&& fr.koka.sanctuary.expansion.NativeStructurePlacement23.allowsHutWrite(pos, state, region.getBlockState(pos))) return;
|
||||
// Generation16 reserves its architectural volume before decoration. Only its own
|
||||
// native pieces may replace water there; published generations retain the old rule.
|
||||
if ((generator.rootGeneration() == 16 || generator.rootGeneration() == 17 || generator.rootGeneration() == 19 || generator.rootGeneration() == 20 || generator.rootGeneration() == 21 || generator.rootGeneration() == 22 || (generator.rootGeneration() == 23 || generator.rootGeneration() == 24))
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
package fr.koka.sanctuary.worldgen;
|
||||
|
||||
/** Bounded domain deformation around native rock noise, followed by native cave cuts. */
|
||||
public final class CavesRelief28 {
|
||||
public static final int CAVE_OFFSET = 96;
|
||||
public static final int DEFORMATION_START = 180;
|
||||
public static final double MAX_HORIZONTAL_SHIFT = 16;
|
||||
public static final double MAX_VERTICAL_SHIFT = 40;
|
||||
private CavesRelief28() {}
|
||||
|
||||
public record Warp(double x, double y, double z) {}
|
||||
|
||||
public static double deformationWeight(int x, int y, int z, double radius) {
|
||||
double distance = Math.hypot((double) x, z);
|
||||
double arrival = smooth((distance - 48) / 48);
|
||||
double coast = 1 - smooth((distance / radius - .66) / .26);
|
||||
return smooth((y - DEFORMATION_START) / 48.0) * arrival * coast;
|
||||
}
|
||||
|
||||
/** Signed smooth displacements: no folded ridge, plateau saturation or height-map uplift. */
|
||||
public static Warp warp(double weight, float across, float vertical, float along) {
|
||||
return new Warp(MAX_HORIZONTAL_SHIFT * Math.tanh(across * 1.6) * weight,
|
||||
MAX_VERTICAL_SHIFT * Math.tanh(vertical * 1.6) * weight,
|
||||
MAX_HORIZONTAL_SHIFT * Math.tanh(along * 1.6) * weight);
|
||||
}
|
||||
|
||||
public static float carve(float rock, float nativeCaves, int y) {
|
||||
if (rock <= 0) return rock;
|
||||
double weight = smooth((y - 40) / 28.0) * (1 - smooth((y - 202) / 30.0));
|
||||
if (weight <= 0) return rock;
|
||||
return Math.min(rock, (float) Math.clamp(nativeCaves + (1 - weight) * 2, -1, 1));
|
||||
}
|
||||
private static double smooth(double value) {
|
||||
double t = Math.clamp(value, 0, 1);
|
||||
return t * t * (3 - 2 * t);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
package fr.koka.sanctuary.worldgen;
|
||||
|
||||
import fr.koka.sanctuary.expansion.ExpansionChunkGenerator;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Direction;
|
||||
import net.minecraft.util.RandomSource;
|
||||
import net.minecraft.world.level.WorldGenLevel;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.chunk.ChunkAccess;
|
||||
import net.minecraft.world.level.chunk.ChunkGenerator;
|
||||
import net.minecraft.world.level.levelgen.feature.MonsterRoomFeature;
|
||||
|
||||
/** A few attempts at real cave walls; the vanilla feature owns its shell, loot and spawner. */
|
||||
public final class CavityMonsterRoom28 {
|
||||
private static final MonsterRoomFeature NATIVE = new MonsterRoomFeature();
|
||||
private static final int MAX_ATTEMPTS = 8;
|
||||
private CavityMonsterRoom28() {}
|
||||
|
||||
public static void decorate(WorldGenLevel level, ChunkGenerator generator, ChunkAccess chunk) {
|
||||
if (!(generator instanceof ExpansionChunkGenerator host) || host.rootGeneration() != 24
|
||||
|| !level.getLevel().structureManager().shouldGenerateStructures()) return;
|
||||
var pos = chunk.getPos(); var island = host.islandForChunk(pos.x(), pos.z());
|
||||
if (island == null || island.generation() != 24) return;
|
||||
long seed = WoodlandGroveGeometry.seed(island.seed(), pos.x(), 28, pos.z(), 0xD086E028L);
|
||||
if (Math.floorMod(seed, 3) != 0) return;
|
||||
var candidates = new ArrayList<BlockPos>();
|
||||
int bottom = Math.max(level.getMinY() + 2, 32), top = Math.min(level.getMaxY() - 6, 280);
|
||||
for (int dx : new int[]{4, 11}) for (int dz : new int[]{4, 11}) {
|
||||
int x = pos.getMinBlockX() + dx, z = pos.getMinBlockZ() + dz;
|
||||
for (int y = bottom; y <= top; y++) {
|
||||
BlockPos at = new BlockPos(x, y, z);
|
||||
if (!level.getBlockState(at.below()).isSolid() || !level.getBlockState(at.above(4)).isSolid()) continue;
|
||||
boolean caveEdge = false;
|
||||
for (Direction side : Direction.Plane.HORIZONTAL) {
|
||||
BlockPos opening = at.relative(side, 4);
|
||||
if (level.getBlockState(opening).isAir() && level.getBlockState(opening.above()).isAir()) { caveEdge = true; break; }
|
||||
}
|
||||
if (caveEdge) candidates.add(at);
|
||||
}
|
||||
}
|
||||
candidates.sort(Comparator.<BlockPos>comparingLong(at -> WoodlandGroveGeometry.seed(seed, at.getX(), at.getY(), at.getZ(), 28)));
|
||||
int attempts = 0;
|
||||
for (BlockPos at : candidates) {
|
||||
if (attempts++ >= MAX_ATTEMPTS) break;
|
||||
if (!safe(level, host, at, island.id())) continue;
|
||||
// No virtual solid floor/roof and no prebuilt room. Native rejection remains authoritative.
|
||||
if (NATIVE.place(level, generator, RandomSource.create(WoodlandGroveGeometry.seed(seed, at.getX(), at.getY(), at.getZ(), 0xC0128)), at)) return;
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean safe(WorldGenLevel level, ExpansionChunkGenerator host, BlockPos at, String owner) {
|
||||
for (int dx = -4; dx <= 4; dx++) for (int dz = -4; dz <= 4; dz++) for (int dy = -1; dy <= 4; dy++) {
|
||||
BlockPos block = at.offset(dx, dy, dz);
|
||||
var destination = host.islandForChunk(block.getX() >> 4, block.getZ() >> 4);
|
||||
if (destination == null || !destination.id().equals(owner)) return false;
|
||||
var state = level.getBlockState(block);
|
||||
if (!state.getFluidState().isEmpty() || state.hasBlockEntity() || state.is(Blocks.BEDROCK)) return false;
|
||||
if (host instanceof SanctuaryChunkGenerator sanctuary
|
||||
&& (sanctuary.protectsDecoration(block) || sanctuary.protectsCityDecoration(block))) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package fr.koka.sanctuary.worldgen;
|
||||
|
||||
import fr.koka.sanctuary.expansion.ExpansionChunkGenerator;
|
||||
import fr.koka.sanctuary.expansion.ExpansionIsland;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.tags.BlockTags;
|
||||
import net.minecraft.util.RandomSource;
|
||||
import net.minecraft.world.level.WorldGenLevel;
|
||||
import net.minecraft.world.level.chunk.ChunkAccess;
|
||||
|
||||
/** Native finite ore veins, positioned within the whole floating rock column rather than vanilla sea-level bands. */
|
||||
public final class IslandOres28 {
|
||||
private IslandOres28() {}
|
||||
private record Deposit(String feature, int attempts, double minDepth, double maxDepth) {}
|
||||
// Small supplementary populations shared by all climates. Existing biome ores remain in place.
|
||||
private static final Deposit[] DEPOSITS = {
|
||||
new Deposit("ore_coal", 8, .05, .65), new Deposit("ore_iron", 7, .12, .88),
|
||||
new Deposit("ore_copper_small", 6, .10, .66), new Deposit("ore_gold", 2, .52, .90),
|
||||
new Deposit("ore_redstone", 3, .56, .92), new Deposit("ore_lapis", 2, .30, .82),
|
||||
new Deposit("ore_diamond_small", 2, .70, .94), new Deposit("ore_emerald", 1, .25, .85)
|
||||
};
|
||||
|
||||
public static void place(WorldGenLevel level, ExpansionChunkGenerator generator, ChunkAccess chunk, ExpansionIsland island) {
|
||||
if (island == null || generator.rootGeneration() != 24) return;
|
||||
int minX = chunk.getPos().getMinBlockX(), minZ = chunk.getPos().getMinBlockZ();
|
||||
// Density columns precede structures and vegetation. Cavities do not reset mineral depth:
|
||||
// retain the full top-to-bottom envelope rather than using the nearest cave ceiling.
|
||||
var column = generator.getBaseColumn(minX + 8, minZ + 8, level, generator.randomState(island));
|
||||
int top = -1, bottom = -1;
|
||||
for (int y = 1; y < 383; y++) if (column.getBlock(y).is(BlockTags.BASE_STONE_OVERWORLD)) {
|
||||
if (bottom < 0) bottom = y;
|
||||
top = y;
|
||||
}
|
||||
if (top - bottom < 24) return;
|
||||
var registry = level.registryAccess().lookupOrThrow(Registries.FEATURE);
|
||||
for (int index = 0; index < DEPOSITS.length; index++) {
|
||||
Deposit deposit = DEPOSITS[index];
|
||||
var feature = registry.getValue(Identifier.withDefaultNamespace(deposit.feature()));
|
||||
if (feature == null) throw new IllegalStateException("Missing native ore " + deposit.feature());
|
||||
var random = RandomSource.create(WoodlandGroveGeometry.seed(island.seed(), minX, index, minZ, 0x0280AE5L));
|
||||
for (int attempt = 0; attempt < deposit.attempts(); attempt++) {
|
||||
double depth = deposit.minDepth() + random.nextDouble() * (deposit.maxDepth() - deposit.minDepth());
|
||||
int y = (int) Math.round(top - depth * (top - bottom));
|
||||
// Largest selected native vein is 17 blocks (radius bound five). Its writes stay
|
||||
// in the decorating chunk, including where neighbours are generated in another order.
|
||||
var at = new BlockPos(minX + 5 + random.nextInt(6), y, minZ + 5 + random.nextInt(6));
|
||||
if (level.getBlockState(at).is(BlockTags.BASE_STONE_OVERWORLD)) feature.place(level, generator, random, at);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+59
-7
@@ -1,6 +1,7 @@
|
||||
package fr.koka.sanctuary.worldgen;
|
||||
|
||||
import com.mojang.serialization.MapCodec;
|
||||
import com.mojang.serialization.Codec;
|
||||
import com.mojang.serialization.codecs.RecordCodecBuilder;
|
||||
import fr.koka.sanctuary.SanctuaryMod;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
@@ -10,7 +11,8 @@ import net.minecraft.world.level.levelgen.densityfunction.*;
|
||||
/** Capacity changes the envelope; all natural noise is still sampled at world coordinates. */
|
||||
public record PopulationIslandDensity(DensityFunction terrain, DensityFunction distortion,
|
||||
DensityFunction sculpt, DensityFunction detail, DensityFunction underside,
|
||||
DensityFunction riftWarp, DensityFunction riftDetail, IslandCapacity capacity) implements DensityFunction {
|
||||
DensityFunction riftWarp, DensityFunction riftDetail, IslandCapacity capacity,
|
||||
DensityFunction caves28, DensityFunction ridges28, DensityFunction erosion28, boolean geology28) implements DensityFunction {
|
||||
public static final MapCodec<PopulationIslandDensity> CODEC = RecordCodecBuilder.mapCodec(instance -> instance.group(
|
||||
DensityFunction.CODEC.fieldOf("terrain").forGetter(PopulationIslandDensity::terrain),
|
||||
DensityFunction.CODEC.fieldOf("distortion").forGetter(PopulationIslandDensity::distortion),
|
||||
@@ -19,36 +21,85 @@ public record PopulationIslandDensity(DensityFunction terrain, DensityFunction d
|
||||
DensityFunction.CODEC.fieldOf("underside").forGetter(PopulationIslandDensity::underside),
|
||||
DensityFunction.CODEC.fieldOf("rift_warp").forGetter(PopulationIslandDensity::riftWarp),
|
||||
DensityFunction.CODEC.fieldOf("rift_detail").forGetter(PopulationIslandDensity::riftDetail),
|
||||
IslandCapacity.CODEC.optionalFieldOf("players", IslandCapacity.TWENTY).forGetter(PopulationIslandDensity::capacity)
|
||||
IslandCapacity.CODEC.optionalFieldOf("players", IslandCapacity.TWENTY).forGetter(PopulationIslandDensity::capacity),
|
||||
DensityFunction.CODEC.optionalFieldOf("caves_28", DensityFunctions.zero()).forGetter(PopulationIslandDensity::caves28),
|
||||
DensityFunction.CODEC.optionalFieldOf("ridges_28", DensityFunctions.zero()).forGetter(PopulationIslandDensity::ridges28),
|
||||
DensityFunction.CODEC.optionalFieldOf("erosion_28", DensityFunctions.zero()).forGetter(PopulationIslandDensity::erosion28),
|
||||
Codec.BOOL.optionalFieldOf("geology_28", false).forGetter(PopulationIslandDensity::geology28)
|
||||
).apply(instance, PopulationIslandDensity::new));
|
||||
|
||||
public PopulationIslandDensity(DensityFunction terrain, DensityFunction distortion,
|
||||
DensityFunction sculpt, DensityFunction detail, DensityFunction underside,
|
||||
DensityFunction riftWarp, DensityFunction riftDetail, IslandCapacity capacity) {
|
||||
this(terrain, distortion, sculpt, detail, underside, riftWarp, riftDetail, capacity,
|
||||
DensityFunctions.zero(), DensityFunctions.zero(), DensityFunctions.zero(), false);
|
||||
}
|
||||
|
||||
@Override public DensitySampler compileSampler(CompileContext context) {
|
||||
var raw = ExactInterpolationSampler.compileTerrain(terrain, context); var edge = distortion.compileSampler(context);
|
||||
var coarse = sculpt.compileSampler(context); var fine = detail.compileSampler(context);
|
||||
var bottom = underside.compileSampler(context); var warp = riftWarp.compileSampler(context);
|
||||
var fractureDetail = riftDetail.compileSampler(context);
|
||||
var caves = geology28 ? caves28.compileSampler(context) : null;
|
||||
var ridges = geology28 ? ridges28.compileSampler(context) : null;
|
||||
var erosion = geology28 ? erosion28.compileSampler(context) : null;
|
||||
long seed = context.createRandom(SanctuaryMod.id("population_rifts_alpha10")).nextLong();
|
||||
var layouts = new ConcurrentHashMap<IslandCapacity.Region, RiftShape.Layout>();
|
||||
return new DensitySampler() {
|
||||
private float terrainAt(SamplerContext sample, double x, double y, double z) {
|
||||
int ix = (int) Math.floor(x), iy = (int) Math.floor(y), iz = (int) Math.floor(z);
|
||||
double fx = x - ix, fy = y - iy, fz = z - iz;
|
||||
double sum = 0;
|
||||
// Trilinear sampling avoids integer snapping of the displaced native field.
|
||||
for (int dz = 0; dz <= (fz == 0 ? 0 : 1); dz++)
|
||||
for (int dy = 0; dy <= (fy == 0 ? 0 : 1); dy++)
|
||||
for (int dx = 0; dx <= (fx == 0 ? 0 : 1); dx++) {
|
||||
double w = (dx == 0 ? 1 - fx : fx) * (dy == 0 ? 1 - fy : fy)
|
||||
* (dz == 0 ? 1 - fz : fz);
|
||||
sum += w * raw.sampleValue(sample, ix + dx, iy + dy, iz + dz);
|
||||
}
|
||||
return (float) sum;
|
||||
}
|
||||
@Override public float sampleValue(SamplerContext sample, int x, int y, int z) {
|
||||
if (Math.hypot((double) x, z) >= capacity.terrainLimit() || y <= 0 || y >= 384) return -1;
|
||||
float terrainValue;
|
||||
double weight = geology28 ? CavesRelief28.deformationWeight(x, y, z, capacity.radius()) : 0;
|
||||
if (weight > 0) {
|
||||
var shift = CavesRelief28.warp(weight,
|
||||
erosion.sampleValue(sample, x + 173, y - 59, z - 89),
|
||||
ridges.sampleValue(sample, x, y, z),
|
||||
erosion.sampleValue(sample, x - 211, y + 137, z + 307));
|
||||
terrainValue = terrainAt(sample, x - shift.x(), y - 64 - shift.y(), z - shift.z());
|
||||
} else terrainValue = raw.sampleValue(sample, x, y - 64, z);
|
||||
// Keep the island envelope at actual coordinates, independent from displaced rock.
|
||||
int localY = y - 64;
|
||||
float natural = PopulationIslandShape.raisedDensity(x, y, z, raw.sampleValue(sample, x, localY, z),
|
||||
float natural = PopulationIslandShape.raisedDensity(x, y, z, terrainValue,
|
||||
edge.sampleValue(sample, x, 0, z), coarse.sampleValue(sample, x, localY, z),
|
||||
fine.sampleValue(sample, x, localY, z), bottom.sampleValue(sample, x, localY, z), 64, 384, capacity);
|
||||
if (natural <= 0) return natural;
|
||||
var region = capacity.regionAt(x, z);
|
||||
var layout = layouts.computeIfAbsent(region, r -> RiftShape.layout(regionSeed(seed, r)));
|
||||
int rx = x - region.originX(), rz = z - region.originZ();
|
||||
if (!RiftShape.mayAffect(layout, rx, rz)) return natural;
|
||||
return RiftShape.density(layout, rx, y, rz, natural,
|
||||
warp.sampleValue(sample, x, y, z), fractureDetail.sampleValue(sample, x, y, z));
|
||||
boolean affects = geology28 ? RiftShape.mayAffect28(layout, rx, rz) : RiftShape.mayAffect(layout, rx, rz);
|
||||
if (affects) natural = geology28
|
||||
? RiftShape.density28(layout, rx, y, rz, natural,
|
||||
warp.sampleValue(sample, x, y, z), fractureDetail.sampleValue(sample, x, y, z))
|
||||
: RiftShape.density(layout, rx, y, rz, natural,
|
||||
warp.sampleValue(sample, x, y, z), fractureDetail.sampleValue(sample, x, y, z));
|
||||
if (geology28 && natural > 0 && y > 40 && y < 232)
|
||||
natural = CavesRelief28.carve(natural, caves.sampleValue(sample, x, y - CavesRelief28.CAVE_OFFSET, z), y);
|
||||
return natural;
|
||||
}
|
||||
@Override public void sampleVolume(SamplerContext sample, DensityBuffer buffer, DensityVolume volume) {
|
||||
DensitySampler.sampleVolumeNaive(sample, buffer, volume, this);
|
||||
}
|
||||
};
|
||||
}
|
||||
/** Native diagnostics can compile the exact accepted27 field without new adaptations. */
|
||||
public PopulationIslandDensity legacy27() {
|
||||
return new PopulationIslandDensity(terrain, distortion, sculpt, detail, underside, riftWarp, riftDetail,
|
||||
capacity, DensityFunctions.zero(), DensityFunctions.zero(), DensityFunctions.zero(), false);
|
||||
}
|
||||
public static long regionSeed(long seed, IslandCapacity.Region region) {
|
||||
long value = seed ^ ((long) region.x() << 32) ^ (region.z() & 0xffffffffL) ^ 0xA10CA7E1L;
|
||||
value = (value ^ (value >>> 30)) * 0xbf58476d1ce4e5b9L;
|
||||
@@ -57,7 +108,8 @@ public record PopulationIslandDensity(DensityFunction terrain, DensityFunction d
|
||||
}
|
||||
@Override public DensityFunction rewriteChildren(DfRewriteRule rule) {
|
||||
return new PopulationIslandDensity(rule.rewrite(terrain), rule.rewrite(distortion), rule.rewrite(sculpt),
|
||||
rule.rewrite(detail), rule.rewrite(underside), rule.rewrite(riftWarp), rule.rewrite(riftDetail), capacity);
|
||||
rule.rewrite(detail), rule.rewrite(underside), rule.rewrite(riftWarp), rule.rewrite(riftDetail), capacity,
|
||||
rule.rewrite(caves28), rule.rewrite(ridges28), rule.rewrite(erosion28), geology28);
|
||||
}
|
||||
@Override public Interval range() { return Interval.of(-1, 1); }
|
||||
@Override public int domainAxes() { return ALL_AXES; }
|
||||
|
||||
@@ -91,6 +91,55 @@ public final class RiftShape {
|
||||
return (float) Math.max(-1.0, terrain - erosion);
|
||||
}
|
||||
|
||||
/** Alpha28 fractures lean sideways, end within the mass and retain interrupted rock spans. */
|
||||
public static boolean mayAffect28(Layout layout, int x, int z) {
|
||||
double radius = Math.hypot((double) x, z);
|
||||
if (radius <= INNER_UNTOUCHED_RADIUS || radius >= OUTER_UNTOUCHED_RADIUS) return false;
|
||||
for (Rift rift : layout.rifts()) {
|
||||
if (near28(rift.main(), x, z)) return true;
|
||||
for (Segment branch : rift.branches()) if (near28(branch, x, z)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public static float density28(Layout layout, int x, int y, int z, float terrain, float warped, float detail) {
|
||||
if (terrain <= 0 || y < 88 || y > 244 || !mayAffect28(layout, x, z)) return terrain;
|
||||
double coarse = Math.clamp(warped, -1, 1), fine = Math.clamp(detail, -1, 1), cut = 0;
|
||||
for (Rift rift : layout.rifts()) {
|
||||
cut = Math.max(cut, erosion28(rift.main(), rift.id(), x, y, z, coarse, fine));
|
||||
for (Segment branch : rift.branches())
|
||||
cut = Math.max(cut, erosion28(branch, rift.id(), x, y, z, coarse, fine));
|
||||
}
|
||||
double radius = Math.hypot((double) x, z);
|
||||
cut *= smoothstep((radius - INNER_UNTOUCHED_RADIUS) / 16)
|
||||
* smoothstep((OUTER_UNTOUCHED_RADIUS - radius) / 20);
|
||||
return (float) Math.max(-1, terrain - cut);
|
||||
}
|
||||
|
||||
private static boolean near28(Segment segment, int x, int z) {
|
||||
double rx = (double) x - segment.x(), rz = (double) z - segment.z();
|
||||
return Math.abs(rx * segment.dx() + rz * segment.dz()) <= segment.halfLength() + 16
|
||||
&& Math.abs(-rx * segment.dz() + rz * segment.dx()) <= segment.bend() * 1.3 + 20;
|
||||
}
|
||||
|
||||
private static double erosion28(Segment segment, long id, int x, int y, int z, double coarse, double fine) {
|
||||
if (!near28(segment, x, z)) return 0;
|
||||
double rx = x - segment.x(), rz = z - segment.z();
|
||||
double along = rx * segment.dx() + rz * segment.dz();
|
||||
double end = Math.clamp(along, -segment.halfLength(), segment.halfLength());
|
||||
double centerY = 150 + unit(id + 41) * 32 + end * .12 + Math.sin(end / 29 + segment.phase()) * 7;
|
||||
double halfHeight = 22 + unit(id + 42) * 14;
|
||||
double vertical = smoothstep((halfHeight - Math.abs(y - centerY)) / 10);
|
||||
if (vertical <= 0) return 0;
|
||||
double lean = (y - centerY) * (.13 + unit(id + 43) * .07);
|
||||
double sideways = -rx * segment.dz() + rz * segment.dx() - curve(segment, end) - lean - coarse * 1.05;
|
||||
double width = Math.clamp(segment.halfWidth() + fine * .5 + coarse * .45, 2.2, 5.7);
|
||||
double distance = Math.hypot(sideways, along - end);
|
||||
double bridgeAt = (unit(id + 44) - .5) * segment.halfLength();
|
||||
double bridge = smoothstep((Math.abs(end - bridgeAt) - 3) / 5);
|
||||
return 2.1 * smoothstep((width + 1.6 - distance) / 2.6) * vertical * bridge;
|
||||
}
|
||||
|
||||
private static boolean near(Segment segment, int x, int z) {
|
||||
double rx = (double) x - segment.x(), rz = (double) z - segment.z();
|
||||
return Math.abs(rx * segment.dx() + rz * segment.dz()) <= segment.halfLength() + 10
|
||||
|
||||
@@ -16,13 +16,15 @@ import net.minecraft.world.level.biome.Climate;
|
||||
/** Alpha.13 keeps Population's origin woodlands and the laboratory's native cave pockets.
|
||||
* Child islands retain the laboratory's complete climate palette. */
|
||||
public final class SanctuaryBiomeSource extends ExpansionBiomeSource {
|
||||
public static final ResourceKey<Biome> SWAMP27 = ResourceKey.create(net.minecraft.core.registries.Registries.BIOME,
|
||||
fr.koka.sanctuary.SanctuaryMod.id("swamp27_caves"));
|
||||
public static final List<String> REQUIRED_BIOMES = Stream.concat(
|
||||
ExpansionBiomeSource.REQUIRED_BIOMES.stream(), Stream.of(
|
||||
PopulationIslandBiomeSource.OAK_FOREST, PopulationIslandBiomeSource.BIRCH_FOREST,
|
||||
PopulationIslandBiomeSource.CLEARING, PopulationIslandBiomeSource.DRY_WOODLAND,
|
||||
PopulationIslandBiomeSource.ROCKY_HEATH, PopulationIslandBiomeSource.DARK_GROVE,
|
||||
PopulationIslandBiomeSource.BAMBOO_GROVE, PopulationIslandBiomeSource.SULFUR_DEPTHS,
|
||||
PopulationIslandBiomeSource.LUSH_CAVES, PopulationIslandBiomeSource.DRIPSTONE_CAVES
|
||||
PopulationIslandBiomeSource.LUSH_CAVES, PopulationIslandBiomeSource.DRIPSTONE_CAVES, SWAMP27
|
||||
).map(key -> key.identifier().toString())).toList();
|
||||
|
||||
public static final MapCodec<SanctuaryBiomeSource> CODEC = RecordCodecBuilder.mapCodec(i -> i.group(
|
||||
@@ -71,6 +73,7 @@ public final class SanctuaryBiomeSource extends ExpansionBiomeSource {
|
||||
private final boolean cities;
|
||||
private final int generation;
|
||||
private final Holder<Biome> cityBiome;
|
||||
private final Holder<Biome> swamp27;
|
||||
private volatile java.util.Map<String, fr.koka.sanctuary.worldgen.city.LostCityPlan> cityPlans = java.util.Map.of();
|
||||
|
||||
public SanctuaryBiomeSource(List<Holder<Biome>> biomes) {
|
||||
@@ -81,6 +84,7 @@ public final class SanctuaryBiomeSource extends ExpansionBiomeSource {
|
||||
this.generation = generation;
|
||||
this.cities = ExpansionIsland.spatialGeneration(generation);
|
||||
allBiomes = List.copyOf(biomes);
|
||||
swamp27 = generation == 24 ? required(SWAMP27) : null;
|
||||
if ((generation == 23 || generation == 24)) for (String name : fr.koka.sanctuary.expansion.ExpansionOcean23.BIOMES)
|
||||
required(ResourceKey.create(net.minecraft.core.registries.Registries.BIOME,
|
||||
net.minecraft.resources.Identifier.withDefaultNamespace(name)));
|
||||
@@ -122,6 +126,20 @@ public final class SanctuaryBiomeSource extends ExpansionBiomeSource {
|
||||
// Reuse the historical selector itself: its noise, thresholds, order and child
|
||||
// climate choices stay exact rather than being approximated by another climate field.
|
||||
Holder<Biome> laboratory = super.resolve(snapshot, x, y, z);
|
||||
if (generation == 24 && y >= Swamp27Volumes.MIN_Y && y < Swamp27Volumes.MAX_Y) {
|
||||
for (ExpansionIsland island : snapshot) {
|
||||
if (!island.origin() || !island.contains(x, z)) continue;
|
||||
Holder<Biome> lower = woodlands.getNoiseBiome(quartX, quartY, quartZ);
|
||||
boolean cave = laboratory.is(Biomes.LUSH_CAVES) || laboratory.is(Biomes.DRIPSTONE_CAVES)
|
||||
|| laboratory.is(Biomes.SULFUR_CAVES) || lower.is(PopulationIslandBiomeSource.LUSH_CAVES)
|
||||
|| lower.is(PopulationIslandBiomeSource.DRIPSTONE_CAVES)
|
||||
|| lower.is(PopulationIslandBiomeSource.SULFUR_DEPTHS)
|
||||
|| lower.is(PopulationIslandBiomeSource.DARK_GROVE);
|
||||
if (cave && Swamp27Volumes.contains(island.seed(), island.diameter(),
|
||||
x - island.centerX(), y, z - island.centerZ())) return swamp27;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (y < 0 || y >= 384 || laboratory.is(Biomes.LUSH_CAVES)
|
||||
|| laboratory.is(Biomes.DRIPSTONE_CAVES) || laboratory.is(Biomes.SULFUR_CAVES)) return laboratory;
|
||||
for (ExpansionIsland island : snapshot) {
|
||||
|
||||
+10
-1
@@ -183,7 +183,12 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
|
||||
public boolean intersectsExperimentalStructures(int minX, int minY, int minZ, int maxX, int maxY, int maxZ) {
|
||||
if (generation == 24) for (var site : aerialPlan24.sites()) {
|
||||
if (minX <= site.maxX() + 1 && maxX >= site.minX() - 1 && minZ <= site.maxZ() + 1 && maxZ >= site.minZ() - 1
|
||||
&& minY <= site.maxY() + 1 && maxY >= (site.waterfall() ? 0 : site.minY() - 1)) return true;
|
||||
&& minY <= site.maxY() + 1 && maxY >= site.minY() - 1) return true;
|
||||
if (site.waterfall()) {
|
||||
var flow = site.waterfallClaimBounds();
|
||||
if (minX <= flow.maxX() && maxX >= flow.minX() && minZ <= flow.maxZ() && maxZ >= flow.minZ()
|
||||
&& minY <= flow.maxY() && maxY >= flow.minY()) return true;
|
||||
}
|
||||
}
|
||||
return (generation == 23 || generation == 24) && mineralSanctuary23.intersectsProtection(minX, minY, minZ, maxX, maxY, maxZ)
|
||||
|| (generation == 17 || generation == 19 || generation == 20 || generation == 21 || generation == 22 || (generation == 23 || generation == 24)) && ruinsPlan17.intersectsProtection(minX, minY, minZ, maxX, maxY, maxZ)
|
||||
@@ -210,6 +215,10 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
|
||||
case "sanctuary:population_rocky_heath" -> "meadow";
|
||||
case "sanctuary:population_dark_grove" -> "dark_forest";
|
||||
case "sanctuary:population_bamboo_grove" -> "bamboo_jungle";
|
||||
case "sanctuary:swamp27_caves" -> "swamp";
|
||||
case "sanctuary:population_lush_caves" -> "lush_caves";
|
||||
case "sanctuary:population_dripstone_caves" -> "dripstone_caves";
|
||||
case "sanctuary:population_sulfur_depths" -> "sulfur_caves";
|
||||
default -> null;
|
||||
};
|
||||
return equivalent == null ? biome : registries.lookupOrThrow(Registries.BIOME).getOrThrow(
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
package fr.koka.sanctuary.worldgen;
|
||||
|
||||
import com.mojang.serialization.MapCodec;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Direction;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.data.worldgen.features.TreeFeatures;
|
||||
import net.minecraft.tags.FluidTags;
|
||||
import net.minecraft.util.RandomSource;
|
||||
import net.minecraft.world.entity.EntitySpawnReason;
|
||||
import net.minecraft.world.entity.EntityTypes;
|
||||
import net.minecraft.world.level.WorldGenLevel;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.block.VineBlock;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.chunk.ChunkGenerator;
|
||||
import net.minecraft.world.level.levelgen.feature.Feature;
|
||||
|
||||
/** Decorates existing floors, water and ceilings only. Writes never leave the current chunk. */
|
||||
public final class Swamp27Decoration implements Feature {
|
||||
public static final MapCodec<Swamp27Decoration> CODEC = MapCodec.unit(Swamp27Decoration::new);
|
||||
@Override public MapCodec<Swamp27Decoration> codec() { return CODEC; }
|
||||
|
||||
@Override public boolean place(WorldGenLevel level, ChunkGenerator generator, RandomSource random, BlockPos origin) {
|
||||
if (!(level.getLevel().getChunkSource().getGenerator() instanceof SanctuaryChunkGenerator host)
|
||||
|| host.rootGeneration() != 24) return false;
|
||||
var binding = PopulationGenerationContext.of(generator, level.getLevel());
|
||||
if (binding == null) return false;
|
||||
int minX = (origin.getX() >> 4) * 16, minZ = (origin.getZ() >> 4) * 16;
|
||||
long seed = binding.random().seed();
|
||||
boolean changed = false, frogPlaced = false;
|
||||
int trees = 0;
|
||||
boolean frogOpportunity = Math.floorMod(WoodlandGroveGeometry.seed(seed, minX, 0, minZ, 0xF207L), 8) == 0;
|
||||
for (int x = minX; x < minX + 16; x++) for (int z = minZ; z < minZ + 16; z++) {
|
||||
int floors = 0;
|
||||
for (int y = Swamp27Volumes.MAX_Y - 1; y >= Swamp27Volumes.MIN_Y; y--) {
|
||||
BlockPos at = new BlockPos(x, y, z);
|
||||
if (!level.getBlockState(at).isAir() || !level.getBiome(at).is(SanctuaryBiomeSource.SWAMP27)) continue;
|
||||
BlockPos floor = at.below();
|
||||
BlockState ground = level.getBlockState(floor);
|
||||
boolean water = ground.getFluidState().is(FluidTags.WATER) && ground.getFluidState().isSource();
|
||||
if (!water && !natural(ground)) continue;
|
||||
if (!roof(level, at) || lavaNearby(level, at)) continue;
|
||||
long local = WoodlandGroveGeometry.seed(seed, x, y, z, 0xB07527L);
|
||||
RandomSource detail = RandomSource.create(local);
|
||||
if (water) {
|
||||
if (detail.nextInt(9) == 0) changed |= plant(level, at, Blocks.LILY_PAD.defaultBlockState());
|
||||
if (++floors >= 3) break;
|
||||
continue;
|
||||
}
|
||||
// Replace a single already solid natural floor, never a support in air or an ore.
|
||||
boolean wet = waterNearby(level, floor, 2);
|
||||
long patch = WoodlandGroveGeometry.seed(seed, Math.floorDiv(x, 6), y / 5, Math.floorDiv(z, 6), 0x501127L);
|
||||
BlockState soil = (wet ? Blocks.MUD : Math.floorMod(patch, 5) < 2
|
||||
? Blocks.MOSS_BLOCK : Blocks.GRASS_BLOCK).defaultBlockState();
|
||||
changed |= level.setBlock(floor, soil, 2);
|
||||
if (trees < 2 && (x & 15) >= 4 && (x & 15) <= 11 && (z & 15) >= 4 && (z & 15) <= 11
|
||||
&& Math.floorMod(local, 23) == 0 && at.getY() + 9 < Swamp27Volumes.MAX_Y) {
|
||||
// Native placement decides whether the trunk and crown fit an irregular room.
|
||||
// A radius-four margin keeps every write inside this chunk; existing hydrology
|
||||
// and structure protection still guard each native write.
|
||||
if (level.setBlock(floor, Blocks.GRASS_BLOCK.defaultBlockState(), 2)
|
||||
&& level.registryAccess().lookupOrThrow(Registries.FEATURE).getOrThrow(TreeFeatures.SWAMP_OAK)
|
||||
.value().place(level, generator, RandomSource.create(local), at)) {
|
||||
trees++;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
if (!level.getBlockState(at).isAir()) continue;
|
||||
if (wet && detail.nextInt(5) == 0) changed |= plant(level, at, Blocks.SUGAR_CANE.defaultBlockState());
|
||||
else if (detail.nextInt(13) == 0) changed |= plant(level, at, Blocks.FIREFLY_BUSH.defaultBlockState());
|
||||
else if (detail.nextInt(4) == 0) changed |= plant(level, at, Blocks.SHORT_GRASS.defaultBlockState());
|
||||
|
||||
if (!frogPlaced && frogOpportunity && wet && level.getBlockState(at).isAir()
|
||||
&& level.getBlockState(at.above()).isAir()
|
||||
&& !level.getBlockState(floor).getCollisionShape(level, floor).isEmpty()) {
|
||||
var frog = EntityTypes.FROG.create(level.getLevel(), EntitySpawnReason.CHUNK_GENERATION);
|
||||
if (frog != null) {
|
||||
frog.snapTo(x + .5, y, z + .5, detail.nextFloat() * 360, 0);
|
||||
frog.finalizeSpawn(level, level.getCurrentDifficultyAt(at), EntitySpawnReason.CHUNK_GENERATION, null);
|
||||
frog.setPersistenceRequired();
|
||||
level.addFreshEntityWithPassengers(frog);
|
||||
frogPlaced = true;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
if (++floors >= 3) break;
|
||||
}
|
||||
}
|
||||
// Short vines cling to actual walls; every segment must still have its own support.
|
||||
for (int attempt = 0; attempt < 40; attempt++) {
|
||||
RandomSource vines = RandomSource.create(WoodlandGroveGeometry.seed(seed, minX, attempt, minZ, 0x710E27L));
|
||||
BlockPos at = new BlockPos(minX + 1 + vines.nextInt(14),
|
||||
Swamp27Volumes.MIN_Y + vines.nextInt(Swamp27Volumes.MAX_Y - Swamp27Volumes.MIN_Y - 4),
|
||||
minZ + 1 + vines.nextInt(14));
|
||||
if (!level.getBiome(at).is(SanctuaryBiomeSource.SWAMP27) || !roof(level, at)) continue;
|
||||
for (Direction face : Direction.Plane.HORIZONTAL) {
|
||||
if (!natural(level.getBlockState(at.relative(face)))) continue;
|
||||
BlockState vine = Blocks.VINE.defaultBlockState().setValue(VineBlock.getPropertyForFace(face), true);
|
||||
for (int n = 0; n < 4 && at.getY() - n >= Swamp27Volumes.MIN_Y; n++) {
|
||||
BlockPos segment = at.below(n);
|
||||
if (!level.getBlockState(segment).isAir()
|
||||
|| !level.getBiome(segment).is(SanctuaryBiomeSource.SWAMP27)
|
||||
|| !natural(level.getBlockState(segment.relative(face))) || lavaNearby(level, segment)) break;
|
||||
changed |= plant(level, segment, vine);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
return changed;
|
||||
}
|
||||
|
||||
private static boolean plant(WorldGenLevel level, BlockPos at, BlockState plant) {
|
||||
return level.getBlockState(at).isAir() && plant.canSurvive(level, at) && level.setBlock(at, plant, 2);
|
||||
}
|
||||
private static boolean roof(WorldGenLevel level, BlockPos at) {
|
||||
for (int y = at.getY() + 3; y <= Math.min(280, at.getY() + 96); y++)
|
||||
if (natural(level.getBlockState(new BlockPos(at.getX(), y, at.getZ())))) return true;
|
||||
return false;
|
||||
}
|
||||
private static boolean waterNearby(WorldGenLevel level, BlockPos floor, int radius) {
|
||||
for (int dx = -radius; dx <= radius; dx++) for (int dz = -radius; dz <= radius; dz++)
|
||||
if (level.getFluidState(floor.offset(dx, 0, dz)).is(FluidTags.WATER)) return true;
|
||||
return false;
|
||||
}
|
||||
private static boolean lavaNearby(WorldGenLevel level, BlockPos at) {
|
||||
for (Direction face : Direction.values()) if (level.getFluidState(at.relative(face)).is(FluidTags.LAVA)) return true;
|
||||
return false;
|
||||
}
|
||||
static boolean natural(BlockState state) {
|
||||
return state.is(Blocks.STONE) || state.is(Blocks.DEEPSLATE) || state.is(Blocks.TUFF)
|
||||
|| state.is(Blocks.CALCITE) || state.is(Blocks.ANDESITE) || state.is(Blocks.GRANITE)
|
||||
|| state.is(Blocks.DIORITE) || state.is(Blocks.DIRT) || state.is(Blocks.COARSE_DIRT)
|
||||
|| state.is(Blocks.GRASS_BLOCK) || state.is(Blocks.ROOTED_DIRT) || state.is(Blocks.PODZOL)
|
||||
|| state.is(Blocks.MYCELIUM) || state.is(Blocks.MOSS_BLOCK) || state.is(Blocks.MUD)
|
||||
|| state.is(Blocks.PACKED_MUD) || state.is(Blocks.CLAY);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package fr.koka.sanctuary.worldgen;
|
||||
|
||||
/** A biome field only: never a density, aquifer, heightmap or terrain mutation. */
|
||||
public final class Swamp27Volumes {
|
||||
public static final int MIN_Y = 72;
|
||||
public static final int MAX_Y = 188;
|
||||
private Swamp27Volumes() {}
|
||||
|
||||
public static boolean contains(long seed, int diameter, int x, int y, int z) {
|
||||
if (y < MIN_Y || y >= MAX_Y || diameter <= 0) return false;
|
||||
double radius = diameter * .46;
|
||||
if ((double) x * x + (double) z * z > radius * radius) return false;
|
||||
double verticalEdge = Math.min(1, Math.min((y - MIN_Y) / 16.0, (MAX_Y - y) / 16.0));
|
||||
// Broad, connected sectors change with height; finer noise softens their borders.
|
||||
double broad = noise(seed, x / 94.0, y / 48.0, z / 94.0);
|
||||
double detail = noise(seed ^ 0x7757A27L, x / 37.0, y / 31.0, z / 37.0);
|
||||
return broad + detail * .22 > .25 + (1 - verticalEdge) * .7;
|
||||
}
|
||||
|
||||
private static double noise(long seed, double x, double y, double z) {
|
||||
int ix = (int) Math.floor(x), iy = (int) Math.floor(y), iz = (int) Math.floor(z);
|
||||
double u = fade(x - ix), v = fade(y - iy), w = fade(z - iz);
|
||||
double low = lerp(w, lerp(u, point(seed, ix, iy, iz), point(seed, ix + 1, iy, iz)),
|
||||
lerp(u, point(seed, ix, iy, iz + 1), point(seed, ix + 1, iy, iz + 1)));
|
||||
double high = lerp(w, lerp(u, point(seed, ix, iy + 1, iz), point(seed, ix + 1, iy + 1, iz)),
|
||||
lerp(u, point(seed, ix, iy + 1, iz + 1), point(seed, ix + 1, iy + 1, iz + 1)));
|
||||
return lerp(v, low, high);
|
||||
}
|
||||
private static double point(long seed, int x, int y, int z) {
|
||||
long h = seed ^ x * 0x632BE59BD9B4E019L ^ y * 0x9E3779B97F4A7C15L ^ z * 0x94D049BB133111EBL;
|
||||
h = (h ^ h >>> 30) * 0xBF58476D1CE4E5B9L;
|
||||
h = (h ^ h >>> 27) * 0x94D049BB133111EBL;
|
||||
return ((h ^ h >>> 31) >>> 11) * 0x1.0p-52 - 1;
|
||||
}
|
||||
private static double fade(double t) { return t * t * (3 - 2 * t); }
|
||||
private static double lerp(double t, double a, double b) { return a + (b - a) * t; }
|
||||
}
|
||||
+5
-8
@@ -14,8 +14,7 @@ public final class AerialGeometry24 {
|
||||
}
|
||||
public static BlockPos chest(AerialPlan24.Site s) {
|
||||
return switch (s.kind()) {
|
||||
case BALLOON -> world(s, s.width() / 2 - 2, 2, s.depth() / 2 + 2);
|
||||
case MERCHANT_PROVISIONS, MERCHANT_TOOLS -> world(s, 7, 6, 22);
|
||||
case MERCHANT_PROVISIONS, MERCHANT_TOOLS -> world(s, 7, 4, 24);
|
||||
case MINERAL_ISLET -> {
|
||||
int x = s.width() / 2 + 4, z = s.depth() / 2 + 2;
|
||||
yield world(s, x, terrainTop(s, x, z) - 3, z);
|
||||
@@ -24,27 +23,25 @@ public final class AerialGeometry24 {
|
||||
}
|
||||
public static BlockPos occupant(AerialPlan24.Site s) {
|
||||
if (!s.merchant()) throw new IllegalArgumentException("Only merchant ships have occupants");
|
||||
return world(s, 5, 6, 20);
|
||||
return world(s, 5, 4, 22);
|
||||
}
|
||||
public static UUID occupantUuid(AerialPlan24.Site s) {
|
||||
return UUID.nameUUIDFromBytes(("sanctuary:aerial24:" + s.id() + ":" + s.seed() + ":merchant")
|
||||
.getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
public static BlockPos workstation(AerialPlan24.Site s) { return world(s, 7, 6, 19); }
|
||||
public static BlockPos workstation(AerialPlan24.Site s) { return world(s, 3, 4, 23); }
|
||||
public static BlockPos waterfallSource(AerialPlan24.Site s) {
|
||||
if (!s.waterfall()) throw new IllegalArgumentException("This site has no waterfall");
|
||||
return world(s, s.width() / 2, rockHeight(s) - 3, 3);
|
||||
}
|
||||
public static BlockPos entry(AerialPlan24.Site s) {
|
||||
return switch (s.kind()) {
|
||||
case BALLOON -> world(s, s.width() / 2, 2, s.depth() / 2 - 3);
|
||||
case MERCHANT_PROVISIONS, MERCHANT_TOOLS -> world(s, 7, 6, 12);
|
||||
case MERCHANT_PROVISIONS, MERCHANT_TOOLS -> world(s, 3, 5, 13);
|
||||
case MINERAL_ISLET -> world(s, s.width() / 2, terrainTop(s, s.width() / 2, s.depth() / 2) + 1, s.depth() / 2);
|
||||
};
|
||||
}
|
||||
public static List<BlockPos> interiorChecks(AerialPlan24.Site s) {
|
||||
if (s.merchant()) return List.of(entry(s), world(s, 5, 6, 16), occupant(s), world(s, 5, 6, 23));
|
||||
if (s.kind() == AerialPlan24.Kind.BALLOON) return List.of(entry(s), world(s, s.width() / 2, 2, s.depth() / 2));
|
||||
if (s.merchant()) return List.of(entry(s), occupant(s), world(s, 5, 4, 21), world(s, 3, 4, 24));
|
||||
return List.of(entry(s));
|
||||
}
|
||||
public static int terrainTop(AerialPlan24.Site s, int x, int z) {
|
||||
|
||||
@@ -19,17 +19,16 @@ public final class AerialPlan24 {
|
||||
private final List<Site> sites;
|
||||
private final Set<Long> claims;
|
||||
|
||||
public enum Kind { BALLOON, MERCHANT_PROVISIONS, MERCHANT_TOOLS, MINERAL_ISLET }
|
||||
public enum Kind { MERCHANT_PROVISIONS, MERCHANT_TOOLS, MINERAL_ISLET }
|
||||
|
||||
public AerialPlan24(List<Site> sites) {
|
||||
this.sites = List.copyOf(sites);
|
||||
if (!sites.isEmpty() && (sites.size() != 5
|
||||
|| sites.stream().filter(s -> s.kind() == Kind.BALLOON).count() != 1
|
||||
if (!sites.isEmpty() && (sites.size() != 4
|
||||
|| sites.stream().filter(s -> s.kind() == Kind.MERCHANT_PROVISIONS).count() != 1
|
||||
|| sites.stream().filter(s -> s.kind() == Kind.MERCHANT_TOOLS).count() != 1
|
||||
|| sites.stream().filter(s -> s.kind() == Kind.MINERAL_ISLET).count() != 2
|
||||
|| sites.stream().filter(Site::waterfall).count() != 1))
|
||||
throw new IllegalArgumentException("An aerial plan contains all five sites and exactly one waterfall, or is disabled");
|
||||
throw new IllegalArgumentException("An aerial plan contains all four sites and exactly one waterfall, or is disabled");
|
||||
var ids = new HashSet<String>();
|
||||
var columns = new HashSet<Long>();
|
||||
for (Site site : sites) {
|
||||
@@ -70,8 +69,7 @@ public final class AerialPlan24 {
|
||||
|| waterfall && kind != Kind.MINERAL_ISLET)
|
||||
throw new IllegalArgumentException("Invalid aerial site geometry");
|
||||
if (kind == Kind.MINERAL_ISLET && (width < 40 || depth < 40 || height < 20 + VEGETATION_HEADROOM)
|
||||
|| kind == Kind.BALLOON && height != 28
|
||||
|| (kind == Kind.MERCHANT_PROVISIONS || kind == Kind.MERCHANT_TOOLS) && (width != 11 || depth != 27 || height != 18))
|
||||
|| (kind == Kind.MERCHANT_PROVISIONS || kind == Kind.MERCHANT_TOOLS) && (width != 11 || depth != 28 || height != 18))
|
||||
throw new IllegalArgumentException("Aerial site differs from its version24 gabarit");
|
||||
int worldWidth = facing % 2 == 0 ? width : depth;
|
||||
int worldDepth = facing % 2 == 0 ? depth : width;
|
||||
|
||||
+107
-25
@@ -15,6 +15,8 @@ import fr.koka.sanctuary.worldgen.aerial.AerialPlan24.Site;
|
||||
public final class AerialPlanner24 {
|
||||
public static final int MAX_ATTEMPTS_PER_SITE = 64;
|
||||
public static final int MAX_COLUMNS = 8192;
|
||||
public static final int ARRIVAL_RADIUS = 96;
|
||||
public static final int MIN_ISLET_GAP = 16;
|
||||
private AerialPlanner24() {}
|
||||
public interface Terrain {
|
||||
NoiseColumn column(int x, int z);
|
||||
@@ -40,41 +42,83 @@ public final class AerialPlanner24 {
|
||||
var probe = new Probe(terrain, radius);
|
||||
var sites = new ArrayList<Site>();
|
||||
var claimed = new HashSet<Long>();
|
||||
Kind[] kinds = {Kind.BALLOON, Kind.MERCHANT_PROVISIONS, Kind.MERCHANT_TOOLS, Kind.MINERAL_ISLET, Kind.MINERAL_ISLET};
|
||||
String[] ids = {"balloon", "merchant_provisions", "merchant_tools", "mineral_islet_water", "mineral_islet_dry"};
|
||||
Kind[] kinds = {Kind.MERCHANT_PROVISIONS, Kind.MERCHANT_TOOLS, Kind.MINERAL_ISLET, Kind.MINERAL_ISLET};
|
||||
String[] ids = {"merchant_provisions", "merchant_tools", "mineral_islet_water", "mineral_islet_dry"};
|
||||
double initialAngle = unit(mix(seed ^ 0xA24AE21AL)) * Math.PI * 2;
|
||||
for (int ordinal = 0; ordinal < kinds.length; ordinal++) {
|
||||
long siteSeed = ExpansionIsland.deriveSeed(seed ^ 0xA24AE21AL, ids[ordinal]);
|
||||
int width = kinds[ordinal] == Kind.BALLOON ? 15 : ordinal < 3 ? 11 : 40 + 4 * (int) Math.floorMod(siteSeed, 7);
|
||||
int depth = kinds[ordinal] == Kind.BALLOON ? 19 : ordinal < 3 ? 27 : width;
|
||||
int bodyHeight = kinds[ordinal] == Kind.BALLOON ? 28 : ordinal < 3 ? 18 : 20 + (int) Math.floorMod(mix(siteSeed), 16);
|
||||
int height = bodyHeight + (ordinal >= 3 ? AerialPlan24.VEGETATION_HEADROOM : 0);
|
||||
boolean merchant = ordinal < 2;
|
||||
int width = merchant ? 11 : 40 + 4 * (int) Math.floorMod(siteSeed, 7);
|
||||
int depth = merchant ? 28 : width;
|
||||
int bodyHeight = merchant ? 18 : 20 + (int) Math.floorMod(mix(siteSeed), 16);
|
||||
int height = bodyHeight + (merchant ? 0 : AerialPlan24.VEGETATION_HEADROOM);
|
||||
Site accepted = null;
|
||||
for (int attempt = 0; attempt < MAX_ATTEMPTS_PER_SITE; attempt++) {
|
||||
long draw = mix(siteSeed + attempt * 0x9e3779b97f4a7c15L);
|
||||
double angle = initialAngle + ordinal * (Math.PI * 2 / 5) + (unit(draw) - .5) * .9
|
||||
// Keep the four remaining bearings and seeded shapes; the former first sector is simply free.
|
||||
double angle = initialAngle + (ordinal + 1) * (Math.PI * 2 / 5) + (unit(draw) - .5) * .9
|
||||
+ (attempt / 16) * .41;
|
||||
Shore shore = shore(probe, angle, radius);
|
||||
if (shore == null) continue;
|
||||
int facing = (int) Math.floorMod(mix(draw), 4);
|
||||
int worldWidth = facing % 2 == 0 ? width : depth, worldDepth = facing % 2 == 0 ? depth : width;
|
||||
// The full claim remains outside initial terrain ownership, including corner chunks.
|
||||
double distance = radius + 32 + Math.hypot(worldWidth, worldDepth) / 2 + 64 + (attempt % 4) * 20;
|
||||
int cx = (int) Math.round(Math.cos(angle) * distance), cz = (int) Math.round(Math.sin(angle) * distance);
|
||||
if (Math.hypot(cx - shore.x, cz - shore.z) > 500) continue;
|
||||
int bottom = ordinal == 0 ? Math.clamp(shore.y + 24, 286, 321)
|
||||
: ordinal < 3 ? Math.clamp(shore.y - 4 + (int) Math.floorMod(draw, 13), 180, 300)
|
||||
: Math.clamp(shore.y + 20 - bodyHeight / 2, 210, 320 - bodyHeight);
|
||||
bottom = Math.min(bottom, 350 - height);
|
||||
Shore shore;
|
||||
int cx, cz, bottom;
|
||||
if (merchant) {
|
||||
shore = shore(probe, angle, radius);
|
||||
if (shore == null) continue;
|
||||
// Start from the real edge, not the nominal island radius. The projected half-box
|
||||
// puts the complete hull 16–32 blocks out, even when the ship is rotated.
|
||||
double dx = Math.cos(angle), dz = Math.sin(angle);
|
||||
double offset = Math.abs(dx) * worldWidth / 2.0 + Math.abs(dz) * worldDepth / 2.0
|
||||
+ 16 + (attempt % 3) * 8;
|
||||
cx = shore.x + (int) Math.round(dx * offset);
|
||||
cz = shore.z + (int) Math.round(dz * offset);
|
||||
// Native boarding deck is five blocks above the envelope's bottom.
|
||||
bottom = shore.y - 5 + (int) Math.floorMod(draw, 5);
|
||||
} else {
|
||||
// Both mineral islands hover over Sanctuary, away from its natural arrival.
|
||||
double distance = radius * (.48 + .15 * unit(mix(draw ^ 0x28A1L)));
|
||||
cx = (int) Math.round(Math.cos(angle) * distance);
|
||||
cz = (int) Math.round(Math.sin(angle) * distance);
|
||||
int top = probe.surface(cx, cz);
|
||||
if (top < 120 || probe.terrain.reserved(cx, cz, top, top + 3)) continue;
|
||||
shore = new Shore(cx, top + 1, cz);
|
||||
int highest = top;
|
||||
for (int ox = -worldWidth / 2; ox <= worldWidth / 2; ox += 4)
|
||||
for (int oz = -worldDepth / 2; oz <= worldDepth / 2; oz += 4)
|
||||
highest = Math.max(highest, probe.highest(cx + ox, cz + oz));
|
||||
bottom = highest + MIN_ISLET_GAP + 1;
|
||||
}
|
||||
if (bottom < 32 || bottom + height > 350) continue;
|
||||
var site = new Site(ids[ordinal], kinds[ordinal], cx - worldWidth / 2, bottom, cz - worldDepth / 2,
|
||||
width, height, depth, facing, siteSeed, shore.x, shore.y, shore.z, ordinal == 3);
|
||||
if (site.claimedChunks().stream().anyMatch(c -> claimed.contains(c.pack()))) continue;
|
||||
boolean rootOverlap = site.claimedChunks().stream().anyMatch(c -> {
|
||||
int nearestX = Math.clamp(0, c.getMinBlockX(), c.getMaxBlockX());
|
||||
int nearestZ = Math.clamp(0, c.getMinBlockZ(), c.getMaxBlockZ());
|
||||
return Math.hypot(nearestX, nearestZ) <= radius + 32;
|
||||
});
|
||||
if (rootOverlap || !visible(probe, shore, cx, bottom + (ordinal == 0 ? 18 : bodyHeight - 4), cz)) continue;
|
||||
width, height, depth, facing, siteSeed, shore.x, shore.y, shore.z, ordinal == 2);
|
||||
if (!outsideArrival(site) || site.claimedChunks().stream().anyMatch(c -> claimed.contains(c.pack()))) continue;
|
||||
if (merchant) {
|
||||
double gap = shoreDistance(site);
|
||||
if (gap < 16 || gap > 48 || !clearShip(probe, site)) continue;
|
||||
} else {
|
||||
// Certify every physical rock column, not only the coarse samples used to
|
||||
// choose a height. This may raise the whole unchanged island, never the ground.
|
||||
int highest = 0, supported = 0, count = 0;
|
||||
for (int x = 0; x < width; x++) for (int z = 0; z < depth; z++) {
|
||||
if (!isletColumn(site, x, z)) continue;
|
||||
int wx = site.worldX(x, z), wz = site.worldZ(x, z);
|
||||
highest = Math.max(highest, probe.highest(wx, wz));
|
||||
if (probe.surface(wx, wz) >= 120) supported++;
|
||||
count++;
|
||||
}
|
||||
if (supported * 4 < count * 3) continue;
|
||||
if (site.waterfall()) {
|
||||
var outlet = site.waterfallClaimBounds();
|
||||
for (int x = outlet.minX(); x <= outlet.maxX(); x++) for (int z = outlet.minZ(); z <= outlet.maxZ(); z++)
|
||||
highest = Math.max(highest, probe.highest(x, z));
|
||||
}
|
||||
bottom = Math.max(bottom, highest + MIN_ISLET_GAP + 1);
|
||||
if (bottom + height > 350) continue;
|
||||
site = new Site(site.id(), site.kind(), site.minX(), bottom, site.minZ(), width, height, depth,
|
||||
facing, siteSeed, shore.x, shore.y, shore.z, site.waterfall());
|
||||
if (!clearReservations(probe, site)) continue;
|
||||
}
|
||||
if (!visible(probe, shore, cx, bottom + bodyHeight - 4, cz)) continue;
|
||||
accepted = site; break;
|
||||
}
|
||||
if (accepted == null) throw new IllegalStateException("aerial_sites_unavailable: could not place " + ids[ordinal] + " within the bounded search");
|
||||
@@ -88,6 +132,39 @@ public final class AerialPlanner24 {
|
||||
return result;
|
||||
}
|
||||
|
||||
/** Horizontal distance from the saved land witness to the nearest point of the hull envelope. */
|
||||
public static double shoreDistance(Site site) {
|
||||
return Math.hypot(site.shoreX() - Math.clamp(site.shoreX(), site.minX(), site.maxX()),
|
||||
site.shoreZ() - Math.clamp(site.shoreZ(), site.minZ(), site.maxZ()));
|
||||
}
|
||||
private static boolean outsideArrival(Site site) {
|
||||
if (Math.hypot(Math.clamp(0, site.minX(), site.maxX()), Math.clamp(0, site.minZ(), site.maxZ())) < ARRIVAL_RADIUS) return false;
|
||||
if (site.waterfall()) {
|
||||
var box = site.waterfallClaimBounds();
|
||||
if (Math.hypot(Math.clamp(0, box.minX(), box.maxX()), Math.clamp(0, box.minZ(), box.maxZ())) < ARRIVAL_RADIUS) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
private static boolean isletColumn(Site site, int x, int z) {
|
||||
return AerialGeometry24.radial(site, x, z) <= 1
|
||||
|| site.waterfall() && Math.abs(x - site.width() / 2) <= 2 && z <= 6;
|
||||
}
|
||||
private static boolean clearShip(Probe probe, Site site) {
|
||||
for (int x = site.minX(); x <= site.maxX(); x++) for (int z = site.minZ(); z <= site.maxZ(); z++)
|
||||
if (probe.highest(x, z) >= site.minY() - 3 || probe.terrain.reserved(x, z, site.minY() - 1, site.maxY() + 1)) return false;
|
||||
return true;
|
||||
}
|
||||
private static boolean clearReservations(Probe probe, Site site) {
|
||||
for (int x = site.minX(); x <= site.maxX(); x++) for (int z = site.minZ(); z <= site.maxZ(); z++)
|
||||
if (probe.terrain.reserved(x, z, site.minY() - 1, site.maxY() + 1)) return false;
|
||||
if (site.waterfall()) {
|
||||
var box = site.waterfallClaimBounds();
|
||||
for (int x = box.minX(); x <= box.maxX(); x++) for (int z = box.minZ(); z <= box.maxZ(); z++)
|
||||
if (probe.terrain.reserved(x, z, box.minY(), box.maxY())) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private static Shore shore(Probe probe, double angle, int radius) {
|
||||
double dx = Math.cos(angle), dz = Math.sin(angle);
|
||||
// An outside-in scan finds the outer shore, never an internal pond. Refine its edge to a block.
|
||||
@@ -137,6 +214,11 @@ public final class AerialPlanner24 {
|
||||
for (int y = 382; y > 0; y--) if (column.getBlock(y).isSolid() && column.getBlock(y).getFluidState().isEmpty()) return y;
|
||||
return 0;
|
||||
}
|
||||
int highest(int x, int z) {
|
||||
var column = column(x, z); if (column == null) return 0;
|
||||
for (int y = 382; y > 0; y--) if (!column.getBlock(y).isAir()) return y;
|
||||
return 0;
|
||||
}
|
||||
boolean solid(int x, int y, int z) {
|
||||
var column = column(x, z); return column != null && !column.getBlock(y).isAir();
|
||||
}
|
||||
|
||||
+14
-93
@@ -41,7 +41,12 @@ public final class AerialRenderer24 {
|
||||
BuiltInRegistries.BLOCK.getKey(state.getBlock()).toString(), state.toString());
|
||||
}).toList();
|
||||
result.add(Map.of("id", site.id(), "kind", site.kind().name(), "min", List.of(site.minX(), site.minY(), site.minZ()),
|
||||
"size", List.of(site.worldWidth(), site.height(), site.worldDepth()), "seed", site.seed(), "facing", site.facing(), "blocks", list));
|
||||
"size", List.of(site.worldWidth(), site.height(), site.worldDepth()), "seed", site.seed(), "facing", site.facing(), "blocks", list,
|
||||
"native_template", site.merchant() ? Map.of("id", NativeMerchantShip27.TEMPLATE, "palette", NativeMerchantShip27.PALETTE,
|
||||
"native_blocks", NativeMerchantShip27.restored().nativeBlocks(), "preserved_blocks", NativeMerchantShip27.restored().preservedBlocks(),
|
||||
"added_blocks", NativeMerchantShip27.restored().addedBlocks(), "replaced_blocks", NativeMerchantShip27.restored().replacedBlocks(),
|
||||
"drained_blocks", NativeMerchantShip27.restored().drainedBlocks(),
|
||||
"restoration", "Native hull and stern cabin; repaired side planks/windows, mast and sail; merchant bed/workstation/light added afterwards") : Map.of()));
|
||||
}
|
||||
if (path.toAbsolutePath().getParent() != null) Files.createDirectories(path.toAbsolutePath().getParent());
|
||||
Files.writeString(path, new com.google.gson.GsonBuilder().create().toJson(result) + "\n");
|
||||
@@ -73,7 +78,6 @@ public final class AerialRenderer24 {
|
||||
var destination = connected == null ? clipped : connected;
|
||||
var b = new Builder(s, destination, connected == null ? clip : s.bounds());
|
||||
switch (s.kind()) {
|
||||
case BALLOON -> b.balloon();
|
||||
case MERCHANT_PROVISIONS, MERCHANT_TOOLS -> b.ship();
|
||||
case MINERAL_ISLET -> b.islet();
|
||||
}
|
||||
@@ -114,99 +118,16 @@ public final class AerialRenderer24 {
|
||||
set(x, y, z, state.setValue(BlockStateProperties.BED_PART, BedPart.FOOT));
|
||||
set(x, y, z + 1, state.setValue(BlockStateProperties.BED_PART, BedPart.HEAD));
|
||||
}
|
||||
void balloon() {
|
||||
int cx = s.width() / 2, cz = s.depth() / 2;
|
||||
// The envelope is a rounded shell. Continuous spars attach the four real suspensions.
|
||||
Block accent = (s.seed() & 1) == 0 ? Blocks.WOOL.yellow() : Blocks.WOOL.lightBlue();
|
||||
for (int y = 9; y <= 27; y++) for (int x = 0; x < s.width(); x++) for (int z = 0; z < s.depth(); z++) {
|
||||
double q = Math.pow((x - cx) / 7.1, 2) + Math.pow((z - cz) / 9.1, 2) + Math.pow((y - 18) / 9.2, 2);
|
||||
if (q <= 1.02 && q >= .73) set(x, y, z, Math.abs(x - cx) <= 1 || y == 19 ? accent : y % 6 == 0 ? Blocks.WOOL.lightGray() : Blocks.WOOL.white());
|
||||
}
|
||||
box(cx - 3, 0, cz - 4, cx + 3, 1, cz + 4, Blocks.OAK_PLANKS);
|
||||
for (int x = cx - 3; x <= cx + 3; x++) for (int z = cz - 4; z <= cz + 4; z++) {
|
||||
if (Math.abs(x - cx) == 3 || Math.abs(z - cz) == 4) {
|
||||
set(x, 2, z, Blocks.OAK_PLANKS); slab(x, 3, z);
|
||||
}
|
||||
}
|
||||
// One entry faces a usable standing deck, with a removable railing for boarding.
|
||||
set(cx, 2, cz - 4, Blocks.OAK_FENCE_GATE); set(cx, 3, cz - 4, Blocks.AIR);
|
||||
for (int x : new int[]{cx - 3, cx + 3}) for (int z : new int[]{cz - 4, cz + 4}) {
|
||||
box(x, 3, z, x, 5, z, Blocks.STRIPPED_OAK_LOG);
|
||||
box(x, 6, z, x, 12, z, Blocks.IRON_CHAIN);
|
||||
}
|
||||
for (int z : new int[]{cz - 4, cz + 4}) box(cx - 4, 12, z, cx + 4, 12, z, Blocks.STRIPPED_OAK_LOG);
|
||||
for (int x : new int[]{cx - 3, cx + 3}) box(x, 12, cz - 5, x, 12, cz + 5, Blocks.STRIPPED_OAK_LOG);
|
||||
box(cx - 3, 6, cz, cx + 3, 6, cz, Blocks.STRIPPED_OAK_LOG);
|
||||
box(cx - 3, 5, cz - 4, cx - 3, 6, cz + 4, Blocks.STRIPPED_OAK_LOG);
|
||||
box(cx + 3, 5, cz - 4, cx + 3, 6, cz + 4, Blocks.STRIPPED_OAK_LOG);
|
||||
set(cx, 5, cz, Blocks.COPPER_BULB.waxed().unaffected().defaultBlockState().setValue(BlockStateProperties.LIT, true));
|
||||
lantern(cx - 2, 5, cz); lantern(cx + 2, 5, cz);
|
||||
set(cx + 2, 2, cz + 2, Blocks.CRAFTING_TABLE);
|
||||
set(cx - 2, 2, cz - 2, Blocks.BARREL);
|
||||
set(cx + 2, 2, cz - 2, Blocks.HAY_BLOCK);
|
||||
}
|
||||
int halfWidth(int y, int z) {
|
||||
int end = Math.min(z, s.depth() - 1 - z);
|
||||
return Math.max(0, Math.min(5, Math.min(1 + end, 1 + y)));
|
||||
}
|
||||
void ship() {
|
||||
int cx = 5;
|
||||
for (int z = 0; z < s.depth(); z++) for (int y = 0; y <= 5; y++) {
|
||||
int half = halfWidth(y, z);
|
||||
for (int x = cx - half; x <= cx + half; x++) {
|
||||
boolean hull = y == 0 || y == 5 || Math.abs(x - cx) == half || z < 2 || z >= 25;
|
||||
set(x, y, z, hull ? (y == 0 ? Blocks.STRIPPED_OAK_LOG : Blocks.OAK_PLANKS) : Blocks.AIR);
|
||||
}
|
||||
// The complete upright vanilla ship, offset one block in X for its saved envelope.
|
||||
for (var entry : NativeMerchantShip27.restored().blocks().entrySet()) {
|
||||
var at = entry.getKey(); set(at.getX() + 1, at.getY(), at.getZ(), entry.getValue());
|
||||
}
|
||||
// Curved gunwales remain closed; the wide centre stays clear for boarding small boats.
|
||||
for (int z = 1; z <= 25; z++) {
|
||||
int half = halfWidth(5, z);
|
||||
for (int side : new int[]{-1, 1}) {
|
||||
int x = cx + side * half;
|
||||
set(x, 6, z, Blocks.OAK_FENCE); // Connection properties are completed below.
|
||||
if (z % 5 == 0) set(x, 6, z, Blocks.STRIPPED_OAK_LOG);
|
||||
}
|
||||
}
|
||||
for (int x = 3; x <= 7; x++) { set(x, 6, 1, Blocks.OAK_FENCE); set(x, 6, 25, Blocks.OAK_FENCE); }
|
||||
// Roof, walls, threshold and cabin circulation share one continuous frame.
|
||||
box(2, 6, 17, 8, 9, 24, Blocks.OAK_PLANKS);
|
||||
box(3, 6, 18, 7, 9, 23, Blocks.AIR);
|
||||
box(2, 10, 17, 8, 10, 24, Blocks.OAK_PLANKS);
|
||||
for (int z = 17; z <= 24; z++) {
|
||||
stair(1, 10, z, Direction.EAST, Half.BOTTOM); stair(9, 10, z, Direction.WEST, Half.BOTTOM);
|
||||
}
|
||||
for (int x : new int[]{2, 8}) for (int z : new int[]{17, 24}) box(x, 6, z, x, 10, z, Blocks.STRIPPED_OAK_LOG);
|
||||
for (int z = 19; z <= 21; z++) for (int y = 7; y <= 8; y++) { set(2, y, z, Blocks.GLASS); set(8, y, z, Blocks.GLASS); }
|
||||
for (int x = 4; x <= 6; x++) set(x, 8, 24, Blocks.GLASS);
|
||||
door(5, 6, 17, Direction.SOUTH);
|
||||
bed(3, 6, 22);
|
||||
set(7, 6, 19, s.kind() == AerialPlan24.Kind.MERCHANT_PROVISIONS ? Blocks.COMPOSTER : Blocks.SMITHING_TABLE);
|
||||
set(3, 6, 19, Blocks.CRAFTING_TABLE); set(3, 6, 20, Blocks.BARREL);
|
||||
lantern(5, 9, 20); lantern(5, 9, 23);
|
||||
// Cale reached by an actual supported stair run; no disconnected decorative floor.
|
||||
for (int step = 0; step < 4; step++) {
|
||||
int z = 10 + step, y = 4 - step;
|
||||
box(5, 1, z, 5, y, z, Blocks.OAK_PLANKS);
|
||||
box(5, y + 1, z, 5, 6, z, Blocks.AIR);
|
||||
stair(5, y, z, Direction.NORTH, Half.BOTTOM);
|
||||
}
|
||||
box(3, 1, 7, 7, 1, 20, Blocks.OAK_PLANKS);
|
||||
set(3, 2, 17, Blocks.BARREL); set(7, 2, 17, Blocks.BARREL);
|
||||
lantern(3, 4, 8); lantern(7, 4, 8);
|
||||
Block cargo = s.kind() == AerialPlan24.Kind.MERCHANT_PROVISIONS ? Blocks.HAY_BLOCK : Blocks.IRON_BLOCK;
|
||||
box(2, 6, 5, 3, 7, 7, cargo); box(7, 6, 5, 8, 6, 7, Blocks.BARREL);
|
||||
box(5, 6, 4, 5, 16, 4, Blocks.STRIPPED_OAK_LOG);
|
||||
for (int x = 1; x <= 9; x++)
|
||||
set(x, 16, 4, Blocks.STRIPPED_OAK_LOG.defaultBlockState().setValue(BlockStateProperties.AXIS, Direction.Axis.X));
|
||||
for (int x = 2; x <= 8; x++)
|
||||
set(x, 8, 4, Blocks.STRIPPED_OAK_LOG.defaultBlockState().setValue(BlockStateProperties.AXIS, Direction.Axis.X));
|
||||
set(5, 17, 4, Blocks.LANTERN);
|
||||
// Seven by seven sail around the continuous mast: yard above, boom below, clear deck beneath.
|
||||
for (int y = 9; y <= 15; y++) for (int x = 2; x <= 8; x++)
|
||||
if (x != cx) set(x, y, 4, y == 11 ? Blocks.WOOL.lightGray() : Blocks.WOOL.white());
|
||||
// Stable handrails are explicit, rather than depending on neighbouring chunk updates.
|
||||
for (int z = 2; z <= 24; z++) for (int x : new int[]{0, 10}) if (halfWidth(5, z) == 5 && z % 5 != 0 && !(x == 0 && z == 12))
|
||||
set(x, 6, z, Blocks.OAK_FENCE.defaultBlockState().setValue(BlockStateProperties.NORTH, true).setValue(BlockStateProperties.SOUTH, true));
|
||||
// Reuse the native stern cabin, with its original floor, roof, staircase and treasure-cache position.
|
||||
set(3, 4, 23, s.kind() == AerialPlan24.Kind.MERCHANT_PROVISIONS ? Blocks.COMPOSTER : Blocks.SMITHING_TABLE);
|
||||
bed(3, 4, 21);
|
||||
set(7, 4, 21, Blocks.CRAFTING_TABLE);
|
||||
lantern(5, 6, 22);
|
||||
}
|
||||
void islet() {
|
||||
for (int x = 0; x < s.width(); x++) for (int z = 0; z < s.depth(); z++) {
|
||||
|
||||
@@ -95,13 +95,9 @@ public final class AerialStore24 {
|
||||
|
||||
private static void validateExtent(AerialPlan24 plan, int diameter) throws IOException {
|
||||
if (diameter != 512 && diameter != 724 && diameter != 1024) throw new IOException("Unsupported aerial root size");
|
||||
int radius = (diameter == 724 ? 395 : diameter / 2 + 32) + 32;
|
||||
for (long packed : plan.claimsChunks()) {
|
||||
var c = net.minecraft.world.level.ChunkPos.unpack(packed);
|
||||
int x = Math.clamp(0, c.getMinBlockX(), c.getMaxBlockX());
|
||||
int z = Math.clamp(0, c.getMinBlockZ(), c.getMaxBlockZ());
|
||||
if (Math.hypot(x, z) <= radius) throw new IOException("Aerial claim overlaps initial terrain ownership");
|
||||
}
|
||||
// Aerial bodies may share X/Z with the initial island. Their vertical clearance is
|
||||
// certified at creation; the immutable manifest preserves it on subsequent loads.
|
||||
// Site/Plan constructors still validate finite height, dimensions, count and disjoint claims.
|
||||
}
|
||||
private static JsonObject encode(AerialPlan24 plan, long seed, int diameter) {
|
||||
var root = new JsonObject();
|
||||
|
||||
+1
-1
@@ -17,7 +17,7 @@ public final class AerialStructure24 extends Structure {
|
||||
private final String family;
|
||||
public AerialStructure24(StructureSettings settings, String family) {
|
||||
super(settings);
|
||||
if (!Set.of("balloon", "merchant_ship", "mineral_islet").contains(family)) throw new IllegalArgumentException("Unknown aerial family");
|
||||
if (!Set.of("merchant_ship", "mineral_islet").contains(family)) throw new IllegalArgumentException("Unknown aerial family");
|
||||
this.family = family;
|
||||
}
|
||||
public String family() { return family; }
|
||||
|
||||
+8
-6
@@ -22,12 +22,11 @@ import net.minecraft.world.level.levelgen.structure.StructureType;
|
||||
import net.minecraft.world.level.levelgen.structure.pieces.StructurePieceType;
|
||||
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplateManager;
|
||||
|
||||
/** Native starts for five immutable initial sites. Their void chunks need no terrain island owner. */
|
||||
/** Native starts for four immutable initial sites, in the void or above initial terrain. */
|
||||
public final class AerialStructures24 {
|
||||
public static final ResourceKey<Structure> BALLOON = key("aerial_balloon");
|
||||
public static final ResourceKey<Structure> MERCHANT_SHIP = key("aerial_merchant_ship");
|
||||
public static final ResourceKey<Structure> MINERAL_ISLET = key("aerial_mineral_islet");
|
||||
public static final List<ResourceKey<Structure>> KEYS = List.of(BALLOON, MERCHANT_SHIP, MINERAL_ISLET);
|
||||
public static final List<ResourceKey<Structure>> KEYS = List.of(MERCHANT_SHIP, MINERAL_ISLET);
|
||||
public static final StructureType<AerialStructure24> TYPE = Registry.register(BuiltInRegistries.STRUCTURE_TYPE,
|
||||
SanctuaryMod.id("aerial_v24"), () -> AerialStructure24.CODEC);
|
||||
public static final StructurePieceType PIECE = Registry.register(BuiltInRegistries.STRUCTURE_PIECE,
|
||||
@@ -37,10 +36,10 @@ public final class AerialStructures24 {
|
||||
private static ResourceKey<Structure> key(String id) { return ResourceKey.create(Registries.STRUCTURE, SanctuaryMod.id(id)); }
|
||||
public static void register() {}
|
||||
public static String family(AerialPlan24.Site site) {
|
||||
return switch (site.kind()) { case BALLOON -> "balloon"; case MINERAL_ISLET -> "mineral_islet"; default -> "merchant_ship"; };
|
||||
return switch (site.kind()) { case MINERAL_ISLET -> "mineral_islet"; default -> "merchant_ship"; };
|
||||
}
|
||||
public static ResourceKey<Structure> key(AerialPlan24.Site site) {
|
||||
return switch (site.kind()) { case BALLOON -> BALLOON; case MINERAL_ISLET -> MINERAL_ISLET; default -> MERCHANT_SHIP; };
|
||||
return switch (site.kind()) { case MINERAL_ISLET -> MINERAL_ISLET; default -> MERCHANT_SHIP; };
|
||||
}
|
||||
public static String structureId(AerialPlan24.Site site) { return key(site).identifier().toString(); }
|
||||
public static boolean isPlacing() { return PLACING.get() != null; }
|
||||
@@ -68,7 +67,10 @@ public final class AerialStructures24 {
|
||||
if (previous != null && previous.isValid()) continue;
|
||||
var start = structure.generate(holder, level, registries, generator, generator.getBiomeSource(), sampler,
|
||||
random, templates, origin.seed(), chunk.getPos(), previous == null ? 0 : previous.getReferences(),
|
||||
chunk, structure.biomes()::contains);
|
||||
// Only this explicit saved departure bypasses biome selection. Its complete
|
||||
// geometry and clearance were certified by the aerial planner; the biome below
|
||||
// may be forest, marsh or void. findGenerationPoint still requires this exact plan.
|
||||
chunk, ignoredBiome -> sites.stream().anyMatch(site -> key(site).equals(key)));
|
||||
if (start.isValid()) manager.setStartForStructure(structure, start, chunk);
|
||||
}
|
||||
}
|
||||
|
||||
+81
@@ -0,0 +1,81 @@
|
||||
package fr.koka.sanctuary.worldgen.aerial;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Direction;
|
||||
import net.minecraft.nbt.NbtAccounter;
|
||||
import net.minecraft.nbt.NbtIo;
|
||||
import net.minecraft.nbt.NbtOps;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.block.state.properties.BlockStateProperties;
|
||||
|
||||
/** The actual Mojang upright ship, with local repairs; the vanilla hull, cabin, stairs and slabs remain the source. */
|
||||
public final class NativeMerchantShip27 {
|
||||
public static final String TEMPLATE = "minecraft:shipwreck/rightsideup_full";
|
||||
public static final int WIDTH = 9, DEPTH = 28, NATIVE_HEIGHT = 9, PALETTE = 0;
|
||||
private NativeMerchantShip27() {}
|
||||
public record Restored(Map<BlockPos, BlockState> blocks, int nativeBlocks, int preservedBlocks,
|
||||
int addedBlocks, int replacedBlocks, int drainedBlocks) {}
|
||||
private static final class Loaded { private static final Restored VALUE = load(); }
|
||||
public static Restored restored() { return Loaded.VALUE; }
|
||||
private static Restored load() {
|
||||
String resource = "/data/minecraft/structure/shipwreck/rightsideup_full.nbt";
|
||||
try (var input = NativeMerchantShip27.class.getResourceAsStream(resource)) {
|
||||
if (input == null) throw new IOException("Missing vanilla ship template " + resource);
|
||||
var tag = NbtIo.readCompressed(input, NbtAccounter.defaultQuota());
|
||||
var size = tag.getListOrEmpty("size");
|
||||
if (size.getIntOr(0, -1) != WIDTH || size.getIntOr(1, -1) != NATIVE_HEIGHT || size.getIntOr(2, -1) != DEPTH)
|
||||
throw new IOException("Unexpected vanilla ship dimensions");
|
||||
var palettes = tag.getListOrEmpty("palettes");
|
||||
var palette = palettes.getListOrEmpty(PALETTE);
|
||||
var states = new java.util.ArrayList<BlockState>();
|
||||
for (var value : palette) states.add(BlockState.CODEC.parse(NbtOps.INSTANCE, value).getOrThrow());
|
||||
var original = new LinkedHashMap<BlockPos, BlockState>();
|
||||
for (var value : tag.getListOrEmpty("blocks")) {
|
||||
var block = value.asCompound().orElseThrow(); var pos = block.getListOrEmpty("pos");
|
||||
var at = new BlockPos(pos.getIntOr(0, -1), pos.getIntOr(1, -1), pos.getIntOr(2, -1));
|
||||
var state = states.get(block.getIntOr("state", -1));
|
||||
if (!state.is(Blocks.STRUCTURE_BLOCK)) original.put(at, state);
|
||||
}
|
||||
var result = new LinkedHashMap<>(original);
|
||||
int drained = (int) original.values().stream().filter(state -> state.hasProperty(BlockStateProperties.WATERLOGGED)
|
||||
&& state.getValue(BlockStateProperties.WATERLOGGED)).count();
|
||||
result.replaceAll((at, state) -> state.hasProperty(BlockStateProperties.WATERLOGGED)
|
||||
? state.setValue(BlockStateProperties.WATERLOGGED, false) : state);
|
||||
// Keep only the treasure chest as the site's loot chest; other native caches become empty barrels.
|
||||
for (var entry : original.entrySet()) if (entry.getValue().is(Blocks.CHEST))
|
||||
result.put(entry.getKey(), Blocks.BARREL.defaultBlockState());
|
||||
result.put(new BlockPos(6, 4, 24), Blocks.CHEST.defaultBlockState());
|
||||
// Repair missing side planks by continuing their actual neighbouring native row.
|
||||
for (int z : new int[]{12, 15, 18}) for (int x : new int[]{1, 2, 6, 7}) {
|
||||
var at = new BlockPos(x, 3, z);
|
||||
var neighbour = new BlockPos(x, 3, z - 1);
|
||||
if (!result.containsKey(at) && original.containsKey(neighbour)) result.put(at, original.get(neighbour));
|
||||
}
|
||||
// Restore the existing stern cabin's two damaged window strips, leaving its native stepped roof intact.
|
||||
for (int x : new int[]{1, 7}) for (int z = 21; z <= 24; z++)
|
||||
for (int y = 5; y <= 6; y++) result.put(new BlockPos(x, y, z), Blocks.GLASS.defaultBlockState());
|
||||
for (int x : new int[]{0, 8}) result.put(new BlockPos(x, 6, 22), Blocks.OAK_PLANKS.defaultBlockState());
|
||||
// The original main mast starts at (4,2,11); extend it rather than inventing another hull.
|
||||
for (int y = 6; y <= 16; y++) result.put(new BlockPos(4, y, 11), Blocks.OAK_LOG.defaultBlockState());
|
||||
var yard = Blocks.OAK_LOG.defaultBlockState().setValue(BlockStateProperties.AXIS, Direction.Axis.X);
|
||||
for (int x = 0; x < WIDTH; x++) result.put(new BlockPos(x, 16, 11), yard);
|
||||
for (int y = 10; y <= 15; y++) for (int x = 1; x <= 7; x++) if (x != 4)
|
||||
result.put(new BlockPos(x, y, 11), (y == 12 ? Blocks.WOOL.lightGray() : Blocks.WOOL.white()).defaultBlockState());
|
||||
// Complete the short rear cargo hoist using its native stump, clear of the cabin entrance.
|
||||
for (int y = 6; y <= 8; y++) result.put(new BlockPos(4, y, 16), Blocks.OAK_LOG.defaultBlockState());
|
||||
for (int x = 3; x <= 5; x++) result.put(new BlockPos(x, 8, 16), yard);
|
||||
int preserved = 0, added = 0, replaced = 0;
|
||||
for (var entry : result.entrySet()) {
|
||||
var old = original.get(entry.getKey());
|
||||
if (old == null) added++; else if (old.equals(entry.getValue())) preserved++; else replaced++;
|
||||
}
|
||||
return new Restored(Map.copyOf(result), original.size(), preserved, added, replaced, drained);
|
||||
} catch (IOException | RuntimeException error) {
|
||||
throw new IllegalStateException("Cannot restore required vanilla ship " + TEMPLATE, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -226,7 +226,7 @@
|
||||
"sanctuary.structures.census.status.incompatible": "Incompatible habitat",
|
||||
"sanctuary.structures.census.status.other_dimension": "Reserved for another dimension",
|
||||
"sanctuary.mineral_sanctuary.entry": "Mineral sanctuary: %s %s %s",
|
||||
"sanctuary.world_options.aerial24_hint": "Secrets, abandoned routes and five aerial sites. Vanilla structures remain independent.",
|
||||
"sanctuary.world_options.aerial24_hint": "Secrets, abandoned routes, two ships and two floating islets. Vanilla structures remain independent.",
|
||||
"sanctuary.structures.census.aerial.none": "No aerial sites reserved in this world.",
|
||||
"sanctuary.structures.census.aerial.unknown_site": "Unknown aerial site: %s",
|
||||
"sanctuary.structures.census.aerial.site": "%s · %s · %s · starts %s/%s · features %s/%s · full chunks %s/%s",
|
||||
@@ -235,7 +235,7 @@
|
||||
"sanctuary.structures.census.aerial.status.generated": "generated",
|
||||
"sanctuary.structures.census.aerial.status.missing": "start missing after verification",
|
||||
"sanctuary.structures.census.aerial.status.unknown": "unknown",
|
||||
"structure.sanctuary.aerial_balloon": "Hot air balloon",
|
||||
"structure.sanctuary.aerial_merchant_ship": "Merchant ship",
|
||||
"structure.sanctuary.aerial_mineral_islet": "Mineral islet"
|
||||
"structure.sanctuary.aerial_mineral_islet": "Mineral islet",
|
||||
"biome.sanctuary.swamp27_caves": "Underground swamp"
|
||||
}
|
||||
|
||||
@@ -226,7 +226,7 @@
|
||||
"sanctuary.structures.census.status.incompatible": "Milieu incompatible",
|
||||
"sanctuary.structures.census.status.other_dimension": "Réservée à une autre dimension",
|
||||
"sanctuary.mineral_sanctuary.entry": "Sanctuaire minéral : %s %s %s",
|
||||
"sanctuary.world_options.aerial24_hint": "Secrets, voies abandonnées et cinq lieux suspendus. Les structures vanilla restent indépendantes.",
|
||||
"sanctuary.world_options.aerial24_hint": "Secrets, voies abandonnées, deux navires et deux îlots suspendus. Les structures vanilla restent indépendantes.",
|
||||
"sanctuary.structures.census.aerial.none": "Aucun lieu suspendu réservé dans ce monde.",
|
||||
"sanctuary.structures.census.aerial.unknown_site": "Lieu suspendu inconnu : %s",
|
||||
"sanctuary.structures.census.aerial.site": "%s · %s · %s · départs %s/%s · décoration %s/%s · chunks complets %s/%s",
|
||||
@@ -235,7 +235,7 @@
|
||||
"sanctuary.structures.census.aerial.status.generated": "généré",
|
||||
"sanctuary.structures.census.aerial.status.missing": "départ absent après vérification",
|
||||
"sanctuary.structures.census.aerial.status.unknown": "inconnu",
|
||||
"structure.sanctuary.aerial_balloon": "Montgolfière",
|
||||
"structure.sanctuary.aerial_merchant_ship": "Bateau marchand",
|
||||
"structure.sanctuary.aerial_mineral_islet": "Îlot minéral"
|
||||
"structure.sanctuary.aerial_mineral_islet": "Îlot minéral",
|
||||
"biome.sanctuary.swamp27_caves": "Marais souterrain"
|
||||
}
|
||||
|
||||
-127
@@ -1,127 +0,0 @@
|
||||
{
|
||||
"type": "minecraft:chest",
|
||||
"pools": [
|
||||
{
|
||||
"rolls": 1,
|
||||
"entries": [
|
||||
{
|
||||
"type": "minecraft:item",
|
||||
"name": "minecraft:spyglass"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"rolls": 1,
|
||||
"entries": [
|
||||
{
|
||||
"type": "minecraft:item",
|
||||
"name": "minecraft:compass"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"rolls": 1,
|
||||
"entries": [
|
||||
{
|
||||
"type": "minecraft:item",
|
||||
"name": "minecraft:clock"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"rolls": 1,
|
||||
"entries": [
|
||||
{
|
||||
"type": "minecraft:item",
|
||||
"name": "minecraft:gold_ingot",
|
||||
"functions": [
|
||||
{
|
||||
"function": "minecraft:set_count",
|
||||
"count": {
|
||||
"type": "minecraft:uniform",
|
||||
"min": 8,
|
||||
"max": 16
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"rolls": 1,
|
||||
"entries": [
|
||||
{
|
||||
"type": "minecraft:item",
|
||||
"name": "minecraft:diamond",
|
||||
"functions": [
|
||||
{
|
||||
"function": "minecraft:set_count",
|
||||
"count": {
|
||||
"type": "minecraft:uniform",
|
||||
"min": 2,
|
||||
"max": 4
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"rolls": 1,
|
||||
"entries": [
|
||||
{
|
||||
"type": "minecraft:item",
|
||||
"name": "minecraft:lead",
|
||||
"functions": [
|
||||
{
|
||||
"function": "minecraft:set_count",
|
||||
"count": {
|
||||
"type": "minecraft:uniform",
|
||||
"min": 4,
|
||||
"max": 8
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"rolls": 1,
|
||||
"entries": [
|
||||
{
|
||||
"type": "minecraft:item",
|
||||
"name": "minecraft:bread",
|
||||
"functions": [
|
||||
{
|
||||
"function": "minecraft:set_count",
|
||||
"count": {
|
||||
"type": "minecraft:uniform",
|
||||
"min": 12,
|
||||
"max": 24
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"rolls": 1,
|
||||
"entries": [
|
||||
{
|
||||
"type": "minecraft:item",
|
||||
"name": "minecraft:emerald",
|
||||
"functions": [
|
||||
{
|
||||
"function": "minecraft:set_count",
|
||||
"count": {
|
||||
"type": "minecraft:uniform",
|
||||
"min": 8,
|
||||
"max": 16
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
{
|
||||
"attributes": {
|
||||
"minecraft:audio/background_music": {
|
||||
"default": {
|
||||
"max_delay": 24000,
|
||||
"min_delay": 12000,
|
||||
"sound": "minecraft:music.overworld.swamp"
|
||||
}
|
||||
},
|
||||
"minecraft:gameplay/increased_fire_burnout": true,
|
||||
"minecraft:gameplay/natural_mob_spawns": {
|
||||
"argument": {
|
||||
"spawn_costs": {},
|
||||
"spawns_by_category": {
|
||||
"ambient": [
|
||||
{
|
||||
"type": "minecraft:bat",
|
||||
"count": 8,
|
||||
"weight": 10
|
||||
}
|
||||
],
|
||||
"creature": [
|
||||
{
|
||||
"type": "minecraft:sheep",
|
||||
"count": 4,
|
||||
"weight": 12
|
||||
},
|
||||
{
|
||||
"type": "minecraft:pig",
|
||||
"count": 4,
|
||||
"weight": 10
|
||||
},
|
||||
{
|
||||
"type": "minecraft:chicken",
|
||||
"count": 4,
|
||||
"weight": 10
|
||||
},
|
||||
{
|
||||
"type": "minecraft:cow",
|
||||
"count": 4,
|
||||
"weight": 8
|
||||
},
|
||||
{
|
||||
"type": "minecraft:frog",
|
||||
"count": {
|
||||
"type": "minecraft:uniform",
|
||||
"max_inclusive": 5,
|
||||
"min_inclusive": 2
|
||||
},
|
||||
"weight": 10
|
||||
}
|
||||
],
|
||||
"monster": [
|
||||
{
|
||||
"type": "minecraft:spider",
|
||||
"count": 4,
|
||||
"weight": 100
|
||||
},
|
||||
{
|
||||
"type": "minecraft:zombie",
|
||||
"count": 4,
|
||||
"weight": 95
|
||||
},
|
||||
{
|
||||
"type": "minecraft:zombie_villager",
|
||||
"count": 1,
|
||||
"weight": 5
|
||||
},
|
||||
{
|
||||
"type": "minecraft:skeleton",
|
||||
"count": 4,
|
||||
"weight": 70
|
||||
},
|
||||
{
|
||||
"type": "minecraft:creeper",
|
||||
"count": 4,
|
||||
"weight": 100
|
||||
},
|
||||
{
|
||||
"type": "minecraft:slime",
|
||||
"count": 4,
|
||||
"weight": 100
|
||||
},
|
||||
{
|
||||
"type": "minecraft:enderman",
|
||||
"count": {
|
||||
"type": "minecraft:uniform",
|
||||
"max_inclusive": 4,
|
||||
"min_inclusive": 1
|
||||
},
|
||||
"weight": 10
|
||||
},
|
||||
{
|
||||
"type": "minecraft:witch",
|
||||
"count": 1,
|
||||
"weight": 5
|
||||
},
|
||||
{
|
||||
"type": "minecraft:slime",
|
||||
"count": 1,
|
||||
"weight": 1
|
||||
},
|
||||
{
|
||||
"type": "minecraft:bogged",
|
||||
"count": 4,
|
||||
"weight": 30
|
||||
}
|
||||
],
|
||||
"underground_water_creature": [
|
||||
{
|
||||
"type": "minecraft:glow_squid",
|
||||
"count": {
|
||||
"type": "minecraft:uniform",
|
||||
"max_inclusive": 6,
|
||||
"min_inclusive": 4
|
||||
},
|
||||
"weight": 10
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"modifier": "overlay"
|
||||
},
|
||||
"minecraft:visual/sky_color": "#78a7ff",
|
||||
"minecraft:visual/water_fog_color": "#232317",
|
||||
"minecraft:visual/water_fog_end_distance": {
|
||||
"argument": 0.85,
|
||||
"modifier": "multiply"
|
||||
}
|
||||
},
|
||||
"carvers": [],
|
||||
"downfall": 0.9,
|
||||
"effects": {
|
||||
"dry_foliage_color": "#7b5334",
|
||||
"foliage_color": "#6a7039",
|
||||
"grass_color_modifier": "swamp",
|
||||
"water_color": "#617b64"
|
||||
},
|
||||
"features": [
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[
|
||||
"sanctuary:rift_ore_coal",
|
||||
"sanctuary:rift_ore_iron",
|
||||
"sanctuary:rift_ore_copper",
|
||||
"sanctuary:rift_ore_gold",
|
||||
"sanctuary:rift_ore_redstone",
|
||||
"sanctuary:rift_ore_lapis",
|
||||
"sanctuary:rift_ore_diamond",
|
||||
"sanctuary:rift_ore_emerald",
|
||||
"sanctuary:rift_ore_coal_surface",
|
||||
"sanctuary:rift_ore_iron_surface",
|
||||
"sanctuary:rift_ore_copper_surface",
|
||||
"sanctuary:rift_ore_coal_ledges",
|
||||
"sanctuary:rift_ore_iron_ledges",
|
||||
"sanctuary:rift_ore_copper_ledges"
|
||||
],
|
||||
[],
|
||||
[],
|
||||
[
|
||||
"minecraft:glow_lichen",
|
||||
"sanctuary:swamp27_decoration"
|
||||
],
|
||||
[]
|
||||
],
|
||||
"has_precipitation": true,
|
||||
"temperature": 0.8
|
||||
}
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
{
|
||||
"type": "minecraft:min",
|
||||
"left": {
|
||||
"type": "minecraft:max",
|
||||
"left": {
|
||||
"type": "minecraft:min",
|
||||
"left": {
|
||||
"type": "minecraft:min",
|
||||
"left": {
|
||||
"type": "minecraft:add",
|
||||
"left": {
|
||||
"type": "minecraft:mul",
|
||||
"left": {
|
||||
"type": "minecraft:square",
|
||||
"input": {
|
||||
"type": "minecraft:noise",
|
||||
"noise": "minecraft:cave_layer",
|
||||
"xz_scale": 1.0,
|
||||
"y_scale": 8.0
|
||||
}
|
||||
},
|
||||
"right": 4.0
|
||||
},
|
||||
"right": {
|
||||
"type": "minecraft:clamp",
|
||||
"input": {
|
||||
"type": "minecraft:add",
|
||||
"left": {
|
||||
"type": "minecraft:noise",
|
||||
"noise": "minecraft:cave_cheese",
|
||||
"xz_scale": 1.0,
|
||||
"y_scale": 0.6666666666666666
|
||||
},
|
||||
"right": 0.27
|
||||
},
|
||||
"max": 1.0,
|
||||
"min": -1.0
|
||||
}
|
||||
},
|
||||
"right": "minecraft:overworld/caves/entrances"
|
||||
},
|
||||
"right": {
|
||||
"type": "minecraft:add",
|
||||
"left": "minecraft:overworld/caves/spaghetti_2d",
|
||||
"right": "minecraft:overworld/caves/spaghetti_roughness_function"
|
||||
}
|
||||
},
|
||||
"right": {
|
||||
"type": "minecraft:range_choice",
|
||||
"input": "minecraft:overworld/caves/pillars",
|
||||
"max_exclusive": 0.03,
|
||||
"min_inclusive": -1000000.0,
|
||||
"when_in_range": -1000000.0,
|
||||
"when_out_of_range": "minecraft:overworld/caves/pillars"
|
||||
}
|
||||
},
|
||||
"right": "minecraft:overworld/caves/noodle"
|
||||
}
|
||||
+14
@@ -49,5 +49,19 @@
|
||||
"noise": "sanctuary:rift_detail",
|
||||
"xz_scale": 1.0,
|
||||
"y_scale": 1.0
|
||||
},
|
||||
"geology_28": true,
|
||||
"caves_28": "sanctuary:caves28_native",
|
||||
"ridges_28": {
|
||||
"type": "minecraft:noise",
|
||||
"noise": "minecraft:ridge",
|
||||
"xz_scale": 1.0,
|
||||
"y_scale": 1.0
|
||||
},
|
||||
"erosion_28": {
|
||||
"type": "minecraft:noise",
|
||||
"noise": "minecraft:erosion",
|
||||
"xz_scale": 2.0,
|
||||
"y_scale": 2.0
|
||||
}
|
||||
}
|
||||
|
||||
+14
@@ -49,5 +49,19 @@
|
||||
"noise": "sanctuary:rift_detail",
|
||||
"xz_scale": 1.0,
|
||||
"y_scale": 1.0
|
||||
},
|
||||
"geology_28": true,
|
||||
"caves_28": "sanctuary:caves28_native",
|
||||
"ridges_28": {
|
||||
"type": "minecraft:noise",
|
||||
"noise": "minecraft:ridge",
|
||||
"xz_scale": 1.0,
|
||||
"y_scale": 1.0
|
||||
},
|
||||
"erosion_28": {
|
||||
"type": "minecraft:noise",
|
||||
"noise": "minecraft:erosion",
|
||||
"xz_scale": 2.0,
|
||||
"y_scale": 2.0
|
||||
}
|
||||
}
|
||||
|
||||
+14
@@ -49,5 +49,19 @@
|
||||
"noise": "sanctuary:rift_detail",
|
||||
"xz_scale": 1.0,
|
||||
"y_scale": 1.0
|
||||
},
|
||||
"geology_28": true,
|
||||
"caves_28": "sanctuary:caves28_native",
|
||||
"ridges_28": {
|
||||
"type": "minecraft:noise",
|
||||
"noise": "minecraft:ridge",
|
||||
"xz_scale": 1.0,
|
||||
"y_scale": 1.0
|
||||
},
|
||||
"erosion_28": {
|
||||
"type": "minecraft:noise",
|
||||
"noise": "minecraft:erosion",
|
||||
"xz_scale": 2.0,
|
||||
"y_scale": 2.0
|
||||
}
|
||||
}
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"feature": {
|
||||
"type": "sanctuary:swamp27_decoration"
|
||||
},
|
||||
"placement": []
|
||||
}
|
||||
-10
@@ -1,10 +0,0 @@
|
||||
{
|
||||
"type": "sanctuary:aerial_v24",
|
||||
"family": "balloon",
|
||||
"biomes": [
|
||||
"minecraft:the_void"
|
||||
],
|
||||
"step": "top_layer_modification",
|
||||
"terrain_adaptation": "none",
|
||||
"spawn_overrides": {}
|
||||
}
|
||||
@@ -51,6 +51,7 @@
|
||||
"sanctuary:population_lush_caves",
|
||||
"sanctuary:population_dripstone_caves",
|
||||
"sanctuary:lost_city",
|
||||
"sanctuary:swamp27_caves",
|
||||
"minecraft:ocean",
|
||||
"minecraft:deep_ocean",
|
||||
"minecraft:cold_ocean",
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
package fr.koka.sanctuary.worldgen;
|
||||
|
||||
/** Adapters and finite fractures only. Native noise presence is checked separately in Minecraft. */
|
||||
public final class CavesRelief28Smoke {
|
||||
public static void main(String[] args) {
|
||||
for (int y = 0; y < 384; y++) {
|
||||
require(CavesRelief28.deformationWeight(0, y, 0, 256) == 0, "arrival must not deform");
|
||||
require(CavesRelief28.deformationWeight(256, y, 0, 256) == 0, "outer coast must not deform");
|
||||
if (y <= 180) require(CavesRelief28.deformationWeight(128, y, 0, 256) == 0,
|
||||
"accepted lower mass must not deform");
|
||||
}
|
||||
require(CavesRelief28.deformationWeight(128, 220, 0, 256)
|
||||
< CavesRelief28.deformationWeight(128, 228, 0, 256), "3D deformation must have a smooth vertical envelope");
|
||||
for (float v : new float[] {-10, -1, -.5F, 0, .5F, 1, 10}) {
|
||||
var warp = CavesRelief28.warp(1, v, v, v);
|
||||
var opposite = CavesRelief28.warp(1, -v, -v, -v);
|
||||
require(Math.abs(warp.x()) <= 16 && Math.abs(warp.z()) <= 16 && Math.abs(warp.y()) <= 40,
|
||||
"deformation must stay bounded in all three axes");
|
||||
require(warp.x() == -opposite.x() && warp.y() == -opposite.y() && warp.z() == -opposite.z(),
|
||||
"noise must permit equivalent uplift and subsidence, without positive bias");
|
||||
var fixed = CavesRelief28.warp(0, v, v, v);
|
||||
require(fixed.x() == 0 && fixed.y() == 0 && fixed.z() == 0, "zero envelope leaves coordinates unchanged");
|
||||
}
|
||||
require(CavesRelief28.warp(1, 0, .5F, 0).y() < CavesRelief28.warp(1, 0, .6F, 0).y(),
|
||||
"deformation must not saturate into broad flat plateaus");
|
||||
for (int y = 0; y < 384; y++) for (float cave : new float[] {-1, -.1F, .1F, 1, 64}) {
|
||||
require(CavesRelief28.carve(-.3F, cave, y) == -.3F, "caves must never fill existing void");
|
||||
require(CavesRelief28.carve(.7F, cave, y) <= .7F, "caves must only remove rock");
|
||||
if (y <= 40 || y >= 232) require(CavesRelief28.carve(.7F, cave, y) == .7F, "bounded cave heights");
|
||||
}
|
||||
require(CavesRelief28.carve(.7F, -.1F, 140) < 0, "negative native cave must open solid interior");
|
||||
for (long seed : new long[] {0, 42, 2026}) {
|
||||
var layout = RiftShape.layout(seed);
|
||||
int removed = 0, retained = 0;
|
||||
for (int x = -216; x <= 216; x += 4) for (int z = -216; z <= 216; z += 4) {
|
||||
require(RiftShape.density28(layout, x, 60, z, .7F, 0, 0) == .7F, "rift cannot cut lower boundary");
|
||||
require(RiftShape.density28(layout, x, 260, z, .7F, 0, 0) == .7F, "rift cannot traverse upper surface");
|
||||
int columnCut = 0;
|
||||
for (int y = 88; y <= 244; y += 4) {
|
||||
float value = RiftShape.density28(layout, x, y, z, .7F, .2F, -.3F);
|
||||
require(value <= .7F, "rift cannot add material");
|
||||
if (value < 0) { removed++; columnCut++; } else retained++;
|
||||
}
|
||||
require(columnCut < 22, "fracture cannot cut the entire vertical island mass");
|
||||
}
|
||||
require(removed > 0 && retained > removed, "finite ravines must open passages and retain rock");
|
||||
System.out.println("caves28 rifts seed=" + seed + " removed=" + removed + " retained=" + retained);
|
||||
}
|
||||
System.out.println("CavesRelief28Smoke: signed 3D deformation, native cut adapter and bounded ravines passed");
|
||||
}
|
||||
private static void require(boolean condition, String message) {
|
||||
if (!condition) throw new AssertionError(message);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package fr.koka.sanctuary.worldgen;
|
||||
|
||||
/** Small pure selector test; it does not claim that biome volumes contain generated air. */
|
||||
public final class Swamp27VolumesSmoke {
|
||||
public static void main(String[] args) {
|
||||
for (int diameter : new int[] {512, 724, 1024}) for (long seed : new long[] {0, 42, 2026}) {
|
||||
int selected = 0, total = 0, stackedChanges = 0;
|
||||
for (int x = -diameter / 3; x <= diameter / 3; x += 16)
|
||||
for (int z = -diameter / 3; z <= diameter / 3; z += 16) {
|
||||
boolean below = Swamp27Volumes.contains(seed, diameter, x, 100, z);
|
||||
if (below != Swamp27Volumes.contains(seed, diameter, x, 164, z)) stackedChanges++;
|
||||
for (int y = 88; y <= 172; y += 12) {
|
||||
boolean first = Swamp27Volumes.contains(seed, diameter, x, y, z);
|
||||
// Other probes must not alter the same result.
|
||||
Swamp27Volumes.contains(seed ^ 87, diameter, -x, 256, -z);
|
||||
require(first == Swamp27Volumes.contains(seed, diameter, x, y, z), "mutable selector");
|
||||
if (first) selected++;
|
||||
total++;
|
||||
}
|
||||
for (int y : new int[] {-64, 0, 71, 188, 200, 256, 383, 384})
|
||||
require(!Swamp27Volumes.contains(seed, diameter, x, y, z), "surface/out-of-height selected");
|
||||
}
|
||||
require(selected > 0 && selected < total, "sector must coexist with other caves");
|
||||
require(stackedChanges > 0, "biome field became a full-height column");
|
||||
require(!Swamp27Volumes.contains(seed, diameter, diameter, 120, 0), "outside island selected");
|
||||
System.out.println("swamp27 " + diameter + "/" + seed + " selected=" + selected
|
||||
+ "/" + total + " vertical_changes=" + stackedChanges);
|
||||
}
|
||||
require(!Swamp27Volumes.contains(0, 0, 0, 120, 0), "invalid diameter");
|
||||
}
|
||||
private static void require(boolean value, String message) {
|
||||
if (!value) throw new AssertionError(message);
|
||||
}
|
||||
}
|
||||
@@ -19,7 +19,7 @@ import net.minecraft.world.level.storage.loot.LootTable;
|
||||
|
||||
/** Pure finite planning witnesses: nine size/seed combinations, rotation and pre-visit claims. */
|
||||
public final class Aerial24Smoke {
|
||||
public static void main(String[] args) {
|
||||
public static void main(String[] args) throws java.io.IOException {
|
||||
SharedConstants.tryDetectVersion(); Bootstrap.bootStrap();
|
||||
BlockState[] column = new BlockState[384];
|
||||
for (int y = 0; y < column.length; y++) column[y] = (y >= 80 && y <= 240 ? Blocks.STONE : Blocks.AIR).defaultBlockState();
|
||||
@@ -27,10 +27,13 @@ public final class Aerial24Smoke {
|
||||
int[] queries = {0};
|
||||
var plan = AerialPlanner24.plan(seed, size, (x, z) -> { queries[0]++; return new NoiseColumn(0, column); });
|
||||
var replay = AerialPlanner24.plan(seed, size, (x, z) -> new NoiseColumn(0, column));
|
||||
String preview = System.getProperty("sanctuary.aerial.preview");
|
||||
if (preview != null && size == 724 && seed == 42)
|
||||
AerialRenderer24.writeDiagnostic(java.nio.file.Path.of(preview), plan.sites().stream().filter(AerialPlan24.Site::merchant).toList());
|
||||
require(plan.sites().equals(replay.sites()) && plan.claimsChunks().equals(replay.claimsChunks()), "Plan does not depend on time or loaded chunks");
|
||||
require(queries[0] <= AerialPlanner24.MAX_COLUMNS, "Finite terrain query budget");
|
||||
require(plan.sites().size() == 5 && plan.sites().stream().filter(AerialPlan24.Site::merchant).count() == 2,
|
||||
"Five complete sites, including two distinct merchant ships");
|
||||
require(plan.sites().size() == 4 && plan.sites().stream().filter(AerialPlan24.Site::merchant).count() == 2,
|
||||
"Four complete sites, including two distinct merchant ships");
|
||||
require(plan.sites().stream().filter(AerialPlan24.Site::waterfall).count() == 1, "Exactly one waterfall site");
|
||||
var allClaims = new HashSet<Long>();
|
||||
var root = ExpansionIsland.origin(size, seed, 24);
|
||||
@@ -45,12 +48,24 @@ public final class Aerial24Smoke {
|
||||
&& site.height() == site.rockHeight() + AerialPlan24.VEGETATION_HEADROOM,
|
||||
"Vegetation adds eight blocks of headroom without enlarging the stone body");
|
||||
double distance = Math.hypot(site.locator().getX() - site.shoreX(), site.locator().getZ() - site.shoreZ());
|
||||
require(distance <= 512, "Every site has a nearby shore witness");
|
||||
require(distance <= 512, "Every site has a nearby ground witness");
|
||||
if (site.merchant()) {
|
||||
double gap = AerialPlanner24.shoreDistance(site);
|
||||
require(gap >= 16 && gap <= 48, "Complete ship envelope is 16–48 blocks from its actual shore");
|
||||
require(Math.abs(AerialGeometry24.entry(site).getY() - site.shoreY()) <= 4,
|
||||
"Boarding deck remains near the shore's walking height");
|
||||
} else {
|
||||
require(root.ownsChunk(site.startChunkX(), site.startChunkZ()), "Mineral islets hover over the initial island");
|
||||
require(site.minY() - 240 - 1 >= AerialPlanner24.MIN_ISLET_GAP,
|
||||
"The unchanged rocky body has at least sixteen air blocks above the terrain");
|
||||
}
|
||||
require(Math.hypot(Math.clamp(0, site.minX(), site.maxX()), Math.clamp(0, site.minZ(), site.maxZ())) >= AerialPlanner24.ARRIVAL_RADIUS,
|
||||
"Aerial bodies preserve the natural arrival zone");
|
||||
require(plan.atStartChunk(site.startChunkX(), site.startChunkZ()).contains(site), "Native departure belongs to its saved site");
|
||||
for (var chunk : site.claimedChunks()) {
|
||||
require(allClaims.add(chunk.pack()), "Site claims do not overlap");
|
||||
require(site.ownsChunk(chunk.x(), chunk.z()) && plan.ownsChunk(chunk.x(), chunk.z()), "Claim query agrees with the immutable mask");
|
||||
require(!root.ownsChunk(chunk.x(), chunk.z()), "Claim does not replace initial island ownership");
|
||||
|
||||
}
|
||||
var overlapping = new ExpansionIsland("overlap", "sanctuary", "east", "temperate", 64,
|
||||
site.startChunkX() * 16, site.startChunkZ() * 16, 42, "natural", "reserved", 24);
|
||||
@@ -68,7 +83,7 @@ public final class Aerial24Smoke {
|
||||
require(AerialPiece24.decode(AerialPiece24.encode(site)).equals(site), "Saved site descriptor round-trips without changing its seed or rotation");
|
||||
if (seed == 0) verifyRender(site);
|
||||
}
|
||||
require(allClaims.equals(plan.claimsChunks()), "Combined claims contain only the five finite site masks");
|
||||
require(allClaims.equals(plan.claimsChunks()), "Combined claims contain only the four finite site masks");
|
||||
var far = new ExpansionIsland("far", "sanctuary", "east", "temperate", 64, 4096, 4096, 1, "natural", "reserved", 24);
|
||||
require(!plan.conflicts(far), "Aerial sites do not reserve an infinite ring around Sanctuary");
|
||||
require(!plan.conflicts(root), "Root ownership is not treated as a future expansion");
|
||||
@@ -78,11 +93,11 @@ public final class Aerial24Smoke {
|
||||
catch (UnsupportedOperationException expected) { }
|
||||
}
|
||||
require(AerialPlan24.EMPTY.isEmpty() && AerialPlan24.EMPTY.claimsChunks().isEmpty(), "Disabled sites reserve no space");
|
||||
try { new AerialPlan24(List.of(new AerialPlan24.Site("balloon", AerialPlan24.Kind.BALLOON,
|
||||
500, 290, 0, 15, 28, 19, 0, 0, 250, 240, 0, false))); throw new AssertionError("Partial plan accepted"); }
|
||||
try { new AerialPlan24(List.of(new AerialPlan24.Site("merchant_provisions", AerialPlan24.Kind.MERCHANT_PROVISIONS,
|
||||
500, 290, 0, 11, 18, 28, 0, 0, 250, 240, 0, false))); throw new AssertionError("Partial plan accepted"); }
|
||||
catch (IllegalArgumentException expected) { }
|
||||
verifyReservedShoreAndView(column);
|
||||
System.out.println("Aerial24Smoke: nine deterministic plans, five sites, four rotations, finite non-overlapping claims and pre-visit conflicts passed.");
|
||||
System.out.println("Aerial24Smoke: nine deterministic plans, four sites, four rotations, finite non-overlapping claims and pre-visit conflicts passed.");
|
||||
}
|
||||
private static void verifyReservedShoreAndView(BlockState[] column) {
|
||||
var original = AerialPlanner24.plan(0, 724, (x, z) -> new NoiseColumn(0, column)).sites().getFirst();
|
||||
@@ -106,7 +121,7 @@ public final class Aerial24Smoke {
|
||||
require(columns[0] <= AerialPlanner24.MAX_COLUMNS, "Reservations preserve the bounded terrain sampling budget");
|
||||
for (var site : replacement.sites())
|
||||
require(!terrain.reserved(site.shoreX(), site.shoreZ(), site.shoreY() - 1, site.shoreY() + 2),
|
||||
"Every accepted shore stays outside planned root structures");
|
||||
"Every accepted ground witness stays outside planned root structures");
|
||||
}
|
||||
}
|
||||
private static AerialPlan24.Site rotate(AerialPlan24.Site site, int facing) {
|
||||
@@ -126,6 +141,24 @@ public final class Aerial24Smoke {
|
||||
require(complete.equals(clipped) && clippedChests[0] == 1, "Chunk clipping reconstructs the complete authored structure without duplicate chests");
|
||||
long sources = complete.values().stream().filter(state -> state.is(Blocks.WATER)).count();
|
||||
require(sources == (site.waterfall() ? 1 : 0), "Renderer writes one source, never a fabricated full waterfall column");
|
||||
if (site.merchant()) {
|
||||
require(complete.values().stream().allMatch(state -> state.getFluidState().isEmpty()),
|
||||
"Restored ships contain no fluid, including inside stairs, slabs, fences or trapdoors");
|
||||
var nativeShip = NativeMerchantShip27.restored();
|
||||
require(nativeShip.nativeBlocks() == 659, "The exact vanilla template contributes 662 authored blocks minus its three data markers");
|
||||
require(nativeShip.preservedBlocks() + nativeShip.replacedBlocks() == nativeShip.nativeBlocks(), "No native hull position disappears during restoration");
|
||||
require(nativeShip.blocks().values().stream().anyMatch(state -> state.is(Blocks.OAK_STAIRS))
|
||||
&& nativeShip.blocks().values().stream().anyMatch(state -> state.is(Blocks.SPRUCE_SLAB)),
|
||||
"The ship retains its native shaped stairs and slabs");
|
||||
require(complete.values().stream().noneMatch(state -> state.is(Blocks.STRUCTURE_BLOCK)), "No native data marker is placed in the restored ship");
|
||||
for (var at : AerialGeometry24.interiorChecks(site)) {
|
||||
require(complete.getOrDefault(at, Blocks.AIR.defaultBlockState()).isAir()
|
||||
&& complete.getOrDefault(at.above(), Blocks.AIR.defaultBlockState()).isAir(),
|
||||
"Boarding, cabin circulation and merchant standing spaces remain open");
|
||||
require(complete.getOrDefault(at.below(), Blocks.AIR.defaultBlockState()).isSolid(),
|
||||
"Each boarding and interior witness retains its real floor");
|
||||
}
|
||||
}
|
||||
if (site.kind() == AerialPlan24.Kind.MINERAL_ISLET) {
|
||||
var chest = AerialGeometry24.chest(site);
|
||||
for (int dy = 1; dy <= 3; dy++) require(complete.get(chest.above(dy)) != null && complete.get(chest.above(dy)).isSolid(), "Mineral chest is buried under three intact layers");
|
||||
|
||||
+11
-9
@@ -48,9 +48,12 @@ public final class AerialPersistence24Smoke {
|
||||
require(!site.ownsChunk((site.maxX() >> 4) + 2, (site.maxZ() >> 4) + 2), "Unused corners of the old broad margin stay free");
|
||||
}
|
||||
require(!Files.exists(directory), "Manifest creation must not prematurely initialize the journal");
|
||||
require(draws.get() == 1 && stored.plan().sites().size() == 5, "One complete initial plan is recorded");
|
||||
require(draws.get() == 1 && stored.plan().sites().size() == 4, "One complete initial plan is recorded");
|
||||
var journal = new ExpansionJournal(directory, SEED, 724, 24, stored.token());
|
||||
var root = ExpansionIsland.origin(724, SEED, 24);
|
||||
require(stored.plan().sites().stream().filter(site -> site.kind() == Kind.MINERAL_ISLET)
|
||||
.allMatch(site -> root.ownsChunk(site.startChunkX(), site.startChunkZ())),
|
||||
"An immutable elevated islet may share initial terrain chunks without replacing root ownership");
|
||||
Path journalFile = directory.resolve(ExpansionJournal.FILE_NAME);
|
||||
byte[] pristineJournal = Files.readAllBytes(journalFile);
|
||||
require(JsonParser.parseString(Files.readString(journalFile)).getAsJsonObject().get("schema").getAsInt() == 2,
|
||||
@@ -65,7 +68,7 @@ public final class AerialPersistence24Smoke {
|
||||
journal.commit(List.of(root));
|
||||
require(Arrays.equals(pristineJournal, Files.readAllBytes(journalFile)), "A no-op commit preserves bytes");
|
||||
|
||||
var collision = child("collision", 672, 0);
|
||||
var collision = child("collision", "south", 0, 672);
|
||||
require(!collision.conflicts(root), "Collision witness is outside initial island reservations");
|
||||
require(stored.plan().conflicts(collision), "A never-generated satellite reserves its chunks");
|
||||
reject(() -> journal.commit(List.of(root, collision)), "Reservation over an unvisited aerial site");
|
||||
@@ -135,17 +138,16 @@ public final class AerialPersistence24Smoke {
|
||||
}
|
||||
}
|
||||
|
||||
private static ExpansionIsland child(String id, int x, int z) {
|
||||
return new ExpansionIsland(id, "sanctuary", "east", "temperate", 64, x, z,
|
||||
private static ExpansionIsland child(String id, String direction, int x, int z) {
|
||||
return new ExpansionIsland(id, "sanctuary", direction, "temperate", 64, x, z,
|
||||
ExpansionIsland.deriveSeed(SEED, id), "natural", "reserved", 24);
|
||||
}
|
||||
private static AerialPlan24 fixture() {
|
||||
return new AerialPlan24(List.of(
|
||||
new Site("balloon", Kind.BALLOON, 660, 290, 0, 15, 28, 19, 0, 1, 390, 240, 0, false),
|
||||
new Site("merchant_provisions", Kind.MERCHANT_PROVISIONS, 0, 235, 660, 11, 18, 27, 0, 2, 0, 240, 390, false),
|
||||
new Site("merchant_tools", Kind.MERCHANT_TOOLS, -740, 242, 0, 11, 18, 27, 0, 3, -390, 240, 0, false),
|
||||
new Site("mineral_islet_water", Kind.MINERAL_ISLET, -450, 255, -650, 48, 36, 48, 0, 4, -200, 240, -330, true),
|
||||
new Site("mineral_islet_dry", Kind.MINERAL_ISLET, 460, 260, -600, 56, 40, 56, 0, 5, 250, 240, -300, false)));
|
||||
new Site("merchant_provisions", Kind.MERCHANT_PROVISIONS, 0, 235, 660, 11, 18, 28, 0, 2, 0, 240, 390, false),
|
||||
new Site("merchant_tools", Kind.MERCHANT_TOOLS, -740, 242, 0, 11, 18, 28, 0, 3, -390, 240, 0, false),
|
||||
new Site("mineral_islet_water", Kind.MINERAL_ISLET, -180, 270, -160, 48, 36, 48, 0, 4, -156, 240, -136, true),
|
||||
new Site("mineral_islet_dry", Kind.MINERAL_ISLET, 125, 270, -170, 56, 40, 56, 0, 5, 153, 240, -142, false)));
|
||||
}
|
||||
private static String digest(byte[] bytes) throws Exception { return HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256").digest(bytes)); }
|
||||
private static void require(boolean value, String message) { if (!value) throw new AssertionError(message); }
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
name = "Sanctuary"
|
||||
author = "KOKA99CAB"
|
||||
version = "0.1.0-alpha.24"
|
||||
version = "0.1.0-alpha.28"
|
||||
pack-format = "packwiz:1.1.0"
|
||||
|
||||
[index]
|
||||
|
||||
Reference in New Issue
Block a user