Compare commits

...
Author SHA1 Message Date
koka 817ab0b2b1 test: freeze alpha.25-preview.1 for terrain feedback
Build Sanctuary / build (push) Waiting to run
2026-09-11 20:27:05 +02:00
112 changed files with 7305 additions and 203 deletions
+184
View File
@@ -0,0 +1,184 @@
# Alpha.25 — reliefs et profondeurs de Sanctuary
## Portée et sauvegardes
La génération `sanctuary:island_v25` concerne les **nouveaux mondes Sanctuary**.
Les trois diamètres nominaux restent **512 / 724 / 1 024 blocs**, avec **Moyen**
par défaut et le bouton Minecraft **Personnaliser**. Le monde conserve sa
hauteur de 384 blocs et les nuages existants. Les sommets naturels peuvent
approcher Y358 ; cette borne n'est pas une hauteur imposée à chaque massif.
Les codecs et réglages historiques restent enregistrés. Le preset24 est
conservé sous `sanctuary:sanctuary_v24` ; charger une sauvegarde24 ne transforme
pas son générateur en25. Aucun chunk existant n'est régénéré, aucun monde
personnel n'est migré et aucun portail vers une nouvelle dimension n'est activé.
Les modifications de relief, de cavités et de géologie décrites ci-dessous
s'appliquent à **l'île initiale uniquement**. Les continents d'expansion
conservent leurs champs de terrain et leurs océans précédents ; la nouveauté
pour leurs océans est le contrat de monument détaillé plus bas.
## Relief, cavités et ressources
Le terrain initial utilise les réglages distincts `unified_25_5`,
`unified_25_10` et `unified_25_20`. Des groupes de pics dispersés étirent
verticalement les masses existantes, avec de la calcite dans leurs hauteurs.
L'étirement reste monotone et s'atténue à l'arrivée ; il ne crée pas un plateau
central artificiel. L'emprise horizontale initiale reste bornée par la taille
choisie. Aucun bambou supplémentaire n'est associé aux massifs de calcite.
Un plan déterministe ajoute des cavités sèches, des refuges à champignons et
des propositions de lacs souterrains, ainsi que des chambres basses de lave.
Les chambres d'un même groupe sont reliées par des galeries sinueuses ; les
failles courtes dérivent avec la hauteur. Les cavités existent indépendamment
du choix de placer un village ou un donjon. Elles ne garantissent pas qu'une
structure y trouvera la place nécessaire.
Les lacs ne sont remplis qu'après admission de leur fond et de leurs berges
dans la densité sculptée. Une proposition non retenue reste une cavité sèche.
Les plans hérités d'eau et de lave participent également aux exclusions.
Il n'y a ni couche d'océan globale sous l'île, ni obligation de remplir tous
les volumes creusés. Les profondeurs de cette première passe sont de cinq
blocs pour les nouveaux lacs d'eau et de trois pour les bassins de lave.
Les refuges combinent mycélium, mousse et podzol. Ils reçoivent des tentatives
de grands champignons vanilla et de mooshrooms aux emplacements habitables.
Leur quantité effective dépend du terrain, de la place disponible et des
décorations voisines. Les biomes `terrain25_*` gardent les forêts tempérées,
les grottes luxuriantes, les dripstones et le sulfure ; un secteur humide
localisé fournit également un véritable biome de marais.
Les nouveaux filons se mesurent à la **profondeur relative de la colonne
naturelle intacte**, entre son sommet et sa base, et non à la hauteur du sol
d'une grotte. Charbon, fer et cuivre accompagnent les couches accessibles ;
or, lapis, redstone et émeraude ont leurs bandes de profondeur. Les nouvelles
poches de diamant sont recherchées au-delà de 65 % de profondeur relative.
Ce sont des réserves physiques finies, sans quota garanti par minerai. Les
filons propres aux anciens secrets et aux îlots aériens conservent leur
distribution locale distincte.
## Rivières ouvertes et réservation des écoulements
Pour chaque extrémité admissible d'une rivière, la génération cherche une
sortie proche sur 32 directions, entre 8 et 64 blocs. Elle conserve un lit
supporté et des berges jusqu'à la lèvre. La sortie peut ensuite s'ouvrir sur
le flanc de l'île et laisser l'eau tomber dans le vide suivant les règles
Minecraft. Aucune paire de sorties n'est forcée : une rivière peut avoir
zéro, une ou deux extrémités adaptées.
Les réservations d'expansion sont établies avant l'exploration. Pour les
nouvelles sorties initiales25, elles couvrent les cellules du chenal et un
élargissement local de sept blocs pour la propagation de l'eau, de Y0 à son
niveau de départ. Elles sont projetées sur les chunks parce qu'une expansion
génère des colonnes complètes. Ce masque ne réserve pas tout le rectangle
englobant une diagonale ni une nouvelle couronne autour de l'île.
## Secrets, lieux aériens et nouvelles structures
La grande traversée, la salle d'expansion et les secrets conservés utilisent
le terrain25 pour chercher leurs appuis. Le sanctuaire minéral garde son
volume de 79 × 73 × 27 blocs et sa chambre centrale. Son nouveau rendu résout
les angles des escaliers en pierre et en cobblestone avant le découpage par
chunk. Un cadre central de blackstone de **7 × 9 blocs**, ouvert sur
**5 × 7 blocs**, reste **inactif**. Il ne contient aucun bloc de portail et
ne rend pas la blackstone utilisable comme cadre Nether ailleurs.
Les cinq lieux aériens24 restent présents dans le plan initial lorsqu'ils
sont activés : montgolfière, deux navires marchands et deux îlots minéraux,
dont un avec cascade. Leur géométrie et leurs limites propres restent
inchangées ; les points de rivage cherchent les secteurs compatibles avec
le nouveau relief. La recherche reste bornée et échoue explicitement si
elle ne peut pas établir les cinq sites, au lieu d'en omettre un en silence.
La nouvelle sélection souterraine recherche, sans les forcer :
- au plus un village abandonné étendu dans une cavité sèche, avec les pièces
et habitants vanilla des variantes abandonnées ;
- une mine native, éventuellement accompagnée d'une petite cabane de mineur
et d'un raccord réellement praticable ;
- une hutte native dans le secteur de marais, à proximité d'eau ;
- jusqu'à trois salles de monstres utilisant la feature vanilla, ses
conditions d'ouverture, ses coffres et son spawner.
Le village recherche au moins six bâtiments et un accès vers une grotte
naturelle. Les fondations, la couverture rocheuse, les fluides et les
réservations des secrets sont vérifiés avant son admission. Les ajustements
de sol et de voûte restent dans les volumes acceptés. La sélection générale
des structures natives ne rajoute pas de village de surface sur l'île25 ;
les villages des expansions gardent leur catalogue.
Les départs et les pièces sont enregistrés comme structures natives. Les
salles de monstres restent des features et sont recensées séparément. Le
chargement d'un chunk terminé ne reconstruit pas son mobilier, son butin ni
ses habitants retirés. Les structures facultatives refusées sont indiquées
dans les diagnostics de planification ; leur absence peut être normale.
## Ordre de préparation et options
Avant de demander les nouveaux chunks, le serveur prépare le contexte de
terrain25 et ses plans de cavités/hydrologie, puis les réservations des
secrets existants. Le plan aérien s'appuie sur ces colonnes finales et ces
protections. Son manifeste immuable est lié au journal25. La sélection
souterraine25 vient ensuite : elle respecte les secrets et les sites déjà
réservés. Les projections de sol admises sont partagées entre les colonnes
consultées par les pièces natives et la production du terrain.
Les options **Générer les structures** et **Structures expérimentales**
contrôlent les secrets expérimentaux, les satellites et les nouveaux lieux
souterrains. Leur désactivation n'enlève pas le terrain naturel, les cavités,
les lacs, les minerais ou leur végétation. Les structures natives générales
restent soumises à l'option Minecraft **Générer les structures**. La grande
traversée et la salle d'expansion conservent leur règle antérieure : elles
restent recherchées avec cette option Minecraft active, même si les structures
expérimentales sont désactivées. Ces options sont enregistrées avec le monde.
Les calculs naturels sont dérivés de la graine, de la taille et du générateur.
Leurs caches bornés ne dépendent pas de l'ordre de visite : cache de relief
par thread, cache de 32 768 colonnes naturelles par contexte et plans
hydrologiques régionaux réutilisés. La sélection souterraine limite ses
candidats et ses sondages ; un budget épuisé refuse la famille concernée.
Ces bornes n'impliquent pas une durée de chargement garantie.
## Monument garanti des nouvelles expansions océaniques25
Avec **Générer les structures** activé, toute nouvelle expansion25 de type
`ocean`, de diamètre **512 ou 1 024**, doit trouver un monument natif adapté
**avant sa réservation**. La recherche s'inscrit dans le délai d'admission
existant de 30 secondes. Si aucun départ ne convient, la demande est refusée
sans publier cette expansion. Une expansion non océanique garde l'admission
d'au moins une structure native adaptée, avec une priorité souple aux
familles absentes des régions réellement vérifiées.
Le départ exact du monument, ses pièces, son enveloppe, son algorithme et
son empreinte SHA-256 sont figés dans `structure_plan25`. Le journal25 utilise
le schéma3 et inclut ces données dans l'identité du certificat de réemploi
du vide. Les champs restent absents des descriptors historiques. La reprise
valide et restaure ce départ ; elle ne retire pas un autre monument au hasard.
Le même départ sert à la génération réelle et à la localisation. Son volume
protégé empêche d'autres départs de le recouvrir. Le comportement des pièces,
des gardiens et des grands gardiens reste celui de Minecraft. Le plan n'est
pas un mécanisme de restauration après destruction. Une expansion encore
réservée ne devient pas localisable comme si sa préparation était terminée :
la localisation attend son véritable état `ready`.
Avec **Générer les structures** désactivé, aucun monument n'est imposé ni
enregistré. Ce changement ne modifie pas les océans et admissions des mondes23
ou24. Le manifeste aérien25 réside dans `data/sanctuary-aerial-v25/` et son
journal dans `data/sanctuary-world-v25/` ; les chemins historiques restent lus
selon leur version.
## Limites et vérifications
Ce document décrit le contrat présent dans les sources, pas une annonce de
publication. Les tests ajoutés couvrent les plans déterministes, la géologie
relative, le rendu minéral par chunks, les réservations d'eau, le codec du
monument et les journaux historiques. Les scénarios natifs de terrain, de
structures conditionnelles et de cycle de vie du monument sont distincts des
tests purs. Les résultats, graines, durées et réouvertures effectivement
exécutés doivent être consignés dans le compte rendu de validation avant
livraison ; aucune matrice native réussie n'est présumée ici.
Cette passe ne fournit pas de boss Sanctuary, de combat de progression, de
portail Cavernes fonctionnel, de nouvelle dimension ni de raid. Les grands
espaces explorables et le cadre central préparent ces usages sans les activer.
+10
View File
@@ -0,0 +1,10 @@
# Alpha.25 preview.1
Instantané de travail pour tester le relief, les cavités et les cascades.
Importer le MRpack dans une instance de test Prism, puis créer un nouveau monde Sanctuary : Moyen (724), graine 0, structures et structures expérimentales activées. Le mode créatif est pratique pour inspecter le relief.
La validation des structures conditionnelles et du cycle de vie des nouvelles générations est encore en cours. Ce paquet ne prétend pas satisfaire toutes les vérifications de l’alpha.25 finale.
Le générateur porte l’identifiant expérimental `sanctuary:island_v25_preview_1`. Les mondes de cet instantané restent des mondes de test et ne sont pas migrés vers le générateur final. Garder cette instance pour les rouvrir. Le canal stable et l’instance habituelle ne sont pas modifiés.
Minecraft 26.3-pre-2 ; mod et pack 0.1.0-alpha.25-preview.1. Compilation et contrôles effectués sur Mac ; Windows non testé.
+2 -2
View File
@@ -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.25-preview.1
pack_version=0.1.0-alpha.25-preview.1
maven_group=fr.koka.sanctuary
+74 -2
View File
@@ -40,7 +40,22 @@ tasks.named('runGameTest') {
systemProperty('sanctuary.test.unified', providers.gradleProperty('sanctuaryUnifiedTests').getOrElse('true'))
systemProperty('sanctuary.test.diameter', providers.gradleProperty('sanctuaryTestDiameter').getOrElse('724'))
systemProperty('sanctuary.test.generation', providers.gradleProperty('sanctuaryTestGeneration').getOrElse(
providers.gradleProperty('sanctuaryLostCityTests').map { it.toBoolean() }.getOrElse(false) ? '14' : providers.gradleProperty('sanctuaryLostCity15Tests').map { it.toBoolean() }.getOrElse(false) ? '15' : providers.gradleProperty('sanctuaryLostCity16Tests').map { it.toBoolean() }.getOrElse(false) ? '16' : providers.gradleProperty('sanctuaryRuins17Tests').map { it.toBoolean() }.getOrElse(false) ? '17' : providers.gradleProperty('sanctuaryHeritage19Tests').map { it.toBoolean() }.getOrElse(false) ? '19' : (providers.gradleProperty('sanctuaryHeritage20Tests').map { it.toBoolean() }.getOrElse(false) || providers.gradleProperty('sanctuaryLostCity20Tests').map { it.toBoolean() }.getOrElse(false)) ? '20' : (providers.gradleProperty('sanctuaryHeritage21Tests').map { it.toBoolean() }.getOrElse(false) || providers.gradleProperty('sanctuaryLostCity21Tests').map { it.toBoolean() }.getOrElse(false) || providers.gradleProperty('sanctuaryTransit21Tests').map { it.toBoolean() }.getOrElse(false)) ? '21' : providers.gradleProperty('sanctuarySelective22Tests').map { it.toBoolean() }.getOrElse(false) ? '22' : ['sanctuarySecrets23Tests', 'sanctuaryNative23Tests', 'sanctuaryOcean23Tests', 'sanctuaryAdmission23Tests'].any { providers.gradleProperty(it).map { v -> v.toBoolean() }.getOrElse(false) } ? '23' : '24'))
providers.gradleProperty('sanctuaryLostCityTests').map { it.toBoolean() }.getOrElse(false) ? '14' : providers.gradleProperty('sanctuaryLostCity15Tests').map { it.toBoolean() }.getOrElse(false) ? '15' : providers.gradleProperty('sanctuaryLostCity16Tests').map { it.toBoolean() }.getOrElse(false) ? '16' : providers.gradleProperty('sanctuaryRuins17Tests').map { it.toBoolean() }.getOrElse(false) ? '17' : providers.gradleProperty('sanctuaryHeritage19Tests').map { it.toBoolean() }.getOrElse(false) ? '19' : (providers.gradleProperty('sanctuaryHeritage20Tests').map { it.toBoolean() }.getOrElse(false) || providers.gradleProperty('sanctuaryLostCity20Tests').map { it.toBoolean() }.getOrElse(false)) ? '20' : (providers.gradleProperty('sanctuaryHeritage21Tests').map { it.toBoolean() }.getOrElse(false) || providers.gradleProperty('sanctuaryLostCity21Tests').map { it.toBoolean() }.getOrElse(false) || providers.gradleProperty('sanctuaryTransit21Tests').map { it.toBoolean() }.getOrElse(false)) ? '21' : providers.gradleProperty('sanctuarySelective22Tests').map { it.toBoolean() }.getOrElse(false) ? '22' : ['sanctuarySecrets23Tests', 'sanctuaryNative23Tests', 'sanctuaryOcean23Tests', 'sanctuaryAdmission23Tests'].any { providers.gradleProperty(it).map { v -> v.toBoolean() }.getOrElse(false) } ? '23' : ['sanctuaryAerial24Tests', 'sanctuaryAerial24FlowAdmission'].any { providers.gradleProperty(it).map { v -> v.toBoolean() }.getOrElse(false) } ? '24' : '25'))
systemProperty('sanctuary.test.terrain25', providers.gradleProperty('sanctuaryTerrain25Tests').getOrElse('false'))
systemProperty('sanctuary.test.terrain25.phase', providers.gradleProperty('sanctuaryTerrain25Phase').getOrElse('create'))
systemProperty('sanctuary.test.terrain25.reverseChunks', providers.gradleProperty('sanctuaryTerrain25ReverseChunks').getOrElse('false'))
systemProperty('sanctuary.test.terrain25.requireMooshrooms', providers.gradleProperty('sanctuaryTerrain25RequireMooshrooms').getOrElse('false'))
systemProperty('sanctuary.test.terrain25.requireWaterLake', providers.gradleProperty('sanctuaryTerrain25RequireWaterLake').getOrElse('false'))
systemProperty('sanctuary.test.terrain25.requireLavaLake', providers.gradleProperty('sanctuaryTerrain25RequireLavaLake').getOrElse('false'))
systemProperty('sanctuary.test.underground25', providers.gradleProperty('sanctuaryUnderground25Tests').getOrElse('false'))
systemProperty('sanctuary.test.monument25', providers.gradleProperty('sanctuaryMonument25Tests').getOrElse('false'))
systemProperty('sanctuary.test.monument25.diameter', providers.gradleProperty('sanctuaryMonument25Diameter').getOrElse('512'))
systemProperty('sanctuary.test.monument25.climate', providers.gradleProperty('sanctuaryMonument25Climate').getOrElse('temperate'))
systemProperty('sanctuary.test.monument25.phase', providers.gradleProperty('sanctuaryMonument25Phase').getOrElse('create'))
systemProperty('sanctuary.test.monument25.awaitReady', providers.gradleProperty('sanctuaryMonument25AwaitReady').getOrElse('false'))
systemProperty('sanctuary.test.underground25.planOnly', providers.gradleProperty('sanctuaryUnderground25PlanOnly').getOrElse('false'))
systemProperty('sanctuary.test.underground25.required', providers.gradleProperty('sanctuaryUnderground25Required').getOrElse(''))
systemProperty('sanctuary.test.underground25.phase', providers.gradleProperty('sanctuaryUnderground25Phase').getOrElse('create'))
systemProperty('sanctuary.test.aerial24.flowAdmission', providers.gradleProperty('sanctuaryAerial24FlowAdmission').getOrElse('false'))
systemProperty('sanctuary.test.aerial24', providers.gradleProperty('sanctuaryAerial24Tests').getOrElse('false'))
systemProperty('sanctuary.test.aerial24.reverseChunks', providers.gradleProperty('sanctuaryAerial24ReverseChunks').getOrElse('false'))
@@ -98,6 +113,14 @@ tasks.named('processGametestResources') {
inputs.property('heritage21', heritage21)
def lostCity21 = providers.gradleProperty('sanctuaryLostCity21Tests').map { it.toBoolean() }.getOrElse(false)
inputs.property('lostCity21', lostCity21)
def terrain25 = providers.gradleProperty('sanctuaryTerrain25Tests').map { it.toBoolean() }.getOrElse(false)
inputs.property('terrain25', terrain25)
def terrain25Diagnostics = providers.gradleProperty('sanctuaryTerrain25Diagnostics').map { it.toBoolean() }.getOrElse(false)
inputs.property('terrain25Diagnostics', terrain25Diagnostics)
def monument25 = providers.gradleProperty('sanctuaryMonument25Tests').map { it.toBoolean() }.getOrElse(false)
inputs.property('monument25', monument25)
def underground25 = providers.gradleProperty('sanctuaryUnderground25Tests').map { it.toBoolean() }.getOrElse(false)
inputs.property('underground25', underground25)
def aerial24Flow = providers.gradleProperty('sanctuaryAerial24FlowAdmission').map { it.toBoolean() }.getOrElse(false)
inputs.property('aerial24Flow', aerial24Flow)
def aerial24 = providers.gradleProperty('sanctuaryAerial24Tests').map { it.toBoolean() }.getOrElse(false)
@@ -119,7 +142,7 @@ tasks.named('processGametestResources') {
def voidReuse = providers.gradleProperty('sanctuaryVoidReuseTests').map { it.toBoolean() }.getOrElse(false)
inputs.property('voidReuse', voidReuse)
filesMatching('fabric.mod.json') {
expand(gametest_entrypoint: aerial24Flow ? 'fr.koka.sanctuary.gametest.Aerial24FlowAdmissionGameTests' : aerial24 ? 'fr.koka.sanctuary.gametest.Aerial24WorldGameTests' : admission23 ? 'fr.koka.sanctuary.gametest.Expansion23AdmissionGameTests' : ocean23 ? 'fr.koka.sanctuary.gametest.Ocean23WorldGameTests' : secrets23 ? 'fr.koka.sanctuary.gametest.Secrets23WorldGameTests' : native23 ? 'fr.koka.sanctuary.gametest.Native23WorldGameTests' : selective22 ? 'fr.koka.sanctuary.gametest.Selective22WorldGameTests' : transit21 ? 'fr.koka.sanctuary.gametest.Transit21WorldGameTests' : lostCity21 ? 'fr.koka.sanctuary.gametest.LostCity21WorldGameTests' : heritage21 ? 'fr.koka.sanctuary.gametest.Heritage21WorldGameTests' : lostCity20 ? 'fr.koka.sanctuary.gametest.LostCity20WorldGameTests' : heritage20 ? 'fr.koka.sanctuary.gametest.Heritage20WorldGameTests' : heritage19 ? 'fr.koka.sanctuary.gametest.Heritage19WorldGameTests' : ruins17 ? 'fr.koka.sanctuary.gametest.Ruins17WorldGameTests' : voidReuse ? 'fr.koka.sanctuary.gametest.ExpansionVoidReuseGameTests' : lostCity16 ? 'fr.koka.sanctuary.gametest.LostCity16WorldGameTests' : lostCity15 ? 'fr.koka.sanctuary.gametest.LostCity15WorldGameTests' : lostCity ? 'fr.koka.sanctuary.gametest.LostCityWorldGameTests' : expansion ? 'fr.koka.sanctuary.gametest.ExpansionWorldGameTests' : benchmark
expand(gametest_entrypoint: terrain25 ? 'fr.koka.sanctuary.gametest.Terrain25WorldGameTests' : terrain25Diagnostics ? 'fr.koka.sanctuary.gametest.Terrain25Diagnostics' : monument25 ? 'fr.koka.sanctuary.gametest.Expansion25MonumentGameTests' : underground25 ? 'fr.koka.sanctuary.gametest.Underground25WorldGameTests' : aerial24Flow ? 'fr.koka.sanctuary.gametest.Aerial24FlowAdmissionGameTests' : aerial24 ? 'fr.koka.sanctuary.gametest.Aerial24WorldGameTests' : admission23 ? 'fr.koka.sanctuary.gametest.Expansion23AdmissionGameTests' : ocean23 ? 'fr.koka.sanctuary.gametest.Ocean23WorldGameTests' : secrets23 ? 'fr.koka.sanctuary.gametest.Secrets23WorldGameTests' : native23 ? 'fr.koka.sanctuary.gametest.Native23WorldGameTests' : selective22 ? 'fr.koka.sanctuary.gametest.Selective22WorldGameTests' : transit21 ? 'fr.koka.sanctuary.gametest.Transit21WorldGameTests' : lostCity21 ? 'fr.koka.sanctuary.gametest.LostCity21WorldGameTests' : heritage21 ? 'fr.koka.sanctuary.gametest.Heritage21WorldGameTests' : lostCity20 ? 'fr.koka.sanctuary.gametest.LostCity20WorldGameTests' : heritage20 ? 'fr.koka.sanctuary.gametest.Heritage20WorldGameTests' : heritage19 ? 'fr.koka.sanctuary.gametest.Heritage19WorldGameTests' : ruins17 ? 'fr.koka.sanctuary.gametest.Ruins17WorldGameTests' : voidReuse ? 'fr.koka.sanctuary.gametest.ExpansionVoidReuseGameTests' : lostCity16 ? 'fr.koka.sanctuary.gametest.LostCity16WorldGameTests' : lostCity15 ? 'fr.koka.sanctuary.gametest.LostCity15WorldGameTests' : lostCity ? 'fr.koka.sanctuary.gametest.LostCityWorldGameTests' : expansion ? 'fr.koka.sanctuary.gametest.ExpansionWorldGameTests' : benchmark
? 'fr.koka.sanctuary.gametest.WorldgenPerformanceGameTests'
: unified ? 'fr.koka.sanctuary.gametest.UnifiedWorldGameTests'
: 'fr.koka.sanctuary.gametest.SanctuaryWorldGameTests')
@@ -613,3 +636,52 @@ tasks.register('aerialPersistence24Smoke', JavaExec) {
mainClass = 'fr.koka.sanctuary.worldgen.aerial.AerialPersistence24Smoke'
}
tasks.named('check') { dependsOn('aerialPersistence24Smoke') }
// Generation25 keeps its geometry tests separate from the historical witnesses.
tasks.register('mineral25Smoke', JavaExec) {
group = 'verification'
dependsOn('testClasses')
classpath = sourceSets.test.runtimeClasspath
mainClass = 'fr.koka.sanctuary.worldgen.mineral.Mineral25Smoke'
}
tasks.named('check') { dependsOn('mineral25Smoke') }
tasks.register('terrain25ShapeSmoke', JavaExec) {
group = 'verification'
dependsOn('testClasses')
classpath = sourceSets.test.runtimeClasspath
mainClass = 'fr.koka.sanctuary.worldgen.Terrain25ShapeSmoke'
}
tasks.named('check') { dependsOn('terrain25ShapeSmoke') }
tasks.register('expansionMonument25Smoke', JavaExec) {
group = 'verification'
dependsOn('testClasses')
classpath = sourceSets.test.runtimeClasspath
mainClass = 'fr.koka.sanctuary.expansion.ExpansionMonument25Smoke'
}
tasks.named('check') { dependsOn('expansionMonument25Smoke') }
tasks.register('initialFlows25Smoke', JavaExec) {
group = 'verification'
dependsOn('testClasses')
classpath = sourceSets.test.runtimeClasspath
mainClass = 'fr.koka.sanctuary.expansion.ExpansionInitialFlows25Smoke'
}
tasks.named('check') { dependsOn('initialFlows25Smoke') }
tasks.register('terrain25CoatingSmoke', JavaExec) {
group = 'verification'
dependsOn('testClasses')
classpath = sourceSets.test.runtimeClasspath
mainClass = 'fr.koka.sanctuary.worldgen.Terrain25CoatingSmoke'
}
tasks.named('check') { dependsOn('terrain25CoatingSmoke') }
tasks.register('underground25Smoke', JavaExec) {
group = 'verification'
dependsOn('testClasses')
classpath = sourceSets.test.runtimeClasspath
mainClass = 'fr.koka.sanctuary.worldgen.underground25.Underground25Smoke'
}
tasks.named('check') { dependsOn('underground25Smoke') }
@@ -155,6 +155,7 @@ public final class Aerial24WorldGameTests {
report.put("expansion_journal_unchanged", true);
StructureCensusService.flush(level.getServer());
helper.assertTrue(level.getServer().saveEverything(true, true, true), "Save actual blocks, loot state, starts and occupants");
report.put("fluid_scheduler", FluidTiming25.snapshot());
report.put("passed", true);
write(world.resolve("aerial24-" + phase + ".json"), report);
write(Path.of("diagnostics/aerial24/" + phase + ".json"), report);
@@ -0,0 +1,230 @@
package fr.koka.sanctuary.gametest;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import com.mojang.serialization.JsonOps;
import fr.koka.sanctuary.expansion.*;
import fr.koka.sanctuary.worldgen.SanctuaryChunkGenerator;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import net.fabricmc.fabric.api.gametest.v1.GameTest;
import net.minecraft.core.BlockPos;
import net.minecraft.core.HolderSet;
import net.minecraft.core.registries.Registries;
import net.minecraft.gametest.framework.GameTestHelper;
import net.minecraft.resources.Identifier;
import net.minecraft.server.level.TicketType;
import net.minecraft.world.entity.monster.ElderGuardian;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.block.Blocks;
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;
import net.minecraft.world.level.storage.LevelResource;
import net.minecraft.world.phys.AABB;
import net.minecraft.world.phys.Vec3;
/** Real admission plus a bounded FULL monument footprint; the whole-island preparation queue is tested separately. */
public final class Expansion25MonumentGameTests {
@GameTest(maxTicks = 24000)
public void reservedMonumentAndEldersSurviveReload(GameTestHelper helper) throws Exception {
new Scenario(helper).start();
}
private static final class Scenario {
final GameTestHelper helper;
final net.minecraft.server.level.ServerLevel level;
final SanctuaryChunkGenerator generator;
final ExpansionRuntime.Session session;
final boolean reload = !System.getProperty("sanctuary.test.monument25.phase", "create").equals("create");
final int diameter = Integer.getInteger("sanctuary.test.monument25.diameter", 512);
final String climate = System.getProperty("sanctuary.test.monument25.climate", "temperate");
final boolean awaitReady = Boolean.getBoolean("sanctuary.test.monument25.awaitReady");
final Path world, directory, journal, proof;
final Map<String, Object> report = new LinkedHashMap<>();
final List<ChunkPos> chunks = new ArrayList<>();
ExpansionIsland island;
ExpansionStructurePlan25 plan;
JsonObject previous;
int index, wait;
long entityWaitStartedNanos;
boolean tickets;
Scenario(GameTestHelper helper) {
this.helper = helper; level = helper.getLevel(); generator = (SanctuaryChunkGenerator) level.getChunkSource().getGenerator();
session = ExpansionRuntime.session(level);
world = level.getServer().getWorldPath(LevelResource.ROOT).toAbsolutePath().normalize();
directory = world.resolve("data").resolve(generator.journalDirectory()); journal = directory.resolve(ExpansionJournal.FILE_NAME);
proof = world.resolve("monument25-created.json");
report.put("process_id", ProcessHandle.current().pid()); report.put("diameter", diameter); report.put("climate", climate);
report.put("phase", reload ? "reload" : "create"); report.put("world_seed", level.getSeed());
report.put("await_ready", awaitReady);
}
void start() throws Exception {
helper.assertTrue(Boolean.getBoolean("sanctuary.test.monument25") && generator.rootGeneration() == 25,
"Dedicated disposable generation25 world required");
helper.assertTrue(level.structureManager().shouldGenerateStructures(), "Native structures enabled");
try {
if (reload) {
previous = JsonParser.parseString(Files.readString(proof)).getAsJsonObject();
helper.assertTrue(previous.get("process_id").getAsLong() != ProcessHandle.current().pid(), "Reload uses a fresh JVM");
var saved = ExpansionIsland.CODEC.parse(JsonOps.INSTANCE, previous.get("island")).getOrThrow();
island = session.island(saved.id());
helper.assertTrue(island.ready().equals(saved.ready()), "Startup restores the exact admitted geometry and plan");
report.put("create_called", false);
} else {
helper.assertTrue(session.islands().size() == 1 && !Files.exists(proof), "Fresh world, no preview before admission");
byte[] pristine = Files.readAllBytes(journal);
var attempts = new ArrayList<Map<String, Object>>(); report.put("attempts", attempts);
for (int draw = 0; draw < 3 && island == null; draw++) {
long start = System.nanoTime();
try {
island = session.create("monument25_" + draw, "sanctuary", "east", climate, diameter, "ocean", 4096);
long elapsed = (System.nanoTime() - start) / 1_000_000;
attempts.add(Map.of("draw", draw, "milliseconds", elapsed, "accepted", true));
helper.assertTrue(elapsed < 31_000, "Real admission uses its thirty-second deadline");
} catch (IllegalArgumentException refused) {
helper.assertTrue(Arrays.equals(pristine, Files.readAllBytes(journal)) && session.islands().size() == 1,
"A refused ocean leaves no reservation");
attempts.add(Map.of("draw", draw, "milliseconds", (System.nanoTime() - start) / 1_000_000, "reason", String.valueOf(refused.getMessage())));
if (!"native_structure_unavailable".equals(refused.getMessage())) throw refused;
}
}
helper.assertTrue(island != null, "A suitable monument must be found within three bounded requests");
var bytes = Files.readAllBytes(journal);
helper.assertTrue(session.create(island.id(), "sanctuary", "east", climate, diameter, "ocean", 4096).equals(island)
&& Arrays.equals(bytes, Files.readAllBytes(journal)), "Repeated admission cannot redraw the selected monument");
}
plan = island.structurePlan25().orElseThrow();
helper.assertTrue(new ExpansionJournal(directory, level.getSeed(), generator.initialDiameter(), 25).islands().contains(island), "Durable journal contains the exact plan");
helper.assertTrue(plan.structure().equals("minecraft:monument"), "Ocean guarantee cannot be satisfied by a ruin or shipwreck");
if (!island.reuseToken().isEmpty()) {
String certificate = Files.readString(directory.resolve("void-reuse").resolve(island.reuseToken() + ".json"));
helper.assertTrue(ExpansionVoidCertificate.digest(certificate).equals(island.reuseToken()), "Certificate digest");
helper.assertTrue(ExpansionIsland.CODEC.parse(JsonOps.INSTANCE, JsonParser.parseString(certificate).getAsJsonObject().get("island")).getOrThrow()
.equals(island.withReuseToken("").ready()), "Certificate includes native pieces and algorithm");
}
chunks.addAll(plan.protection().intersectingChunks().toList()); chunks.add(new ChunkPos(0, 0));
helper.runAfterDelay(1, this::step);
} catch (Exception error) { fail(error); }
}
void step() {
try {
if (index < chunks.size()) {
var pos = chunks.get(index++); report.put("stage", "FULL " + pos);
level.getChunk(pos.x(), pos.z(), ChunkStatus.FULL, true); helper.runAfterDelay(1, this::step); return;
}
if (!tickets) {
chunks.forEach(pos -> level.getChunkSource().addTicketWithRadius(TicketType.DRAGON, pos, 2));
tickets = true; entityWaitStartedNanos = System.nanoTime();
}
boolean ready = recordReadiness();
var elders = level.getEntitiesOfClass(ElderGuardian.class, AABB.of(plan.protection()).inflate(12));
int expected = reload ? 1 : 3;
report.put("elders_observed", elders.stream().map(e -> Map.of("uuid", e.getUUID().toString(), "position", List.of(e.getX(), e.getY(), e.getZ()))).toList());
report.put("entity_wait_ticks", wait);
if ((!ready || elders.size() != expected) && wait++ < 600) { report.put("stage", "entity readiness"); helper.runAfterDelay(1, this::step); return; }
helper.assertTrue(ready, "All monument entity chunks and the extraction chunk are loaded and ticking: " + report.get("pending_entity_chunks"));
helper.assertTrue(elders.size() == expected, "Three initial elders; no replacements after one death and one extraction");
if (awaitReady && !session.island(island.id()).status().equals("ready")) {
report.put("stage", "wait for real whole-island READY");
helper.assertTrue(session.progress().matches(java.util.regex.Pattern.quote(island.id()) + ": \\d+/\\d+"),
"Whole-island preparation must continue: " + session.progress());
helper.runAfterDelay(5, this::step); return;
}
inspect(elders);
fr.koka.sanctuary.knowledge.structures.StructureCensusService.flush(level.getServer());
helper.assertTrue(level.getServer().saveEverything(true, true, true), "Persist native starts, mutations and entities");
report.put("island", ExpansionIsland.CODEC.encodeStart(JsonOps.INSTANCE, session.island(island.id())).getOrThrow());
report.put("full_monument_chunks", chunks.size()); report.put("whole_island_ready_checked", awaitReady); report.put("passed", true);
String json = new GsonBuilder().setPrettyPrinting().create().toJson(report);
write(reload ? "reload" : "create", json);
if (!reload) Files.writeString(proof, json);
release(); helper.succeed();
} catch (Exception error) { fail(error); }
}
boolean recordReadiness() {
var statuses = new ArrayList<Map<String, Object>>();
var pending = new ArrayList<String>();
for (var pos : chunks) {
boolean loaded = level.areEntitiesLoaded(pos.pack());
boolean ticking = level.areEntitiesActuallyLoadedAndTicking(pos);
statuses.add(Map.of("x", pos.x(), "z", pos.z(), "entities_loaded", loaded,
"entities_ticking", ticking, "chunk_debug", level.getChunkSource().getChunkDebugData(pos),
"extraction_chunk", pos.x() == 0 && pos.z() == 0));
if (!loaded || !ticking) pending.add(pos.toString());
}
report.put("entity_chunks", statuses); report.put("pending_entity_chunks", pending);
report.put("entity_wait_milliseconds", entityWaitStartedNanos == 0 ? 0 : (System.nanoTime() - entityWaitStartedNanos) / 1_000_000);
return pending.isEmpty();
}
void inspect(List<ElderGuardian> elders) {
var registry = level.registryAccess().lookupOrThrow(Registries.STRUCTURE);
var holder = registry.get(Identifier.parse("minecraft:monument")).orElseThrow();
var chunk = level.getChunk(plan.chunkX(), plan.chunkZ());
StructureStart start = level.structureManager().getStartForStructure(holder.value(), chunk);
helper.assertTrue(start != null && start.isValid(), "Reserved start is really created at STRUCTURE_STARTS");
var context = StructurePieceSerializationContext.fromLevel(level);
var tag = start.createTag(context, start.getChunkPos());
var withoutReferences = tag.copy(); withoutReferences.putInt("references", 0);
helper.assertTrue(withoutReferences.equals(plan.tag()), "Native generated pieces equal the pre-admission payload");
long prismarine = 0, lanterns = 0, gold = 0; BlockPos removable = null;
for (var pos : BlockPos.betweenClosed(plan.minX(), plan.minY(), plan.minZ(), plan.maxX(), plan.maxY(), plan.maxZ())) {
var block = level.getBlockState(pos);
if (block.is(Blocks.PRISMARINE) || block.is(Blocks.PRISMARINE_BRICKS) || block.is(Blocks.DARK_PRISMARINE)) { prismarine++; if (removable == null) removable = pos.immutable(); }
if (block.is(Blocks.SEA_LANTERN)) lanterns++;
if (block.is(Blocks.GOLD_BLOCK)) gold++;
}
helper.assertTrue(prismarine > 100 && lanterns > 0 && gold >= 8, "A real detailed vanilla monument with its eight gold blocks is submerged");
report.put("blocks", Map.of("prismarine", prismarine, "sea_lantern", lanterns, "gold_block", gold));
if (!reload) {
var origin = new BlockPos(plan.startChunk().getMiddleBlockX(), plan.maxY(), plan.startChunk().getMiddleBlockZ());
var found = generator.findNearestMapStructure(level, HolderSet.direct(holder), origin, 1, true);
boolean realReady = session.island(island.id()).status().equals("ready");
if (realReady) {
helper.assertTrue(found != null && found.getSecond().equals(holder) && start.getReferences() == 1, "Locator selects and references the reserved start exactly once");
} else {
helper.assertTrue(found == null && start.getReferences() == 0, "Locator cannot advertise an unfinished expansion");
}
report.put("locator_on_real_ready", realReady); report.put("references", start.getReferences());
elders = elders.stream().sorted(Comparator.comparing(e -> e.getUUID().toString())).toList();
String killed = elders.get(0).getUUID().toString(); elders.get(0).kill(level);
var extracted = elders.get(1); extracted.setNoAi(true); extracted.setNoGravity(true); extracted.setPermanentlyInvulnerable(true);
extracted.setDeltaMovement(Vec3.ZERO); extracted.snapTo(4.5, 330, 4.5); extracted.setPersistenceRequired();
report.put("killed_uuid", killed); report.put("extracted_uuid", extracted.getUUID().toString());
report.put("remaining_uuid", elders.get(2).getUUID().toString());
helper.assertTrue(removable != null, "A real monument block is available for destructive persistence testing");
level.setBlock(removable, Blocks.AIR.defaultBlockState(), 2);
report.put("removed_block", List.of(removable.getX(), removable.getY(), removable.getZ()));
} else {
helper.assertTrue(level.getEntity(UUID.fromString(previous.get("killed_uuid").getAsString())) == null, "Killed elder stays absent");
var extracted = level.getEntity(UUID.fromString(previous.get("extracted_uuid").getAsString()));
helper.assertTrue(extracted instanceof ElderGuardian && extracted.getX() < 16 && extracted.getZ() < 16, "The extracted elder survives outside the monument");
helper.assertTrue(elders.getFirst().getUUID().toString().equals(previous.get("remaining_uuid").getAsString()), "Only the untouched original elder remains");
var at = previous.getAsJsonArray("removed_block"); var state = level.getBlockState(new BlockPos(at.get(0).getAsInt(), at.get(1).getAsInt(), at.get(2).getAsInt()));
helper.assertTrue(!state.is(Blocks.PRISMARINE) && !state.is(Blocks.PRISMARINE_BRICKS) && !state.is(Blocks.DARK_PRISMARINE), "Demolished monument block is not restored");
helper.assertTrue(start.getReferences() == previous.get("references").getAsInt(), "Reload preserves map reference counter");
}
report.put("native_tag", start.createTag(context, start.getChunkPos()).toString());
var census = fr.koka.sanctuary.knowledge.structures.StructureCensusService.snapshot(level.getServer());
var key = new fr.koka.sanctuary.knowledge.structures.StructureCensusMemory.SiteKey(level.dimension().identifier().toString(), "minecraft:monument", plan.chunkX(), 0, plan.chunkZ(), false);
var evidence = census.evidence().sites().get(key);
helper.assertTrue(evidence != null && census.evidence().generated(evidence), "Census records the generated monument");
}
void release() { if (tickets) chunks.forEach(pos -> level.getChunkSource().removeTicketWithRadius(TicketType.DRAGON, pos, 2)); tickets = false; }
void write(String name, String json) throws java.io.IOException { Path out = Path.of("diagnostics", "monument25"); Files.createDirectories(out); Files.writeString(out.resolve(name + ".json"), json); }
void fail(Exception error) {
if (tickets) recordReadiness();
release(); report.put("passed", false); report.put("failure", error.toString()); report.put("progress", session.progress());
var stack = new java.io.StringWriter(); error.printStackTrace(new java.io.PrintWriter(stack)); report.put("stacktrace", stack.toString());
try { write("failure", new GsonBuilder().setPrettyPrinting().create().toJson(report)); } catch (Exception ignored) {}
throw new IllegalStateException("Monument25 native lifecycle failed", error);
}
}
}
@@ -0,0 +1,17 @@
package fr.koka.sanctuary.gametest;
/** Development-only counters around the actual vanilla fluid scheduler, never additional ticks. */
public final class FluidTiming25 {
private static long nanos, maximum, calls, executed;
private static int pendingPeak;
private FluidTiming25() {}
public static synchronized void record(long elapsed, long count, int pending) {
nanos += elapsed; maximum = Math.max(maximum, elapsed); calls++; executed += count;
pendingPeak = Math.max(pendingPeak, pending);
}
public static synchronized java.util.Map<String, Object> snapshot() {
return java.util.Map.of("scheduler_calls", calls, "executed_fluid_ticks", executed,
"scheduler_total_ms", nanos / 1_000_000.0, "scheduler_max_ms", maximum / 1_000_000.0,
"pending_peak", pendingPeak, "scope", "Actual vanilla Overworld fluid scheduler, development instrumentation only");
}
}
@@ -0,0 +1,126 @@
package fr.koka.sanctuary.gametest;
import fr.koka.sanctuary.worldgen.SanctuaryChunkGenerator;
import fr.koka.sanctuary.worldgen.city.LostCityRenderer;
import fr.koka.sanctuary.worldgen.mineral.MineralSanctuaryPiece23;
import fr.koka.sanctuary.worldgen.mineral.MineralSanctuaryPlan23;
import fr.koka.sanctuary.worldgen.mineral.MineralSanctuaryRenderer25;
import fr.koka.sanctuary.worldgen.mineral.MineralSanctuaryStructures23;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import net.minecraft.core.BlockPos;
import net.minecraft.core.registries.Registries;
import net.minecraft.gametest.framework.GameTestHelper;
import net.minecraft.resources.ResourceKey;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.StairBlock;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.chunk.LevelChunk;
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;
import net.minecraft.world.level.storage.loot.LootTable;
/** Native block and NBT checks for an accepted vault; never turns conditional absence into presence evidence. */
public final class Mineral25NativeChecks {
private Mineral25NativeChecks() {}
public static void verify(GameTestHelper helper, Map<String, Object> report) {
var level = helper.getLevel();
helper.assertTrue(level.getChunkSource().getGenerator() instanceof SanctuaryChunkGenerator host
&& host.rootGeneration() == 25, "Mineral25 checks require the actual generation25 world");
var generator = (SanctuaryChunkGenerator) level.getChunkSource().getGenerator();
var plan = generator.mineralSanctuary23();
var evidence = new LinkedHashMap<String, Object>();
report.put("mineral_sanctuary25", evidence);
evidence.put("presence_verified", false); evidence.put("plan_diagnostics", plan.diagnostic());
evidence.put("plan_columns", plan.columns()); evidence.put("plan_samples", plan.samples());
if (plan.isEmpty()) {
evidence.put("status", "not_admitted");
evidence.put("reason", !level.structureManager().shouldGenerateStructures() || !generator.experimentalStructures()
? "structures_disabled" : "no_suitable_candidate");
return;
}
evidence.put("status", "checking_accepted_plan");
var c = plan.chamber();
evidence.put("chamber", List.of(c.minX(), c.minY(), c.minZ(), c.width(), c.height(), c.depth(), c.facing()));
var chunks = c.bounds().intersectingChunks().toList();
for (var pos : chunks) helper.assertTrue(level.getChunk(pos.x(), pos.z(), ChunkStatus.FULL, true) instanceof LevelChunk,
"Accepted mineral geometry is checked in genuinely FULL chunks: " + pos);
evidence.put("full_chunks", chunks.size());
var type = level.registryAccess().lookupOrThrow(Registries.STRUCTURE).getOrThrow(MineralSanctuaryStructures23.KEY).value();
var start = level.getChunk(c.startChunkX(), c.startChunkZ()).getStartForStructure(type);
helper.assertTrue(start != null && start.isValid() && start.getPieces().size() == 1,
"An accepted mineral vault has its single native saved piece");
helper.assertTrue(start.getPieces().getFirst() instanceof MineralSanctuaryPiece23 mineral && mineral.chamber().equals(c),
"Native mineral piece exactly matches the accepted immutable geometry");
var context = StructurePieceSerializationContext.fromLevel(level);
var tag = start.createTag(context, start.getChunkPos());
var pieceTag = tag.getList("Children").orElseThrow().getCompound(0).orElseThrow();
helper.assertTrue(pieceTag.getInt("SanctuaryMineralRenderer").orElse(-1) == 25,
"The actually generated piece persists the renderer25 discriminator");
var decoded = StructureStart.loadStaticStart(context, tag.copy(), level.getSeed());
helper.assertTrue(decoded != null && decoded.isValid() && tag.equals(decoded.createTag(context, decoded.getChunkPos())),
"The complete native mineral start survives NBT read/write exactly, without replanning");
helper.assertTrue(decoded.getPieces().getFirst() instanceof MineralSanctuaryPiece23 mineral && mineral.chamber().equals(c),
"NBT re-reading retains the exact mineral chamber");
evidence.put("native_tag", tag.toString()); evidence.put("nbt_round_trip", true);
int bottom = c.floor(c.width() / 2, c.depth() / 2);
int frame = 0, aperture = 0, accessAir = 0;
for (int lateral = -3; lateral <= 3; lateral++) for (int y = bottom; y <= bottom + 8; y++) {
var pos = portalPosition(c, lateral, 0, y);
boolean border = Math.abs(lateral) == 3 || y == bottom || y == bottom + 8;
var actual = level.getBlockState(pos);
helper.assertTrue(border ? actual.is(Blocks.BLACKSTONE) : actual.isAir(),
"The real central frame is 7x9 blackstone around an inactive 5x7 opening: " + pos + "=" + actual);
if (border) frame++; else aperture++;
}
helper.assertTrue(frame == 28 && aperture == 35, "Exact frame and aperture block counts");
for (int lateral = -2; lateral <= 2; lateral++) for (int normal = -2; normal <= 2; normal++) {
var ground = portalPosition(c, lateral, normal, bottom);
helper.assertTrue(level.getBlockState(ground).isCollisionShapeFullBlock(level, ground),
"Both sides of the inactive frame have continuous usable ground: " + ground);
for (int y = bottom + 1; y <= bottom + 7; y++) {
var pos = portalPosition(c, lateral, normal, y);
helper.assertTrue(level.getBlockState(pos).isAir(), "The opening and approaches on both sides stay clear: " + pos);
accessAir++;
}
}
Map<BlockPos, BlockState> expectedStairs = new HashMap<>();
MineralSanctuaryRenderer25.render(c, new LostCityRenderer.BlockWriter() {
@Override public void set(int x, int y, int z, BlockState state) {
if (state.getBlock() instanceof StairBlock) expectedStairs.put(new BlockPos(c.minX() + x, c.minY() + y, c.minZ() + z), state);
}
@Override public void chest(int x, int y, int z, ResourceKey<LootTable> loot, long seed) {
throw new AssertionError("The mineral sanctuary has natural veins, not loot containers");
}
});
helper.assertTrue(!expectedStairs.isEmpty(), "The accepted vault includes actual stone terraces with stairs");
var shapes = new TreeMap<String, Integer>();
for (var entry : expectedStairs.entrySet()) {
var pos = entry.getKey(); var actual = level.getBlockState(pos);
helper.assertTrue(actual.equals(entry.getValue()), "Native placement retains the resolved stair orientation and corner: " + pos + "=" + actual);
helper.assertTrue(actual.is(Blocks.STONE_STAIRS) || actual.is(Blocks.COBBLESTONE_STAIRS), "Mineral stair palette");
helper.assertTrue(level.getBlockState(pos.below()).isCollisionShapeFullBlock(level, pos.below()), "A placed stair has solid natural support: " + pos);
shapes.merge(actual.getValue(StairBlock.SHAPE).getSerializedName(), 1, Integer::sum);
}
int portalBlocks = 0;
for (var pos : BlockPos.betweenClosed(c.minX(), c.minY(), c.minZ(), c.maxX(), c.maxY(), c.maxZ()))
if (level.getBlockState(pos).is(Blocks.NETHER_PORTAL)) portalBlocks++;
helper.assertTrue(portalBlocks == 0, "The mineral vault does not activate any Nether portal");
evidence.put("frame_blocks", frame); evidence.put("aperture_air_blocks", aperture);
evidence.put("clear_access_air_blocks", accessAir); evidence.put("actual_stairs", expectedStairs.size());
evidence.put("actual_stair_shapes", shapes); evidence.put("nether_portal_blocks", portalBlocks);
evidence.put("status", "present_verified"); evidence.put("presence_verified", true);
}
private static BlockPos portalPosition(MineralSanctuaryPlan23.Chamber c, int lateral, int normal, int y) {
return new BlockPos(c.centerX() + (c.facing() % 2 == 0 ? lateral : normal),
c.minY() + y, c.centerZ() + (c.facing() % 2 == 0 ? normal : lateral));
}
}
@@ -57,16 +57,21 @@ public final class PopulationDiagnostics {
"New trees must refuse older saved worlds");
helper.assertFalse(new PopulationDecorationsFeature().place(level, old, RandomSource.create(0), BlockPos.ZERO),
"New cave decorations must refuse older saved worlds");
helper.assertFalse(Terrain25Runtime.enabled(old), "Terrain25 cannot attach to a historical settings key: " + key);
}
if (level.getChunkSource().getGenerator() instanceof SanctuaryChunkGenerator unified) {
var expected = new java.util.HashSet<>(SanctuaryBiomeSource.REQUIRED_BIOMES);
if (unified.source().citiesEnabled()) expected.add("sanctuary:lost_city");
if (unified.rootGeneration() == 23 || unified.rootGeneration() == 24) fr.koka.sanctuary.expansion.ExpansionOcean23.BIOMES
if (unified.rootGeneration() == 23 || unified.rootGeneration() == 24 || unified.rootGeneration() == 25) fr.koka.sanctuary.expansion.ExpansionOcean23.BIOMES
.forEach(name -> expected.add("minecraft:" + name));
if (unified.rootGeneration() == 25) Terrain25Biomes.NAMES
.forEach(name -> expected.add(Terrain25Biomes.key(name).identifier().toString()));
helper.assertTrue(generator.getBiomeSource() instanceof SanctuaryBiomeSource
&& generator.getBiomeSource().possibleBiomes().stream().map(b -> b.getRegisteredName())
.collect(java.util.stream.Collectors.toSet()).equals(expected),
"Historical holders stay intact; generations23/24 add the expansion ocean and shore palette");
"The exact saved biome catalogue belongs to the active generation; historical holders stay intact");
helper.assertTrue(Terrain25Runtime.enabled(generator) == (unified.rootGeneration() == 25),
"Only generation25 activates the new terrain/hydrology pass");
helper.assertTrue(binding.random() != level.getChunkSource().randomState(),
"The unified starter must use its real terrain state, never the expansion host fallback");
} else {
@@ -82,6 +87,7 @@ public final class PopulationDiagnostics {
var level = helper.getLevel();
var binding = binding(level);
var generator = binding.generator();
if (Terrain25Runtime.enabled(generator)) { verifyDensity25(helper, binding); return; }
var capacity = IslandCapacity.forGenerator(generator);
var density = level.registryAccess().lookupOrThrow(Registries.DENSITY_FUNCTION).getOrThrow(
ResourceKey.create(Registries.DENSITY_FUNCTION, SanctuaryMod.id("population_" + capacity.players()))).value();
@@ -146,11 +152,68 @@ public final class PopulationDiagnostics {
capacity.players(), level.getSeed(), solidColumns, outerColumns);
}
private static void verifyDensity25(GameTestHelper helper, PopulationGenerationContext.Binding binding) throws IOException {
var level = helper.getLevel(); var generator = binding.generator(); var random = binding.random();
var capacity = IslandCapacity.forGenerator(generator);
var registered = level.registryAccess().lookupOrThrow(Registries.DENSITY_FUNCTION).getOrThrow(
ResourceKey.create(Registries.DENSITY_FUNCTION, SanctuaryMod.id("terrain25_" + capacity.players()))).value();
helper.assertTrue(registered instanceof Terrain25Density terrain && terrain.capacity() == capacity,
"Terrain25 settings and its named density agree on saved capacity");
var density = generator.generatorSettings().value().noiseRouter().finalDensity();
var view = Terrain25Runtime.view(generator, random);
var replay = RandomState.create(level.registryAccess().lookupOrThrow(Registries.NOISE), random.seed(), generator.generatorSettings().value());
helper.assertTrue(view.plan().equals(Terrain25Runtime.view(generator, replay).plan()),
"Fresh native random states reproduce the same immutable terrain25 plan");
int step = Math.max(8, (int) Math.ceil(capacity.terrainLimit() / 32.0));
int extent = (int) Math.ceil(capacity.terrainLimit() / (double) step) * step;
int solidColumns = 0, emptyColumns = 0, replayed = 0, cutSamples = 0;
Set<Integer> bottoms = new TreeSet<>();
StringBuilder map = new StringBuilder("x,z,bottom_y,top_y\n");
for (int x = -extent; x <= extent; x += step) for (int z = -extent; z <= extent; z += step) {
int bottom = -1, top = -1;
for (int y = 0; y < 384; y += 8) {
float value = random.sampleBlockValueUncached(density, x, y, z);
float intact = view.natural(net.minecraft.world.level.levelgen.densityfunction.SamplerContext.EMPTY_UNCACHED, x, y, z);
helper.assertTrue(Float.isFinite(value) && Float.isFinite(intact), "Terrain25 samples remain finite");
helper.assertTrue(value <= 0 || intact > 0, "Cavities remove intact rock rather than add artificial masses");
if (y > Terrain25Shape.MAX_TERRAIN_Y || y == 0 || Math.hypot(x, z) >= capacity.terrainLimit())
helper.assertTrue(value <= 0, "Terrain25 respects its finite footprint and vertical bounds");
if (intact > 0 && value <= 0) cutSamples++;
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)),
"Actual terrain25 density is bit-identical in a fresh native random state");
replayed++;
}
}
if (top >= 0) { solidColumns++; bottoms.add(bottom); } else emptyColumns++;
map.append(x).append(',').append(z).append(',').append(bottom).append(',').append(top).append('\n');
}
helper.assertTrue(solidColumns > 100 && emptyColumns > 100 && bottoms.size() >= 4 && cutSamples > 0 && replayed > 100,
"The actual terrain25 has natural masses, void, varied underside and sampled cave cuts");
Path directory = Path.of("diagnostics"); Files.createDirectories(directory);
String prefix = "terrain25-" + capacity.players() + "-seed-" + level.getSeed();
Files.writeString(directory.resolve(prefix + "-density.csv"), map.toString());
Files.writeString(directory.resolve(prefix + "-density.json"), json(Map.of(
"generation", 25, "players", capacity.players(), "seed", level.getSeed(), "step", step,
"solid_columns", solidColumns, "empty_columns", emptyColumns, "underside_heights", bottoms,
"cave_cut_samples", cutSamples, "bit_exact_replayed_samples", replayed, "passed", true)));
}
static void verifyExterior(GameTestHelper helper) {
var level = helper.getLevel();
var capacity = IslandCapacity.forGenerator(binding(level).generator());
int edge = Math.floorDiv(capacity.terrainLimit() + 48, 16) + 1;
for (int[] point : new int[][]{{edge,0},{-edge-1,0},{0,edge},{0,-edge-1},{256,0},{-257,0}}) {
if (level.getChunkSource().getGenerator() instanceof SanctuaryChunkGenerator host && host.rootGeneration() == 25) {
var initialFlows = fr.koka.sanctuary.expansion.ExpansionInitialFlows25.prepare(host);
int attempts = 0;
while (host.aerialPlan24().claimsChunks().contains(ChunkPos.pack(point[0], point[1]))
|| initialFlows.contains(ChunkPos.pack(point[0], point[1]))) {
helper.assertTrue(++attempts <= 64, "A finite reservation must leave an exterior void witness");
if (point[0] != 0) point[0] += Integer.signum(point[0]); else point[1] += Integer.signum(point[1]);
}
}
var chunk = full(helper, level, point[0], point[1]);
for (var section : chunk.getSections()) helper.assertTrue(section.hasOnlyAir(),
"Completed terrain and decoration must remain void beyond this capacity: " + chunk.getPos());
@@ -215,7 +278,7 @@ public final class PopulationDiagnostics {
void prepare() throws IOException {
boolean heritage = level.getChunkSource().getGenerator() instanceof SanctuaryChunkGenerator sanctuary
&& (sanctuary.rootGeneration() == 24 || sanctuary.rootGeneration() == 23 || sanctuary.rootGeneration() == 22 || (sanctuary.rootGeneration() == 19 || sanctuary.rootGeneration() == 20 || sanctuary.rootGeneration() == 21) && sanctuary.experimentalStructures());
&& (sanctuary.rootGeneration() == 25 || sanctuary.rootGeneration() == 24 || sanctuary.rootGeneration() == 23 || sanctuary.rootGeneration() == 22 || (sanctuary.rootGeneration() == 19 || sanctuary.rootGeneration() == 20 || sanctuary.rootGeneration() == 21) && sanctuary.experimentalStructures());
// Generation19 explicitly prospects destinations across the root island before publishing them.
// A consulted hydrology region is a bounded immutable plan, not a loaded/generated chunk.
// Historical generators retain their original near-spawn contract.
@@ -0,0 +1,122 @@
package fr.koka.sanctuary.gametest;
import com.google.gson.GsonBuilder;
import fr.koka.sanctuary.SanctuaryMod;
import fr.koka.sanctuary.worldgen.*;
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.registries.Registries;
import net.minecraft.gametest.framework.GameTestHelper;
import net.minecraft.resources.ResourceKey;
import net.minecraft.world.level.biome.FixedBiomeSource;
import net.minecraft.world.level.levelgen.NoiseBasedChunkGenerator;
import net.minecraft.world.level.levelgen.RandomState;
import net.minecraft.world.level.levelgen.densityfunction.SamplerContext;
/** Registry-backed diagnostics of nine plans; no evaluated world chunks are loaded or modified. */
public final class Terrain25Diagnostics {
@GameTest(maxTicks = 24000)
public void nineInitialTerrainPlans(GameTestHelper helper) throws Exception {
var registries=helper.getLevel().registryAccess();
var settings=registries.lookupOrThrow(Registries.NOISE_SETTINGS);
var noises=registries.lookupOrThrow(Registries.NOISE);
var forest=registries.lookupOrThrow(Registries.BIOME).getOrThrow(Terrain25Biomes.key("oak_forest"));
List<Map<String,Object>> reports=new ArrayList<>();
int acceptedWater=0,acceptedLava=0,totalFalls=0;
Path reportPath=Path.of("diagnostics/terrain25/plans.json"); Files.createDirectories(reportPath.getParent());
for(int players:new int[]{5,10,20})for(long seed:new long[]{0,42,2026}) {
long start=System.nanoTime();
var holder=settings.getOrThrow(ResourceKey.create(Registries.NOISE_SETTINGS,SanctuaryMod.id("unified_25_"+players)));
var generator=new NoiseBasedChunkGenerator(new FixedBiomeSource(forest),holder);
var state=RandomState.create(noises,seed,holder.value());
var view=Terrain25Runtime.view(generator,state); var plan=view.plan();
var capacity=IslandCapacity.fromPlayers(players);
Map<String,Object> row=new LinkedHashMap<>(); row.put("seed",seed);row.put("diameter",players==5?512:players==10?724:1024);
int max=-1,columns=0,raised=0,vegetationRoom=0,solidColumns=0; long digest=0xcbf29ce484222325L;
var context=SamplerContext.builder().enableCaches().build();
for(int x=-capacity.terrainLimit();x<=capacity.terrainLimit();x+=16)for(int z=-capacity.terrainLimit();z<=capacity.terrainLimit();z+=16) {
if(Math.hypot(x,z)>=capacity.radius()*.82)continue;
columns++;if(plan.lift(x,z)>0)raised++;
int top=top(view,context,x,z);max=Math.max(max,top);
if(top>=0){solidColumns++;if(top<335)vegetationRoom++;}
for(int y:new int[]{0,32,80,120,151,192,256,300,358,359,383}) {
float density=view.sampleValue(context,x,y,z);digest=(digest^Float.floatToIntBits(density))*0x100000001b3L;
if(y>358)helper.assertTrue(density<=0,"Terrain stays below the immutable build-height margin");
}
}
for(var peak:plan.peaks())max=Math.max(max,top(view,context,(int)Math.round(peak.x()),(int)Math.round(peak.z())));
var lakes=Terrain25Runtime.admittedLakes(generator,state);
long water=lakes.stream().filter(c->c.kind()==Terrain25Shape.Kind.LAKE).count();
long lava=lakes.stream().filter(c->c.kind()==Terrain25Shape.Kind.LAVA).count();
acceptedWater+=water;acceptedLava+=lava;
List<Map<String,Object>> regions=new ArrayList<>();
var first=capacity.regionAt(-capacity.terrainLimit(),-capacity.terrainLimit());
var last=capacity.regionAt(capacity.terrainLimit(),capacity.terrainLimit());
for(int rx=first.x();rx<=last.x();rx++)for(int rz=first.z();rz<=last.z();rz++) {
var region=new IslandCapacity.Region(rx,rz);
double nx=Math.max(0,Math.max(region.minX(),-region.maxX())),nz=Math.max(0,Math.max(region.minZ(),-region.maxZ()));
if(Math.hypot(nx,nz)>=capacity.terrainLimit())continue;
var waterPlan=PopulationHydrologyRuntime.regionPlan(generator,state,region).water();
var falls=Terrain25Runtime.falls(generator,state,region);
for(var fall:falls) {
helper.assertTrue(Math.hypot(fall.endX()-fall.startX(),fall.endZ()-fall.startZ())<=64.75,"Outlet search remains within64 blocks including integer rounding");
helper.assertTrue(fall.width()>=8&&fall.width()<=16,"Waterfall lip width8–16");
}
totalFalls+=falls.size();
regions.add(Map.of("region",List.of(rx,rz),"rivers",waterPlan.features().stream().filter(f->f.kind()==PopulationHydrology.Kind.RIVER).count(),"falls",falls));
}
List<Map<String,Object>> chambers=new ArrayList<>();
for(var c:plan.chambers()) {
int dryFloor=0,open=0,roof=0;
for(int dx=-12;dx<=12;dx+=4)for(int dz=-12;dz<=12;dz+=4){int x=(int)c.x()+dx,z=(int)c.z()+dz;
if(view.sampleValue(context,x,c.floorY(),z)>0&&view.sampleValue(context,x,c.floorY()+1,z)<=0)dryFloor++;
if(view.sampleValue(context,x,(int)c.y(),z)<=0)open++;
if(view.sampleValue(context,x,(int)(c.y()+c.ry()+2),z)>0)roof++;
}
chambers.add(Map.of("kind",c.kind(),"center",List.of(c.x(),c.y(),c.z()),"radii",List.of(c.rx(),c.ry(),c.rz()),
"floor_y",c.floorY(),"dry_floor_samples_of_49",dryFloor,"air_samples_of_49",open,"roof_samples_of_49",roof,"fluid_admitted",lakes.contains(c)));
}
row.put("native_calcite_coating",verifyCoating(helper,generator,state));
row.put("maximum_surface",max);row.put("massif_fraction",raised/(double)columns);row.put("room_for_vegetation_columns",vegetationRoom);
row.put("solid_surface_columns",solidColumns);row.put("sampled_columns",columns);row.put("chambers",chambers);
row.put("admitted_water_lakes",water);row.put("admitted_lava_lakes",lava);row.put("regions",regions);
row.put("density_digest",Long.toUnsignedString(digest,16));row.put("planning_ms",(System.nanoTime()-start)/1_000_000);
reports.add(row); write(reportPath,Map.of("scope","Native samplers and plans only; no blocks, ticks, entities or visual verification", "complete",false,"worlds",reports));
SanctuaryMod.LOGGER.info("Terrain25 diagnostics: seed {} size {} top {} water {} lava {} planning {}ms",seed,row.get("diameter"),max,water,lava,row.get("planning_ms"));
helper.assertTrue(max<=358,"Terrain ceiling");
helper.assertTrue(vegetationRoom>solidColumns*.5,"A majority of sampled existing land retains building and vegetation headroom");
}
boolean hasWater=acceptedWater>0,hasLava=acceptedLava>0;
write(reportPath,Map.of("scope","Native samplers and plans only; no blocks, ticks, entities or visual verification","complete",true,
"worlds",reports,"accepted_water_lakes",acceptedWater,"accepted_lava_lakes",acceptedLava,"accepted_waterfalls",totalFalls,
"water_lake_witness",hasWater,"lava_lake_witness",hasLava));
helper.assertTrue(hasWater,"At least one genuinely admitted water lake across the nine native witness worlds; refusal alone is not success");
helper.assertTrue(hasLava,"At least one genuinely admitted lava lake across the nine native witness worlds; refusal alone is not success");
helper.succeed();
}
private static Map<String,Integer> verifyCoating(GameTestHelper helper,NoiseBasedChunkGenerator generator,RandomState state) {
var view=Terrain25Runtime.view(generator,state);int core=0,shell=0;
Terrain25Coating.Solid intact=(x,y,z)->view.natural(SamplerContext.EMPTY_UNCACHED,x,y,z)>0;
for(var peak:view.plan().peaks()) {
int x=(int)Math.round(peak.x()),z=(int)Math.round(peak.z());
for(int y=281;y<=358;y+=3) {
if(!intact.at(x,y,z)||view.sampleValue(SamplerContext.EMPTY_UNCACHED,x,y,z)<=0)continue;
boolean coats=Terrain25Coating.calcite(x,y,z,view.plan().lift(x,z),intact);
var actual=Terrain25Runtime.finalBlock(generator,state,x,y,z,net.minecraft.world.level.block.Blocks.STONE.defaultBlockState());
if(actual.isAir()||!actual.getFluidState().isEmpty())continue;
helper.assertTrue(actual.is(net.minecraft.world.level.block.Blocks.CALCITE)==coats,"Cached calcite coating agrees with the intact native rock envelope");
if(coats)shell++;else core++;
}
}
helper.assertTrue(core>0,"High massifs retain an actual rock core beneath their calcite exterior");
helper.assertTrue(shell>0,"High massifs retain an exposed calcite coating");
return Map.of("checked_core_positions",core,"checked_coating_positions",shell);
}
private static int top(Terrain25Density.View view,SamplerContext context,int x,int z) {
for(int y=358;y>0;y-=4)if(view.sampleValue(context,x,y,z)>0){for(int fine=Math.min(358,y+3);fine>=y;fine--)if(view.sampleValue(context,x,fine,z)>0)return fine;}
return -1;
}
private static void write(Path path,Object report) throws Exception {Files.writeString(path,new GsonBuilder().setPrettyPrinting().create().toJson(report)+"\n");}
}
@@ -0,0 +1,471 @@
package fr.koka.sanctuary.gametest;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import com.mojang.serialization.JsonOps;
import fr.koka.sanctuary.SanctuaryMod;
import fr.koka.sanctuary.expansion.ExpansionIsland;
import fr.koka.sanctuary.expansion.ExpansionStructures23;
import fr.koka.sanctuary.knowledge.structures.StructureCensusService;
import fr.koka.sanctuary.worldgen.SanctuaryChunkGenerator;
import fr.koka.sanctuary.worldgen.aerial.*;
import fr.koka.sanctuary.worldgen.city.LostCityRenderer;
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.Holder;
import net.minecraft.core.HolderSet;
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.resources.RegistryOps;
import net.minecraft.resources.ResourceKey;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.TicketType;
import net.minecraft.world.entity.npc.villager.Villager;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.entity.RandomizableContainerBlockEntity;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.block.state.properties.BlockStateProperties;
import net.minecraft.world.level.chunk.ChunkGenerator;
import net.minecraft.world.level.chunk.status.ChunkStatus;
import net.minecraft.world.level.levelgen.structure.Structure;
import net.minecraft.world.level.levelgen.structure.StructureStart;
import net.minecraft.world.level.levelgen.structure.pieces.StructurePieceSerializationContext;
import net.minecraft.world.level.storage.LevelResource;
import net.minecraft.world.level.storage.loot.LootTable;
import net.minecraft.world.phys.AABB;
import net.minecraft.world.phys.Vec3;
/** One real world per size/seed. Separate JVM phases preserve looting, mining and extracted occupants. */
public final class Terrain25WorldGameTests {
private static final com.google.gson.Gson JSON = new GsonBuilder().setPrettyPrinting().create();
@GameTest(maxTicks = 24000)
public void naturalTerrainAndFiveSitesPersistAfterUse(GameTestHelper helper) throws Exception {
var report = new LinkedHashMap<String, Object>();
try { begin(helper, report); }
catch (Exception failure) { failure(report, failure); throw failure; }
}
private static void begin(GameTestHelper helper, Map<String, Object> report) throws Exception {
var level = helper.getLevel();
var generator = (SanctuaryChunkGenerator) level.getChunkSource().getGenerator();
helper.assertTrue(generator.rootGeneration() == 25, "Terrain25 requires a disposable generation25 world");
String phase = System.getProperty("sanctuary.test.terrain25.phase", "create");
helper.assertTrue(List.of("create", "reload1", "reload2").contains(phase), "Known aerial lifecycle phase");
boolean create = phase.equals("create");
var plan = generator.aerialPlan24();
report.put("phase", phase); report.put("seed", level.getSeed()); report.put("diameter", generator.initialDiameter());
report.put("process_id", ProcessHandle.current().pid()); report.put("experimental", generator.experimentalStructures());
report.put("generate_structures", level.structureManager().shouldGenerateStructures());
if (!level.structureManager().shouldGenerateStructures() || !generator.experimentalStructures()) {
helper.assertTrue(plan.isEmpty() && plan.claimsChunks().isEmpty(), "Disabled aerial generation reserves no space");
helper.assertTrue(StructureCensusService.aerialSites(level).isEmpty(), "Disabled aerial sites are not presented as planned");
report.put("passed", true); write(Path.of("diagnostics/terrain25/world/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().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)
AerialRenderer24.writeDiagnostic(Path.of("diagnostics/terrain25/world/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));
report.put("generator_codec", ChunkGenerator.CODEC.encodeStart(RegistryOps.create(JsonOps.INSTANCE, level.registryAccess()), generator).getOrThrow());
var journalBefore = files(level.getServer().getWorldPath(LevelResource.ROOT).resolve("data").resolve(generator.journalDirectory()));
verifyClaims(helper, generator, plan, report);
Path world = level.getServer().getWorldPath(LevelResource.ROOT);
JsonObject previous = create ? null : JsonParser.parseString(Files.readString(world.resolve("terrain25-" + (phase.equals("reload1") ? "create" : "reload1") + ".json"))).getAsJsonObject();
if (previous != null) {
helper.assertTrue(previous.get("passed").getAsBoolean() && previous.get("process_id").getAsLong() != ProcessHandle.current().pid(), "Reload occurs in a separate JVM after a successful phase");
helper.assertTrue(previous.get("geometry").equals(JSON.toJsonTree(plan.sites())), "Site geometry and palette seeds survive reload");
helper.assertTrue(previous.get("generator_codec").equals(report.get("generator_codec")), "Generator options survive reload");
}
var chunks = new TreeSet<ChunkPos>(Comparator.comparingInt(ChunkPos::x).thenComparingInt(ChunkPos::z));
plan.sites().forEach(site -> chunks.addAll(site.claimedChunks()));
plan.sites().forEach(site -> chunks.add(ChunkPos.containing(new BlockPos(site.shoreX(), site.shoreY(), site.shoreZ()))));
// The two extracted merchants are retained in a separate, existing root chunk.
chunks.add(new ChunkPos(0, 0));
var starter = generator.starterContext();
var terrainPlan = fr.koka.sanctuary.worldgen.Terrain25Runtime.shape(starter.generator(), starter.random());
var terrainChunks = new TreeSet<ChunkPos>(Comparator.comparingInt(ChunkPos::x).thenComparingInt(ChunkPos::z));
for (var chamber : terrainPlan.chambers()) {
int cx = (int)Math.floor(chamber.x()) >> 4, cz = (int)Math.floor(chamber.z()) >> 4;
for(int dx=-1;dx<=1;dx++)for(int dz=-1;dz<=1;dz++)terrainChunks.add(new ChunkPos(cx+dx,cz+dz));
}
chunks.addAll(terrainChunks);
var loadOrder = new ArrayList<>(chunks);
boolean reverseChunks = Boolean.getBoolean("sanctuary.test.terrain25.reverseChunks");
if (reverseChunks) Collections.reverse(loadOrder);
report.put("reverse_chunks", reverseChunks);
var iterator = loadOrder.iterator();
long fullStarted=System.nanoTime();long[] fullNanos={0};int[] fullCalls={0};
report.put("heap_before_full",Runtime.getRuntime().totalMemory()-Runtime.getRuntime().freeMemory());
var entityChunks = new HashSet<ChunkPos>();
plan.sites().stream().filter(AerialPlan24.Site::merchant).forEach(site -> entityChunks.addAll(site.claimedChunks()));
entityChunks.add(new ChunkPos(0, 0));
entityChunks.addAll(terrainChunks);
boolean[] tickets = {false}; int[] wait = {0};
Runnable release = () -> {
if (tickets[0]) entityChunks.forEach(pos -> level.getChunkSource().removeTicketWithRadius(TicketType.DRAGON, pos, 2));
tickets[0] = false;
};
Runnable[] step = new Runnable[1];
step[0] = () -> {
try {
if (iterator.hasNext()) {
var chunk = iterator.next(); report.put("stage", "FULL " + chunk);
long chunkStart=System.nanoTime();
level.getChunk(chunk.x(), chunk.z(), ChunkStatus.FULL, true);
fullNanos[0]+=System.nanoTime()-chunkStart;fullCalls[0]++;
helper.runAtTickTime(helper.getTick() + 1, () -> step[0].run()); return;
}
if (!tickets[0]) {
entityChunks.forEach(pos -> level.getChunkSource().addTicketWithRadius(TicketType.DRAGON, pos, 2)); tickets[0] = true;
}
var occupants = occupants(level, plan);
var readiness = new TreeMap<String, Object>(); boolean entitiesReady = true;
for (var pos : entityChunks) {
boolean loaded = level.areEntitiesLoaded(pos.pack()), ticking = level.areEntitiesActuallyLoadedAndTicking(pos);
readiness.put(pos.toString(), Map.of("loaded", loaded, "ticking", ticking));
entitiesReady &= loaded && ticking;
}
report.put("entity_chunks", readiness);
report.put("occupants_observed", occupants.stream().map(villager -> Map.of("uuid", villager.getUUID().toString(),
"position", List.of(villager.getX(), villager.getY(), villager.getZ()))).toList());
report.put("visible_occupants", occupants.size()); report.put("entity_wait_ticks", wait[0]);
if ((!entitiesReady || occupants.size() < 2) && wait[0]++ < 600) {
report.put("stage", "wait for persistent merchants");
helper.runAtTickTime(helper.getTick() + 1, () -> step[0].run()); return;
}
helper.assertTrue(entitiesReady, "All ship chunks finish native entity loading and become ticking before occupant inspection");
helper.assertTrue(occupants.size() == 2, "Both one-time merchants are present after native entity loading");
report.put("full_get_chunk_calls",fullCalls[0]);report.put("full_get_chunk_ms",fullNanos[0]/1_000_000);
report.put("full_loop_elapsed_ms",(System.nanoTime()-fullStarted)/1_000_000);
report.put("heap_after_full",Runtime.getRuntime().totalMemory()-Runtime.getRuntime().freeMemory());
report.put("memory_pool_peak",java.lang.management.ManagementFactory.getMemoryPoolMXBeans().stream()
.map(pool->Map.of("name",pool.getName(),"used",pool.getPeakUsage().getUsed(),"committed",pool.getPeakUsage().getCommitted())).toList());
report.put("observed_server_ticks",helper.getTick());
report.put("stage", "inspect native structures");
inspectTerrain(helper, generator, terrainChunks, report, create, previous);
var starts = starts(helper, level, plan);
var before = inspect(helper, level, plan, create, report);
if (create) {
verifyLocator(helper, level, plan, starts);
report.put("stage", "loot, extract and dismantle disposable witnesses");
report.put("mutations", mutate(helper, level, plan, occupants, before));
} else {
report.put("mutations", previous.get("mutations"));
verifyMutations(helper, level, plan, previous.getAsJsonObject("mutations"), occupants);
}
var tags = tags(level, starts);
if (!create) helper.assertTrue(previous.get("native_tags").equals(JSON.toJsonTree(tags)), "Native starts, references and occupant flags survive both reloads");
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");
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);
StructureCensusService.flush(level.getServer());
helper.assertTrue(level.getServer().saveEverything(true, true, true), "Save actual blocks, loot state, starts and occupants");
report.put("fluid_scheduler", FluidTiming25.snapshot());
report.put("passed", true);
write(world.resolve("terrain25-" + phase + ".json"), report);
write(Path.of("diagnostics/terrain25/world/" + phase + ".json"), report);
release.run(); helper.succeed();
} catch (Exception error) {
release.run(); failure(report, error); throw new IllegalStateException("Terrain25 failed: " + error.getMessage(), error);
}
};
helper.runAtTickTime(helper.getTick() + 1, () -> step[0].run());
}
private static void inspectTerrain(GameTestHelper helper, SanctuaryChunkGenerator generator, Set<ChunkPos> terrainChunks,
Map<String,Object> report, boolean create, JsonObject previous) {
var level=helper.getLevel();var binding=generator.starterContext();
var plan=fr.koka.sanctuary.worldgen.Terrain25Runtime.shape(binding.generator(),binding.random());
var counts=new TreeMap<String,Integer>();
for(var cp:terrainChunks)for(int x=cp.getMinBlockX();x<=cp.getMaxBlockX();x++)for(int z=cp.getMinBlockZ();z<=cp.getMaxBlockZ();z++)
for(int y=1;y<364;y++) {
var state=level.getBlockState(new BlockPos(x,y,z));
if(!state.isAir())counts.merge(BuiltInRegistries.BLOCK.getKey(state.getBlock()).toString(),1,Integer::sum);
}
report.put("terrain_blocks",counts);report.put("terrain_sample_chunks",terrainChunks.size());
var chambers=new ArrayList<Map<String,Object>>();
for(var c:plan.chambers()) {
int walkable=0,water=0,lava=0;
for(int dx=-12;dx<=12;dx+=3)for(int dz=-12;dz<=12;dz+=3) {
var floor=new BlockPos((int)c.x()+dx,c.floorY(),(int)c.z()+dz);
if(level.getBlockState(floor).isSolid()&&level.getBlockState(floor.above()).isAir()&&level.getBlockState(floor.above(2)).isAir())walkable++;
if(level.getBlockState(floor.above()).is(Blocks.WATER))water++;
if(level.getBlockState(floor.above()).is(Blocks.LAVA))lava++;
}
chambers.add(Map.of("id",c.id(),"kind",c.kind(),"walkable_samples",walkable,"water_samples",water,"lava_samples",lava));
}
report.put("actual_chambers",chambers);
var cows=level.getEntitiesOfClass(net.minecraft.world.entity.animal.cow.MushroomCow.class,new AABB(-2048,0,-2048,2048,384,2048));
var ids=cows.stream().map(c->c.getUUID().toString()).sorted().toList();
report.put("mooshrooms",ids);
if(create) {
var changes=new TreeMap<String,Object>();
if(!cows.isEmpty()) {
var moved=cows.getFirst();
var at=new BlockPos(12,301,12);
for(int x=10;x<=14;x++)for(int z=10;z<=14;z++)level.setBlock(new BlockPos(x,300,z),Blocks.GRASS_BLOCK.defaultBlockState(),3);
moved.setNoAi(true); moved.teleportTo(at.getX()+.5,at.getY(),at.getZ()+.5);
changes.put("extracted_uuid",moved.getUUID().toString());
if(cows.size()>1){var removed=cows.get(1);changes.put("removed_uuid",removed.getUUID().toString());removed.discard();}
}
report.put("mooshroom_mutations",changes);
report.put("mooshroom_survivors",cows.stream().filter(c->!c.isRemoved()).map(c->c.getUUID().toString()).sorted().toList());
} else {
helper.assertTrue(previous.get("mooshroom_survivors").equals(JSON.toJsonTree(ids)),"Removed and extracted mooshrooms are not regenerated on reload");
report.put("mooshroom_mutations",previous.get("mooshroom_mutations"));report.put("mooshroom_survivors",previous.get("mooshroom_survivors"));
var changes=previous.getAsJsonObject("mooshroom_mutations");
if(changes.has("extracted_uuid")) {
var uuid=UUID.fromString(changes.get("extracted_uuid").getAsString());
helper.assertTrue(cows.stream().anyMatch(c->c.getUUID().equals(uuid)&&c.getY()>295),"Extracted mooshroom remains outside its original cave");
}
}
if(Boolean.getBoolean("sanctuary.test.terrain25.requireMooshrooms"))helper.assertTrue(!cows.isEmpty(),"Selected native witness contains actual mooshrooms, not merely a proposed refuge");
if(Boolean.getBoolean("sanctuary.test.terrain25.requireWaterLake"))helper.assertTrue(chambers.stream().anyMatch(c->c.get("kind")==fr.koka.sanctuary.worldgen.Terrain25Shape.Kind.LAKE && (int)c.get("water_samples")>0),"Selected native witness contains real underground lake water");
if(Boolean.getBoolean("sanctuary.test.terrain25.requireLavaLake"))helper.assertTrue(chambers.stream().anyMatch(c->c.get("kind")==fr.koka.sanctuary.worldgen.Terrain25Shape.Kind.LAVA && (int)c.get("lava_samples")>0),"Selected native witness contains real underground lake lava");
}
private static void verifyClaims(GameTestHelper helper, SanctuaryChunkGenerator generator, AerialPlan24 plan, Map<String, Object> report) {
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");
var overlap = new ExpansionIsland("aerial_overlap", root.id(), "east", "temperate", 64,
site.startChunkX() * 16, site.startChunkZ() * 16, 42, "natural", "reserved", 25);
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,
"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", 25)),
"Unclaimed space remains available; there is no blanket exclusion ring");
report.put("look_views", views);
}
private static List<Villager> occupants(ServerLevel level, AerialPlan24 plan) {
var expected = plan.sites().stream().filter(AerialPlan24.Site::merchant).map(AerialGeometry24::occupantUuid).collect(java.util.stream.Collectors.toSet());
return level.getEntitiesOfClass(Villager.class, new AABB(-2048, 0, -2048, 2048, 384, 2048))
.stream().filter(villager -> expected.contains(villager.getUUID())).toList();
}
private static Map<String, StructureStart> starts(GameTestHelper helper, ServerLevel level, AerialPlan24 plan) {
var registry = level.registryAccess().lookupOrThrow(Registries.STRUCTURE);
var result = new TreeMap<String, StructureStart>();
for (var site : plan.sites()) {
var type = registry.get(Identifier.parse(AerialStructures24.structureId(site))).orElseThrow().value();
var start = level.getChunk(site.startChunkX(), site.startChunkZ(), ChunkStatus.FULL, true).getStartForStructure(type);
helper.assertTrue(start != null && start.isValid(), "Planned aerial site has a real persisted native start: " + site.id());
helper.assertTrue(start.getPieces().stream().filter(piece -> piece instanceof AerialPiece24 aerial && aerial.site().equals(site)).count() == 1,
"Exactly one piece retains the site's immutable descriptor");
var context = StructurePieceSerializationContext.fromLevel(level);
var tag = start.createTag(context, start.getChunkPos());
var restored = StructureStart.loadStaticStart(context, tag, level.getSeed());
helper.assertTrue(restored != null && tag.equals(restored.createTag(context, start.getChunkPos())), "Aerial start round-trips through native serialization");
if (site.merchant()) helper.assertTrue(start.getPieces().stream().filter(AerialPiece24.class::isInstance).map(AerialPiece24.class::cast)
.allMatch(AerialPiece24::occupantPlaced), "Merchant placement flag is stored after its first creation");
result.put(site.id(), start);
}
return result;
}
private static Map<String, Map<BlockPos, BlockState>> inspect(GameTestHelper helper, ServerLevel level, AerialPlan24 plan,
boolean fresh, Map<String, Object> report) {
var expected = new LinkedHashMap<String, Map<BlockPos, BlockState>>(); int chestCount = 0, waterSources = 0;
var vegetation = new TreeMap<String, Object>();
var shores = new TreeMap<String, Object>(); report.put("shore_witnesses", shores);
for (var site : plan.sites()) {
var model = render(site); expected.put(site.id(), model);
var shore = new BlockPos(site.shoreX(), site.shoreY(), site.shoreZ());
var column = new TreeMap<Integer, String>();
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()));
var chestAt = AerialGeometry24.chest(site);
helper.assertTrue(level.getBlockEntity(chestAt) instanceof RandomizableContainerBlockEntity, "Real chest at " + site.id()); chestCount++;
var chest = (RandomizableContainerBlockEntity) level.getBlockEntity(chestAt);
if (fresh) {
helper.assertTrue(AerialRenderer24.loot(site).equals(chest.getLootTable()), "Aerial chest retains its unopened site-specific loot table");
for (var entry : model.entrySet()) if (!entry.getValue().isAir() && !entry.getValue().is(Blocks.WATER))
helper.assertTrue(level.getBlockState(entry.getKey()).is(entry.getValue().getBlock()), "Native FULL keeps authored material at " + site.id() + " " + entry.getKey());
for (var pos : AerialGeometry24.interiorChecks(site)) {
helper.assertTrue(level.getBlockState(pos).getCollisionShape(level, pos).isEmpty()
&& level.getBlockState(pos.above()).getCollisionShape(level, pos.above()).isEmpty(), "Usable two-block headroom at " + site.id());
helper.assertTrue(!level.getBlockState(pos.below()).getCollisionShape(level, pos.below()).isEmpty(), "Interior stands on a real floor");
}
if (site.kind() == AerialPlan24.Kind.MINERAL_ISLET) for (int dy = 1; dy <= 3; dy++)
helper.assertTrue(level.getBlockState(chestAt.above(dy)).isSolid(), "Mineral chest needs excavation through three layers");
} else helper.assertTrue(chest.getLootTable() == null && chest.isEmpty(), "Looted chest never refills on reload");
if (site.kind() == AerialPlan24.Kind.MINERAL_ISLET)
vegetation.put(site.id(), vegetation(helper, level, site));
if (site.waterfall()) {
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");
}
}
helper.assertTrue(chestCount == 5 && waterSources == 1, "Five 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 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,
"Tree headroom preserves the original stone height");
int surface = 0, grass = 0;
for (int x = 0; x < site.width(); x++) for (int z = 0; z < site.depth(); z++) {
int top = AerialGeometry24.terrainTop(site, x, z);
if (!AerialGeometry24.mineralBody(site, x, top, z)) continue;
surface++;
if (level.getBlockState(AerialGeometry24.world(site, x, top, z)).is(Blocks.GRASS_BLOCK)) grass++;
}
helper.assertTrue(grass * 2 > surface, "Most real intact surface columns carry grass: " + site.id());
var trees = AerialGeometry24.trees(site);
helper.assertTrue(!trees.isEmpty(), "At least one real tree on each mineral islet");
for (var tree : trees) {
var ground = AerialGeometry24.world(site, tree.x(), tree.groundY(), tree.z());
var floor = level.getBlockState(ground);
helper.assertTrue(floor.is(Blocks.GRASS_BLOCK) || floor.is(Blocks.DIRT), "Tree stands on soil, never in the air");
for (int y = 1; y <= tree.trunkHeight(); y++)
helper.assertTrue(level.getBlockState(ground.above(y)).is(tree.birch() ? Blocks.BIRCH_LOG : Blocks.OAK_LOG), "Native tree trunk stays continuous");
var crown = ground.above(tree.trunkHeight() + 1);
var leaves = level.getBlockState(crown);
helper.assertTrue(site.bounds().isInside(crown) && leaves.is(tree.birch() ? Blocks.BIRCH_LEAVES : Blocks.OAK_LEAVES)
&& !leaves.getValue(BlockStateProperties.PERSISTENT) && leaves.getValue(BlockStateProperties.DISTANCE) < 7,
"Complete natural crown survives native chunk decoration and reload");
}
return Map.of("rock_height", site.rockHeight(), "headroom", AerialPlan24.VEGETATION_HEADROOM,
"surface_columns", surface, "grass_columns", grass, "grass_percent", 100.0 * grass / surface, "trees", trees);
}
private static Map<String, Object> mutate(GameTestHelper helper, ServerLevel level, AerialPlan24 plan, List<Villager> occupants,
Map<String, Map<BlockPos, BlockState>> models) {
var emptied = new ArrayList<List<Integer>>(); var removed = new ArrayList<List<Integer>>(); var loot = new TreeMap<String, Integer>();
for (var site : plan.sites()) {
var at = AerialGeometry24.chest(site); var chest = (RandomizableContainerBlockEntity) level.getBlockEntity(at);
chest.unpackLootTable(null); int quantity = 0;
for (int slot = 0; slot < chest.getContainerSize(); slot++) quantity += chest.getItem(slot).getCount();
helper.assertTrue(quantity > 0, "Site-specific chest produces usable vanilla loot"); loot.put(site.id(), quantity);
chest.clearContent(); chest.setChanged(); emptied.add(coordinates(at));
if (site.kind() == AerialPlan24.Kind.MINERAL_ISLET) {
var ores = models.get(site.id()).entrySet().stream().filter(entry -> ore(entry.getValue())).sorted(Map.Entry.comparingByKey(
Comparator.<BlockPos>comparingInt(BlockPos::getX).thenComparingInt(BlockPos::getY).thenComparingInt(BlockPos::getZ))).limit(16).toList();
helper.assertTrue(!ores.isEmpty(), "Each mineral island contains actual finite veins");
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();
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;
for (var villager : occupants.stream().sorted(Comparator.comparing(v -> v.getUUID().toString())).toList()) {
// This fixture parks extracted merchants in an existing root chunk, outside both ships.
villager.setNoAi(true); villager.setNoGravity(true); villager.setDeltaMovement(Vec3.ZERO);
villager.snapTo(4.5 + i++ * 4, 330, 4.5); villager.setPersistenceRequired();
extracted.put(villager.getUUID().toString(), List.of(villager.getX(), villager.getY(), villager.getZ()));
}
return Map.of("emptied_chests", emptied, "removed_blocks", removed, "extracted_merchants", extracted, "loot_items_obtained", loot);
}
private static void verifyMutations(GameTestHelper helper, ServerLevel level, AerialPlan24 plan, JsonObject mutations, List<Villager> occupants) {
for (var value : mutations.getAsJsonArray("removed_blocks")) {
var pos = position(value.getAsJsonArray()); helper.assertTrue(level.getBlockState(pos).isAir(), "Mined ore and dismantled floor remain absent after reload");
}
var extracted = mutations.getAsJsonObject("extracted_merchants");
for (var villager : occupants) {
var at = extracted.getAsJsonArray(villager.getUUID().toString());
helper.assertTrue(at != null && Math.abs(villager.getX() - at.get(0).getAsDouble()) < .01
&& Math.abs(villager.getY() - at.get(1).getAsDouble()) < .01 && Math.abs(villager.getZ() - at.get(2).getAsDouble()) < .01,
"Extracted merchant stays outside its original ship without respawning");
}
for (var site : plan.sites()) if (site.merchant()) helper.assertTrue(level.getEntitiesOfClass(Villager.class, AABB.of(site.bounds())).isEmpty(),
"The visited ship does not replenish an extracted merchant");
}
private static void verifyLocator(GameTestHelper helper, ServerLevel level, AerialPlan24 plan, Map<String, StructureStart> starts) {
var registry = level.registryAccess().lookupOrThrow(Registries.STRUCTURE);
var holders = new ArrayList<Holder<Structure>>();
plan.sites().stream().map(AerialStructures24::structureId).distinct().forEach(id -> holders.add(registry.get(Identifier.parse(id)).orElseThrow()));
holders.add(registry.get(Identifier.withDefaultNamespace("stronghold")).orElseThrow());
var wanted = HolderSet.direct(holders); var origin = plan.sites().getFirst().locator();
var activeBefore = ((SanctuaryChunkGenerator) level.getChunkSource().getGenerator()).source().islands();
int before = starts.values().stream().mapToInt(StructureStart::getReferences).sum();
var found = ExpansionStructures23.findNearest(level, wanted, origin, 100, false);
helper.assertTrue(found != null, "Mixed vanilla/aerial locator finds a real accepted start");
helper.assertTrue(starts.values().stream().mapToInt(StructureStart::getReferences).sum() == before, "Ordinary locate never marks a site referenced");
var referenced = ExpansionStructures23.findNearest(level, wanted, origin, 100, true);
helper.assertTrue(referenced != null && starts.values().stream().mapToInt(StructureStart::getReferences).sum() == before + 1,
"Mixed map lookup references exactly its one winning aerial start");
helper.assertTrue(activeBefore.equals(((SanctuaryChunkGenerator) level.getChunkSource().getGenerator()).source().islands()), "Locator never activates an expansion");
}
private static Map<BlockPos, BlockState> render(AerialPlan24.Site site) {
var result = new HashMap<BlockPos, BlockState>();
AerialRenderer24.render(site, new LostCityRenderer.BlockWriter() {
public void set(int x, int y, int z, BlockState state) { result.put(new BlockPos(site.minX() + x, site.minY() + y, site.minZ() + z), state); }
public void chest(int x, int y, int z, ResourceKey<LootTable> table, long seed) { set(x, y, z, Blocks.CHEST.defaultBlockState()); }
});
return result;
}
private static Map<String, Map<String, Integer>> countSites(ServerLevel level, AerialPlan24 plan) {
var all = new TreeMap<String, Map<String, Integer>>();
for (var site : plan.sites()) {
var counts = new TreeMap<String, Integer>();
for (int x = site.minX(); x <= site.maxX(); x++) for (int z = site.minZ(); z <= site.maxZ(); z++) for (int y = site.minY(); y <= site.maxY(); y++) {
var state = level.getBlockState(new BlockPos(x, y, z));
if (!state.isAir()) counts.merge(BuiltInRegistries.BLOCK.getKey(state.getBlock()).toString(), 1, Integer::sum);
}
all.put(site.id(), counts);
}
return all;
}
private static boolean ore(BlockState state) { return BuiltInRegistries.BLOCK.getKey(state.getBlock()).getPath().endsWith("_ore"); }
private static Map<String, String> tags(ServerLevel level, Map<String, StructureStart> starts) {
var result = new TreeMap<String, String>(); var context = StructurePieceSerializationContext.fromLevel(level);
starts.forEach((id, start) -> result.put(id, start.createTag(context, start.getChunkPos()).toString())); return result;
}
private static List<Integer> coordinates(BlockPos pos) { return List.of(pos.getX(), pos.getY(), pos.getZ()); }
private static BlockPos position(com.google.gson.JsonArray value) { return new BlockPos(value.get(0).getAsInt(), value.get(1).getAsInt(), value.get(2).getAsInt()); }
private static Map<String, byte[]> files(Path directory) throws java.io.IOException {
var found = new TreeMap<String, byte[]>();
if (Files.exists(directory)) try (var paths = Files.walk(directory)) {
for (var path : paths.filter(Files::isRegularFile).toList()) found.put(directory.relativize(path).toString(), Files.readAllBytes(path));
}
return found;
}
private static boolean equalFiles(Map<String, byte[]> a, Map<String, byte[]> b) {
return a.keySet().equals(b.keySet()) && a.entrySet().stream().allMatch(entry -> Arrays.equals(entry.getValue(), b.get(entry.getKey())));
}
private static void write(Path path, Map<String, Object> report) throws java.io.IOException { Files.createDirectories(path.toAbsolutePath().getParent()); Files.writeString(path, JSON.toJson(report)); }
private static void failure(Map<String, Object> report, Exception error) {
report.put("passed", false); report.put("failure", error.toString());
var stack = new java.io.StringWriter(); error.printStackTrace(new java.io.PrintWriter(stack)); report.put("stacktrace", stack.toString());
SanctuaryMod.LOGGER.error("Terrain25 fixture failed at {}", report.get("stage"), error);
try { write(Path.of("diagnostics/terrain25/world/failure.json"), report); } catch (java.io.IOException failed) { error.addSuppressed(failed); }
}
}
@@ -0,0 +1,169 @@
package fr.koka.sanctuary.gametest;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import fr.koka.sanctuary.worldgen.SanctuaryChunkGenerator;
import fr.koka.sanctuary.worldgen.underground25.*;
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.server.level.TicketType;
import net.minecraft.world.entity.monster.zombie.ZombieVillager;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.entity.RandomizableContainerBlockEntity;
import net.minecraft.world.level.chunk.status.ChunkStatus;
import net.minecraft.world.level.levelgen.structure.PoolElementStructurePiece;
import net.minecraft.world.level.levelgen.structure.StructureStart;
import net.minecraft.world.level.levelgen.structure.pieces.StructurePieceSerializationContext;
import net.minecraft.world.level.storage.LevelResource;
import net.minecraft.world.phys.AABB;
/** Conditional starts are inspected only when admitted; required-family fixtures can assert positive witnesses. */
public final class Underground25WorldGameTests {
private static final com.google.gson.Gson JSON = new GsonBuilder().setPrettyPrinting().create();
@GameTest(maxTicks = 24000)
public void nativeUndergroundLifecycle(GameTestHelper helper) throws Exception {
ServerLevel level = helper.getLevel();
var generator = (SanctuaryChunkGenerator) level.getChunkSource().getGenerator();
helper.assertTrue(generator.rootGeneration() == 25, "Use a disposable generation25 world");
var plan = UndergroundStructures25.plan(generator);
var report = new LinkedHashMap<String, Object>();
String phase = System.getProperty("sanctuary.test.underground25.phase", "create");
boolean reload = phase.equals("reload");
report.put("seed", level.getSeed()); report.put("diameter", generator.initialDiameter());
report.put("phase", phase); report.put("process_id", ProcessHandle.current().pid());
report.put("diagnostics", plan.diagnostics()); report.put("columns", plan.columns());
var descriptors = plan.starts().stream().map(p -> Map.of("id", p.key().identifier().toString(),
"chunk", List.of(p.start().getChunkPos().x(), p.start().getChunkPos().z()), "entry", position(p.entrance()),
"pieces", p.start().getPieces().size())).toList();
report.put("planned_starts", descriptors);
report.put("planned_rooms", plan.rooms().stream().map(p -> Map.of("entry", position(p.getLocatorPosition()), "seed", p.seed())).toList());
helper.assertTrue(plan.columns() <= UndergroundPlanner25.MAX_COLUMNS * 4, "Every stage keeps a finite, independent terrain budget");
if (!level.structureManager().shouldGenerateStructures() || !generator.experimentalStructures()) {
helper.assertTrue(plan.starts().isEmpty() && plan.rooms().isEmpty(), "OFF adds no conditional starts or feature attempts");
report.put("passed", true); write(Path.of("diagnostics/underground25/off.json"), report); helper.succeed(); return;
}
var types = plan.starts().stream().map(p -> p.key().identifier().toString()).toList();
for (String required : System.getProperty("sanctuary.test.underground25.required", "").split(",")) if (!required.isBlank())
helper.assertTrue(required.equals("rooms") ? !plan.rooms().isEmpty() : types.contains(required), "Required positive witness " + required);
if (Boolean.getBoolean("sanctuary.test.underground25.planOnly")) {
report.put("plan_only", true); report.put("passed", true); write(Path.of("diagnostics/underground25/plan.json"), report); helper.succeed(); return;
}
Path world = level.getServer().getWorldPath(LevelResource.ROOT), previousPath = world.resolve("underground25-create.json");
JsonObject previous = reload ? JsonParser.parseString(Files.readString(previousPath)).getAsJsonObject() : null;
if (reload) {
helper.assertTrue(previous.get("passed").getAsBoolean() && previous.get("process_id").getAsLong() != ProcessHandle.current().pid(), "Reload uses a different JVM");
helper.assertTrue(previous.get("planned_starts").equals(JSON.toJsonTree(descriptors)), "Conditional admissions replay identically");
helper.assertTrue(previous.get("planned_rooms").equals(JSON.toJsonTree(report.get("planned_rooms"))), "Feature attempts replay identically");
}
var chunks = new TreeSet<ChunkPos>(Comparator.comparingInt(ChunkPos::x).thenComparingInt(ChunkPos::z));
plan.starts().forEach(p -> p.start().getPieces().forEach(piece -> piece.getBoundingBox().intersectingChunks().forEach(chunks::add)));
plan.rooms().forEach(p -> p.getBoundingBox().intersectingChunks().forEach(chunks::add));
var iterator = chunks.iterator(); int[] wait = {0}; boolean[] tickets = {false};
var entityChunks = new HashSet<ChunkPos>();
plan.starts().stream().filter(p -> p.key().equals(UndergroundStructures25.VILLAGE) || p.key().identifier().toString().equals("minecraft:swamp_hut"))
.forEach(p -> p.start().getPieces().forEach(piece -> piece.getBoundingBox().intersectingChunks().forEach(entityChunks::add)));
Runnable release = () -> { if (tickets[0]) entityChunks.forEach(c -> level.getChunkSource().removeTicketWithRadius(TicketType.DRAGON, c, 2)); tickets[0] = false; };
Runnable[] step = new Runnable[1];
step[0] = () -> {
try {
if (iterator.hasNext()) {
var c = iterator.next(); level.getChunk(c.x(), c.z(), ChunkStatus.FULL, true);
helper.runAtTickTime(helper.getTick() + 1, () -> step[0].run()); return;
}
if (!tickets[0]) { entityChunks.forEach(c -> level.getChunkSource().addTicketWithRadius(TicketType.DRAGON, c, 2)); tickets[0] = true; }
boolean ready = entityChunks.stream().allMatch(c -> level.areEntitiesLoaded(c.pack()) && level.areEntitiesActuallyLoadedAndTicking(c));
if (!ready && wait[0]++ < 600) { helper.runAtTickTime(helper.getTick() + 1, () -> step[0].run()); return; }
helper.assertTrue(ready, "Native occupants are inspected after all their chunks are ticking");
var tags = new TreeMap<String, String>();
var summaries = new ArrayList<Map<String, Object>>();
var context = StructurePieceSerializationContext.fromLevel(level);
for (var planned : plan.starts()) {
var c = planned.start().getChunkPos();
var start = level.getChunk(c.x(), c.z(), ChunkStatus.FULL, true).getStartForStructure(planned.start().getStructure());
helper.assertTrue(start != null && start.isValid(), "Admitted site has a real native start");
var tag = start.createTag(context, c); var decoded = StructureStart.loadStaticStart(context, tag, level.getSeed());
helper.assertTrue(decoded != null && tag.equals(decoded.createTag(context, c)), "All native and cavity pieces round-trip through NBT");
tags.put(planned.key().identifier() + "/" + c.x() + "/" + c.z(), tag.toString());
var summary = new LinkedHashMap<String, Object>(); summary.put("id", planned.key().identifier().toString()); summary.put("pieces", start.getPieces().size());
if (planned.key().equals(UndergroundStructures25.VILLAGE)) inspectVillage(helper, level, start, summary);
if (planned.key().identifier().toString().equals("minecraft:swamp_hut")) {
var hutTag = start.getPieces().getFirst().createTag(context);
helper.assertTrue(hutTag.getBooleanOr("Witch", false) && hutTag.getBooleanOr("Cat", false), "Native hut places and saves its one-time witch and cat");
summary.put("native_witch_and_cat_placed", true);
}
if (planned.key().equals(UndergroundStructures25.MINER_CABIN)) {
var cabin = start.getPieces().stream().filter(p -> p instanceof UndergroundPiece25 u && u.kind() == UndergroundPiece25.Kind.MINER_CABIN)
.map(UndergroundPiece25.class::cast).findFirst().orElseThrow();
var approach = start.getPieces().stream().filter(p -> p instanceof UndergroundPiece25 u && u.kind() == UndergroundPiece25.Kind.ACCESS)
.map(UndergroundPiece25.class::cast).findFirst().orElseThrow();
helper.assertTrue(plan.starts().stream().filter(p -> p.key().identifier().toString().equals("minecraft:mineshaft"))
.anyMatch(m -> m.start().getPieces().stream().anyMatch(p -> approach.volumes().stream().anyMatch(v -> v.intersects(p.getBoundingBox())))),
"The cabin approach reaches a piece of an actual admitted native mineshaft");
helper.assertTrue(level.getBlockState(cabin.chestPosition()).is(Blocks.CHEST), "Attached miner cabin has a real chest");
helper.assertTrue(level.getBlockEntity(cabin.chestPosition()) instanceof RandomizableContainerBlockEntity, "Miner chest has native inventory and loot state");
var chest = (RandomizableContainerBlockEntity) level.getBlockEntity(cabin.chestPosition());
if (!reload) { chest.unpackLootTable(null); chest.clearContent(); chest.setChanged(); }
else helper.assertTrue(chest.isEmpty(), "Looted miner chest stays empty after reload");
summary.put("chest", position(cabin.chestPosition()));
}
summaries.add(summary);
}
for (var room : plan.rooms()) {
helper.assertTrue(level.getBlockState(room.getLocatorPosition()).is(Blocks.SPAWNER), "Successful native room contains its spawner");
if (!reload) helper.assertTrue(room.featureAttempted() && room.featurePlaced(), "Admitted room passes the real vanilla doorway check");
}
if (reload) helper.assertTrue(previous.get("native_tags").equals(JSON.toJsonTree(tags)), "No start, piece or one-time native flag changes on reload");
report.put("actual_starts", summaries); report.put("native_tags", tags); report.put("full_chunks", chunks.size());
report.put("native_room_spawners", plan.rooms().size()); report.put("entity_wait_ticks", wait[0]);
helper.assertTrue(level.getServer().saveEverything(true, true, true), "Save native starts, blocks, entities and emptied inventories");
report.put("passed", true); write(world.resolve("underground25-" + phase + ".json"), report);
write(Path.of("diagnostics/underground25/" + phase + ".json"), report); release.run(); helper.succeed();
} catch (Exception failure) {
release.run(); report.put("passed", false); report.put("failure", failure.toString());
try { write(Path.of("diagnostics/underground25/failure.json"), report); } catch (Exception ignored) {}
throw new IllegalStateException("Underground25 lifecycle failed", failure);
}
};
helper.runAtTickTime(helper.getTick() + 1, () -> step[0].run());
}
private static void inspectVillage(GameTestHelper helper, ServerLevel level, StructureStart start, Map<String, Object> report) {
var cavern = start.getPieces().stream().filter(p -> p instanceof UndergroundPiece25 u && u.kind() == UndergroundPiece25.Kind.VILLAGE_CAVERN)
.map(UndergroundPiece25.class::cast).findFirst().orElseThrow();
helper.assertTrue(start.getPieces().getFirst() == cavern, "Cavity is placed before native roads and buildings");
int houses = 0, paths = 0;
var visited = new HashSet<BlockPos>();
for (var piece : start.getPieces()) if (piece instanceof PoolElementStructurePiece pool) {
String template = pool.getElement().toString();
if (template.contains("/houses/")) houses++;
if (!template.contains("/streets/")) continue;
var b = piece.getBoundingBox();
for (int z = b.minZ(); z <= b.maxZ(); z++) for (int x = b.minX(); x <= b.maxX(); x++) {
var pos = new BlockPos(x, cavern.floorY() - 1, z);
if (visited.add(pos) && level.getBlockState(pos).is(Blocks.DIRT_PATH)) {
helper.assertTrue(level.getBlockState(pos.below()).isSolid(), "Native underground road has a foundation"); paths++;
}
}
}
helper.assertTrue(houses >= 6, "An extended native village, not a truncated hamlet");
helper.assertTrue(paths > 10, "Native streets actually project to the saved underground floor");
var entry = cavern.getLocatorPosition();
helper.assertTrue(level.getBlockState(entry.below()).isSolid() && level.getBlockState(entry).getCollisionShape(level, entry).isEmpty()
&& level.getBlockState(entry.above()).getCollisionShape(level, entry.above()).isEmpty(), "Village access is supported and passable");
var b = start.getBoundingBox();
var zombies = level.getEntitiesOfClass(ZombieVillager.class, new AABB(b.minX() - 16, b.minY() - 8, b.minZ() - 16, b.maxX() + 17, b.maxY() + 17, b.maxZ() + 17));
helper.assertTrue(!zombies.isEmpty(), "Native abandoned templates place zombie-villagers");
report.put("houses", houses); report.put("projected_path_blocks", paths);
report.put("zombie_villagers", zombies.stream().map(v -> v.getUUID().toString()).sorted().toList());
report.put("floor", cavern.floorY()); report.put("entry", position(entry));
}
private static List<Integer> position(BlockPos p) { return List.of(p.getX(), p.getY(), p.getZ()); }
private static void write(Path path, Object value) throws java.io.IOException { Files.createDirectories(path.toAbsolutePath().getParent()); Files.writeString(path, JSON.toJson(value) + "\n"); }
}
@@ -107,7 +107,7 @@ public final class UnifiedWorldGameTests {
for (int y = 0; y < 256; y++) {
var pos = new BlockPos(x, y, z);
var state = chunk.getBlockState(pos);
String biome = chunk.getNoiseBiome(x >> 2, y >> 2, z >> 2).getRegisteredName();
String biome = caveFamily(binding, chunk.getNoiseBiome(x >> 2, y >> 2, z >> 2).getRegisteredName());
var bucket = counts.get(biome);
if (bucket != null && !state.isAir()) bucket.merge(BuiltInRegistries.BLOCK.getKey(state.getBlock()).toString(), 1L, Long::sum);
// Large vanilla pillars add thick dripstone into original cave air. A painted
@@ -129,8 +129,9 @@ public final class UnifiedWorldGameTests {
Map<String, Object> atmospheres = new TreeMap<>();
var ops = RegistryOps.create(JsonOps.INSTANCE, level.registryAccess());
for (String name : CAVES) {
String actualId = caveId(binding, name);
var biome = level.registryAccess().lookupOrThrow(Registries.BIOME)
.getValue(net.minecraft.resources.Identifier.parse(name));
.getValue(net.minecraft.resources.Identifier.parse(actualId));
var encoded = Biome.DIRECT_CODEC.encodeStart(ops, biome).getOrThrow().getAsJsonObject();
atmospheres.put(name, encoded.get("attributes"));
helper.assertTrue(encoded.get("attributes").toString().contains("music.overworld." + name.substring("minecraft:".length())),
@@ -164,8 +165,9 @@ public final class UnifiedWorldGameTests {
"Actual native sulfur caverns must include native rock and spikes");
verified.add("minecraft:sulfur_caves");
}
helper.assertTrue(surfaceBiomes.keySet().stream().anyMatch(name -> name.startsWith("sanctuary:population_")),
"The starter surface must retain its Population biome family above these lower cave patches");
String surfacePrefix = generator.rootGeneration() == 25 ? "sanctuary:terrain25_" : "sanctuary:population_";
helper.assertTrue(surfaceBiomes.keySet().stream().anyMatch(name -> name.startsWith(surfacePrefix)),
"The starter surface must retain its generation-specific woodland biome family above these lower cave patches");
passed = true; helper.succeed();
} finally {
report.put("encountered_native_types", encountered); report.put("verified_native_types", verified);
@@ -248,7 +250,7 @@ public final class UnifiedWorldGameTests {
&& chunk.getBlockState(free).is(Blocks.DIAMOND_BLOCK),
"Decoration outside protected volumes must remain writable");
} finally { chunk.setBlockState(free, freeBefore, 0); }
write(level, "decoration-guard", Map.of("generation", 13, "seed", level.getSeed(),
write(level, "decoration-guard", Map.of("generation", generator.rootGeneration(), "seed", level.getSeed(),
"diameter", generator.initialDiameter(), "compared_plan_points", compared,
"natural_wall_witnesses", shells, "walls_owned_by_adjacent_chunk", boundaryWitnesses, "actual_write_rejected", true, "unprotected_write_allowed", true, "passed", true));
helper.succeed();
@@ -266,7 +268,7 @@ public final class UnifiedWorldGameTests {
int x = cx * 16 + 8, z = cz * 16 + 8;
if (Math.hypot(x, z) >= capacity.radius() - 24) continue;
for (int y = 164; y >= 40; y--) {
String biome = resolver.getNoiseBiome(x >> 2, (y + 1) >> 2, z >> 2).getRegisteredName();
String biome = caveFamily(binding, resolver.getNoiseBiome(x >> 2, (y + 1) >> 2, z >> 2).getRegisteredName());
var bucket = candidates.get(biome); if (bucket == null) continue;
if (sample(binding, density, x, y, z) <= 0 || sample(binding, density, x, y + 1, z) > 0
|| sample(binding, density, x, y + 2, z) > 0 || sample(binding, density, x, y - 1, z) <= 0) continue;
@@ -286,8 +288,10 @@ public final class UnifiedWorldGameTests {
.thenComparingDouble(s -> Math.hypot(s.x(), s.z())).thenComparingInt(Site::x).thenComparingInt(Site::z)).toList();
for (Site candidate : ordered.stream().limit(32).toList()) {
var base = new BlockPos(candidate.x(), candidate.floorY() + 1, candidate.z());
if (!PopulationHydrologyRuntime.protects(binding.generator(), binding.random(), base, 8,
candidate.roofY() - candidate.floorY(), 3)) { sites.add(candidate); break; }
boolean protectedSite = Terrain25Runtime.enabled(binding.generator())
? Terrain25Runtime.protects(binding.generator(), binding.random(), base, 8, candidate.roofY() - candidate.floorY(), 3)
: PopulationHydrologyRuntime.protects(binding.generator(), binding.random(), base, 8, candidate.roofY() - candidate.floorY(), 3);
if (!protectedSite) { sites.add(candidate); break; }
}
}
return List.copyOf(sites);
@@ -296,6 +300,24 @@ public final class UnifiedWorldGameTests {
private static float sample(PopulationGenerationContext.Binding binding, DensityFunction density, int x, int y, int z) {
return binding.random().sampleBlockValueUncached(density, x, y, z);
}
private static String caveFamily(PopulationGenerationContext.Binding binding, String name) {
if (!Terrain25Runtime.enabled(binding.generator())) return name;
return switch (name) {
case "sanctuary:terrain25_lush_caves" -> "minecraft:lush_caves";
case "sanctuary:terrain25_dripstone_caves" -> "minecraft:dripstone_caves";
case "sanctuary:terrain25_sulfur_depths" -> "minecraft:sulfur_caves";
default -> name;
};
}
private static String caveId(PopulationGenerationContext.Binding binding, String name) {
if (!Terrain25Runtime.enabled(binding.generator())) return name;
return switch (name) {
case "minecraft:lush_caves" -> "sanctuary:terrain25_lush_caves";
case "minecraft:dripstone_caves" -> "sanctuary:terrain25_dripstone_caves";
case "minecraft:sulfur_caves" -> "sanctuary:terrain25_sulfur_depths";
default -> throw new IllegalArgumentException("Unknown cave family " + name);
};
}
private static float natural(PopulationGenerationContext.Binding binding, BlockPos pos) {
return sample(binding, binding.generator().generatorSettings().value().noiseRouter().finalDensity(), pos.getX(), pos.getY(), pos.getZ());
}
@@ -309,7 +331,7 @@ public final class UnifiedWorldGameTests {
}
private static Map<String, Object> report(ServerLevel level) {
var report = new LinkedHashMap<String, Object>();
report.put("generation", 13); report.put("seed", level.getSeed()); report.put("diameter", unified(level).initialDiameter());
report.put("generation", unified(level).rootGeneration()); report.put("seed", level.getSeed()); report.put("diameter", unified(level).initialDiameter());
report.put("mod_version", FabricLoader.getInstance().getModContainer("sanctuary").orElseThrow().getMetadata().getVersion().getFriendlyString());
return report;
}
@@ -64,7 +64,10 @@ final class UnifiedWorldSettingsDiagnostics {
var historical23 = presets.getOrThrow(ResourceKey.create(Registries.WORLD_PRESET, SanctuaryMod.id("sanctuary_v23")));
helper.assertTrue(historical23.value().createWorldDimensions().overworld() instanceof SanctuaryChunkGenerator old
&& old.rootGeneration() == 23 && old.source().generation() == 23, "Historical23 remains separately readable");
helper.assertTrue(generator.rootGeneration() == 24 && generator.source().generation() == 24 && generator.experimentalStructures(), "The public preset uses generation24 with its new-world option");
var historical24 = presets.getOrThrow(ResourceKey.create(Registries.WORLD_PRESET, SanctuaryMod.id("sanctuary_v24")));
helper.assertTrue(historical24.value().createWorldDimensions().overworld() instanceof SanctuaryChunkGenerator old
&& old.rootGeneration() == 24 && old.source().generation() == 24, "Historical24 remains separately readable");
helper.assertTrue(generator.rootGeneration() == 25 && generator.source().generation() == 25 && generator.experimentalStructures(), "The public preset uses generation25 with its new-world option");
helper.assertTrue(generator.initialDiameter() == 724, "The public preset defaults to Medium / 724 blocks");
verifyConstructionMasks(helper, (SanctuaryChunkGenerator) historical21.value().createWorldDimensions().overworld(), generator);
for (var tag : List.of(WorldPresetTags.NORMAL, WorldPresetTags.EXTENDED)) {
@@ -75,11 +78,11 @@ final class UnifiedWorldSettingsDiagnostics {
}
var ops = RegistryOps.create(JsonOps.INSTANCE, access);
var activeCodec = generator.rootGeneration() == 24 ? SanctuaryChunkGenerator.AERIAL24_CODEC : generator.rootGeneration() == 23 ? SanctuaryChunkGenerator.SECRETS23_CODEC : generator.rootGeneration() == 22 ? SanctuaryChunkGenerator.SELECTIVE22_CODEC : generator.rootGeneration() == 21 ? SanctuaryChunkGenerator.NETWORK21_CODEC : generator.rootGeneration() == 20 ? SanctuaryChunkGenerator.ARCHITECTURE20_CODEC : generator.rootGeneration() == 19 ? SanctuaryChunkGenerator.HERITAGE19_CODEC : generator.rootGeneration() == 17 ? SanctuaryChunkGenerator.RUINS17_CODEC : generator.rootGeneration() == 16 ? SanctuaryChunkGenerator.CITY16_CODEC : generator.rootGeneration() == 15 ? SanctuaryChunkGenerator.CITY15_CODEC : generator.rootGeneration() == 14 ? SanctuaryChunkGenerator.CITY_CODEC : SanctuaryChunkGenerator.CODEC;
var activeCodec = generator.rootGeneration() == 25 ? SanctuaryChunkGenerator.TERRAIN25_CODEC : generator.rootGeneration() == 24 ? SanctuaryChunkGenerator.AERIAL24_CODEC : generator.rootGeneration() == 23 ? SanctuaryChunkGenerator.SECRETS23_CODEC : generator.rootGeneration() == 22 ? SanctuaryChunkGenerator.SELECTIVE22_CODEC : generator.rootGeneration() == 21 ? SanctuaryChunkGenerator.NETWORK21_CODEC : generator.rootGeneration() == 20 ? SanctuaryChunkGenerator.ARCHITECTURE20_CODEC : generator.rootGeneration() == 19 ? SanctuaryChunkGenerator.HERITAGE19_CODEC : generator.rootGeneration() == 17 ? SanctuaryChunkGenerator.RUINS17_CODEC : generator.rootGeneration() == 16 ? SanctuaryChunkGenerator.CITY16_CODEC : generator.rootGeneration() == 15 ? SanctuaryChunkGenerator.CITY15_CODEC : generator.rootGeneration() == 14 ? SanctuaryChunkGenerator.CITY_CODEC : SanctuaryChunkGenerator.CODEC;
var serialized = activeCodec.codec().encodeStart(ops, generator).getOrThrow().getAsJsonObject();
serialized.remove("experimental_structures");
helper.assertTrue(activeCodec.codec().parse(ops, serialized).getOrThrow().experimentalStructures(),
"Generation24 omitted experimental option defaults to enabled");
"Generation25 omitted experimental option defaults to enabled");
helper.assertTrue(!((SanctuaryChunkGenerator) historical16.overworld()).withExperimentalStructures(true).experimentalStructures(),
"The new-world switch cannot activate experimental generation in a historical16 generator");
serialized.remove("initial_diameter");
@@ -94,9 +97,12 @@ final class UnifiedWorldSettingsDiagnostics {
Files.createDirectories(directory);
var storage = LevelStorageSource.createDefault(directory);
List<Map<String, Object>> results = new ArrayList<>();
for (int diameter : new int[]{512, 724, 1024}) for (boolean experimental : new boolean[]{false, true}) {
var chosen = generator.withInitialDiameter(diameter).withExperimentalStructures(experimental);
helper.assertTrue(generator.initialDiameter() == 724 && chosen.source() != generator.source(),
for (var generation : List.of(
(SanctuaryChunkGenerator) historical23.value().createWorldDimensions().overworld(),
(SanctuaryChunkGenerator) historical24.value().createWorldDimensions().overworld(), generator))
for (int diameter : new int[]{512, 724, 1024}) for (boolean experimental : new boolean[]{false, true}) {
var chosen = generation.withInitialDiameter(diameter).withExperimentalStructures(experimental);
helper.assertTrue(generation.initialDiameter() == 724 && chosen.source() != generation.source(),
"Customizing a new world must copy the generator and its mutable biome source");
var dimensions = standard.replaceOverworldGenerator(access, chosen);
for (var dimension : List.of(LevelStem.NETHER, LevelStem.END)) {
@@ -104,17 +110,19 @@ final class UnifiedWorldSettingsDiagnostics {
"Customizing the island preserves the other dimensions");
}
var original = new WorldGenSettings(new WorldOptions(helper.getLevel().getSeed(), false, true), dimensions);
String name = "diameter-" + diameter + "-experimental-" + experimental;
String name = "generation-" + generation.rootGeneration() + "-diameter-" + diameter + "-experimental-" + experimental;
try (var save = storage.createAccess(name)) {
LevelStorageSource.writeWorldGenSettings(access, storage.getLevelPath(name), original);
var restored = LevelStorageSource.readExistingSavedData(save, access, WorldGenSettings.TYPE).getOrThrow();
helper.assertTrue(restored.dimensions().overworld() instanceof SanctuaryChunkGenerator reloaded
&& reloaded.initialDiameter() == diameter && reloaded.rootGeneration() == generator.rootGeneration() && reloaded.experimentalStructures() == experimental,
&& reloaded.initialDiameter() == diameter && reloaded.rootGeneration() == generation.rootGeneration()
&& reloaded.source().generation() == generation.rootGeneration() && reloaded.experimentalStructures() == experimental,
"The selected diameter must survive real world-settings storage");
helper.assertTrue(WorldGenSettings.CODEC.encodeStart(ops, restored).getOrThrow()
.equals(WorldGenSettings.CODEC.encodeStart(ops, original).getOrThrow()),
"Seed, generation options and all dimensions survive the disk round trip");
results.add(Map.of("diameter", diameter, "experimental_structures", experimental, "disk_round_trip", true));
results.add(Map.of("generation", generation.rootGeneration(), "diameter", diameter,
"experimental_structures", experimental, "disk_round_trip", true));
}
}
Files.writeString(directory.resolve("round-trip.json"), new GsonBuilder().setPrettyPrinting().create().toJson(Map.of(
@@ -138,7 +146,7 @@ final class UnifiedWorldSettingsDiagnostics {
}
// Reuse the attached world's real plans: no extra native-noise planning or chunk loads.
if (!(helper.getLevel().getChunkSource().getGenerator() instanceof SanctuaryChunkGenerator active)
|| active.rootGeneration() != 21 && active.rootGeneration() != 22 && active.rootGeneration() != 23 && active.rootGeneration() != 24) return;
|| active.rootGeneration() != 21 && active.rootGeneration() != 22 && active.rootGeneration() != 23 && active.rootGeneration() != 24 && active.rootGeneration() != 25) return;
helper.assertTrue(!active.protectsCityDecoration(outside)
&& !active.intersectsExperimentalStructures(outside.getX(), outside.getY(), outside.getZ(),
outside.getX() + 1, outside.getY() + 1, outside.getZ() + 1),
@@ -148,11 +156,28 @@ final class UnifiedWorldSettingsDiagnostics {
interior.add(new net.minecraft.core.BlockPos(module.centerX(), module.floorY(), module.centerZ()));
for (var network : active.undergroundHall22().networks()) for (var module : network.modules())
interior.add(new net.minecraft.core.BlockPos(module.centerX(), module.floorY(), module.centerZ()));
if ((active.rootGeneration() == 22 || active.rootGeneration() == 23 || active.rootGeneration() == 24) && helper.getLevel().structureManager().shouldGenerateStructures())
helper.assertTrue(!interior.isEmpty(), "The attached generation22 world has core construction to protect");
if ((active.rootGeneration() == 22 || active.rootGeneration() == 23 || active.rootGeneration() == 24 || active.rootGeneration() == 25) && helper.getLevel().structureManager().shouldGenerateStructures())
helper.assertTrue(!interior.isEmpty(), "The attached generation" + active.rootGeneration() + " world has core construction to protect");
for (var position : interior) helper.assertTrue(active.protectsCityDecoration(position)
&& active.intersectsExperimentalStructures(position.getX(), position.getY(), position.getZ(),
position.getX(), position.getY(), position.getZ()),
"Real underground construction remains protected from decoration and overlapping native structures");
if (active.rootGeneration() == 25) {
var copied = active.source().copy();
copied.setIslands(active.source().islands());
var binding = active.starterContext();
var originalResolver = active.source().createUncachedResolver(binding.random());
var copiedResolver = copied.createUncachedResolver(binding.random());
int checked = 0;
for (int x = -active.initialDiameter() / 2; x <= active.initialDiameter() / 2; x += 32)
for (int z = -active.initialDiameter() / 2; z <= active.initialDiameter() / 2; z += 32)
for (int y : new int[]{80, 128, 160, 192, 224, 288}) {
var expected = originalResolver.getNoiseBiome(x >> 2, y >> 2, z >> 2);
helper.assertTrue(copiedResolver.getNoiseBiome(x >> 2, y >> 2, z >> 2).equals(expected),
"An isolated generation25 preflight retains the exact immutable terrain-biome plan");
checked++;
}
helper.assertTrue(checked > 100, "Compare copied biomes across surface and cave layers");
}
}
}
@@ -0,0 +1,30 @@
package fr.koka.sanctuary.gametest.mixin;
import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
import fr.koka.sanctuary.gametest.FluidTiming25;
import net.minecraft.core.BlockPos;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.level.Level;
import net.minecraft.world.ticks.LevelTicks;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import java.util.function.BiConsumer;
@Mixin(ServerLevel.class)
public abstract class FluidTiming25Mixin {
@WrapOperation(method = "tick", at = @At(value = "INVOKE", target =
"Lnet/minecraft/world/ticks/LevelTicks;tick(JILjava/util/function/BiConsumer;)V"))
private <T> void sanctuary$measureFluidScheduler(LevelTicks<T> ticks, long time, int limit,
BiConsumer<BlockPos, T> consumer, Operation<Void> original) {
var level = (ServerLevel) (Object) this;
if (ticks != level.getFluidTicks() || !level.dimension().equals(Level.OVERWORLD)) {
original.call(ticks, time, limit, consumer); return;
}
long[] count = {0};
BiConsumer<BlockPos, T> measured = (pos, fluid) -> { count[0]++; consumer.accept(pos, fluid); };
long start = System.nanoTime();
try { original.call(ticks, time, limit, measured); }
finally { FluidTiming25.record(System.nanoTime() - start, count[0], ticks.count()); }
}
}
@@ -21,7 +21,7 @@ public abstract class SanctuaryGameTestServerMixin {
// generation test's deadline before its IO/workers can complete. This is test-only pacing.
@Inject(method = "waitUntilNextTick", at = @At("RETURN"))
private void sanctuary$paceExpansionWorkers(CallbackInfo ci) {
if (Boolean.getBoolean("sanctuary.test.aerial24.flowAdmission") || Boolean.getBoolean("sanctuary.test.aerial24") || Boolean.getBoolean("sanctuary.test.admission23") || Boolean.getBoolean("sanctuary.test.ocean23") || Boolean.getBoolean("sanctuary.test.secrets23") || Boolean.getBoolean("sanctuary.test.native23") || Boolean.getBoolean("sanctuary.test.selective22") || (Boolean.getBoolean("sanctuary.test.heritage20") || Boolean.getBoolean("sanctuary.test.heritage21") || Boolean.getBoolean("sanctuary.test.lostCity21") || Boolean.getBoolean("sanctuary.test.transit21")) || Boolean.getBoolean("sanctuary.test.lostCity20")
if (Integer.getInteger("sanctuary.test.generation", 25) == 25 || Boolean.getBoolean("sanctuary.test.monument25") || Boolean.getBoolean("sanctuary.test.terrain25") || Boolean.getBoolean("sanctuary.test.underground25") || Boolean.getBoolean("sanctuary.test.hydrology25") || Boolean.getBoolean("sanctuary.test.aerial24.flowAdmission") || Boolean.getBoolean("sanctuary.test.aerial24") || Boolean.getBoolean("sanctuary.test.admission23") || Boolean.getBoolean("sanctuary.test.ocean23") || Boolean.getBoolean("sanctuary.test.secrets23") || Boolean.getBoolean("sanctuary.test.native23") || Boolean.getBoolean("sanctuary.test.selective22") || (Boolean.getBoolean("sanctuary.test.heritage20") || Boolean.getBoolean("sanctuary.test.heritage21") || Boolean.getBoolean("sanctuary.test.lostCity21") || Boolean.getBoolean("sanctuary.test.transit21")) || Boolean.getBoolean("sanctuary.test.lostCity20")
|| Boolean.getBoolean("sanctuary.test.heritage19") || Boolean.getBoolean("sanctuary.test.ruins17") || Boolean.getBoolean("sanctuary.test.expansion") || Boolean.getBoolean("sanctuary.test.lostCity")
|| Boolean.getBoolean("sanctuary.test.lostCity16") || Boolean.getBoolean("sanctuary.test.lostCity15") || Boolean.getBoolean("sanctuary.test.voidReuse")) {
long until = System.nanoTime() + 20_000_000L;
@@ -47,11 +47,11 @@ public abstract class SanctuaryGameTestServerMixin {
"Lnet/minecraft/world/level/levelgen/presets/WorldPresets;FLAT_ALL_DIMENSIONS:Lnet/minecraft/resources/ResourceKey;"))
private static ResourceKey<WorldPreset> sanctuary$selectActualPreset() {
if (Boolean.getBoolean("sanctuary.test.unified")) {
int generation = Integer.getInteger("sanctuary.test.generation", 24);
if (generation != 13 && generation != 14 && generation != 15 && generation != 16 && generation != 17 && generation != 19 && generation != 20 && generation != 21 && generation != 22 && generation != 23 && generation != 24) throw new IllegalArgumentException("Unsupported test generation");
return ResourceKey.create(Registries.WORLD_PRESET, SanctuaryMod.id(generation == 24 ? "sanctuary" : "sanctuary_v" + generation));
int generation = Integer.getInteger("sanctuary.test.generation", 25);
if (generation != 13 && generation != 14 && generation != 15 && generation != 16 && generation != 17 && generation != 19 && generation != 20 && generation != 21 && generation != 22 && generation != 23 && generation != 24 && generation != 25) throw new IllegalArgumentException("Unsupported test generation");
return ResourceKey.create(Registries.WORLD_PRESET, SanctuaryMod.id(generation == 25 ? "sanctuary" : "sanctuary_v" + generation));
}
if (Boolean.getBoolean("sanctuary.test.expansion")) return ResourceKey.create(Registries.WORLD_PRESET, SanctuaryMod.id("expansion_lab"));
if (Boolean.getBoolean("sanctuary.test.terrain25") || Boolean.getBoolean("sanctuary.test.underground25") || Boolean.getBoolean("sanctuary.test.monument25") || Boolean.getBoolean("sanctuary.test.expansion")) return ResourceKey.create(Registries.WORLD_PRESET, SanctuaryMod.id("expansion_lab"));
int players = Integer.getInteger("sanctuary.test.players", 10);
String preset = switch (players) {
case 5 -> "sanctuary_5";
@@ -2,6 +2,11 @@
"required": true,
"package": "fr.koka.sanctuary.gametest.mixin",
"compatibilityLevel": "JAVA_25",
"mixins": ["SanctuaryGameTestServerMixin"],
"injectors": {"defaultRequire": 1}
"mixins": [
"SanctuaryGameTestServerMixin",
"FluidTiming25Mixin"
],
"injectors": {
"defaultRequire": 1
}
}
@@ -78,6 +78,12 @@ public final class SanctuaryMod implements ModInitializer {
Registry.register(BuiltInRegistries.BIOME_SOURCE, id("island_v23"), SanctuaryBiomeSource.SECRETS23_CODEC);
Registry.register(BuiltInRegistries.CHUNK_GENERATOR, id("island_v24"), SanctuaryChunkGenerator.AERIAL24_CODEC);
Registry.register(BuiltInRegistries.BIOME_SOURCE, id("island_v24"), SanctuaryBiomeSource.AERIAL24_CODEC);
Registry.register(BuiltInRegistries.CHUNK_GENERATOR, id("island_v25_preview_1"), SanctuaryChunkGenerator.TERRAIN25_CODEC);
Registry.register(BuiltInRegistries.BIOME_SOURCE, id("island_v25_preview_1"), SanctuaryBiomeSource.TERRAIN25_CODEC);
Registry.register(BuiltInRegistries.DENSITY_FUNCTION_TYPE, id("terrain25"), fr.koka.sanctuary.worldgen.Terrain25Density.CODEC);
fr.koka.sanctuary.worldgen.underground25.UndergroundStructures25.register();
Registry.register(BuiltInRegistries.FEATURE_TYPE, id("terrain25_groves"), fr.koka.sanctuary.worldgen.Terrain25GrovesFeature.CODEC);
Registry.register(BuiltInRegistries.FEATURE_TYPE, id("terrain25_refuge"), fr.koka.sanctuary.worldgen.Terrain25RefugeFeature.CODEC);
fr.koka.sanctuary.worldgen.aerial.AerialStructures24.register();
fr.koka.sanctuary.worldgen.mineral.MineralSanctuaryStructures23.register();
fr.koka.sanctuary.worldgen.transit.TransitStructures21.register();
@@ -51,11 +51,11 @@ public final class SanctuaryWorldOptionsScreen extends Screen {
contents.addChild(sizeButton);
diameterHint = contents.addChild(new MultiLineTextWidget(diameterHint(), font)
.setMaxWidth(310).setCentered(true));
if (generator.rootGeneration() == 17 || generator.rootGeneration() == 19 || generator.rootGeneration() == 20 || generator.rootGeneration() == 21 || generator.rootGeneration() == 22 || (generator.rootGeneration() == 23 || generator.rootGeneration() == 24)) {
if (generator.rootGeneration() == 17 || generator.rootGeneration() == 19 || generator.rootGeneration() == 20 || generator.rootGeneration() == 21 || generator.rootGeneration() == 22 || (generator.rootGeneration() == 23 || generator.rootGeneration() == 24 || generator.rootGeneration() == 25)) {
contents.addChild(CycleButton.builder((Boolean enabled) -> enabled ? CommonComponents.OPTION_ON : CommonComponents.OPTION_OFF, experimental)
.withValues(true, false).create(0, 0, 280, 20, Component.translatable("sanctuary.world_options.experimental"),
(button, value) -> experimental = value));
contents.addChild(new MultiLineTextWidget(Component.translatable(generator.rootGeneration() == 24 ? "sanctuary.world_options.aerial24_hint" : generator.rootGeneration() == 23 ? "sanctuary.world_options.secrets23_hint" : generator.rootGeneration() == 22 ? "sanctuary.world_options.heritage22_hint" : (generator.rootGeneration() == 19 || generator.rootGeneration() == 20 || generator.rootGeneration() == 21 || generator.rootGeneration() == 22 || (generator.rootGeneration() == 23 || generator.rootGeneration() == 24)) ? "sanctuary.world_options.heritage_hint" : "sanctuary.world_options.experimental_hint"), font)
contents.addChild(new MultiLineTextWidget(Component.translatable(generator.rootGeneration() == 25 ? "sanctuary.world_options.terrain25_hint" : generator.rootGeneration() == 24 ? "sanctuary.world_options.aerial24_hint" : generator.rootGeneration() == 23 ? "sanctuary.world_options.secrets23_hint" : generator.rootGeneration() == 22 ? "sanctuary.world_options.heritage22_hint" : (generator.rootGeneration() == 19 || generator.rootGeneration() == 20 || generator.rootGeneration() == 21 || generator.rootGeneration() == 22 || (generator.rootGeneration() == 23 || generator.rootGeneration() == 24 || generator.rootGeneration() == 25)) ? "sanctuary.world_options.heritage_hint" : "sanctuary.world_options.experimental_hint"), font)
.setMaxWidth(310).setCentered(true));
}
layout.addToContents(contents);
@@ -130,6 +130,7 @@ public class ExpansionChunkGenerator extends ChunkGenerator {
fr.koka.sanctuary.worldgen.city.LostCityUndergroundStructures22.generate(this, registries, state, structures, chunk, templates, dimension);
fr.koka.sanctuary.worldgen.mineral.MineralSanctuaryStructures23.generate(this, registries, state, structures, chunk, templates, dimension);
fr.koka.sanctuary.worldgen.aerial.AerialStructures24.generate(this, registries, state, structures, chunk, templates, dimension);
fr.koka.sanctuary.worldgen.underground25.UndergroundStructures25.generate(this, registries, state, structures, chunk, templates, dimension);
ExpansionStructures.generate(this, registries, state, structures, chunk, templates, dimension);
}
@Override public void spawnOriginalMobs(WorldGenRegion region) {
@@ -146,7 +147,12 @@ 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; }
if (!ExpansionOcean23.active(island)) {
super.applyBiomeDecoration(level, chunk, structures);
if (rootGeneration() == 25 && island != null && island.origin())
fr.koka.sanctuary.worldgen.underground25.UndergroundStructures25.decorate(level, chunk);
return;
}
Integer previous = ExpansionOcean23.enterDecoration(ExpansionOcean23.seaLevel(island));
try { super.applyBiomeDecoration(level, chunk, structures); }
finally { ExpansionOcean23.restoreDecoration(previous); }
@@ -0,0 +1,74 @@
package fr.koka.sanctuary.expansion;
import fr.koka.sanctuary.worldgen.IslandCapacity;
import fr.koka.sanctuary.worldgen.PopulationHydrologyRuntime;
import fr.koka.sanctuary.worldgen.SanctuaryChunkGenerator;
import fr.koka.sanctuary.worldgen.Terrain25Runtime;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.WeakHashMap;
import net.minecraft.world.level.ChunkPos;
/** Initial river outlets reserve their future falling water before exploration; no chunks are requested. */
public final class ExpansionInitialFlows25 {
public static final int SPREAD = 7;
private static final Map<SanctuaryChunkGenerator, Set<Long>> CLAIMS = Collections.synchronizedMap(new WeakHashMap<>());
private ExpansionInitialFlows25() {}
public static Set<Long> prepare(SanctuaryChunkGenerator generator) {
if (generator.rootGeneration() != 25) return Set.of();
synchronized (CLAIMS) {
var existing = CLAIMS.get(generator);
if (existing != null) return existing;
var binding = generator.starterContext();
var capacity = PopulationHydrologyRuntime.capacity(binding.generator());
int limit = capacity.terrainLimit();
var first = capacity.regionAt(-limit, -limit); var last = capacity.regionAt(limit, limit);
var falls = new ArrayList<Terrain25Runtime.Fall>();
for (int x = first.x(); x <= last.x(); x++) for (int z = first.z(); z <= last.z(); z++) {
var region = new IslandCapacity.Region(x, z);
double nearestX = Math.max(0, Math.max(region.minX(), -region.maxX()));
double nearestZ = Math.max(0, Math.max(region.minZ(), -region.maxZ()));
if (Math.hypot(nearestX, nearestZ) < limit)
falls.addAll(Terrain25Runtime.falls(binding.generator(), binding.random(), region));
}
var result = fromFalls(falls);
CLAIMS.put(generator, result);
return result;
}
}
/** A column from Y0 to the recorded waterline occupies its chunk for expansion admission.
* Only actual channel cells receive the local seven-block fluid margin, never the full diagonal bounding box. */
public static Set<Long> fromFalls(List<Terrain25Runtime.Fall> falls) {
var chunks = new HashSet<Long>();
for (var fall : falls) {
if (fall.waterY() < 1 || fall.waterY() > 383 || fall.width() < 1 || fall.width() > 32
|| Math.abs((long) fall.endX() - fall.startX()) > 64 || Math.abs((long) fall.endZ() - fall.startZ()) > 64
|| fall.startX() == fall.endX() && fall.startZ() == fall.endZ())
throw new IllegalArgumentException("Invalid initial waterfall reservation");
for (int x = fall.minX(); x <= fall.maxX(); x++) for (int z = fall.minZ(); z <= fall.maxZ(); z++) {
if (!fall.channel(x, z)) continue;
for (int cx = (x - SPREAD) >> 4; cx <= (x + SPREAD) >> 4; cx++)
for (int cz = (z - SPREAD) >> 4; cz <= (z + SPREAD) >> 4; cz++) chunks.add(new ChunkPos(cx, cz).pack());
}
}
return Set.copyOf(chunks);
}
public static boolean conflicts(SanctuaryChunkGenerator generator, ExpansionIsland island) {
return generator.rootGeneration() == 25 && conflicts(prepare(generator), island);
}
public static boolean conflicts(Set<Long> claims, ExpansionIsland island) {
if (island.origin()) return false;
for (long key : claims) {
var chunk = ChunkPos.unpack(key);
if (island.ownsChunk(chunk.x(), chunk.z())) return true;
}
return false;
}
}
@@ -7,7 +7,7 @@ import java.util.List;
/** Immutable alpha.12 region contract. Directions place islands; climate remains an explicit choice. */
public record ExpansionIsland(String id, String parent, String direction, String climate,
int diameter, int centerX, int centerZ, long seed, String relief, String status, int generation, String reuseToken,
List<String> structurePriorities) {
List<String> structurePriorities, java.util.Optional<ExpansionStructurePlan25> structurePlan25) {
public static final int GENERATION = 12;
public static final List<String> DIRECTIONS = List.of("north", "northeast", "east", "southeast", "south", "southwest", "west", "northwest");
public static final List<String> CLIMATES = List.of("temperate", "boreal", "glacial", "cold_humid", "humid", "tropical", "warm", "arid", "dry", "cold_dry", "autumn", "cherry", "lost_city");
@@ -30,7 +30,8 @@ public record ExpansionIsland(String id, String parent, String direction, String
Codec.STRING.fieldOf("status").forGetter(ExpansionIsland::status),
Codec.INT.fieldOf("generation").forGetter(ExpansionIsland::generation),
Codec.STRING.optionalFieldOf("reuse_token", "").forGetter(ExpansionIsland::reuseToken),
Codec.STRING.listOf().optionalFieldOf("structure_priorities", List.of()).forGetter(ExpansionIsland::structurePriorities)
Codec.STRING.listOf().optionalFieldOf("structure_priorities", List.of()).forGetter(ExpansionIsland::structurePriorities),
ExpansionStructurePlan25.CODEC.optionalFieldOf("structure_plan25").forGetter(ExpansionIsland::structurePlan25)
).apply(i, ExpansionIsland::new));
public ExpansionIsland(String id, String parent, String direction, String climate, int diameter,
@@ -42,10 +43,15 @@ public record ExpansionIsland(String id, String parent, String direction, String
this(id, parent, direction, climate, diameter, centerX, centerZ, seed, relief, status, generation, reuseToken, List.of());
}
public ExpansionIsland(String id, String parent, String direction, String climate, int diameter,
int centerX, int centerZ, long seed, String relief, String status, int generation, String reuseToken, List<String> structurePriorities) {
this(id, parent, direction, climate, diameter, centerX, centerZ, seed, relief, status, generation, reuseToken, structurePriorities, java.util.Optional.empty());
}
public ExpansionIsland {
if (!id.matches("[a-z][a-z0-9_]{0,31}") || !parent.matches("[a-z][a-z0-9_]{0,31}")) throw new IllegalArgumentException("Invalid island identifier");
if (!DIRECTIONS.contains(direction) || !CLIMATES.contains(climate) || !reliefs(generation).contains(relief)) throw new IllegalArgumentException("Unknown island profile");
if (relief.equals("ocean") && ((generation != 23 && generation != 24) || id.equals("sanctuary") || diameter != 512 && diameter != 1024))
if (relief.equals("ocean") && ((generation != 23 && generation != 24 && generation != 25) || id.equals("sanctuary") || diameter != 512 && diameter != 1024))
throw new IllegalArgumentException("Ocean relief requires a generation23 expansion of diameter512 or1024");
if (!((spatialGeneration(generation) ? DIAMETERS : LEGACY_DIAMETERS).contains(diameter) || (id.equals("sanctuary") && (diameter == 724 || (generation == 13 || spatialGeneration(generation)) && diameter == 1024)))) throw new IllegalArgumentException("Unsupported nominal diameter");
if (Math.abs((long) centerX) > 100_000 || Math.abs((long) centerZ) > 100_000 || (centerX & 15) != 0 || (centerZ & 15) != 0) throw new IllegalArgumentException("Invalid island centre");
@@ -59,8 +65,12 @@ public record ExpansionIsland(String id, String parent, String direction, String
var priorities = new java.util.TreeSet<>(structurePriorities);
if (priorities.size() != structurePriorities.size()) throw new IllegalArgumentException("Duplicate structure priority family");
structurePriorities = List.copyOf(priorities);
if (!structurePriorities.isEmpty() && ((generation != 23 && generation != 24) || id.equals("sanctuary")))
if (!structurePriorities.isEmpty() && ((generation != 23 && generation != 24 && generation != 25) || id.equals("sanctuary")))
throw new IllegalArgumentException("Structure priorities are only available for generation23 expansions");
if (structurePlan25 == null || structurePlan25.isPresent() && (generation != 25 || id.equals("sanctuary") || !relief.equals("ocean")))
throw new IllegalArgumentException("Saved native structure plans require an ocean25 expansion");
if (structurePlan25.isPresent()) structurePlan25.get().validateFor(new ExpansionIsland(id, parent, direction, climate,
diameter, centerX, centerZ, seed, relief, status, generation, reuseToken, structurePriorities));
}
public static ExpansionIsland origin(int diameter, long seed) {
@@ -70,8 +80,8 @@ public record ExpansionIsland(String id, String parent, String direction, String
return new ExpansionIsland("sanctuary", "sanctuary", "north", "temperate", diameter, 0, 0, seed, "natural", "ready", generation);
}
public static boolean spatialGeneration(int generation) { return generation == 14 || generation == 15 || generation == 16 || generation == 17 || generation == 19 || generation == 20 || generation == 21 || generation == 22 || (generation == 23 || generation == 24); }
public static List<String> reliefs(int generation) { return (generation == 23 || generation == 24) ? RELIEFS23 : RELIEFS; }
public static boolean spatialGeneration(int generation) { return generation == 14 || generation == 15 || generation == 16 || generation == 17 || generation == 19 || generation == 20 || generation == 21 || generation == 22 || (generation == 23 || generation == 24 || generation == 25); }
public static List<String> reliefs(int generation) { return (generation == 23 || generation == 24 || generation == 25) ? RELIEFS23 : RELIEFS; }
public boolean origin() { return id.equals("sanctuary"); }
public double referenceDiameter() { return origin() && diameter == 724 ? Math.sqrt(2) * 512 : 512; }
public double horizontalScale() { return origin() ? 1.0 : diameter / referenceDiameter(); }
@@ -89,10 +99,13 @@ public record ExpansionIsland(String id, String parent, String direction, String
return Math.abs((long) centerX - other.centerX) <= reservedRadius() + other.reservedRadius()
&& Math.abs((long) centerZ - other.centerZ) <= reservedRadius() + other.reservedRadius();
}
public ExpansionIsland ready() { return new ExpansionIsland(id, parent, direction, climate, diameter, centerX, centerZ, seed, relief, "ready", generation, reuseToken, structurePriorities); }
public ExpansionIsland withReuseToken(String token) { return new ExpansionIsland(id, parent, direction, climate, diameter, centerX, centerZ, seed, relief, status, generation, token, structurePriorities); }
public ExpansionIsland ready() { return new ExpansionIsland(id, parent, direction, climate, diameter, centerX, centerZ, seed, relief, "ready", generation, reuseToken, structurePriorities, structurePlan25); }
public ExpansionIsland withReuseToken(String token) { return new ExpansionIsland(id, parent, direction, climate, diameter, centerX, centerZ, seed, relief, status, generation, token, structurePriorities, structurePlan25); }
public ExpansionIsland withStructurePriorities(List<String> priorities) {
return new ExpansionIsland(id, parent, direction, climate, diameter, centerX, centerZ, seed, relief, status, generation, reuseToken, priorities);
return new ExpansionIsland(id, parent, direction, climate, diameter, centerX, centerZ, seed, relief, status, generation, reuseToken, priorities, structurePlan25);
}
public ExpansionIsland withStructurePlan25(ExpansionStructurePlan25 plan) {
return new ExpansionIsland(id, parent, direction, climate, diameter, centerX, centerZ, seed, relief, status, generation, reuseToken, structurePriorities, java.util.Optional.of(plan));
}
public static String directionalClimate(String direction) {
return switch (direction) {
@@ -76,13 +76,13 @@ public final class ExpansionJournal {
this.file = this.directory.resolve(FILE_NAME);
this.seed = seed;
this.rootDiameter = rootDiameter;
if (rootGeneration == 24 && !initialAerialToken.isEmpty()) bindAerial(initialAerialToken);
else if (rootGeneration != 24 && !initialAerialToken.isEmpty()) throw new IOException("Historical journals cannot bind aerial sites");
if ((rootGeneration == 24 || rootGeneration == 25) && !initialAerialToken.isEmpty()) bindAerial(initialAerialToken);
else if ((rootGeneration != 24 && rootGeneration != 25) && !initialAerialToken.isEmpty()) throw new IOException("Historical journals cannot bind aerial sites");
if (Files.exists(this.directory, LinkOption.NOFOLLOW_LINKS)) {
if (!Files.isDirectory(this.directory, LinkOption.NOFOLLOW_LINKS)) throw new IOException("Expansion journal path is not a directory");
this.islands = read();
} else {
if (rootGeneration == 24 && aerialToken.isEmpty()) throw new IOException("New generation24 journal requires its immutable aerial manifest");
if ((rootGeneration == 24 || rootGeneration == 25) && aerialToken.isEmpty()) throw new IOException("New generation24 journal requires its immutable aerial manifest");
// Creating the directory is the bootstrap marker. An interrupted bootstrap requires repair.
Files.createDirectories(this.directory.getParent());
Files.createDirectory(this.directory);
@@ -164,31 +164,33 @@ public final class ExpansionJournal {
private List<ExpansionIsland> read() throws IOException {
if (!Files.isRegularFile(file, LinkOption.NOFOLLOW_LINKS)) throw new IOException("Existing expansion directory has no regular journal file");
if (Files.size(file) > MAX_BYTES) throw new IOException("Expansion journal exceeds its size limit");
if (Files.size(file) > (rootGeneration == 25 ? 8_388_608 : MAX_BYTES)) throw new IOException("Expansion journal exceeds its size limit");
try (JsonReader reader = new JsonReader(new StringReader(Files.readString(file, StandardCharsets.UTF_8)))) {
reader.setStrictness(Strictness.STRICT);
JsonObject root = readJson(reader, 0).getAsJsonObject();
if (reader.peek() != JsonToken.END_DOCUMENT) throw new IOException("Trailing data in expansion journal");
var journalFields = new java.util.HashSet<>(JOURNAL_FIELDS);
if (rootGeneration == 24) journalFields.add("aerial_plan");
if ((rootGeneration == 24 || rootGeneration == 25)) journalFields.add("aerial_plan");
requireFields(root, journalFields);
if (integer(root, "schema") != (rootGeneration == 24 ? 2 : 1) || number(root, "world_seed").longValueExact() != seed || integer(root, "root_diameter") != rootDiameter) {
if (integer(root, "schema") != (rootGeneration == 25 ? 3 : rootGeneration == 24 ? 2 : 1) || number(root, "world_seed").longValueExact() != seed || integer(root, "root_diameter") != rootDiameter) {
throw new IOException("Expansion journal schema, world seed or root diameter does not match");
}
if (rootGeneration == 24) bindAerial(string(root, "aerial_plan"));
if ((rootGeneration == 24 || rootGeneration == 25)) bindAerial(string(root, "aerial_plan"));
JsonArray entries = root.getAsJsonArray("islands");
List<ExpansionIsland> decoded = new ArrayList<>();
for (JsonElement element : entries) {
JsonObject entry = element.getAsJsonObject();
var fields = new java.util.HashSet<>(ISLAND_FIELDS);
if (ExpansionIsland.spatialGeneration(rootGeneration) && entry.has("reuse_token")) fields.add("reuse_token");
if ((rootGeneration == 23 || rootGeneration == 24) && entry.has("structure_priorities")) fields.add("structure_priorities");
if ((rootGeneration == 23 || (rootGeneration == 24 || rootGeneration == 25)) && entry.has("structure_priorities")) fields.add("structure_priorities");
if (rootGeneration == 25 && entry.has("structure_plan25")) fields.add("structure_plan25");
requireFields(entry, fields);
decoded.add(new ExpansionIsland(string(entry, "id"), string(entry, "parent"), string(entry, "direction"), string(entry, "climate"),
integer(entry, "diameter"), integer(entry, "center_x"), integer(entry, "center_z"), number(entry, "seed").longValueExact(),
string(entry, "relief"), string(entry, "status"), integer(entry, "generation"),
entry.has("reuse_token") ? string(entry, "reuse_token") : "",
entry.has("structure_priorities") ? priorities(entry) : List.of()));
entry.has("structure_priorities") ? priorities(entry) : List.of(),
entry.has("structure_plan25") ? java.util.Optional.of(structurePlan(entry.getAsJsonObject("structure_plan25"))) : java.util.Optional.empty()));
}
return validate(decoded);
} catch (RuntimeException e) {
@@ -213,7 +215,9 @@ public final class ExpansionJournal {
long dx = (long) island.centerX() - parent.centerX(), dz = (long) island.centerZ() - parent.centerZ();
if (!(ExpansionIsland.spatialGeneration(island.generation()) ? ExpansionSpatialPlacement14.followsDirection(island.direction(), dx, dz) : followsDirection(island.direction(), dx, dz))) throw new IOException("Expansion position does not follow its direction: " + island.id());
for (ExpansionIsland other : previous.values()) if (island.conflicts(other)) throw new IOException("Expansion reservations overlap: " + island.id() + " / " + other.id());
if (rootGeneration == 24 && aerialPlan24.conflicts(island)) throw new IOException("Expansion reservation overlaps a permanent aerial site: " + island.id());
if ((rootGeneration == 24 || rootGeneration == 25) && aerialPlan24.conflicts(island)) throw new IOException("Expansion reservation overlaps a permanent aerial site: " + island.id());
try { island.structurePlan25().ifPresent(plan -> plan.validateFor(island)); }
catch (RuntimeException invalid) { throw new IOException("Invalid native monument reservation", invalid); }
previous.put(island.id(), island);
}
return checked;
@@ -235,8 +239,8 @@ public final class ExpansionJournal {
private JsonObject encode(List<ExpansionIsland> entries) {
JsonObject root = new JsonObject();
root.addProperty("schema", rootGeneration == 24 ? 2 : 1); root.addProperty("world_seed", seed); root.addProperty("root_diameter", rootDiameter);
if (rootGeneration == 24) root.addProperty("aerial_plan", aerialToken);
root.addProperty("schema", rootGeneration == 25 ? 3 : rootGeneration == 24 ? 2 : 1); root.addProperty("world_seed", seed); root.addProperty("root_diameter", rootDiameter);
if ((rootGeneration == 24 || rootGeneration == 25)) root.addProperty("aerial_plan", aerialToken);
JsonArray array = new JsonArray();
for (ExpansionIsland island : entries) {
JsonObject entry = new JsonObject();
@@ -250,6 +254,8 @@ public final class ExpansionJournal {
var priorities = new JsonArray(); island.structurePriorities().forEach(priorities::add);
entry.add("structure_priorities", priorities);
}
island.structurePlan25().ifPresent(plan -> entry.add("structure_plan25", ExpansionStructurePlan25.CODEC
.encodeStart(com.mojang.serialization.JsonOps.INSTANCE, plan).getOrThrow()));
array.add(entry);
}
root.add("islands", array);
@@ -258,7 +264,7 @@ public final class ExpansionJournal {
private void bindAerial(String token) throws IOException {
if (!aerialToken.isEmpty() && !aerialToken.equals(token)) throw new IOException("Aerial manifest identity changed outside this journal");
var stored = AerialStore24.load(AerialStore24.directory(directory), token, seed, rootDiameter);
var stored = AerialStore24.load(AerialStore24.directory(directory, rootGeneration), token, seed, rootDiameter, rootGeneration);
if (!aerialToken.isEmpty() && !aerialPlan24.sites().equals(stored.plan().sites())) throw new IOException("Aerial layout changed outside this journal");
aerialToken = token;
aerialPlan24 = stored.plan();
@@ -296,6 +302,10 @@ public final class ExpansionJournal {
private static void requireFields(JsonObject value, Set<String> fields) throws IOException {
if (!value.keySet().equals(fields)) throw new IOException("Missing or unknown expansion journal fields");
}
private static ExpansionStructurePlan25 structurePlan(JsonObject value) throws IOException {
requireFields(value, Set.of("algorithm", "structure", "chunk_x", "chunk_z", "min_x", "min_y", "min_z", "max_x", "max_y", "max_z", "pieces", "digest"));
return ExpansionStructurePlan25.CODEC.parse(com.mojang.serialization.JsonOps.INSTANCE, value).getOrThrow();
}
private static List<String> priorities(JsonObject value) throws IOException {
var array = value.get("structure_priorities");
if (!array.isJsonArray()) throw new IOException("Structure priorities must be an array");
@@ -24,14 +24,14 @@ public final class ExpansionOcean23 {
}
private ExpansionOcean23() {}
public static boolean active(ExpansionIsland island) {
return island != null && (island.generation() == 23 || island.generation() == 24) && !island.origin() && island.relief().equals("ocean");
return island != null && (island.generation() == 23 || island.generation() == 24 || island.generation() == 25) && !island.origin() && island.relief().equals("ocean");
}
public static int seaLevel(ExpansionIsland island) {
return (int) Math.round(ExpansionRelief.worldY(island, 220));
}
/** Quick expansions retain their size/climate draw; only unspecified relief participates. */
public static String quickRelief(long seed, String id, int diameter, String climate, int generation) {
if ((generation != 23 && generation != 24) || diameter < 512 || climate.equals("lost_city")) return "natural";
if ((generation != 23 && generation != 24 && generation != 25) || diameter < 512 || climate.equals("lost_city")) return "natural";
return Long.remainderUnsigned(ExpansionIsland.deriveSeed(seed ^ 0x0CEA231L, id), 3) == 0 ? "ocean" : "natural";
}
public static Column column(ExpansionIsland island, int x, int z) {
@@ -41,18 +41,25 @@ public final class ExpansionRuntime {
&& !java.nio.file.Files.isDirectory(directory, java.nio.file.LinkOption.NOFOLLOW_LINKS))
throw new IOException("Initialized laboratory world is missing its expansion journal directory");
String initialAerialToken = "";
if (generator.rootGeneration() == 24 && !java.nio.file.Files.exists(directory, java.nio.file.LinkOption.NOFOLLOW_LINKS)) {
if ((generator.rootGeneration() == 24 || generator.rootGeneration() == 25) && !java.nio.file.Files.exists(directory, java.nio.file.LinkOption.NOFOLLOW_LINKS)) {
if (!(generator instanceof SanctuaryChunkGenerator sanctuary)) throw new IOException("Aerial world requires the Sanctuary generator");
initialAerialToken = AerialStore24.create(AerialStore24.directory(directory), level.getSeed(), generator.initialDiameter(),
initialAerialToken = AerialStore24.create(AerialStore24.directory(directory, generator.rootGeneration()), level.getSeed(), generator.initialDiameter(), generator.rootGeneration(),
() -> AerialPlanner24.forWorld(level, sanctuary)).token();
}
var journal = generator.rootGeneration() == 24
? new ExpansionJournal(directory, level.getSeed(), generator.initialDiameter(), 24, initialAerialToken)
var journal = (generator.rootGeneration() == 24 || generator.rootGeneration() == 25)
? new ExpansionJournal(directory, level.getSeed(), generator.initialDiameter(), generator.rootGeneration(), initialAerialToken)
: new ExpansionJournal(directory, level.getSeed(), generator.initialDiameter(), generator.rootGeneration());
if (generator.rootGeneration() == 24) {
if ((generator.rootGeneration() == 24 || generator.rootGeneration() == 25)) {
if (!(generator instanceof SanctuaryChunkGenerator sanctuary)) throw new IOException("Aerial world requires the Sanctuary generator");
sanctuary.setAerialPlan24(journal.aerialPlan24());
}
for (var island : journal.islands()) if (island.structurePlan25().isPresent())
island.structurePlan25().get().restore(net.minecraft.world.level.levelgen.structure.pieces.StructurePieceSerializationContext.fromLevel(level), island);
if (generator.rootGeneration() == 25 && generator instanceof SanctuaryChunkGenerator sanctuary) {
// Reload has no aerial planning callback: attach the same immutable root sampler before checking future water.
sanctuary.prepareAerialSampling24(level);
ExpansionInitialFlows25.prepare(sanctuary);
}
ExpansionVoidReuse.open(level, generator, journal.islands());
generator.initialize(level, journal.islands());
var session = new Session(level, generator, journal);
@@ -124,15 +131,19 @@ public final class ExpansionRuntime {
if (islands().size() >= 64) throw new IllegalArgumentException("island_limit");
// Capture one immutable decision before any candidate is probed or certified. A resumed
// reservation uses its recorded profile even if the world's census has since changed.
var priorities = generator.rootGeneration() == 23 || generator.rootGeneration() == 24
var priorities = generator.rootGeneration() == 23 || (generator.rootGeneration() == 24 || generator.rootGeneration() == 25)
? fr.koka.sanctuary.knowledge.structures.StructureCensusService.priorities(level) : List.<String>of();
var candidates = ExpansionPlacement.candidates(level.getSeed(), islands(), id, parent, direction, climate, diameter, relief, minimumDistance)
.stream().filter(value -> !aerialConflicts(value)).map(value -> value.withStructurePriorities(priorities)).toList();
boolean nativeRefused = false;
for (var value : candidates) {
for (var candidate : candidates) {
ExpansionIsland value = candidate;
ExpansionAdmission.checkDeadline(deadline);
try {
if (value.generation() == 23 || value.generation() == 24) ExpansionStructures23.verifyExpansion(level, value, deadline);
if (value.generation() == 25) {
value = ExpansionStructures23.prepareExpansion25(level, value, deadline);
if (!value.relief().equals("ocean")) ExpansionStructures23.verifyExpansion(level, value, deadline);
} else if (value.generation() == 23 || value.generation() == 24) ExpansionStructures23.verifyExpansion(level, value, deadline);
var inspection = ExpansionIsland.spatialGeneration(value.generation()) ? ExpansionVoidReuse.inspect(level, value, deadline) : null;
if (inspection == null) ExpansionAdmission.verify(level, value, deadline);
if (probe(value) == null) throw new IllegalArgumentException("no_ground");
@@ -153,14 +164,15 @@ public final class ExpansionRuntime {
nativeRefused = true; // Try another location with the same immutable priorities.
} finally {
generator.discardUnpublishedContext(value);
if (!value.equals(candidate)) generator.discardUnpublishedContext(candidate);
}
}
ExpansionAdmission.checkDeadline(deadline);
throw new IllegalArgumentException(nativeRefused ? "native_structure_unavailable" : "no_free_space");
}
private boolean aerialConflicts(ExpansionIsland island) {
return generator.rootGeneration() == 24 && generator instanceof SanctuaryChunkGenerator sanctuary
&& sanctuary.aerialPlan24().conflicts(island);
return (generator.rootGeneration() == 24 || generator.rootGeneration() == 25) && generator instanceof SanctuaryChunkGenerator sanctuary
&& (sanctuary.aerialPlan24().conflicts(island) || ExpansionInitialFlows25.conflicts(sanctuary, island));
}
public void resume(String id) {
requireHealthyJournal();
@@ -0,0 +1,118 @@
package fr.koka.sanctuary.expansion;
import com.mojang.serialization.Codec;
import com.mojang.serialization.codecs.RecordCodecBuilder;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.util.Base64;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.nbt.NbtAccounter;
import net.minecraft.nbt.NbtIo;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.levelgen.structure.BoundingBox;
import net.minecraft.world.level.levelgen.structure.StructureStart;
import net.minecraft.world.level.levelgen.structure.pieces.StructurePieceSerializationContext;
/** Immutable native monument selected before an ocean25 reservation. No block, entity or chunk is stored here. */
public record ExpansionStructurePlan25(String algorithm, String structure, int chunkX, int chunkZ,
int minX, int minY, int minZ, int maxX, int maxY, int maxZ, String pieces, String digest) {
public static final String ALGORITHM = "ocean_monument25_v1";
public static final int MAX_NBT_BYTES = 65_536;
public static final Codec<ExpansionStructurePlan25> CODEC = RecordCodecBuilder.create(i -> i.group(
Codec.STRING.fieldOf("algorithm").forGetter(ExpansionStructurePlan25::algorithm),
Codec.STRING.fieldOf("structure").forGetter(ExpansionStructurePlan25::structure),
Codec.INT.fieldOf("chunk_x").forGetter(ExpansionStructurePlan25::chunkX),
Codec.INT.fieldOf("chunk_z").forGetter(ExpansionStructurePlan25::chunkZ),
Codec.INT.fieldOf("min_x").forGetter(ExpansionStructurePlan25::minX),
Codec.INT.fieldOf("min_y").forGetter(ExpansionStructurePlan25::minY),
Codec.INT.fieldOf("min_z").forGetter(ExpansionStructurePlan25::minZ),
Codec.INT.fieldOf("max_x").forGetter(ExpansionStructurePlan25::maxX),
Codec.INT.fieldOf("max_y").forGetter(ExpansionStructurePlan25::maxY),
Codec.INT.fieldOf("max_z").forGetter(ExpansionStructurePlan25::maxZ),
Codec.STRING.fieldOf("pieces").forGetter(ExpansionStructurePlan25::pieces),
Codec.STRING.fieldOf("digest").forGetter(ExpansionStructurePlan25::digest)
).apply(i, ExpansionStructurePlan25::new));
public ExpansionStructurePlan25 {
if (!ALGORITHM.equals(algorithm) || !"minecraft:monument".equals(structure))
throw new IllegalArgumentException("Unsupported saved native structure algorithm");
if (Math.abs((long) chunkX) > 6_300 || Math.abs((long) chunkZ) > 6_300
|| minX > maxX || minY > maxY || minZ > maxZ || minY < 3 || maxY > 379
|| (long) maxX - minX > 256 || (long) maxZ - minZ > 256
|| minX < (long) chunkX * 16 - 128 || maxX > (long) chunkX * 16 + 143
|| minZ < (long) chunkZ * 16 - 128 || maxZ > (long) chunkZ * 16 + 143)
throw new IllegalArgumentException("Invalid saved native structure bounds");
if (pieces == null || pieces.isEmpty() || pieces.length() > (MAX_NBT_BYTES + 2) / 3 * 4
|| digest == null || !digest.matches("[0-9a-f]{64}") || !ExpansionVoidCertificate.digest(pieces).equals(digest))
throw new IllegalArgumentException("Invalid saved native structure checksum");
CompoundTag tag = decode(pieces);
if (!structure.equals(tag.getStringOr("id", "")) || tag.getIntOr("ChunkX", Integer.MIN_VALUE) != chunkX
|| tag.getIntOr("ChunkZ", Integer.MIN_VALUE) != chunkZ || tag.getIntOr("references", -1) != 0
|| tag.getListOrEmpty("Children").isEmpty())
throw new IllegalArgumentException("Saved native structure identity differs from its pieces");
}
public static ExpansionStructurePlan25 capture(StructurePieceSerializationContext context, StructureStart start) {
if (!start.isValid() || start.getReferences() != 0) throw new IllegalArgumentException("Expected an unreferenced native start");
return fromTag(start.getBoundingBox(), start.createTag(context, start.getChunkPos()));
}
/** Also used by serialization tests without a Minecraft world. */
static ExpansionStructurePlan25 fromTag(BoundingBox bounds, CompoundTag tag) {
try {
var bytes = new ByteArrayOutputStream();
NbtIo.write(tag, new DataOutputStream(bytes));
if (bytes.size() > MAX_NBT_BYTES) throw new IllegalArgumentException("Native monument exceeds its saved-piece budget");
String encoded = Base64.getEncoder().encodeToString(bytes.toByteArray());
return new ExpansionStructurePlan25(ALGORITHM, tag.getStringOr("id", ""), tag.getIntOr("ChunkX", 0), tag.getIntOr("ChunkZ", 0),
bounds.minX(), bounds.minY(), bounds.minZ(), bounds.maxX(), bounds.maxY(), bounds.maxZ(), encoded, ExpansionVoidCertificate.digest(encoded));
} catch (IOException error) { throw new IllegalArgumentException("Cannot encode native monument", error); }
}
public ChunkPos startChunk() { return new ChunkPos(chunkX, chunkZ); }
public BoundingBox bounds() { return new BoundingBox(minX, minY, minZ, maxX, maxY, maxZ); }
/** Includes native foundation/clearing work around the visible body. */
public BoundingBox protection() { return new BoundingBox(minX - 16, Math.max(0, minY - 16), minZ - 16, maxX + 16, maxY + 8, maxZ + 16); }
public boolean conflicts(BoundingBox other) { return protection().intersects(other); }
public boolean protects(net.minecraft.core.BlockPos pos) {
return pos.getX() >= minX - 16 && pos.getX() <= maxX + 16 && pos.getZ() >= minZ - 16 && pos.getZ() <= maxZ + 16
&& pos.getY() >= Math.max(0, minY - 16) && pos.getY() <= maxY + 8;
}
public void validateFor(ExpansionIsland island) {
if (island.generation() != 25 || island.origin() || !island.relief().equals("ocean")
|| !island.ownsChunk(chunkX, chunkZ)) throw new IllegalArgumentException("Native monument belongs to an ocean25 expansion");
for (int x : new int[]{minX, maxX}) for (int z : new int[]{minZ, maxZ})
if (!island.contains(x, z)) throw new IllegalArgumentException("Saved monument extends outside its island");
CompoundTag metadata = tag().getCompoundOrEmpty("sanctuary_native");
if (metadata.getIntOr("generation", 0) != 23 || metadata.getLongOr("island_seed", 0) != island.seed()
|| metadata.getIntOr("sea_level", -64) != ExpansionOcean23.seaLevel(island))
throw new IllegalArgumentException("Saved monument seed or sea level differs from its island");
}
public StructureStart restore(StructurePieceSerializationContext context, ExpansionIsland island) {
validateFor(island);
CompoundTag saved = tag();
StructureStart result = StructureStart.loadStaticStart(context, saved, island.seed());
if (result == null || !result.isValid() || !bounds().equals(result.getBoundingBox())
|| !startChunk().equals(result.getChunkPos()) || !saved.equals(result.createTag(context, result.getChunkPos())))
throw new IllegalStateException("Reserved native monument no longer decodes exactly; refusing a new draw");
return result;
}
public CompoundTag tag() { return decode(pieces); }
private static CompoundTag decode(String encoded) {
try {
byte[] bytes = Base64.getDecoder().decode(encoded);
if (bytes.length > MAX_NBT_BYTES) throw new IllegalArgumentException("Native monument exceeds its byte budget");
var input = new DataInputStream(new ByteArrayInputStream(bytes));
var tag = NbtIo.read(input, NbtAccounter.create(1_048_576));
if (input.available() != 0) throw new IllegalArgumentException("Trailing bytes in saved native monument");
return tag;
} catch (IOException error) { throw new IllegalArgumentException("Cannot decode saved native monument", error); }
}
}
@@ -44,7 +44,7 @@ public final class ExpansionStructures {
public static void generate(ExpansionChunkGenerator generator, RegistryAccess registries,
ChunkGeneratorStructureState state, StructureManager manager, ChunkAccess chunk,
StructureTemplateManager templates, ResourceKey<Level> level) {
if ((generator.rootGeneration() == 23 || generator.rootGeneration() == 24)) {
if ((generator.rootGeneration() == 23 || generator.rootGeneration() == 24 || generator.rootGeneration() == 25)) {
ExpansionStructures23.generate(generator, registries, state, manager, chunk, templates, level);
return;
}
@@ -61,7 +61,7 @@ public final class ExpansionStructures23 {
public static List<Entry> catalogue() { return ExpansionStructureCatalog23.entries(); }
public static String family(String id) { return ExpansionStructureCatalog23.family(id); }
public static boolean isPlacing() { return NativeStructurePlacement23.seaLevel() != null; }
private static boolean nativeGeneration(int generation) { return generation == 23 || generation == 24; }
private static boolean nativeGeneration(int generation) { return generation == 23 || generation == 24 || generation == 25; }
public record Candidate(ChunkPos chunk, long roll) {}
public record Accepted(Holder<Structure> structure, StructureStart start) {}
public static final class NoSuitableStructureException extends IllegalArgumentException {
@@ -75,6 +75,8 @@ public final class ExpansionStructures23 {
int rootX = island.centerX() >> 4, rootZ = island.centerZ() >> 4;
var central = new ChunkPos(rootX, rootZ);
result.put(central.pack(), new Candidate(central, mix(island.seed() ^ 0xA2300C37L)));
island.structurePlan25().ifPresent(saved -> result.putIfAbsent(saved.startChunk().pack(),
new Candidate(saved.startChunk(), mix(island.seed() ^ saved.startChunk().pack() ^ 0xA25C37L))));
int cells = (island.terrainRadius() + 127) / 128;
for (int cz = -cells; cz <= cells; cz++) for (int cx = -cells; cx <= cells; cx++) {
long roll = mix(island.seed() ^ 0xA23CA7A10L ^ cx * 0x9e3779b97f4a7c15L ^ cz * 0xc2b2ae3d27d4eb4fL);
@@ -92,12 +94,23 @@ public final class ExpansionStructures23 {
if (!Level.OVERWORLD.equals(dimension) || !manager.shouldGenerateStructures()) return;
ExpansionIsland island = generator.islandAt(chunk.getPos().getMiddleBlockX(), chunk.getPos().getMiddleBlockZ());
if (island == null || !nativeGeneration(island.generation())) return;
if (island.origin() && island.generation() == 25 && generator instanceof SanctuaryChunkGenerator sanctuary
&& fr.koka.sanctuary.worldgen.underground25.UndergroundStructures25.plan(sanctuary).starts().stream()
.anyMatch(site -> site.start().getChunkPos().equals(chunk.getPos()))) return;
Candidate candidate = candidates(island).stream().filter(c -> c.chunk().equals(chunk.getPos())).findFirst().orElse(null);
if (candidate == null) return;
for (var old : chunk.getAllStarts().values()) {
if (old.isValid() && ((Object) old) instanceof NativeStructureMetadata23 metadata
&& metadata.sanctuary$native23() != null) return;
}
if (island.structurePlan25().isPresent() && island.structurePlan25().get().startChunk().equals(chunk.getPos())) {
var saved = island.structurePlan25().get();
var serialization = new net.minecraft.world.level.levelgen.structure.pieces.StructurePieceSerializationContext(
net.minecraft.server.packs.resources.ResourceManager.Empty.INSTANCE, registries, templates);
StructureStart start = saved.restore(serialization, island);
manager.setStartForStructure(start.getStructure(), start, chunk);
return;
}
var view = new NativeView(generator, island, generator.source(), registries);
Accepted accepted = plan(generator, view, island, candidate, registries, templates, dimension, chunk, Long.MAX_VALUE);
if (accepted != null) {
@@ -112,6 +125,10 @@ public final class ExpansionStructures23 {
*/
public static void verifyExpansion(ServerLevel level, ExpansionIsland island, long deadlineNanos) {
if (!nativeGeneration(island.generation()) || island.origin() || !level.structureManager().shouldGenerateStructures()) return;
if (island.generation() == 25 && island.relief().equals("ocean")) {
prepareExpansion25(level, island, deadlineNanos);
return;
}
if (!(level.getChunkSource().getGenerator() instanceof ExpansionChunkGenerator host))
throw new IllegalArgumentException("Native structure preflight requires a Sanctuary world");
ExpansionBiomeSource isolated = isolatedSource(host, island, level.registryAccess());
@@ -126,11 +143,28 @@ public final class ExpansionStructures23 {
throw new NoSuitableStructureException();
}
/** The selected native start becomes part of the journal/certificate identity before any terrain inspection. */
public static ExpansionIsland prepareExpansion25(ServerLevel level, ExpansionIsland island, long deadlineNanos) {
if (island.generation() != 25 || island.origin() || !island.relief().equals("ocean")
|| !level.structureManager().shouldGenerateStructures()) return island;
var context = net.minecraft.world.level.levelgen.structure.pieces.StructurePieceSerializationContext.fromLevel(level);
if (island.structurePlan25().isPresent()) {
island.structurePlan25().get().restore(context, island);
return island;
}
Accepted accepted = preview(level, island, "minecraft:monument", deadlineNanos);
if (accepted == null) throw new NoSuitableStructureException();
var result = island.withStructurePlan25(ExpansionStructurePlan25.capture(context, accepted.start()));
// A native monument recreates its rooms from seed when loading. Verify that path before accepting it.
result.structurePlan25().orElseThrow().restore(context, result);
return result;
}
/** Public for native diagnostics: only accepted starts are returned; no registry or chunk mutation. */
public static Accepted plan(ExpansionChunkGenerator host, ExpansionChunkGenerator view, ExpansionIsland island,
Candidate candidate, RegistryAccess registries, StructureTemplateManager templates, ResourceKey<Level> dimension,
ChunkAccess chunk, long deadlineNanos) {
try { return planFiltered(host, view, island, candidate, registries, templates, dimension, chunk, deadlineNanos, entry -> true); }
try { return planFiltered(host, view, island, candidate, registries, templates, dimension, chunk, deadlineNanos, entry -> generalSelection(island, entry)); }
catch (SampleBudgetExceeded refused) { diagnostic(island, candidate, "terrain-budget-refused", ""); return null; }
}
@@ -142,6 +176,16 @@ public final class ExpansionStructures23 {
public static Accepted previewSelected(ServerLevel level, ExpansionIsland island, String structureId, long deadlineNanos) {
return preview(level, island, structureId, deadlineNanos, true);
}
/** One explicit candidate for root25 conditional planners; no chunk request or publication. */
public static Accepted previewAt(ServerLevel level, ExpansionIsland island, String structureId, Candidate candidate, long deadlineNanos) {
var host = (ExpansionChunkGenerator) level.getChunkSource().getGenerator();
var view = new NativeView(host, island, isolatedSource(host, island, level.registryAccess()), level.registryAccess());
var chunk = new ProtoChunk(candidate.chunk(), UpgradeData.EMPTY, level, PalettedContainerFactory.create(level.registryAccess()), null);
try {
return planFiltered(host, view, island, candidate, level.registryAccess(), level.getStructureTemplateManager(),
level.dimension(), chunk, deadlineNanos, entry -> entry.id().equals(structureId));
} catch (SampleBudgetExceeded refused) { return null; }
}
private static Accepted preview(ServerLevel level, ExpansionIsland island, String structureId, long deadlineNanos, boolean selectedOnly) {
var host = (ExpansionChunkGenerator) level.getChunkSource().getGenerator();
ExpansionBiomeSource source = isolatedSource(host, island, level.registryAccess());
@@ -152,13 +196,18 @@ public final class ExpansionStructures23 {
var chunk = new ProtoChunk(candidate.chunk(), UpgradeData.EMPTY, level, factory, null);
try {
Accepted accepted = planFiltered(host, view, island, candidate, level.registryAccess(), level.getStructureTemplateManager(),
level.dimension(), chunk, deadlineNanos, entry -> selectedOnly || entry.id().equals(structureId));
level.dimension(), chunk, deadlineNanos, entry -> selectedOnly ? generalSelection(island, entry) : entry.id().equals(structureId));
if (accepted != null && accepted.structure().unwrapKey().orElseThrow().identifier().toString().equals(structureId)) return accepted;
} catch (SampleBudgetExceeded refused) { /* An unsuitable candidate does not suppress the following sites. */ }
}
return null;
}
/** The initial25 village belongs to the conditional underground planner; expansion and explicit preview catalogues stay complete. */
private static boolean generalSelection(ExpansionIsland island, Entry entry) {
return island.generation() != 25 || !island.origin() || !entry.family().equals("village");
}
private static Accepted planFiltered(ExpansionChunkGenerator host, ExpansionChunkGenerator view, ExpansionIsland island,
Candidate candidate, RegistryAccess registries, StructureTemplateManager templates, ResourceKey<Level> dimension,
ChunkAccess chunk, long deadlineNanos, Predicate<Entry> included) {
@@ -308,6 +357,13 @@ public final class ExpansionStructures23 {
private static String supportRejection(ExpansionChunkGenerator host, ExpansionIsland island, Entry entry,
StructureStart start, GroundSampler ground) {
BoundingBox total = start.getBoundingBox();
if (island.origin() && island.generation() == 25 && host instanceof SanctuaryChunkGenerator sanctuary
&& fr.koka.sanctuary.worldgen.underground25.UndergroundStructures25.plan(sanctuary).intersects(
new BoundingBox(total.minX() - 8, total.minY() - 16, total.minZ() - 8, total.maxX() + 8, total.maxY() + 8, total.maxZ() + 8)))
return "reserved-underground25";
if (island.structurePlan25().isPresent() && island.structurePlan25().get().conflicts(
new BoundingBox(total.minX() - 8, total.minY() - 16, total.minZ() - 8, total.maxX() + 8, total.maxY() + 8, total.maxZ() + 8)))
return "reserved-monument25";
if (island.origin() && entry.habitat() != Habitat.UNDERGROUND && !undergroundPortal(start)) {
int nearestX = Math.clamp(island.centerX(), total.minX(), total.maxX());
int nearestZ = Math.clamp(island.centerZ(), total.minZ(), total.maxZ());
@@ -400,6 +456,9 @@ public final class ExpansionStructures23 {
}
}
if (sanctuaryRequested && generator instanceof SanctuaryChunkGenerator sanctuary) addSanctuaryCandidates(sanctuary, candidates);
if (generator.rootGeneration() == 25 && nativeRequested && generator instanceof SanctuaryChunkGenerator sanctuary)
for (var site : fr.koka.sanctuary.worldgen.underground25.UndergroundStructures25.plan(sanctuary).starts())
addPosition(candidates, site.start().getChunkPos().x(), site.start().getChunkPos().z());
candidates.removeIf(c -> Math.abs((long) c.chunk().x() - (origin.getX() >> 4)) > (long) radius * CELL_CHUNKS
|| Math.abs((long) c.chunk().z() - (origin.getZ() >> 4)) > (long) radius * CELL_CHUNKS);
candidates = new ArrayList<>(candidates.stream().collect(java.util.stream.Collectors.toMap(
@@ -410,7 +469,7 @@ public final class ExpansionStructures23 {
for (Candidate candidate : candidates) {
// Every admitted start is within the native 128-block reference range of its start chunk.
double minimumDistance = Math.max(0, Math.sqrt(horizontalDistanceSquared(origin, candidate.chunk().getMiddleBlockX(), candidate.chunk().getMiddleBlockZ())) - 192);
if (!sanctuaryRequested && minimumDistance * minimumDistance > bestDistance) break;
if (!sanctuaryRequested && generator.rootGeneration() != 25 && minimumDistance * minimumDistance > bestDistance) break;
var chunk = level.getChunk(candidate.chunk().x(), candidate.chunk().z(), ChunkStatus.STRUCTURE_STARTS, true);
for (Holder<Structure> holder : wanted) {
Entry entry = ExpansionStructureCatalog23.entry(holder.unwrapKey().orElseThrow().identifier().toString());
@@ -440,7 +499,7 @@ public final class ExpansionStructures23 {
generator.undergroundHall22().networks().forEach(s -> addPosition(positions, s.startChunkX(), s.startChunkZ()));
var mineral = generator.mineralSanctuary23();
if (!mineral.isEmpty()) addPosition(positions, mineral.chamber().startChunkX(), mineral.chamber().startChunkZ());
if (generator.rootGeneration() == 24) generator.aerialPlan24().sites()
if (generator.rootGeneration() == 24 || generator.rootGeneration() == 25) generator.aerialPlan24().sites()
.forEach(site -> addPosition(positions, site.startChunkX(), site.startChunkZ()));
}
private static void addPosition(List<Candidate> positions, int x, int z) { positions.add(new Candidate(new ChunkPos(x, z), 0)); }
@@ -86,9 +86,12 @@ public final class ExpansionVoidReuse {
if (!(level.getChunkSource().getGenerator() instanceof ExpansionChunkGenerator generator)
|| !ExpansionIsland.spatialGeneration(generator.rootGeneration()) || island.generation() != generator.rootGeneration() || island.origin())
throw new IOException("Void reuse is restricted to a matching generation-14/15 expansion");
if (generator.rootGeneration() == 24 && generator instanceof fr.koka.sanctuary.worldgen.SanctuaryChunkGenerator sanctuary
if ((generator.rootGeneration() == 24 || generator.rootGeneration() == 25) && generator instanceof fr.koka.sanctuary.worldgen.SanctuaryChunkGenerator sanctuary
&& sanctuary.aerialPlan24().conflicts(island))
throw new ExpansionAdmission.OccupiedException("aerial_reserved: expansion overlaps a permanent initial site");
if (generator.rootGeneration() == 25 && generator instanceof fr.koka.sanctuary.worldgen.SanctuaryChunkGenerator sanctuary
&& ExpansionInitialFlows25.conflicts(sanctuary, island))
throw new ExpansionAdmission.OccupiedException("initial_waterfall_reserved: expansion overlaps a future initial river outlet");
List<ChunkPos> positions = positions(island);
var entityStorage = entityRegion(level);
var poiStorage = ((ExpansionVoidPoiStorageAccess) level.getPoiManager()).sanctuary$simpleRegionStorage();
@@ -176,9 +179,12 @@ public final class ExpansionVoidReuse {
public static void open(ServerLevel level, ExpansionChunkGenerator generator, List<ExpansionIsland> islands) throws IOException {
requireServerThread(level); close(level);
if (!ExpansionIsland.spatialGeneration(generator.rootGeneration())) return;
if (generator.rootGeneration() == 24 && generator instanceof fr.koka.sanctuary.worldgen.SanctuaryChunkGenerator sanctuary)
if ((generator.rootGeneration() == 24 || generator.rootGeneration() == 25) && generator instanceof fr.koka.sanctuary.worldgen.SanctuaryChunkGenerator sanctuary)
for (var island : islands) if (sanctuary.aerialPlan24().conflicts(island))
throw new IOException("Saved expansion overlaps a permanent aerial site: " + island.id());
if (generator.rootGeneration() == 25 && generator instanceof fr.koka.sanctuary.worldgen.SanctuaryChunkGenerator sanctuary)
for (var island : islands) if (ExpansionInitialFlows25.conflicts(sanctuary, island))
throw new IOException("Saved expansion overlaps an initial river outlet: " + island.id());
var state = new State();
for (var island : islands) if (!island.reuseToken().isEmpty()) add(state, loadManifest(level, island));
// Validate every transaction before writing even an EMPTY placeholder or publishing any island.
@@ -84,7 +84,7 @@ public final class StructureCensusService {
assertServerThread(level.getServer());
if (!level.dimension().equals(Level.OVERWORLD) || !level.structureManager().shouldGenerateStructures()
|| !(level.getChunkSource().getGenerator() instanceof ExpansionChunkGenerator generator)
|| generator.rootGeneration() != 23 && generator.rootGeneration() != 24)
|| generator.rootGeneration() != 23 && generator.rootGeneration() != 24 && generator.rootGeneration() != 25)
return List.of();
return confirmedPriorities(snapshot(level.getServer()), level.dimension().identifier().toString());
}
@@ -157,7 +157,7 @@ public final class StructureCensusService {
}).toList();
if (matching.isEmpty()) return Presence.OTHER_DIMENSION;
// This is an explicit generation23 habitat requirement, not an inferred lack of biomes.
if (island != null && (island.generation() == 23 || island.generation() == 24) && !island.relief().equals("ocean")
if (island != null && (island.generation() == 23 || island.generation() == 24 || island.generation() == 25) && !island.relief().equals("ocean")
&& matching.stream().allMatch(entry -> entry.habitat() == ExpansionStructureCatalog23.Habitat.OCEAN)) return Presence.INCOMPATIBLE;
return null;
}
@@ -169,6 +169,7 @@ public final class StructureCensusService {
}
public static String family(String id) {
if (id.equals(StructureCensusMemory.MONSTER_ROOM)) return "monster_room";
if (id.equals("sanctuary:underground_abandoned_village")) return "village";
return ExpansionStructureCatalog23.entries().stream().filter(entry -> entry.id().equals(id))
.map(ExpansionStructureCatalog23.Entry::family).findFirst().orElse(id);
}
@@ -179,7 +180,7 @@ public final class StructureCensusService {
assertServerThread(level.getServer());
if (!level.dimension().equals(Level.OVERWORLD) || !level.structureManager().shouldGenerateStructures()
|| !(level.getChunkSource().getGenerator() instanceof fr.koka.sanctuary.worldgen.SanctuaryChunkGenerator generator)
|| generator.rootGeneration() != 24) return List.of();
|| (generator.rootGeneration() != 24 && generator.rootGeneration() != 25)) return List.of();
var session = current(level.getServer());
var evidence = session.memory.snapshot();
String dimension = level.dimension().identifier().toString();
@@ -31,7 +31,12 @@ public abstract class SurfaceHydrologyMixin {
StructureManager structures, BiomeManager biomeManager, WorldGenRegion region,
Set<Holder<Biome>> biomes, CallbackInfoReturnable<CompletableFuture<ChunkAccess>> callback) {
var generator = (NoiseBasedChunkGenerator) (Object) this;
if (PopulationHydrologyRuntime.enabled(generator)) {
if (fr.koka.sanctuary.worldgen.Terrain25Runtime.enabled(generator)) {
callback.setReturnValue(callback.getReturnValue().thenApply(generated -> {
fr.koka.sanctuary.worldgen.Terrain25Runtime.apply(generator, randomState, generated);
return generated;
}));
} else if (PopulationHydrologyRuntime.enabled(generator)) {
callback.setReturnValue(callback.getReturnValue().thenApply(generated -> {
PopulationHydrologyRuntime.apply(generator, randomState, generated);
return generated;
@@ -0,0 +1,40 @@
package fr.koka.sanctuary.mixin;
import fr.koka.sanctuary.worldgen.Terrain25Runtime;
import net.minecraft.world.level.LevelHeightAccessor;
import net.minecraft.world.level.NoiseColumn;
import net.minecraft.world.level.levelgen.Heightmap;
import net.minecraft.world.level.levelgen.NoiseBasedChunkGenerator;
import net.minecraft.world.level.levelgen.RandomState;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
/** Only the named generation25 delegate exposes its final water and cave surfaces to admission. */
@Mixin(NoiseBasedChunkGenerator.class)
public abstract class Terrain25ColumnMixin {
@Inject(method = "getBaseColumn", at = @At("RETURN"), cancellable = true)
private void sanctuary$finalColumn25(int x, int z, LevelHeightAccessor height, RandomState random,
CallbackInfoReturnable<NoiseColumn> callback) {
var generator = (NoiseBasedChunkGenerator) (Object) this;
if (!Terrain25Runtime.enabled(generator)) return;
var column = callback.getReturnValue();
Terrain25Runtime.applyColumn(generator, random, x, z, column);
fr.koka.sanctuary.worldgen.Terrain25Integration.column(generator, x, z, column);
callback.setReturnValue(column);
}
@Inject(method = "getBaseHeight", at = @At("HEAD"), cancellable = true)
private void sanctuary$finalHeight25(int x, int z, Heightmap.Types type, LevelHeightAccessor height,
RandomState random, CallbackInfoReturnable<Integer> callback) {
var generator = (NoiseBasedChunkGenerator) (Object) this;
if (!Terrain25Runtime.enabled(generator)) return;
var column = generator.getBaseColumn(x, z, height, random);
var predicate = type.isOpaque();
for (int y = 383; y >= 0; y--) if (predicate.test(column.getBlock(y))) {
callback.setReturnValue(y + 1); return;
}
callback.setReturnValue(0);
}
}
@@ -0,0 +1,20 @@
package fr.koka.sanctuary.mixin;
import fr.koka.sanctuary.worldgen.underground25.UndergroundPlacement25;
import net.minecraft.server.level.WorldGenRegion;
import net.minecraft.world.level.levelgen.Heightmap;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
/** Vanilla terrain_matching roads query this at placement time, independently of the planning generator. */
@Mixin(WorldGenRegion.class)
public abstract class UndergroundHeight25Mixin {
@Inject(method = "getHeight", at = @At("HEAD"), cancellable = true)
private void sanctuary$undergroundRoad25(Heightmap.Types type, int x, int z, CallbackInfoReturnable<Integer> cir) {
Integer floor = UndergroundPlacement25.projectedHeight(x, z);
if (floor != null && (type == Heightmap.Types.WORLD_SURFACE_WG || type == Heightmap.Types.OCEAN_FLOOR_WG
|| type == Heightmap.Types.WORLD_SURFACE || type == Heightmap.Types.OCEAN_FLOOR)) cir.setReturnValue(floor);
}
}
@@ -0,0 +1,23 @@
package fr.koka.sanctuary.mixin;
import com.llamalad7.mixinextras.injector.wrapmethod.WrapMethod;
import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
import fr.koka.sanctuary.worldgen.underground25.UndergroundPlacement25;
import net.minecraft.util.RandomSource;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.StructureManager;
import net.minecraft.world.level.WorldGenLevel;
import net.minecraft.world.level.chunk.ChunkGenerator;
import net.minecraft.world.level.levelgen.structure.BoundingBox;
import net.minecraft.world.level.levelgen.structure.StructureStart;
import org.spongepowered.asm.mixin.Mixin;
@Mixin(StructureStart.class)
public abstract class UndergroundStart25Mixin {
@WrapMethod(method = "placeInChunk")
private void sanctuary$projectVillage25(WorldGenLevel level, StructureManager manager, ChunkGenerator generator,
RandomSource random, BoundingBox clip, ChunkPos chunk, Operation<Void> original) {
UndergroundPlacement25.placing(level, (StructureStart) (Object) this,
() -> original.call(level, manager, generator, random, clip, chunk));
}
}
@@ -21,13 +21,22 @@ public abstract class UnifiedDecorationProtectionMixin {
if (!(region.getLevel().getChunkSource().getGenerator() instanceof SanctuaryChunkGenerator generator)
|| !fr.koka.sanctuary.expansion.ExpansionIsland.spatialGeneration(generator.rootGeneration())) return;
// Aerial pieces write in genuine void chunks, within their own immutable site and current chunk only.
if (generator.rootGeneration() == 24 && fr.koka.sanctuary.worldgen.aerial.AerialStructures24.isPlacing()) {
if ((generator.rootGeneration() == 24 || generator.rootGeneration() == 25) && fr.koka.sanctuary.worldgen.aerial.AerialStructures24.isPlacing()) {
if (!fr.koka.sanctuary.worldgen.aerial.AerialStructures24.mayWrite(pos)) callback.setReturnValue(false);
return; // Vanilla still applies its write-zone and build-height checks.
}
if (generator.rootGeneration() == 25 && fr.koka.sanctuary.worldgen.underground25.UndergroundStructures25.isPlacing()) {
if (!fr.koka.sanctuary.worldgen.underground25.UndergroundStructures25.mayWrite(pos)) callback.setReturnValue(false);
return;
}
var center = region.getCenter();
var owner = generator.islandForChunk(center.x(), center.z());
var destination = generator.islandForChunk(pos.getX() >> 4, pos.getZ() >> 4);
if (destination != null && destination.structurePlan25().isPresent()
&& !fr.koka.sanctuary.expansion.ExpansionStructures23.isPlacing()
&& destination.structurePlan25().orElseThrow().protects(pos)) {
callback.setReturnValue(false); return;
}
if (owner == null || destination == null || !owner.id().equals(destination.id())
|| !fr.koka.sanctuary.worldgen.city.LostCityStructures.isPlacing()
&& !fr.koka.sanctuary.worldgen.ruins.RuinsStructures17.isPlacing()
@@ -44,13 +53,18 @@ public abstract class UnifiedDecorationProtectionMixin {
if (!(region.getLevel().getChunkSource().getGenerator() instanceof SanctuaryChunkGenerator generator)) return;
// Vanilla rejects writes outside its region before reading the destination chunk.
if (!region.isWithinWriteZone(pos)) return;
if (generator.rootGeneration() == 24 && fr.koka.sanctuary.worldgen.aerial.AerialStructures24.isPlacing()) {
if ((generator.rootGeneration() == 24 || generator.rootGeneration() == 25) && fr.koka.sanctuary.worldgen.aerial.AerialStructures24.isPlacing()) {
if (!fr.koka.sanctuary.worldgen.aerial.AerialStructures24.mayWrite(pos)) callback.setReturnValue(false);
return;
}
if ((generator.rootGeneration() == 23 || generator.rootGeneration() == 24)) {
if ((generator.rootGeneration() == 23 || generator.rootGeneration() == 24 || generator.rootGeneration() == 25)) {
if (fr.koka.sanctuary.worldgen.mineral.MineralSanctuaryStructures23.isPlacing()) return;
var island = generator.islandAt(pos.getX(), pos.getZ());
if (island != null && island.structurePlan25().isPresent()
&& !fr.koka.sanctuary.expansion.ExpansionStructures23.isPlacing()
&& island.structurePlan25().orElseThrow().protects(pos)) {
callback.setReturnValue(false); return;
}
if (fr.koka.sanctuary.expansion.ExpansionOcean23.active(island)
&& !fr.koka.sanctuary.expansion.ExpansionStructures23.isPlacing()) {
var ocean = fr.koka.sanctuary.expansion.ExpansionOcean23.column(island, pos.getX(), pos.getZ());
@@ -60,11 +74,19 @@ public abstract class UnifiedDecorationProtectionMixin {
}
}
}
if (generator.rootGeneration() == 25 && fr.koka.sanctuary.worldgen.underground25.UndergroundStructures25.isPlacing()) {
if (!fr.koka.sanctuary.worldgen.underground25.UndergroundStructures25.mayWrite(pos)) callback.setReturnValue(false);
return;
}
if (generator.rootGeneration() == 25 && pos.getY() >= 364
&& (state.is(net.minecraft.tags.BlockTags.LOGS) || state.is(net.minecraft.tags.BlockTags.LEAVES))) {
callback.setReturnValue(false); return;
}
// The dedicated shoreline feature still grows cane on its naturally valid dry banks.
if (state.is(Blocks.SUGAR_CANE)) 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))
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 || generator.rootGeneration() == 25))
&& fr.koka.sanctuary.worldgen.city.LostCityStructures.isPlacing()
&& generator.protectsCityDecoration(pos)) return;
if (generator.protectsDecoration(pos) && !region.getBlockState(pos).equals(state)) {
@@ -27,9 +27,9 @@ public enum IslandCapacity {
public static IslandCapacity forGenerator(ChunkGenerator generator) {
if (!(generator instanceof NoiseBasedChunkGenerator noise)) return null;
var settings = noise.generatorSettings();
if (settings.is(SanctuarySpawn.POPULATION_5_SETTINGS) || settings.is(fr.koka.sanctuary.SanctuaryMod.id("unified_5"))) return FIVE;
if (settings.is(SanctuarySpawn.POPULATION_10_SETTINGS) || settings.is(fr.koka.sanctuary.SanctuaryMod.id("unified_10"))) return TEN;
if (settings.is(SanctuarySpawn.POPULATION_20_SETTINGS) || settings.is(fr.koka.sanctuary.SanctuaryMod.id("unified_20"))) return TWENTY;
if (settings.is(SanctuarySpawn.POPULATION_5_SETTINGS) || settings.is(fr.koka.sanctuary.SanctuaryMod.id("unified_5")) || settings.is(fr.koka.sanctuary.SanctuaryMod.id("unified_25_5"))) return FIVE;
if (settings.is(SanctuarySpawn.POPULATION_10_SETTINGS) || settings.is(fr.koka.sanctuary.SanctuaryMod.id("unified_10")) || settings.is(fr.koka.sanctuary.SanctuaryMod.id("unified_25_10"))) return TEN;
if (settings.is(SanctuarySpawn.POPULATION_20_SETTINGS) || settings.is(fr.koka.sanctuary.SanctuaryMod.id("unified_20")) || settings.is(fr.koka.sanctuary.SanctuaryMod.id("unified_25_20"))) return TWENTY;
if (settings.is(SanctuarySpawn.POPULATION_100_SETTINGS)) return HUNDRED;
return null;
}
@@ -65,7 +65,16 @@ public final class SanctuaryBiomeSource extends ExpansionBiomeSource {
public static final MapCodec<SanctuaryBiomeSource> AERIAL24_CODEC = RecordCodecBuilder.mapCodec(i -> i.group(
Biome.CODEC.listOf().fieldOf("biomes").forGetter(source -> source.allBiomes)
).apply(i, biomes -> new SanctuaryBiomeSource(biomes, 24)));
public static final MapCodec<SanctuaryBiomeSource> TERRAIN25_CODEC = RecordCodecBuilder.mapCodec(i -> i.group(
Biome.CODEC.listOf().fieldOf("biomes").forGetter(source -> source.allBiomes)
).apply(i, biomes -> new SanctuaryBiomeSource(biomes, 25)));
private volatile Terrain25Shape.Plan terrain25Plan;
public void setTerrain25Plan(Terrain25Shape.Plan plan) {
if (generation != 25) throw new IllegalStateException("Terrain25 plan on historical biome source");
terrain25Plan = java.util.Objects.requireNonNull(plan);
}
private final List<Holder<Biome>> allBiomes;
private final PopulationIslandBiomeSource population;
private final boolean cities;
@@ -81,7 +90,7 @@ public final class SanctuaryBiomeSource extends ExpansionBiomeSource {
this.generation = generation;
this.cities = ExpansionIsland.spatialGeneration(generation);
allBiomes = List.copyOf(biomes);
if ((generation == 23 || generation == 24)) for (String name : fr.koka.sanctuary.expansion.ExpansionOcean23.BIOMES)
if ((generation == 23 || generation == 24 || generation == 25)) for (String name : fr.koka.sanctuary.expansion.ExpansionOcean23.BIOMES)
required(ResourceKey.create(net.minecraft.core.registries.Registries.BIOME,
net.minecraft.resources.Identifier.withDefaultNamespace(name)));
cityBiome = cities ? required(ResourceKey.create(net.minecraft.core.registries.Registries.BIOME,
@@ -95,14 +104,18 @@ public final class SanctuaryBiomeSource extends ExpansionBiomeSource {
}
/** A world or delegate gets its own activation snapshot; registered holders remain immutable. */
public SanctuaryBiomeSource copy() { return new SanctuaryBiomeSource(allBiomes, generation); }
public SanctuaryBiomeSource copy() {
var copy = new SanctuaryBiomeSource(allBiomes, generation);
copy.terrain25Plan = terrain25Plan;
return copy;
}
public boolean citiesEnabled() { return cities; }
public int generation() { return generation; }
public void setCityPlans(java.util.Map<String, fr.koka.sanctuary.worldgen.city.LostCityPlan> plans) {
cityPlans = java.util.Map.copyOf(plans);
}
@Override protected MapCodec<SanctuaryBiomeSource> codec() { return generation == 24 ? AERIAL24_CODEC : generation == 23 ? SECRETS23_CODEC : generation == 22 ? SELECTIVE22_CODEC : generation == 21 ? NETWORK21_CODEC : generation == 20 ? ARCHITECTURE20_CODEC : generation == 19 ? HERITAGE19_CODEC : generation == 17 ? RUINS17_CODEC : generation == 16 ? CITY16_CODEC : generation == 15 ? CITY15_CODEC : cities ? CITY_CODEC : CODEC; }
@Override protected MapCodec<SanctuaryBiomeSource> codec() { return generation == 25 ? TERRAIN25_CODEC : generation == 24 ? AERIAL24_CODEC : generation == 23 ? SECRETS23_CODEC : generation == 22 ? SELECTIVE22_CODEC : generation == 21 ? NETWORK21_CODEC : generation == 20 ? ARCHITECTURE20_CODEC : generation == 19 ? HERITAGE19_CODEC : generation == 17 ? RUINS17_CODEC : generation == 16 ? CITY16_CODEC : generation == 15 ? CITY15_CODEC : cities ? CITY_CODEC : CODEC; }
@Override public BiomeResolver createResolver(Climate.Sampler sampler) {
BiomeResolver woodlands = population.createResolver(sampler);
@@ -122,6 +135,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 == 25 && y >= 0 && y < 384) for (var island : snapshot) {
if (!island.origin() || !island.contains(x, z)) continue;
var shape = terrain25Plan;
if (shape != null && shape.wetland(x, y, z)) return required(Terrain25Biomes.key("swamp"));
var chamber = shape == null ? null : shape.chamberAt(x, y, z);
if (chamber != null && chamber.kind() == Terrain25Shape.Kind.MUSHROOM)
return required(Terrain25Biomes.MUSHROOM_REFUGE);
if (laboratory.is(Biomes.LUSH_CAVES)) return required(Terrain25Biomes.key("lush_caves"));
if (laboratory.is(Biomes.DRIPSTONE_CAVES)) return required(Terrain25Biomes.key("dripstone_caves"));
if (laboratory.is(Biomes.SULFUR_CAVES)) return required(Terrain25Biomes.key("sulfur_depths"));
var natural = woodlands.getNoiseBiome(quartX, quartY, quartZ);
String name = natural.unwrapKey().orElseThrow().identifier().getPath();
return required(Terrain25Biomes.key(name.substring("population_".length())));
}
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) {
@@ -80,19 +80,25 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
DIAMETER_CODEC.optionalFieldOf("initial_diameter", 724).forGetter(SanctuaryChunkGenerator::initialDiameter),
Codec.BOOL.optionalFieldOf("experimental_structures", true).forGetter(SanctuaryChunkGenerator::experimentalStructures)
).apply(i, (source, diameter, experimental) -> new SanctuaryChunkGenerator(source, diameter, 24, experimental)));
public static final MapCodec<SanctuaryChunkGenerator> TERRAIN25_CODEC = RecordCodecBuilder.mapCodec(i -> i.group(
SanctuaryBiomeSource.TERRAIN25_CODEC.fieldOf("biome_source").forGetter(SanctuaryChunkGenerator::source),
DIAMETER_CODEC.optionalFieldOf("initial_diameter", 724).forGetter(SanctuaryChunkGenerator::initialDiameter),
Codec.BOOL.optionalFieldOf("experimental_structures", true).forGetter(SanctuaryChunkGenerator::experimentalStructures)
).apply(i, (source, diameter, experimental) -> new SanctuaryChunkGenerator(source, diameter, 25, experimental)));
private volatile fr.koka.sanctuary.worldgen.aerial.AerialPlan24 aerialPlan24 = fr.koka.sanctuary.worldgen.aerial.AerialPlan24.EMPTY;
public fr.koka.sanctuary.worldgen.aerial.AerialPlan24 aerialPlan24() { return aerialPlan24; }
public void setAerialPlan24(fr.koka.sanctuary.worldgen.aerial.AerialPlan24 plan) {
if (generation != 24) throw new IllegalStateException("Aerial plans require generation24");
if (generation != 24 && generation != 25) throw new IllegalStateException("Aerial plans require generation24");
aerialPlan24 = java.util.Objects.requireNonNull(plan);
}
/** Plan root secrets before certifying aerial shore viewpoints, without requesting chunks. */
public void prepareAerialSampling24(ServerLevel level) {
if (generation != 24) throw new IllegalStateException("Aerial sampling requires generation24");
if (generation != 24 && generation != 25) throw new IllegalStateException("Aerial sampling requires generation24");
heightAccessor = level;
cityStructuresEnabled = level.structureManager().shouldGenerateStructures();
var root = java.util.List.of(ExpansionIsland.origin(initialDiameter(), level.getSeed(), 24));
var root = java.util.List.of(ExpansionIsland.origin(initialDiameter(), level.getSeed(), generation));
super.initialize(level, root);
context(root.getFirst());
// Same order as initialize. Prepared plans are reused after the manifest is persisted.
@@ -165,12 +171,12 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
}
private SanctuaryChunkGenerator(SanctuaryBiomeSource source, int initialDiameter, int generation, boolean experimental) {
super(source, initialDiameter, true);
this.experimentalStructures = (generation == 17 || generation == 19 || generation == 20 || generation == 21 || generation == 22 || (generation == 23 || generation == 24)) && experimental;
this.experimentalStructures = (generation == 17 || generation == 19 || generation == 20 || generation == 21 || generation == 22 || (generation == 23 || generation == 24 || generation == 25)) && experimental;
this.generation = generation;
if (generation != source.generation()) throw new IllegalArgumentException("Sanctuary generator and biome source versions must agree");
}
@Override public SanctuaryBiomeSource source() { return (SanctuaryBiomeSource) super.source(); }
@Override protected MapCodec<SanctuaryChunkGenerator> codec() { return generation == 24 ? AERIAL24_CODEC : generation == 23 ? SECRETS23_CODEC : generation == 22 ? SELECTIVE22_CODEC : generation == 21 ? NETWORK21_CODEC : generation == 20 ? ARCHITECTURE20_CODEC : generation == 19 ? HERITAGE19_CODEC : generation == 17 ? RUINS17_CODEC : generation == 16 ? CITY16_CODEC : generation == 15 ? CITY15_CODEC : generation == 14 ? CITY_CODEC : CODEC; }
@Override protected MapCodec<SanctuaryChunkGenerator> codec() { return generation == 25 ? TERRAIN25_CODEC : generation == 24 ? AERIAL24_CODEC : generation == 23 ? SECRETS23_CODEC : generation == 22 ? SELECTIVE22_CODEC : generation == 21 ? NETWORK21_CODEC : generation == 20 ? ARCHITECTURE20_CODEC : generation == 19 ? HERITAGE19_CODEC : generation == 17 ? RUINS17_CODEC : generation == 16 ? CITY16_CODEC : generation == 15 ? CITY15_CODEC : generation == 14 ? CITY_CODEC : CODEC; }
public SanctuaryChunkGenerator withInitialDiameter(int diameter) {
return new SanctuaryChunkGenerator(source().copy(), diameter, generation, experimentalStructures);
}
@@ -181,15 +187,17 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
public fr.koka.sanctuary.worldgen.ruins.RuinsPlan17 ruinsPlan17() { return ruinsPlan17; }
public fr.koka.sanctuary.worldgen.heritage.HeritagePlan19 heritagePlan19() { return heritagePlan19; }
public boolean intersectsExperimentalStructures(int minX, int minY, int minZ, int maxX, int maxY, int maxZ) {
if (generation == 24) for (var site : aerialPlan24.sites()) {
if (generation == 25 && fr.koka.sanctuary.worldgen.underground25.UndergroundStructures25.plan(this).intersects(
new net.minecraft.world.level.levelgen.structure.BoundingBox(minX, minY, minZ, maxX, maxY, maxZ))) return true;
if (generation == 24 || generation == 25) 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;
}
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)
|| (generation == 19 || generation == 20 || generation == 21 || generation == 22 || (generation == 23 || generation == 24)) && heritagePlan19.intersectsProtection(minX, minY, minZ, maxX, maxY, maxZ)
|| (generation == 21 || generation == 22 || (generation == 23 || generation == 24)) && transitPlan21.intersectsProtection(minX, minY, minZ, maxX, maxY, maxZ)
|| (generation == 22 || (generation == 23 || generation == 24)) && undergroundHallProtection22.intersects(minX, minY, minZ, maxX, maxY, maxZ);
return (generation == 23 || generation == 24 || generation == 25) && mineralSanctuary23.intersectsProtection(minX, minY, minZ, maxX, maxY, maxZ)
|| (generation == 17 || generation == 19 || generation == 20 || generation == 21 || generation == 22 || (generation == 23 || generation == 24 || generation == 25)) && ruinsPlan17.intersectsProtection(minX, minY, minZ, maxX, maxY, maxZ)
|| (generation == 19 || generation == 20 || generation == 21 || generation == 22 || (generation == 23 || generation == 24 || generation == 25)) && heritagePlan19.intersectsProtection(minX, minY, minZ, maxX, maxY, maxZ)
|| (generation == 21 || generation == 22 || (generation == 23 || generation == 24 || generation == 25)) && transitPlan21.intersectsProtection(minX, minY, minZ, maxX, maxY, maxZ)
|| (generation == 22 || (generation == 23 || generation == 24 || generation == 25)) && undergroundHallProtection22.intersects(minX, minY, minZ, maxX, maxY, maxZ);
}
public IslandCapacity capacity() {
return switch (initialDiameter()) {
@@ -202,6 +210,15 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
@Override public net.minecraft.core.Holder<net.minecraft.world.level.biome.Biome> structureBiome(
net.minecraft.core.Holder<net.minecraft.world.level.biome.Biome> biome) {
String name = biome.unwrapKey().orElseThrow().identifier().toString();
if (name.startsWith("sanctuary:terrain25_")) {
String path = name.substring("sanctuary:terrain25_".length());
if (path.equals("swamp")) return registries.lookupOrThrow(Registries.BIOME).getOrThrow(net.minecraft.world.level.biome.Biomes.SWAMP);
if (path.equals("mushroom_refuge")) return registries.lookupOrThrow(Registries.BIOME).getOrThrow(net.minecraft.world.level.biome.Biomes.MUSHROOM_FIELDS);
if (path.equals("lush_caves")) return registries.lookupOrThrow(Registries.BIOME).getOrThrow(net.minecraft.world.level.biome.Biomes.LUSH_CAVES);
if (path.equals("dripstone_caves")) return registries.lookupOrThrow(Registries.BIOME).getOrThrow(net.minecraft.world.level.biome.Biomes.DRIPSTONE_CAVES);
if (path.equals("sulfur_depths")) return registries.lookupOrThrow(Registries.BIOME).getOrThrow(net.minecraft.world.level.biome.Biomes.SULFUR_CAVES);
name = "sanctuary:population_" + path;
}
String equivalent = switch (name) {
case "sanctuary:population_oak_forest" -> "forest";
case "sanctuary:population_birch_forest" -> "birch_forest";
@@ -224,16 +241,17 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
// Publish one root context before any generation workers use it.
context(islands.getFirst());
prepareCities(islands);
if (generation == 22 || (generation == 23 || generation == 24)) { prepareTransit(islands); prepareUndergroundHall22(islands); }
if (generation == 22 || (generation == 23 || generation == 24 || generation == 25)) { prepareTransit(islands); prepareUndergroundHall22(islands); }
prepareMineral23(islands);
prepareRuins(islands);
prepareHeritage(islands);
prepareTransit(islands);
if (generation == 25) fr.koka.sanctuary.worldgen.underground25.UndergroundStructures25.prepare(level);
}
@Override public void setIslands(List<ExpansionIsland> islands) {
// Complete and publish city geometry before workers can observe the new owner list.
prepareCities(islands);
if (generation == 22 || (generation == 23 || generation == 24)) { prepareTransit(islands); prepareUndergroundHall22(islands); }
if (generation == 22 || (generation == 23 || generation == 24 || generation == 25)) { prepareTransit(islands); prepareUndergroundHall22(islands); }
prepareMineral23(islands);
prepareRuins(islands);
prepareHeritage(islands);
@@ -248,7 +266,7 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
source().setCityPlans(java.util.Map.of());
return;
}
if ((generation == 16 || generation == 17 || generation == 19 || generation == 20 || generation == 21 || generation == 22 || (generation == 23 || generation == 24)) && !cityStructuresEnabled) {
if ((generation == 16 || generation == 17 || generation == 19 || generation == 20 || generation == 21 || generation == 22 || (generation == 23 || generation == 24 || generation == 25)) && !cityStructuresEnabled) {
cityProtection16 = fr.koka.sanctuary.worldgen.city.LostCityProtection16.EMPTY;
activeCityPlans = java.util.Map.of();
source().setCityPlans(java.util.Map.of());
@@ -264,7 +282,7 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
}
cityProtection15 = protection.build();
}
if (generation == 16 || generation == 17 || generation == 19 || generation == 20 || generation == 21 || generation == 22 || (generation == 23 || generation == 24)) {
if (generation == 16 || generation == 17 || generation == 19 || generation == 20 || generation == 21 || generation == 22 || (generation == 23 || generation == 24 || generation == 25)) {
var protection = new fr.koka.sanctuary.worldgen.city.LostCityProtection16.Builder();
for (var island : islands) {
protection.add(published.get(island.id()), undergroundPlan16(island));
@@ -279,13 +297,13 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
source().setCityPlans(published);
}
private void prepareRuins(List<ExpansionIsland> islands) {
if ((generation != 17 && generation != 19 && generation != 20 && generation != 21 && generation != 22 && (generation != 23 && generation != 24)) || !experimentalStructures || !cityStructuresEnabled || ruinsPrepared) return;
if ((generation != 17 && generation != 19 && generation != 20 && generation != 21 && generation != 22 && (generation != 23 && generation != 24 && generation != 25)) || !experimentalStructures || !cityStructuresEnabled || ruinsPrepared) return;
var root = islands.stream().filter(ExpansionIsland::origin).findFirst().orElseThrow();
var terrain = context(root);
var sampled = new fr.koka.sanctuary.worldgen.ruins.RuinsTerrain17(root, terrain.generator(), terrain.random(),
heightAccessor, pos -> protectsCore22(pos) || protectsDecoration(pos) || cityProtection16.contains(pos.getX(), pos.getY(), pos.getZ()));
long start = System.nanoTime();
var plan = (generation == 23 || generation == 24)
var plan = (generation == 23 || generation == 24 || generation == 25)
? fr.koka.sanctuary.worldgen.ruins.RuinsPlanner23.plan(root.seed(), root.centerX(), root.centerZ(), root.diameter(), sampled)
: fr.koka.sanctuary.worldgen.ruins.RuinsPlanner17.plan(root.seed(), root.centerX(), root.centerZ(), root.diameter(), sampled);
ruinsPlan17 = plan;
@@ -294,16 +312,16 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
root.seed(), plan.networks().size(), plan.samples(), sampled.columnsUsed(), (System.nanoTime() - start) / 1_000_000);
}
private void prepareHeritage(List<ExpansionIsland> islands) {
if ((generation != 19 && generation != 20 && generation != 21 && generation != 22 && (generation != 23 && generation != 24)) || !experimentalStructures || !cityStructuresEnabled || heritagePrepared) return;
if ((generation != 19 && generation != 20 && generation != 21 && generation != 22 && (generation != 23 && generation != 24 && generation != 25)) || !experimentalStructures || !cityStructuresEnabled || heritagePrepared) return;
var root = islands.stream().filter(ExpansionIsland::origin).findFirst().orElseThrow();
var terrain = context(root);
if (generation == 21 || generation == 22 || (generation == 23 || generation == 24)) {
if (generation == 21 || generation == 22 || (generation == 23 || generation == 24 || generation == 25)) {
var sampled = new fr.koka.sanctuary.worldgen.heritage.HeritageTerrain21(root, terrain.generator(), terrain.random(),
heightAccessor, pos -> protectsCore22(pos) || protectsDecoration(pos) || cityProtection16.contains(pos.getX(), pos.getY(), pos.getZ())
|| ruinsPlan17.protects(pos.getX(), pos.getY(), pos.getZ()));
long start = System.nanoTime();
sampled.discoverLandmarks(lostCityPlan(root), ruinsPlan17);
var plan = (generation == 23 || generation == 24)
var plan = (generation == 23 || generation == 24 || generation == 25)
? fr.koka.sanctuary.worldgen.heritage.HeritagePlanner23.plan(root.seed(), root.centerX(), root.centerZ(), root.diameter(), sampled)
: generation == 22
? fr.koka.sanctuary.worldgen.heritage.HeritagePlanner22.plan(root.seed(), root.centerX(), root.centerZ(), root.diameter(), sampled)
@@ -328,7 +346,7 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
(System.nanoTime() - start) / 1_000_000, plan.diagnostics());
}
private void prepareTransit(List<ExpansionIsland> islands) {
if ((generation != 21 && generation != 22 && (generation != 23 && generation != 24)) || generation == 21 && !experimentalStructures || !cityStructuresEnabled || transitPrepared) return;
if ((generation != 21 && generation != 22 && (generation != 23 && generation != 24 && generation != 25)) || generation == 21 && !experimentalStructures || !cityStructuresEnabled || transitPrepared) return;
var root = islands.stream().filter(ExpansionIsland::origin).findFirst().orElseThrow();
var terrain = context(root);
var sampled = new fr.koka.sanctuary.worldgen.transit.TransitTerrain21(root, terrain.generator(), terrain.random(),
@@ -339,7 +357,7 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
? fr.koka.sanctuary.worldgen.transit.TransitPlanner22.plan(root.seed(), root.centerX(), root.centerZ(), root.diameter(), sampled)
: fr.koka.sanctuary.worldgen.transit.TransitPlanner21.plan(root.seed(), root.centerX(), root.centerZ(), root.diameter(), sampled);
int directColumns = 0;
if ((generation == 22 || (generation == 23 || generation == 24)) && transitPlan21.isEmpty()) {
if ((generation == 22 || (generation == 23 || generation == 24 || generation == 25)) && transitPlan21.isEmpty()) {
directColumns = sampled.columnsUsed();
sampled = new fr.koka.sanctuary.worldgen.transit.TransitTerrain21(root, terrain.generator(), terrain.random(),
heightAccessor, pos -> protectsDecoration(pos) || cityProtection16.contains(pos.getX(), pos.getY(), pos.getZ()));
@@ -350,12 +368,12 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
transitPlan21.modules().size(), transitPlan21.samples(), directColumns + sampled.columnsUsed(), (System.nanoTime() - start) / 1_000_000, transitPlan21.diagnostic());
}
private boolean protectsCore22(net.minecraft.core.BlockPos pos) {
return (generation == 22 || (generation == 23 || generation == 24)) && (transitPlan21.protects(pos.getX(), pos.getY(), pos.getZ())
return (generation == 22 || (generation == 23 || generation == 24 || generation == 25)) && (transitPlan21.protects(pos.getX(), pos.getY(), pos.getZ())
|| undergroundHallProtection22.contains(pos.getX(), pos.getY(), pos.getZ())
|| (generation == 23 || generation == 24) && mineralSanctuary23.protects(pos.getX(), pos.getY(), pos.getZ()));
|| (generation == 23 || generation == 24 || generation == 25) && mineralSanctuary23.protects(pos.getX(), pos.getY(), pos.getZ()));
}
private void prepareUndergroundHall22(List<ExpansionIsland> islands) {
if ((generation != 22 && (generation != 23 && generation != 24)) || !cityStructuresEnabled || undergroundHallPrepared22) return;
if ((generation != 22 && (generation != 23 && generation != 24 && generation != 25)) || !cityStructuresEnabled || undergroundHallPrepared22) return;
var root = islands.stream().filter(ExpansionIsland::origin).findFirst().orElseThrow();
if (undergroundPlan16(root).networks().stream().flatMap(n -> n.modules().stream())
.anyMatch(m -> m.kind() == fr.koka.sanctuary.worldgen.city.LostCityUnderground16.Kind.EXPANSION_HALL)) {
@@ -372,7 +390,7 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
@Override public boolean protectedAt(int x, int y, int z) { return sampled.protectedAt(x, y, z); }
@Override public boolean exteriorAir(int x, int y, int z, int dx, int dz) { return sampled.exteriorAir(x, y, z, dx, dz); }
};
undergroundHall22 = (generation == 23 || generation == 24)
undergroundHall22 = (generation == 23 || generation == 24 || generation == 25)
? fr.koka.sanctuary.worldgen.city.LostCityUnderground23.plan(root.seed(), root.centerX(), root.centerZ(), root.diameter(), hallTerrain)
: fr.koka.sanctuary.worldgen.city.LostCityUnderground22.plan(root.seed(), root.centerX(), root.centerZ(), root.diameter(), hallTerrain);
var protection = new fr.koka.sanctuary.worldgen.city.LostCityProtection16.Builder();
@@ -382,7 +400,7 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
undergroundHall22.networks().size(), undergroundHall22.samples(), sampled.columnsUsed(), (System.nanoTime() - start) / 1_000_000);
}
private void prepareMineral23(List<ExpansionIsland> islands) {
if ((generation != 23 && generation != 24) || !experimentalStructures || !cityStructuresEnabled || mineralPrepared23) return;
if ((generation != 23 && generation != 24 && generation != 25) || !experimentalStructures || !cityStructuresEnabled || mineralPrepared23) return;
var root = islands.stream().filter(ExpansionIsland::origin).findFirst().orElseThrow();
var terrain = context(root);
var sampled = new fr.koka.sanctuary.worldgen.city.LostCityTerrain21(root, terrain.generator(), terrain.random(), heightAccessor,
@@ -420,13 +438,13 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
}
}
public fr.koka.sanctuary.worldgen.city.LostCityPlan lostCityPlan(ExpansionIsland island) {
if ((generation == 23 || generation == 24) && island.origin()) return fr.koka.sanctuary.worldgen.city.LostCityPlan.EMPTY;
if ((generation == 23 || generation == 24 || generation == 25) && island.origin()) return fr.koka.sanctuary.worldgen.city.LostCityPlan.EMPTY;
if (!ExpansionIsland.spatialGeneration(generation) || island.generation() != generation || (!island.origin() && !island.climate().equals("lost_city")))
return fr.koka.sanctuary.worldgen.city.LostCityPlan.EMPTY;
return cityPlans.computeIfAbsent(cityIdentity(island), ignored -> {
long start = System.nanoTime();
var terrain = context(island);
if (generation == 21 || generation == 22 || (generation == 23 || generation == 24)) {
if (generation == 21 || generation == 22 || (generation == 23 || generation == 24 || generation == 25)) {
var sampled = new fr.koka.sanctuary.worldgen.city.LostCityTerrain21(island, terrain.generator(), terrain.random(),
heightAccessor, pos -> island.origin() && protectsDecoration(pos));
var placement = generation >= 22
@@ -531,13 +549,14 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
});
}
public boolean protectsCityDecoration(net.minecraft.core.BlockPos pos) {
if (generation == 24 && aerialPlan24.protects(pos)) return true;
if ((generation == 23 || generation == 24) && mineralSanctuary23.protects(pos.getX(), pos.getY(), pos.getZ())) return true;
if (generation == 16 || generation == 17 || generation == 19 || generation == 20 || generation == 21 || generation == 22 || (generation == 23 || generation == 24)) return cityProtection16.contains(pos.getX(), pos.getY(), pos.getZ())
|| (generation == 17 || generation == 19 || generation == 20 || generation == 21 || generation == 22 || (generation == 23 || generation == 24)) && ruinsPlan17.protects(pos.getX(), pos.getY(), pos.getZ())
|| (generation == 19 || generation == 20 || generation == 21 || generation == 22 || (generation == 23 || generation == 24)) && heritagePlan19.protects(pos.getX(), pos.getY(), pos.getZ())
|| (generation == 21 || generation == 22 || (generation == 23 || generation == 24)) && transitPlan21.protects(pos.getX(), pos.getY(), pos.getZ())
|| (generation == 22 || (generation == 23 || generation == 24)) && undergroundHallProtection22.contains(pos.getX(), pos.getY(), pos.getZ());
if (generation == 25 && fr.koka.sanctuary.worldgen.underground25.UndergroundStructures25.plan(this).protects(pos)) return true;
if ((generation == 24 || generation == 25) && aerialPlan24.protects(pos)) return true;
if ((generation == 23 || generation == 24 || generation == 25) && mineralSanctuary23.protects(pos.getX(), pos.getY(), pos.getZ())) return true;
if (generation == 16 || generation == 17 || generation == 19 || generation == 20 || generation == 21 || generation == 22 || (generation == 23 || generation == 24 || generation == 25)) return cityProtection16.contains(pos.getX(), pos.getY(), pos.getZ())
|| (generation == 17 || generation == 19 || generation == 20 || generation == 21 || generation == 22 || (generation == 23 || generation == 24 || generation == 25)) && ruinsPlan17.protects(pos.getX(), pos.getY(), pos.getZ())
|| (generation == 19 || generation == 20 || generation == 21 || generation == 22 || (generation == 23 || generation == 24 || generation == 25)) && heritagePlan19.protects(pos.getX(), pos.getY(), pos.getZ())
|| (generation == 21 || generation == 22 || (generation == 23 || generation == 24 || generation == 25)) && transitPlan21.protects(pos.getX(), pos.getY(), pos.getZ())
|| (generation == 22 || (generation == 23 || generation == 24 || generation == 25)) && undergroundHallProtection22.contains(pos.getX(), pos.getY(), pos.getZ());
if (generation == 15) return cityProtection15.contains(pos.getX(), pos.getY(), pos.getZ());
if (generation != 14) return false;
var island = islandAt(pos.getX(), pos.getZ());
@@ -559,20 +578,20 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
return citySupports.getOrDefault(cityIdentity(island), List.of());
}
public boolean intersectsCityConstruction16(int minX, int minY, int minZ, int maxX, int maxY, int maxZ) {
return (generation == 16 || generation == 17 || generation == 19 || generation == 20 || generation == 21 || generation == 22 || (generation == 23 || generation == 24)) && cityProtection16.intersects(minX, minY, minZ, maxX, maxY, maxZ);
return (generation == 16 || generation == 17 || generation == 19 || generation == 20 || generation == 21 || generation == 22 || (generation == 23 || generation == 24 || generation == 25)) && cityProtection16.intersects(minX, minY, minZ, maxX, maxY, maxZ);
}
public fr.koka.sanctuary.worldgen.city.LostCityUnderground16.Plan undergroundPlan16(ExpansionIsland island) {
if (generation != 16 && generation != 17 && generation != 19 && generation != 20 && generation != 21 && generation != 22 && (generation != 23 && generation != 24)) return new fr.koka.sanctuary.worldgen.city.LostCityUnderground16.Plan(List.of(), 0);
if (generation != 16 && generation != 17 && generation != 19 && generation != 20 && generation != 21 && generation != 22 && (generation != 23 && generation != 24 && generation != 25)) return new fr.koka.sanctuary.worldgen.city.LostCityUnderground16.Plan(List.of(), 0);
lostCityPlan(island);
return undergroundPlans16.getOrDefault(cityIdentity(island), new fr.koka.sanctuary.worldgen.city.LostCityUnderground16.Plan(List.of(), 0));
}
public List<LostCityPlanner16.Support> citySupports16(ExpansionIsland island) {
if (generation != 16 && generation != 17 && generation != 19 && generation != 20 && generation != 21 && generation != 22 && (generation != 23 && generation != 24)) return List.of();
if (generation != 16 && generation != 17 && generation != 19 && generation != 20 && generation != 21 && generation != 22 && (generation != 23 && generation != 24 && generation != 25)) return List.of();
lostCityPlan(island);
return citySupports16.getOrDefault(cityIdentity(island), List.of());
}
public LostCityPlanner16.Fallback cityReservation16(ExpansionIsland island) {
if (generation != 16 && generation != 17 && generation != 19 && generation != 20 && generation != 21 && generation != 22 && (generation != 23 && generation != 24)) return null;
if (generation != 16 && generation != 17 && generation != 19 && generation != 20 && generation != 21 && generation != 22 && (generation != 23 && generation != 24 && generation != 25)) return null;
lostCityPlan(island);
return cityReservations16.get(cityIdentity(island));
}
@@ -582,16 +601,16 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
@Override public com.mojang.datafixers.util.Pair<net.minecraft.core.BlockPos, net.minecraft.core.Holder<net.minecraft.world.level.levelgen.structure.Structure>> findNearestMapStructure(
ServerLevel level, net.minecraft.core.HolderSet<net.minecraft.world.level.levelgen.structure.Structure> structures,
net.minecraft.core.BlockPos origin, int radius, boolean skipReferenced) {
if ((generation == 23 || generation == 24)) return fr.koka.sanctuary.expansion.ExpansionStructures23.findNearest(level, structures, origin, radius, skipReferenced);
if ((generation == 23 || generation == 24 || generation == 25)) return fr.koka.sanctuary.expansion.ExpansionStructures23.findNearest(level, structures, origin, radius, skipReferenced);
if (generation == 22 && structures.size() == 1 && structures.get(0).is(fr.koka.sanctuary.worldgen.city.LostCityUndergroundStructures22.KEY))
return fr.koka.sanctuary.worldgen.city.LostCityUndergroundStructures22.findNearest(level, structures, origin, radius, skipReferenced);
if (structures.size() == 1 && structures.get(0).is(fr.koka.sanctuary.worldgen.city.LostCityStructures.KEY))
return fr.koka.sanctuary.worldgen.city.LostCityStructures.findNearest(level, structures, origin, radius, skipReferenced);
if ((generation == 17 || generation == 19 || generation == 20 || generation == 21 || generation == 22 || (generation == 23 || generation == 24)) && structures.size() == 1 && structures.get(0).is(fr.koka.sanctuary.worldgen.ruins.RuinsStructures17.KEY))
if ((generation == 17 || generation == 19 || generation == 20 || generation == 21 || generation == 22 || (generation == 23 || generation == 24 || generation == 25)) && structures.size() == 1 && structures.get(0).is(fr.koka.sanctuary.worldgen.ruins.RuinsStructures17.KEY))
return fr.koka.sanctuary.worldgen.ruins.RuinsStructures17.findNearest(level, structures, origin, radius, skipReferenced);
if ((generation == 19 || generation == 20 || generation == 21 || generation == 22 || (generation == 23 || generation == 24)) && structures.size() == 1 && structures.get(0).is(fr.koka.sanctuary.worldgen.heritage.HeritageStructures19.KEY))
if ((generation == 19 || generation == 20 || generation == 21 || generation == 22 || (generation == 23 || generation == 24 || generation == 25)) && structures.size() == 1 && structures.get(0).is(fr.koka.sanctuary.worldgen.heritage.HeritageStructures19.KEY))
return fr.koka.sanctuary.worldgen.heritage.HeritageStructures19.findNearest(level, structures, origin, radius, skipReferenced);
if ((generation == 21 || generation == 22 || (generation == 23 || generation == 24)) && structures.size() == 1 && structures.get(0).is(fr.koka.sanctuary.worldgen.transit.TransitStructures21.KEY))
if ((generation == 21 || generation == 22 || (generation == 23 || generation == 24 || generation == 25)) && structures.size() == 1 && structures.get(0).is(fr.koka.sanctuary.worldgen.transit.TransitStructures21.KEY))
return fr.koka.sanctuary.worldgen.transit.TransitStructures21.findNearest(level, structures, origin, radius, skipReferenced);
return super.findNearestMapStructure(level, structures, origin, radius, skipReferenced);
}
@@ -604,10 +623,14 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
if (registries == null) throw new IllegalStateException("Sanctuary used before world attachment");
var settings = registries.lookupOrThrow(Registries.NOISE_SETTINGS)
.getOrThrow(net.minecraft.resources.ResourceKey.create(Registries.NOISE_SETTINGS,
SanctuaryMod.id("unified_" + capacity().players())));
SanctuaryMod.id((generation == 25 ? "unified_25_" : "unified_") + capacity().players())));
var delegate = new NoiseBasedChunkGenerator(source(), settings);
var random = RandomState.create(registries.lookupOrThrow(Registries.NOISE), island.seed(), settings.value());
originContext = new Context(delegate, random);
if (generation == 25) {
Terrain25Integration.bind(delegate, this);
source().setTerrain25Plan(Terrain25Runtime.view(delegate, random).plan());
}
}
return originContext;
}
@@ -620,7 +643,8 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
@Override protected void afterTerrain(ExpansionIsland island, ChunkAccess chunk) {
// Root water/lava are already applied by the Population hook on the named delegate.
if (!island.origin()) super.afterTerrain(island, chunk);
if ((generation == 16 || generation == 17 || generation == 19 || generation == 20 || generation == 21 || generation == 22 || (generation == 23 || generation == 24)) && cityStructuresEnabled) {
if (generation == 25 && island.origin()) Terrain25Integration.apply(this, chunk);
if ((generation == 16 || generation == 17 || generation == 19 || generation == 20 || generation == 21 || generation == 22 || (generation == 23 || generation == 24 || generation == 25)) && cityStructuresEnabled) {
var reserved = cityReservations16.get(cityIdentity(island));
if (reserved != null) {
var mutable = new net.minecraft.core.BlockPos.MutableBlockPos();
@@ -33,6 +33,19 @@ public final class SanctuaryDecorationProtection {
for (var cell : region.lava().cells()) result.cube(cell.x(), cell.y(), cell.z(), 2);
for (var support : region.lava().supports()) result.cube(support.x(), support.y(), support.z(), 2);
}
if (Terrain25Runtime.enabled(binding.generator())) {
for (var lake : Terrain25Runtime.admittedLakes(binding.generator(), binding.random())) {
int rx = (int) Math.ceil(lake.rx()), rz = (int) Math.ceil(lake.rz());
result.box((int) Math.floor(lake.x()) - rx - 2, lake.floorY() - 3, (int) Math.floor(lake.z()) - rz - 2,
(int) Math.ceil(lake.x()) + rx + 2, lake.fluidY() + 2, (int) Math.ceil(lake.z()) + rz + 2);
}
var capacity = IslandCapacity.forGenerator(binding.generator());
var lo = capacity.regionAt(result.minX - 80, result.minZ - 80);
var hi = capacity.regionAt(result.minX + 95, result.minZ + 95);
for (int rx = lo.x(); rx <= hi.x(); rx++) for (int rz = lo.z(); rz <= hi.z(); rz++)
for (var fall : Terrain25Runtime.falls(binding.generator(), binding.random(), new IslandCapacity.Region(rx, rz)))
result.box(fall.minX() - 2, 0, fall.minZ() - 2, fall.maxX() + 2, fall.waterY() + 13, fall.maxZ() + 2);
}
return result;
}
public boolean protects(BlockPos pos) {
@@ -0,0 +1,13 @@
package fr.koka.sanctuary.worldgen;
import fr.koka.sanctuary.SanctuaryMod;
import java.util.List;
import net.minecraft.core.registries.Registries;
import net.minecraft.resources.ResourceKey;
import net.minecraft.world.level.biome.Biome;
public final class Terrain25Biomes {
public static final List<String> NAMES = List.of("oak_forest","birch_forest","clearing","dry_woodland","rocky_heath",
"dark_grove","bamboo_grove","sulfur_depths","lush_caves","dripstone_caves","mushroom_refuge","swamp");
public static final ResourceKey<Biome> DARK_GROVE = key("dark_grove"), BAMBOO_GROVE = key("bamboo_grove"), MUSHROOM_REFUGE = key("mushroom_refuge");
public static ResourceKey<Biome> key(String name) { return ResourceKey.create(Registries.BIOME,SanctuaryMod.id("terrain25_"+name)); }
private Terrain25Biomes() {}
}
@@ -0,0 +1,18 @@
package fr.koka.sanctuary.worldgen;
/** Five-block mineral rind sampled against the intact rock, not against a later excavated room. */
public final class Terrain25Coating {
public static final int THICKNESS = 5;
public static final int MIN_Y = 225;
@FunctionalInterface public interface Solid { boolean at(int x, int y, int z); }
private Terrain25Coating() {}
public static boolean calcite(int x, int y, int z, double lift, Solid intact) {
if (y <= MIN_Y || lift <= 26) return false;
// Retain the existing lower bands, but never extend them through a mountain's core.
if (y <= 280 && (Math.floorMod(y, 11) < 3 || Math.floorMod(y, 11) > 9)) return false;
return !intact.at(x, y + THICKNESS, z) || !intact.at(x, y - THICKNESS, z)
|| !intact.at(x - THICKNESS, y, z) || !intact.at(x + THICKNESS, y, z)
|| !intact.at(x, y, z - THICKNESS) || !intact.at(x, y, z + THICKNESS);
}
}
@@ -0,0 +1,84 @@
package fr.koka.sanctuary.worldgen;
import com.mojang.serialization.MapCodec;
import com.mojang.serialization.codecs.RecordCodecBuilder;
import net.minecraft.util.Interval;
import net.minecraft.world.level.levelgen.densityfunction.*;
/** Alpha.25 relief and finite cave networks; earlier density codecs remain unchanged. */
public record Terrain25Density(DensityFunction terrain, DensityFunction distortion,
DensityFunction sculpt, DensityFunction detail, DensityFunction underside,
DensityFunction riftWarp, DensityFunction riftDetail, IslandCapacity capacity) implements DensityFunction {
public static final MapCodec<Terrain25Density> CODEC = RecordCodecBuilder.mapCodec(instance -> instance.group(
DensityFunction.CODEC.fieldOf("terrain").forGetter(Terrain25Density::terrain),
DensityFunction.CODEC.fieldOf("distortion").forGetter(Terrain25Density::distortion),
DensityFunction.CODEC.fieldOf("sculpt").forGetter(Terrain25Density::sculpt),
DensityFunction.CODEC.fieldOf("detail").forGetter(Terrain25Density::detail),
DensityFunction.CODEC.fieldOf("underside").forGetter(Terrain25Density::underside),
DensityFunction.CODEC.fieldOf("rift_warp").forGetter(Terrain25Density::riftWarp),
DensityFunction.CODEC.fieldOf("rift_detail").forGetter(Terrain25Density::riftDetail),
IslandCapacity.CODEC.optionalFieldOf("players", IslandCapacity.TWENTY).forGetter(Terrain25Density::capacity)
).apply(instance, Terrain25Density::new));
public interface View extends DensitySampler {
Terrain25Shape.Plan plan();
float natural(SamplerContext context, int x, int y, int z);
}
@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);
long seed = context.createEndIslandRandom().nextLong();
var proposed = Terrain25Shape.create(seed, capacity.radius());
var columnLifts = ThreadLocal.withInitial(() -> new java.util.LinkedHashMap<Long, Double>(256, .75f, true) {
@Override protected boolean removeEldestEntry(java.util.Map.Entry<Long, Double> eldest) { return size() > 4096; }
});
DensitySampler intact = new DensitySampler() {
@Override public float sampleValue(SamplerContext sample, int x, int y, int z) {
if (Math.hypot((double) x, z) >= capacity.terrainLimit() || y <= 0 || y > Terrain25Shape.MAX_TERRAIN_Y) return -1;
long columnKey = ((long)x << 32) ^ (z & 0xffffffffL);
double lift = columnLifts.get().computeIfAbsent(columnKey, ignored -> proposed.lift(x,z));
int mappedY = proposed.naturalY(y, lift), localY = mappedY - 64;
float result = PopulationIslandShape.raisedDensity(x, mappedY, z, raw.sampleValue(sample, x, localY, z),
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);
// A broad fade rounds each pinnacle before the immutable build-height margin.
return (float) Math.max(-1, result - Math.max(0, y - 348) / 10.0);
}
@Override public void sampleVolume(SamplerContext sample,DensityBuffer buffer,DensityVolume volume) {
DensitySampler.sampleVolumeNaive(sample,buffer,volume,this);
}
};
var plan = Terrain25Shape.fit(proposed,(x,y,z)->intact.sampleValue(SamplerContext.EMPTY_UNCACHED,x,y,z));
return new View() {
@Override public Terrain25Shape.Plan plan() { return plan; }
@Override public float natural(SamplerContext sample,int x,int y,int z) { return intact.sampleValue(sample,x,y,z); }
@Override public float sampleValue(SamplerContext sample, int x, int y, int z) {
float natural = natural(sample, x, y, z);
if (natural <= 0) return natural;
double cave = plan.excavation(x, y, z);
// Short, slanting fissures; their axes drift with altitude and close at both ends.
double fracture = -1;
boolean retainBasin = plan.retainingBasin(x,y,z) != null;
if (!retainBasin && y > 72 && y < 224 && Math.hypot(x, z) > 72) {
double band = Math.sin((y - 72) / 152.0 * Math.PI);
double a = warp.sampleValue(sample, x + (y - 140) / 2, y, z - (y - 140) / 3);
double b = fractureDetail.sampleValue(sample, x, y, z);
fracture = (.025 - Math.abs(a + b * .16)) * 8 * band - (1 - band) * .22;
}
return (float) Math.min(natural, -Math.max(cave, fracture));
}
@Override public void sampleVolume(SamplerContext sample, DensityBuffer buffer, DensityVolume volume) {
DensitySampler.sampleVolumeNaive(sample, buffer, volume, this);
}
};
}
@Override public DensityFunction rewriteChildren(DfRewriteRule rule) {
return new Terrain25Density(rule.rewrite(terrain), rule.rewrite(distortion), rule.rewrite(sculpt),
rule.rewrite(detail), rule.rewrite(underside), rule.rewrite(riftWarp), rule.rewrite(riftDetail), capacity);
}
@Override public Interval range() { return Interval.of(-1, 1); }
@Override public int domainAxes() { return ALL_AXES; }
@Override public MapCodec<Terrain25Density> codec() { return CODEC; }
}
@@ -0,0 +1,299 @@
package fr.koka.sanctuary.worldgen;
import com.mojang.serialization.MapCodec;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.Map;
import java.util.Optional;
import java.util.WeakHashMap;
import net.minecraft.core.BlockPos;
import net.minecraft.core.registries.Registries;
import net.minecraft.data.worldgen.features.TreeFeatures;
import net.minecraft.data.worldgen.features.VegetationFeatures;
import net.minecraft.resources.ResourceKey;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.tags.BlockTags;
import net.minecraft.util.RandomSource;
import net.minecraft.world.level.WorldGenLevel;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.VegetationBlock;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.chunk.ChunkGenerator;
import net.minecraft.world.level.levelgen.NoiseBasedChunkGenerator;
import net.minecraft.world.level.levelgen.Heightmap;
import net.minecraft.world.level.levelgen.RandomState;
import net.minecraft.world.level.levelgen.feature.Feature;
import net.minecraft.world.level.levelgen.feature.AbstractHugeMushroomFeature;
/** Alpha.10: sheltered vanilla mushroom trees and dark oak on natural lower floors,
* plus one globally selected remarkable tree.
* Placement runs only in new generation. No world journal or mutable placement counter is retained. */
public final class Terrain25GrovesFeature implements Feature {
public static final MapCodec<Terrain25GrovesFeature> CODEC = MapCodec.unit(Terrain25GrovesFeature::new);
private static final Map<RandomState, Optional<RemarkableTree>> REMARKABLE =
Collections.synchronizedMap(new WeakHashMap<>());
public record RemarkableTree(BlockPos base, String featureId, int radius, int height) {
public RemarkableTree { base = base.immutable(); }
}
private record Species(ResourceKey<Feature> key, int width, int radius, int height) {}
private record Candidate(int x, int z, long rank) {}
@FunctionalInterface
private interface Protection {
boolean intersects(BlockPos base, int radius, int height, int rootDepth);
}
private static final Species CHERRY = new Species(TreeFeatures.CHERRY, 1, 9, 15);
private static final Species[] SPECIES = {CHERRY, CHERRY, CHERRY, CHERRY,
new Species(TreeFeatures.SPRUCE, 1, 5, 15), new Species(TreeFeatures.JUNGLE_TREE_NO_VINE, 1, 5, 14),
new Species(TreeFeatures.ACACIA, 1, 7, 14), new Species(TreeFeatures.MANGROVE, 1, 10, 20),
new Species(TreeFeatures.PALE_OAK_BONEMEAL, 2, 6, 12)};
@Override public MapCodec<Terrain25GrovesFeature> codec() { return CODEC; }
public static Optional<RemarkableTree> remarkablePlan(ServerLevel level) {
var binding = PopulationGenerationContext.of(level);
return binding == null ? Optional.empty() : remarkablePlan(binding.generator(), binding.random());
}
public static Optional<RemarkableTree> remarkablePlan(NoiseBasedChunkGenerator noise, RandomState state) {
if (!PopulationHydrologyRuntime.enabled(noise)) return Optional.empty();
return REMARKABLE.computeIfAbsent(state, ignored -> {
var density = noise.generatorSettings().value().noiseRouter().finalDensity();
WoodlandGroveGeometry.Sampler sampler = (x, y, z) -> state.sampleBlockValueUncached(density, x, y, z);
Protection protection = (base, radius, height, rootDepth) ->
Terrain25Runtime.protects(noise, state, base, radius, height, rootDepth);
Species species = SPECIES[Math.floorMod(WoodlandGroveGeometry.seed(state.seed(), 0, 0, 0, 0x70EEL), SPECIES.length)];
var candidates = new ArrayList<Candidate>();
// One global candidate stays in the starter centre; ordinary groves use the full capacity.
for (int x = -152; x <= 152; x += 16) for (int z = -152; z <= 152; z += 16) {
if (Math.hypot(x, z) <= 168) candidates.add(new Candidate(x, z,
WoodlandGroveGeometry.seed(state.seed(), x, 0, z, 0x71AFL)));
}
candidates.sort(Comparator.comparingLong(Candidate::rank).thenComparingInt(Candidate::x).thenComparingInt(Candidate::z));
for (Candidate candidate : candidates) {
int bed = WoodlandGroveGeometry.highestSurface(sampler, candidate.x(), candidate.z());
if (!WoodlandGroveGeometry.naturalFootprint(sampler, candidate.x(), bed, candidate.z(), species.width())) continue;
BlockPos base = new BlockPos(candidate.x(), bed + 1, candidate.z());
if (base.getY() + species.height() >= 364 || blocked(protection, base, species.radius() + 1, species.height(), rootDepth(species))) continue;
if (!WoodlandGroveGeometry.treeRoom(sampler, base.getX(), base.getY(), base.getZ(),
species.width(), species.radius(), species.height())) continue;
return Optional.of(new RemarkableTree(base, species.key().identifier().toString(), species.radius(), species.height()));
}
return Optional.empty();
});
}
@Override
public boolean place(WorldGenLevel level, ChunkGenerator generator, RandomSource ignored, BlockPos origin) {
var binding = PopulationGenerationContext.of(generator, level.getLevel());
if (binding == null || !Terrain25Runtime.enabled(binding.generator())) return false;
var noise = binding.generator();
int minX = (origin.getX() >> 4) * 16, minZ = (origin.getZ() >> 4) * 16;
if (!PopulationHydrologyRuntime.capacity(noise).areaExists(minX >> 4, minZ >> 4)) return false;
var state = binding.random();
var density = noise.generatorSettings().value().noiseRouter().finalDensity();
WoodlandGroveGeometry.Sampler sampler = (x, y, z) -> state.sampleBlockValueUncached(density, x, y, z);
Protection protection = (base, radius, height, rootDepth) ->
Terrain25Runtime.protects(noise, state, base, radius, height, rootDepth);
boolean changed = false;
int[] heights = new int[256];
Arrays.fill(heights, Integer.MIN_VALUE);
var remarkable = remarkablePlan(noise, state);
if (remarkable.isPresent()) {
var tree = remarkable.get();
BlockPos base = tree.base();
if ((base.getX() >> 4) == (minX >> 4) && (base.getZ() >> 4) == (minZ >> 4)) {
for (Species species : SPECIES) if (species.key().identifier().toString().equals(tree.featureId())) {
changed |= tree(level, generator, sampler, state.seed(), base, species, protection, 0x723BL);
break;
}
}
}
// The scarce tall rooms usually go to dark oak first; one quarter of chunks
// give mushroom trees first choice. This changes priority, never attempt budgets
// or natural fit requirements, and uses a separate deterministic chunk seed.
boolean mushroomsFirst = Math.floorMod(WoodlandGroveGeometry.seed(state.seed(), minX, 0, minZ, 0x8503L), 4) == 0;
if (mushroomsFirst) changed |= mushroomTrees(level, generator, sampler, state.seed(), protection, minX, minZ, heights);
// Four spaced origins per chunk; the 2x2 trunk and full canopy still have to fit the cave.
for (int x = minX + 4; x < minX + 16; x += 8) for (int z = minZ + 4; z < minZ + 16; z += 8) {
int highest = localTop(level, sampler, protection, minX, minZ, x, z, heights), placed = 0;
for (int bed = highest - 8; bed >= 40; bed--) {
BlockPos base = new BlockPos(x, bed + 1, z);
if (!ground(level, base.below()) || !level.getBlockState(base).isAir()) continue;
var biome = level.getBiome(base);
boolean dark = biome.is(Terrain25Biomes.DARK_GROVE);
boolean bamboo = biome.is(Terrain25Biomes.BAMBOO_GROVE);
if (!dark && !bamboo) continue;
long placementSeed = WoodlandGroveGeometry.seed(state.seed(), x, bed, z, 0x730DL);
if (dark) {
if (tree(level, generator, sampler, state.seed(), base,
new Species(TreeFeatures.DARK_OAK, 2, 6, 12), protection, 0x730DL)) { changed = true; placed++; }
} else if (Math.floorMod(placementSeed, 8) == 0) {
if (tree(level, generator, sampler, state.seed(), base,
new Species(TreeFeatures.JUNGLE_TREE_NO_VINE, 1, 5, 14), protection, 0x735BL)) { changed = true; placed++; }
} else if (Math.floorMod(placementSeed, 3) == 0
&& WoodlandGroveGeometry.naturalFootprint(sampler, x, bed, z, 1)
&& WoodlandGroveGeometry.treeRoom(sampler, x, bed + 1, z, 1, 3, 16)
&& !blocked(protection, base, 4, 17) && actualRoom(level, base, 1, 3, 16)) {
setBlock(level, base.below(), Blocks.PODZOL.defaultBlockState());
changed |= level.registryAccess().lookupOrThrow(Registries.FEATURE).getOrThrow(VegetationFeatures.BAMBOO_NO_PODZOL)
.value().place(level, generator, RandomSource.create(placementSeed), base);
placed++;
}
if (placed >= 2) break;
}
}
if (!mushroomsFirst) changed |= mushroomTrees(level, generator, sampler, state.seed(), protection, minX, minZ, heights);
// Coherent ground patches: about 40% moss, 40% podzol, 20% mycelium.
// Small mushrooms are accents (one opportunity per hundred columns), not the canopy.
for (int x = minX; x < minX + 16; x++) for (int z = minZ; z < minZ + 16; z++) {
int highest = localTop(level, sampler, protection, minX, minZ, x, z, heights), floors = 0;
long detail = WoodlandGroveGeometry.seed(state.seed(), x, 0, z, 0x843EL);
boolean smallMushroomPlaced = false;
for (int bed = highest - 8; bed >= 32; bed--) {
BlockPos floor = new BlockPos(x, bed, z), plant = floor.above();
if (!ground(level, floor) || !level.getBlockState(plant).isAir()) continue;
var biome = level.getBiome(plant);
if (!biome.is(Terrain25Biomes.DARK_GROVE) && !biome.is(Terrain25Biomes.BAMBOO_GROVE)) continue;
if (!WoodlandGroveGeometry.naturalFootprint(sampler, x, bed, z, 1) || blocked(protection, plant, 1, 2)) continue;
long patch = WoodlandGroveGeometry.seed(state.seed(), Math.floorDiv(x, 7), 0, Math.floorDiv(z, 7), 0x742DL);
int soilKind = Math.floorMod(patch, 5);
BlockState soil = (soilKind < 2 ? Blocks.MOSS_BLOCK : soilKind < 4 ? Blocks.PODZOL : Blocks.MYCELIUM).defaultBlockState();
setBlock(level, floor, soil);
if (!smallMushroomPlaced && Math.floorMod(detail, 100) == 0) {
BlockState mushroom = (Math.floorMod(detail >>> 8, 2) == 0 ? Blocks.BROWN_MUSHROOM : Blocks.RED_MUSHROOM).defaultBlockState();
if (mushroom.canSurvive(level, plant)) {
setBlock(level, plant, mushroom);
smallMushroomPlaced = true;
}
}
changed = true;
if (++floors >= 3) break;
}
}
return changed;
}
private boolean mushroomTrees(WorldGenLevel level, ChunkGenerator generator, WoodlandGroveGeometry.Sampler sampler,
long seed, Protection protection, int minX, int minZ, int[] heights) {
boolean changed = false;
RandomSource mushroomSites = RandomSource.create(WoodlandGroveGeometry.seed(seed, minX, 0, minZ, 0x8501L));
int mushroomAttempts = 1 + mushroomSites.nextInt(3);
for (int attempt = 0; attempt < mushroomAttempts; attempt++) {
int x = minX + 3 + mushroomSites.nextInt(10), z = minZ + 3 + mushroomSites.nextInt(10);
int highest = localTop(level, sampler, protection, minX, minZ, x, z, heights);
for (int bed = highest - 8; bed >= 32; bed--) {
BlockPos base = new BlockPos(x, bed + 1, z);
if (!ground(level, base.below()) || !level.getBlockState(base).isAir()
|| !level.getBiome(base).is(Terrain25Biomes.DARK_GROVE)) continue;
long mushroomSeed = WoodlandGroveGeometry.seed(seed, x, bed, z, 0x8502L + attempt);
var key = Math.floorMod(mushroomSeed, 2) == 0 ? TreeFeatures.HUGE_BROWN_MUSHROOM : TreeFeatures.HUGE_RED_MUSHROOM;
Feature feature = level.registryAccess().lookupOrThrow(Registries.FEATURE).getOrThrow(key).value();
if (!(feature instanceof AbstractHugeMushroomFeature mushroom)) break;
int height = mushroom.getTreeHeight(RandomSource.create(mushroomSeed));
int radius = mushroom.foliageRadius();
if (!mushroomRoom(level, sampler, protection, base, radius, height)) continue;
BlockState originalFloor = level.getBlockState(base.below());
setBlock(level, base.below(), (Math.floorMod(mushroomSeed, 5) == 0 ? Blocks.MYCELIUM : Blocks.PODZOL).defaultBlockState());
// Vanilla consumes its height draw first. Replaying the same random seed
// gives precisely the height whose natural envelope was checked above.
boolean placed = mushroom.place(level, generator, RandomSource.create(mushroomSeed), base);
if (!placed) setBlock(level, base.below(), originalFloor);
changed |= placed;
break;
}
}
return changed;
}
private boolean tree(WorldGenLevel level, ChunkGenerator generator, WoodlandGroveGeometry.Sampler sampler, long seed,
BlockPos base, Species species, Protection protection, long salt) {
if (base.getY() + species.height() >= 364 || blocked(protection, base, species.radius() + 1, species.height(), rootDepth(species))
|| !WoodlandGroveGeometry.naturalFootprint(sampler, base.getX(), base.getY() - 1, base.getZ(), species.width())
|| !actualRoom(level, base, species.width(), species.radius(), species.height())
|| !WoodlandGroveGeometry.treeRoom(sampler, base.getX(), base.getY(), base.getZ(), species.width(), species.radius(), species.height())) return false;
for (int dx = 0; dx < species.width(); dx++) for (int dz = 0; dz < species.width(); dz++) {
BlockPos floor = base.offset(dx, -1, dz);
if (!ground(level, floor)) return false;
}
for (int dx = 0; dx < species.width(); dx++) for (int dz = 0; dz < species.width(); dz++) {
setBlock(level, base.offset(dx, -1, dz), Blocks.PODZOL.defaultBlockState());
}
return level.registryAccess().lookupOrThrow(Registries.FEATURE).getOrThrow(species.key()).value()
.place(level, generator, RandomSource.create(WoodlandGroveGeometry.seed(seed, base.getX(), base.getY(), base.getZ(), salt)), base);
}
private static boolean actualRoom(WorldGenLevel level, BlockPos base, int width, int radius, int height) {
for (int y = 0; y <= height; y++) {
int from = y < 3 ? 0 : -radius, to = y < 3 ? width - 1 : radius;
for (int dx = from; dx <= to; dx++) for (int dz = from; dz <= to; dz++) {
BlockState current = level.getBlockState(base.offset(dx, y, dz));
if (!current.getFluidState().isEmpty() || !(current.isAir() || current.is(BlockTags.LEAVES)
|| current.getBlock() instanceof VegetationBlock)) return false;
}
}
return true;
}
private static boolean ground(WorldGenLevel level, BlockPos floor) {
BlockState state = level.getBlockState(floor);
if (state.is(Blocks.DEEPSLATE)) {
// Deep humid ledges can support organic soil. Check every tree foot rather
// than admitting this rock globally, including for the remarkable tree.
var biome = level.getBiome(floor.above());
return biome.is(Terrain25Biomes.DARK_GROVE) || biome.is(Terrain25Biomes.BAMBOO_GROVE);
}
return state.is(Blocks.STONE) || state.is(Blocks.DIRT) || state.is(Blocks.GRASS_BLOCK) || state.is(Blocks.PODZOL)
|| state.is(Blocks.MYCELIUM) || state.is(Blocks.ROOTED_DIRT) || state.is(Blocks.COARSE_DIRT)
|| state.is(Blocks.GRANITE) || state.is(Blocks.DIORITE) || state.is(Blocks.ANDESITE) || state.is(Blocks.TUFF)
|| state.is(Blocks.COBBLESTONE) || state.is(Blocks.MOSSY_COBBLESTONE) || state.is(Blocks.PACKED_MUD);
}
private static int localTop(WorldGenLevel level, WoodlandGroveGeometry.Sampler sampler, Protection protection,
int minX, int minZ, int x, int z, int[] heights) {
int index = (x - minX) * 16 + z - minZ;
if (heights[index] != Integer.MIN_VALUE) return heights[index];
// In an untouched column the heightmap bounds the original surface cheaply.
// A protected water/lava column may have been carved: recover its raw top
// without depending on the current hydrology plan's representation.
int ceiling = level.getHeight(Heightmap.Types.WORLD_SURFACE_WG, x, z) - 1;
if (protection.intersects(new BlockPos(x, level.getMinY(), z), 0, level.getHeight() - 1, 0))
ceiling = level.getMaxY();
return heights[index] = WoodlandGroveGeometry.highestSurfaceBelow(sampler, x, z, ceiling);
}
private static boolean mushroomRoom(WorldGenLevel level, WoodlandGroveGeometry.Sampler sampler, Protection protection,
BlockPos base, int radius, int height) {
if (blocked(protection, base, radius + 1, height + 1, 4)
|| !WoodlandGroveGeometry.naturalFootprint(sampler, base.getX(), base.getY() - 1, base.getZ(), 1)) return false;
for (int depth = 1; depth <= 3; depth++) {
BlockPos floor = base.below(depth);
if (!level.getBlockState(floor).isCollisionShapeFullBlock(level, floor)) return false;
}
// A complete conservative column around the cap handles both brown umbrellas
// and the lower rounded red cap. Neither natural rock nor existing logs move.
for (int y = 0; y <= height + 1; y++) for (int dx = -radius; dx <= radius; dx++) for (int dz = -radius; dz <= radius; dz++) {
BlockPos p = base.offset(dx, y, dz);
BlockState current = level.getBlockState(p);
if (sampler.sample(p.getX(), p.getY(), p.getZ()) > 0 || !current.getFluidState().isEmpty()
|| !(current.isAir() || current.is(BlockTags.LEAVES))) return false;
}
// Being below another surface is not enough: the same column needs an actual
// original rock ceiling above the whole mushroom, not an open hillside.
for (int y = base.getY() + height + 2; y <= level.getMaxY(); y++)
if (sampler.sample(base.getX(), y, base.getZ()) > 0) return true;
return false;
}
private static int rootDepth(Species species) { return species.key().equals(TreeFeatures.MANGROVE) ? 16 : 4; }
private static boolean blocked(Protection protection, BlockPos base, int radius, int height) {
return blocked(protection, base, radius, height, 4);
}
private static boolean blocked(Protection protection, BlockPos base, int radius, int height, int rootDepth) {
return protection.intersects(base, radius, height, rootDepth);
}
}
@@ -0,0 +1,57 @@
package fr.koka.sanctuary.worldgen;
import java.lang.ref.WeakReference;
import java.util.Map;
import java.util.WeakHashMap;
import fr.koka.sanctuary.worldgen.underground25.UndergroundPlan25;
import fr.koka.sanctuary.worldgen.underground25.UndergroundStructures25;
import net.minecraft.core.BlockPos;
import net.minecraft.world.level.NoiseColumn;
import net.minecraft.world.level.chunk.ChunkAccess;
import net.minecraft.world.level.levelgen.NoiseBasedChunkGenerator;
/** Makes accepted local floors visible both to native piece projection and terrain generation. */
public final class Terrain25Integration {
private static final Map<NoiseBasedChunkGenerator, WeakReference<SanctuaryChunkGenerator>> HOSTS = new WeakHashMap<>();
private Terrain25Integration() {}
public static synchronized void bind(NoiseBasedChunkGenerator delegate, SanctuaryChunkGenerator host) {
HOSTS.put(delegate, new WeakReference<>(host));
}
private static synchronized UndergroundPlan25 plan(NoiseBasedChunkGenerator delegate) {
var reference = HOSTS.get(delegate);
var host = reference == null ? null : reference.get();
return host == null ? UndergroundPlan25.EMPTY : UndergroundStructures25.plan(host);
}
public static void column(NoiseBasedChunkGenerator delegate, int x, int z, NoiseColumn column) {
var plan = plan(delegate);
if (plan.starts().isEmpty()) return;
for (var start : plan.starts()) for (var piece : start.start().getPieces())
if (piece instanceof fr.koka.sanctuary.worldgen.underground25.UndergroundPiece25 extra) {
var box = extra.getBoundingBox();
if (x < box.minX() || x > box.maxX() || z < box.minZ() || z > box.maxZ()) continue;
for (int y = box.minY(); y <= box.maxY(); y++) {
var state = extra.terrainOverride(x, y, z);
if (state != null) column.setBlock(y, state);
}
}
}
public static void apply(SanctuaryChunkGenerator host, ChunkAccess chunk) {
var plan = UndergroundStructures25.plan(host);
if (plan.starts().isEmpty()) return;
var at = chunk.getPos();
var box = new net.minecraft.world.level.levelgen.structure.BoundingBox(at.getMinBlockX(), 0, at.getMinBlockZ(), at.getMaxBlockX(), 383, at.getMaxBlockZ());
if (!plan.intersects(box)) return;
var pos = new BlockPos.MutableBlockPos();
for (var start : plan.starts()) for (var piece : start.start().getPieces())
if (piece instanceof fr.koka.sanctuary.worldgen.underground25.UndergroundPiece25 extra) {
var bounds = extra.getBoundingBox();
if (!bounds.intersects(box)) continue;
for (int x = Math.max(at.getMinBlockX(), bounds.minX()); x <= Math.min(at.getMaxBlockX(), bounds.maxX()); x++)
for (int z = Math.max(at.getMinBlockZ(), bounds.minZ()); z <= Math.min(at.getMaxBlockZ(), bounds.maxZ()); z++)
for (int y = bounds.minY(); y <= bounds.maxY(); y++) {
var state = extra.terrainOverride(x, y, z);
if (state != null) chunk.setBlockState(pos.set(x, y, z), state, 0);
}
}
}
}
@@ -0,0 +1,65 @@
package fr.koka.sanctuary.worldgen;
import com.mojang.serialization.MapCodec;
import java.nio.charset.StandardCharsets;
import java.util.UUID;
import net.minecraft.core.BlockPos;
import net.minecraft.core.registries.Registries;
import net.minecraft.data.worldgen.features.TreeFeatures;
import net.minecraft.tags.BlockTags;
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.chunk.ChunkGenerator;
import net.minecraft.world.level.levelgen.feature.Feature;
/** Chunk decoration owns initial occupants; normal chunk completion persists them once, like vanilla fauna. */
public final class Terrain25RefugeFeature implements Feature {
public static final MapCodec<Terrain25RefugeFeature> CODEC = MapCodec.unit(Terrain25RefugeFeature::new);
@Override public MapCodec<Terrain25RefugeFeature> codec() { return CODEC; }
@Override public boolean place(WorldGenLevel level, ChunkGenerator generator, RandomSource ignored, BlockPos origin) {
var binding=PopulationGenerationContext.of(generator,level.getLevel());
if(binding==null || !Terrain25Runtime.enabled(binding.generator())) return false;
int minX=(origin.getX()>>4)*16,minZ=(origin.getZ()>>4)*16;
var plan=Terrain25Runtime.shape(binding.generator(),binding.random());
boolean changed=false;
for(var chamber:plan.chambers()) {
if(chamber.kind()!=Terrain25Shape.Kind.MUSHROOM || minX>chamber.x()+chamber.rx() || minX+15<chamber.x()-chamber.rx()
|| minZ>chamber.z()+chamber.rz() || minZ+15<chamber.z()-chamber.rz()) continue;
for(int x=minX+5;x<=minX+10;x+=5) for(int z=minZ+5;z<=minZ+10;z+=5) {
if(chamber.horizontal(x,z)>.52)continue;
BlockPos base=new BlockPos(x,chamber.floorY()+1,z);
if(!level.getBlockState(base).isAir() || !(level.getBlockState(base.below()).is(Blocks.MYCELIUM)
|| level.getBlockState(base.below()).is(Blocks.MOSS_BLOCK) || level.getBlockState(base.below()).is(Blocks.PODZOL)))continue;
if(Terrain25Runtime.protects(binding.generator(),binding.random(),base,4,13,1))continue;
long seed=WoodlandGroveGeometry.seed(level.getSeed(),x,base.getY(),z,0x25A115L);
if(Math.floorMod(seed,3)!=0)continue;
var mushroom=level.registryAccess().lookupOrThrow(Registries.FEATURE).getOrThrow(
Math.floorMod(seed,2)==0?TreeFeatures.HUGE_BROWN_MUSHROOM:TreeFeatures.HUGE_RED_MUSHROOM).value();
changed|=mushroom.place(level,generator,RandomSource.create(seed),base);
}
int cx=(int)Math.round(chamber.x()),cz=(int)Math.round(chamber.z());
if((cx>>4)!=(minX>>4) || (cz>>4)!=(minZ>>4))continue;
for(int i=0;i<3;i++) {
BlockPos at=new BlockPos(cx+(i-1)*3,chamber.floorY()+1,cz);
if(!level.getBlockState(at).isAir() || !level.getBlockState(at.above()).isAir()
|| !level.getBlockState(at.below()).isSolid())continue;
var cow=EntityTypes.MOOSHROOM.create(level.getLevel(),EntitySpawnReason.CHUNK_GENERATION);
if(cow==null)continue;
cow.snapTo(at.getX()+.5,at.getY(),at.getZ()+.5,0,0);
cow.finalizeSpawn(level,level.getCurrentDifficultyAt(at),EntitySpawnReason.CHUNK_GENERATION,null);
cow.setPersistenceRequired();
cow.setUUID(UUID.nameUUIDFromBytes((level.getSeed()+":terrain25:mooshroom:"+chamber.id()+":"+i).getBytes(StandardCharsets.UTF_8)));
level.addFreshEntityWithPassengers(cow);changed=true;
}
}
// Preserve a strict vegetation margin even for the inherited vanilla tree features.
for(int x=minX;x<minX+16;x++)for(int z=minZ;z<minZ+16;z++)for(int y=364;y<384;y++) {
BlockPos at=new BlockPos(x,y,z);var block=level.getBlockState(at);
if(block.is(BlockTags.LOGS) || block.is(BlockTags.LEAVES)) {level.setBlock(at,Blocks.AIR.defaultBlockState(),2);changed=true;}
}
return changed;
}
}
@@ -0,0 +1,321 @@
package fr.koka.sanctuary.worldgen;
import fr.koka.sanctuary.SanctuaryMod;
import java.util.*;
import net.minecraft.core.BlockPos;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.chunk.ChunkAccess;
import net.minecraft.world.level.levelgen.Heightmap;
import net.minecraft.world.level.levelgen.NoiseBasedChunkGenerator;
import net.minecraft.world.level.levelgen.RandomState;
import net.minecraft.world.level.levelgen.densityfunction.SamplerContext;
/** Shared final terrain view. Planners read samplers only: never chunks or intercepted base columns. */
public final class Terrain25Runtime {
private static final Map<RandomState, World> WORLDS = Collections.synchronizedMap(new WeakHashMap<>());
private record Column(int bottom, int top, double lift) {}
private record UpperRock(long low, long middle, long high) {
private static final int MIN_Y = Terrain25Coating.MIN_Y - Terrain25Coating.THICKNESS;
private static final UpperRock EMPTY = new UpperRock(0,0,0);
boolean solid(int y) {
int bit=y-MIN_Y;
if(bit<0 || bit>=192)return false;
long word=bit<64?low:bit<128?middle:high;
return (word & (1L << (bit&63))) != 0;
}
}
private record MaterialColumn(Column geological, Terrain25Coating.Solid shell) {}
public record Fall(long river, int startX, int startZ, int endX, int endZ, int waterY, int width) {
public int minX() { return Math.min(startX, endX) - width; }
public int maxX() { return Math.max(startX, endX) + width; }
public int minZ() { return Math.min(startZ, endZ) - width; }
public int maxZ() { return Math.max(startZ, endZ) + width; }
public double progress(int x, int z) {
double dx = endX - startX, dz = endZ - startZ;
return ((x - startX) * dx + (z - startZ) * dz) / (dx * dx + dz * dz);
}
public boolean channel(int x, int z) {
double t = progress(x, z);
if (t < 0 || t > 1) return false;
double dx = endX - startX, dz = endZ - startZ;
double distance = Math.abs((x - startX) * dz - (z - startZ) * dx) / Math.hypot(dx, dz);
return distance <= 2.5 + (width * .5 - 2.5) * t;
}
}
private static final class World {
final Terrain25Density.View density;
final List<Terrain25Shape.Chamber> lakes;
final Map<Long, Column> columns = Collections.synchronizedMap(new LinkedHashMap<>(1024, .75f, true) {
@Override protected boolean removeEldestEntry(Map.Entry<Long, Column> eldest) { return size() > 32768; }
});
final Map<Long, UpperRock> upperRock = Collections.synchronizedMap(new LinkedHashMap<>(1024, .75f, true) {
@Override protected boolean removeEldestEntry(Map.Entry<Long, UpperRock> eldest) { return size() > 32768; }
});
final PopulationPlanCache<IslandCapacity.Region, List<Fall>> falls = new PopulationPlanCache<>();
World(Terrain25Density.View density, NoiseBasedChunkGenerator generator, RandomState state) {
this.density = density;
var proposed = density.plan().chambers().stream().filter(c -> c.kind() == Terrain25Shape.Kind.LAKE || c.kind() == Terrain25Shape.Kind.LAVA).toList();
List<Terrain25Shape.Chamber> accepted = new ArrayList<>();
for (var candidate : proposed) if (accepted.stream().noneMatch(other -> Terrain25Shape.fluidConflict(candidate,other))
&& retained(density,candidate) && legacyFluidSafe(generator,state,candidate)) accepted.add(candidate);
lakes = List.copyOf(accepted);
SanctuaryMod.LOGGER.info("Terrain25 seed {}: {} underground fluid basins accepted / {} proposed", state.seed(), lakes.size(), proposed.size());
}
Column column(int x, int z) {
long key = key(x, z);
Column found = columns.get(key);
if (found != null) return found;
int top = -1, bottom = -1;
for (int y = 358; y > 0; y--) if (density.natural(SamplerContext.EMPTY_UNCACHED, x, y, z) > 0) { top = y; break; }
if (top >= 0) for (int y = 1; y <= top; y++) if (density.natural(SamplerContext.EMPTY_UNCACHED, x, y, z) > 0) { bottom = y; break; }
found = new Column(bottom, top, density.plan().lift(x,z)); columns.put(key, found); return found;
}
UpperRock upperRock(int x,int z) {
long key=key(x,z);
UpperRock cached=upperRock.get(key);
if(cached!=null)return cached;
Column column=column(x,z);
if(column.top<UpperRock.MIN_Y)return UpperRock.EMPTY;
long[] bits=new long[3];
// At most139 natural-density samples per requested column. The same profile is
// reused by adjacent blocks and neighbouring chunks; no per-block density stencil.
for(int y=UpperRock.MIN_Y;y<=Math.min(358,column.top);y++)
if(density.natural(SamplerContext.EMPTY_UNCACHED,x,y,z)>0) {
int bit=y-UpperRock.MIN_Y;bits[bit>>>6]|=1L<<(bit&63);
}
UpperRock result=new UpperRock(bits[0],bits[1],bits[2]);upperRock.put(key,result);return result;
}
MaterialColumn materials(int x,int z) {
Column column=column(x,z);
if(column.lift<=26 || column.top<=Terrain25Coating.MIN_Y)return new MaterialColumn(column,null);
UpperRock centre=upperRock(x,z),west=upperRock(x-5,z),east=upperRock(x+5,z),north=upperRock(x,z-5),south=upperRock(x,z+5);
Terrain25Coating.Solid shell=(nx,y,nz)->(nx<x?west:nx>x?east:nz<z?north:nz>z?south:centre).solid(y);
return new MaterialColumn(column,shell);
}
}
private Terrain25Runtime() {}
public static boolean enabled(NoiseBasedChunkGenerator generator) {
return generator.generatorSettings().is(SanctuaryMod.id("unified_25_5"))
|| generator.generatorSettings().is(SanctuaryMod.id("unified_25_10"))
|| generator.generatorSettings().is(SanctuaryMod.id("unified_25_20"));
}
public static Terrain25Density.View view(NoiseBasedChunkGenerator generator, RandomState state) {
var sampler = state.getSampler(generator.generatorSettings().value().noiseRouter().finalDensity());
if (sampler instanceof Terrain25Density.View view) return view;
throw new IllegalStateException("Terrain25 settings must compile a Terrain25Density.View: " + sampler.getClass().getName());
}
private static World world(NoiseBasedChunkGenerator generator, RandomState state) {
synchronized (WORLDS) { return WORLDS.computeIfAbsent(state, ignored -> new World(view(generator, state),generator,state)); }
}
public static Terrain25Shape.Plan shape(NoiseBasedChunkGenerator generator, RandomState state) { return world(generator, state).density.plan(); }
public static boolean isMushroomRefuge(NoiseBasedChunkGenerator generator, RandomState state, int x, int y, int z) {
var chamber = shape(generator, state).chamberAt(x, y, z);
return chamber != null && chamber.kind() == Terrain25Shape.Kind.MUSHROOM;
}
public static List<Terrain25Shape.Chamber> admittedLakes(NoiseBasedChunkGenerator generator, RandomState state) { return world(generator, state).lakes; }
/** The water envelope and its bed are checked against FINAL density, including other cuts. */
private static boolean retained(Terrain25Density.View density, Terrain25Shape.Chamber chamber) {
int water = chamber.fluidY(), floor = chamber.floorY();
int wet = 0;
for (int x = (int) (chamber.x() - chamber.rx() - 2); x <= chamber.x() + chamber.rx() + 2; x++)
for (int z = (int) (chamber.z() - chamber.rz() - 2); z <= chamber.z() + chamber.rz() + 2; z++) {
if (chamber.cut(x, water, z) <= 0) continue;
wet++;
if (density.sampleValue(SamplerContext.EMPTY_UNCACHED, x, floor, z) <= 0
|| density.sampleValue(SamplerContext.EMPTY_UNCACHED, x, floor - 1, z) <= 0) {
SanctuaryMod.LOGGER.info("Terrain25 basin {} {} rejected: unsupported floor at {},{},{} (natural={})", chamber.id(),chamber.kind(),x,floor,z,
density.natural(SamplerContext.EMPTY_UNCACHED,x,floor,z));
return false;
}
for (int[] d : new int[][]{{1,0},{-1,0},{0,1},{0,-1}})
for (int y = floor + 1; y <= water; y++)
if (chamber.cut(x + d[0], y, z + d[1]) <= 0
&& density.sampleValue(SamplerContext.EMPTY_UNCACHED, x + d[0], y, z + d[1]) <= 0) {
SanctuaryMod.LOGGER.info("Terrain25 basin {} {} rejected: open bank at {},{},{} (natural={})",chamber.id(),chamber.kind(),x+d[0],y,z+d[1],
density.natural(SamplerContext.EMPTY_UNCACHED,x+d[0],y,z+d[1]));
return false;
}
}
return wet >= 50;
}
private static boolean legacyFluidSafe(NoiseBasedChunkGenerator generator, RandomState state, Terrain25Shape.Chamber chamber) {
int minX=(int)(chamber.x()-chamber.rx()-8), maxX=(int)(chamber.x()+chamber.rx()+8);
int minZ=(int)(chamber.z()-chamber.rz()-8), maxZ=(int)(chamber.z()+chamber.rz()+8);
for(var region:PopulationHydrologyRuntime.plansIntersecting(generator,state,minX,minZ,maxX,maxZ)) {
// A same-fluid excavation can drain a basin too: certify against all older cuts,
// not only heat/cold conflicts. Joining a different retained waterline is not implicit.
for(var cell:region.water().cells()) if(cell.x()>=minX && cell.x()<=maxX && cell.z()>=minZ && cell.z()<=maxZ
&& cell.bedY()<=chamber.fluidY()+3 && Math.max(cell.waterY(),cell.carveTop())>=chamber.floorY()-3) return false;
for(var source:region.water().springs()) if(source.flowBounds().minX()<=maxX && source.flowBounds().maxX()>=minX
&& source.flowBounds().minZ()<=maxZ && source.flowBounds().maxZ()>=minZ
&& source.flowBounds().minY()<=chamber.fluidY()+3 && source.flowBounds().maxY()>=chamber.floorY()-3) return false;
for(var source:region.water().spills()) if(source.flowBounds().minX()<=maxX && source.flowBounds().maxX()>=minX
&& source.flowBounds().minZ()<=maxZ && source.flowBounds().maxZ()>=minZ
&& source.flowBounds().minY()<=chamber.fluidY()+3 && source.flowBounds().maxY()>=chamber.floorY()-3) return false;
for(var cell:region.lava().cells()) if(cell.x()>=minX && cell.x()<=maxX && cell.z()>=minZ && cell.z()<=maxZ
&& cell.y()<=chamber.fluidY()+3 && cell.y()>=chamber.floorY()-3) return false;
for(var bounds:region.lava().flowBounds()) if(bounds.minX()<=maxX && bounds.maxX()>=minX
&& bounds.minZ()<=maxZ && bounds.maxZ()>=minZ && bounds.minY()<=chamber.fluidY()+8
&& bounds.maxY()>=chamber.floorY()-8) return false;
}
return true;
}
public static List<Fall> falls(NoiseBasedChunkGenerator generator, RandomState state, IslandCapacity.Region region) {
var world = world(generator, state);
return world.falls.getOrCreate(region, () -> {
List<Fall> result = new ArrayList<>();
var plan = PopulationHydrologyRuntime.regionPlan(generator, state, region).water();
for (var river : plan.features()) if (river.kind() == PopulationHydrology.Kind.RIVER && river.path().size() >= 2) {
for (var endpoint : List.of(river.path().getFirst(), river.path().getLast())) {
Fall best = null; double bestLength = 65;
for (int direction = 0; direction < 32; direction++) {
double angle = direction * Math.PI / 16;
for (int length = 8; length <= 64; length += 4) {
int ex = endpoint.x() + (int) Math.round(Math.cos(angle) * length);
int ez = endpoint.z() + (int) Math.round(Math.sin(angle) * length);
if (world.column(ex, ez).top >= river.waterY() - 10) continue;
int width = 8 + Math.floorMod(mix(state.seed() ^ river.id() ^ key(ex, ez)), 9);
Fall candidate = new Fall(river.id(), endpoint.x(), endpoint.z(), ex, ez, river.waterY(), width);
if (length < bestLength && admissible(world, plan, candidate)) { best = candidate; bestLength = length; }
break;
}
}
if (best != null) result.add(best);
else SanctuaryMod.LOGGER.debug("Terrain25 river {} endpoint {},{}: no retained outlet within 64 blocks", river.id(), endpoint.x(), endpoint.z());
}
}
return List.copyOf(result);
});
}
private static boolean admissible(World world, PopulationHydrology.Plan water, Fall fall) {
int bed = fall.waterY - 3;
for (int x = fall.minX(); x <= fall.maxX(); x++) for (int z = fall.minZ(); z <= fall.maxZ(); z++) {
if (!fall.channel(x, z)) continue;
double t = fall.progress(x, z);
if (Math.hypot(x, z) < 64) return false;
int top = world.column(x, z).top;
if (top > fall.waterY + 12) return false;
if (t < .82 && (world.density.sampleValue(SamplerContext.EMPTY_UNCACHED, x, bed, z) <= 0
|| world.density.sampleValue(SamplerContext.EMPTY_UNCACHED, x, bed - 1, z) <= 0)) return false;
// Existing adjacent catchments may be connected only at this river's own waterline.
for (var cell : water.cellsAt(x, z)) if (cell.hasWater() && cell.waterY() != fall.waterY) return false;
if (t < .9) for (int[] d : new int[][]{{1,0},{-1,0},{0,1},{0,-1}}) {
int nx = x + d[0], nz = z + d[1];
if (!fall.channel(nx, nz) && water.cellsAt(nx, nz).isEmpty()
&& world.density.sampleValue(SamplerContext.EMPTY_UNCACHED, nx, fall.waterY, nz) <= 0) return false;
}
}
return true;
}
private static List<Fall> nearbyFalls(NoiseBasedChunkGenerator generator, RandomState state, int x, int z) {
var cap = PopulationHydrologyRuntime.capacity(generator);
List<Fall> result = new ArrayList<>();
var first = cap.regionAt(x - 80, z - 80); var last = cap.regionAt(x + 80, z + 80);
for (int rx = first.x(); rx <= last.x(); rx++) for (int rz = first.z(); rz <= last.z(); rz++) {
var region = new IslandCapacity.Region(rx, rz);
double nx = Math.max(0, Math.max(region.minX(), -region.maxX())), nz = Math.max(0, Math.max(region.minZ(), -region.maxZ()));
if (Math.hypot(nx, nz) < cap.terrainLimit()) result.addAll(falls(generator, state, region));
}
return result;
}
/** Null means no additional alpha.25 replacement; original contains inherited water/materials. */
public static BlockState finalBlock(NoiseBasedChunkGenerator generator, RandomState state, int x, int y, int z, BlockState original) {
var world = world(generator, state);
return finalBlock(world, nearbyFalls(generator, state, x, z), world.materials(x,z), state.seed(), x, y, z, original);
}
private static BlockState finalBlock(World world, List<Fall> falls, MaterialColumn materials, long seed, int x, int y, int z, BlockState original) {
for (Fall fall : falls) if (fall.channel(x, z)) {
if (y > fall.waterY - 3 && y <= fall.waterY + 13) {
if (y > fall.waterY) return Blocks.AIR.defaultBlockState();
// The lip alone releases water; no prefilled falling column or global sea layer.
return world.density.sampleValue(SamplerContext.EMPTY_UNCACHED,x,fall.waterY-3,z)>0
? Blocks.WATER.defaultBlockState() : Blocks.AIR.defaultBlockState();
}
if (y == fall.waterY - 3 && fall.progress(x,z) < .82) return Blocks.STONE.defaultBlockState();
}
for (var lake : world.lakes) if (y > lake.floorY() && y <= lake.fluidY() && lake.cut(x, y, z) > 0)
return (lake.kind() == Terrain25Shape.Kind.LAVA ? Blocks.LAVA : Blocks.WATER).defaultBlockState();
if (original.isAir() || !original.getFluidState().isEmpty()) return original;
var chamber = world.density.plan().chamberAt(x, y, z);
if (chamber != null && chamber.kind() == Terrain25Shape.Kind.MUSHROOM && y == chamber.floorY()
&& chamber.horizontal(x,z) < .62 && world.density.sampleValue(SamplerContext.EMPTY_UNCACHED, x, y + 1, z) <= 0) {
int patch = Math.floorMod(mix(seed ^ key(Math.floorDiv(x, 8), Math.floorDiv(z,8))), 5);
return (patch < 2 ? Blocks.MYCELIUM : patch < 4 ? Blocks.MOSS_BLOCK : Blocks.PODZOL).defaultBlockState();
}
if (!(original.is(Blocks.STONE) || original.is(Blocks.DEEPSLATE) || original.is(Blocks.ANDESITE)
|| original.is(Blocks.DIORITE) || original.is(Blocks.GRANITE))) return original;
Column column = materials.geological;
if (materials.shell != null && Terrain25Coating.calcite(x,y,z,column.lift,materials.shell))
return Blocks.CALCITE.defaultBlockState();
if (column.top - column.bottom < 24) return original;
double depth = Terrain25Shape.depth(y, column.bottom, column.top);
int gx = Math.floorDiv(x, 14), gy = Math.floorDiv(y, 12), gz = Math.floorDiv(z, 14);
long h = mix(seed ^ key(gx, gz) ^ ((long) gy * 0x25BA5A17L));
int cx = gx * 14 + 3 + Math.floorMod(h, 8), cy = gy * 12 + 3 + Math.floorMod(h >>> 9, 6), cz = gz * 14 + 3 + Math.floorMod(h >>> 18, 8);
double dx = (x - cx) / 4.3, dy = (y - cy) / 2.8, dz = (z - cz) / 3.3;
if (dx * dx + dy * dy + dz * dz >= 1) return original;
int choice = Math.floorMod(h >>> 27, 100);
boolean deep = original.is(Blocks.DEEPSLATE) || depth > .72;
if (choice < 33) return depth < .75 ? (deep ? Blocks.DEEPSLATE_COAL_ORE : Blocks.COAL_ORE).defaultBlockState() : original;
if (choice < 53) return (deep ? Blocks.DEEPSLATE_IRON_ORE : Blocks.IRON_ORE).defaultBlockState();
if (choice < 72) return depth < .65 ? Blocks.COPPER_ORE.defaultBlockState() : original;
if (choice < 81) return depth > .42 ? (deep ? Blocks.DEEPSLATE_REDSTONE_ORE : Blocks.REDSTONE_ORE).defaultBlockState() : original;
if (choice < 86) return depth > .35 ? (deep ? Blocks.DEEPSLATE_GOLD_ORE : Blocks.GOLD_ORE).defaultBlockState() : original;
if (choice < 91) return depth > .3 ? (deep ? Blocks.DEEPSLATE_LAPIS_ORE : Blocks.LAPIS_ORE).defaultBlockState() : original;
if (choice < 94) return depth > .65 ? (deep ? Blocks.DEEPSLATE_DIAMOND_ORE : Blocks.DIAMOND_ORE).defaultBlockState() : original;
if (choice == 95) return depth > .5 ? (deep ? Blocks.DEEPSLATE_EMERALD_ORE : Blocks.EMERALD_ORE).defaultBlockState() : original;
return original;
}
public static boolean protects(NoiseBasedChunkGenerator generator, RandomState state, BlockPos base, int radius, int height, int rootDepth) {
if (PopulationHydrologyRuntime.protects(generator, state, base, radius, height, rootDepth)) return true;
var world = world(generator,state);
for (var lake : world.lakes) if (base.getY()-rootDepth <= lake.fluidY()+2 && base.getY()+height >= lake.floorY()-2
&& Math.abs(base.getX()-lake.x()) <= lake.rx()+radius+2 && Math.abs(base.getZ()-lake.z()) <= lake.rz()+radius+2) return true;
for (var fall : nearbyFalls(generator,state,base.getX(),base.getZ()))
if (base.getX()+radius >= fall.minX() && base.getX()-radius <= fall.maxX()
&& base.getZ()+radius >= fall.minZ() && base.getZ()-radius <= fall.maxZ() && base.getY()-rootDepth <= fall.waterY+13) return true;
return false;
}
/** Called once per base column, not once per block, to reuse indexed regional plans. */
public static net.minecraft.world.level.NoiseColumn applyColumn(NoiseBasedChunkGenerator generator, RandomState state,
int x, int z, net.minecraft.world.level.NoiseColumn original) {
var world = world(generator,state); var falls = nearbyFalls(generator,state,x,z);
var cap = PopulationHydrologyRuntime.capacity(generator);
if (Math.hypot(x,z) < cap.terrainLimit()) {
var region = PopulationHydrologyRuntime.regionPlan(generator,state,cap.regionAt(x,z));
for (var cell : region.water().cellsAt(x,z)) {
for (int d=0;d<cell.sedimentDepth();d++) original.setBlock(cell.bedY()-d,PopulationMaterials.sedimentBlock(cell,state.seed(),d));
for (int y=cell.bedY()+1;y<=Math.max(cell.waterY(),cell.carveTop());y++)
original.setBlock(y,(cell.hasWater() && y<=cell.waterY()?Blocks.WATER:Blocks.AIR).defaultBlockState());
}
for (var spring : region.water().springs()) if (spring.source().x()==x && spring.source().z()==z)
original.setBlock(spring.source().y(),Blocks.WATER.defaultBlockState());
for (var cell : region.lava().cellsInChunk(x>>4,z>>4)) if(cell.x()==x && cell.z()==z)
original.setBlock(cell.y(),(cell.lava()?Blocks.LAVA:Blocks.AIR).defaultBlockState());
}
var materials=world.materials(x,z);
for (int y=0;y<384;y++) original.setBlock(y,finalBlock(world,falls,materials,state.seed(),x,y,z,original.getBlock(y)));
return original;
}
public static void apply(NoiseBasedChunkGenerator generator, RandomState state, ChunkAccess chunk) {
PopulationHydrologyRuntime.apply(generator,state,chunk);
var world = world(generator,state); var cp = chunk.getPos();
var falls = nearbyFalls(generator,state,cp.getMinBlockX()+8,cp.getMinBlockZ()+8);
BlockPos.MutableBlockPos pos = new BlockPos.MutableBlockPos();
for (int x = cp.getMinBlockX(); x <= cp.getMaxBlockX(); x++) for (int z = cp.getMinBlockZ(); z <= cp.getMaxBlockZ(); z++) {
var materials=world.materials(x,z);
for (int y=1; y<384; y++) {
pos.set(x,y,z); var original = chunk.getBlockState(pos);
var replacement = finalBlock(world,falls,materials,state.seed(),x,y,z,original);
if (replacement != original) {
chunk.setBlockState(pos,replacement,0);
if (!replacement.getFluidState().isEmpty()) chunk.markPosForPostProcessing(pos);
}
}
}
Heightmap.primeHeightmaps(chunk,EnumSet.of(Heightmap.Types.WORLD_SURFACE_WG,Heightmap.Types.OCEAN_FLOOR_WG));
}
private static long key(int x,int z) { return ((long)x << 32) ^ (z & 0xffffffffL); }
private static long mix(long n) { n=(n^(n>>>30))*0xbf58476d1ce4e5b9L; n=(n^(n>>>27))*0x94d049bb133111ebL; return n^(n>>>31); }
}
@@ -0,0 +1,218 @@
package fr.koka.sanctuary.worldgen;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
/** Immutable, seed-local analytic terrain. No chunk or mutable generation order enters this plan. */
public final class Terrain25Shape {
public static final int MAX_TERRAIN_Y = 358;
public enum Kind { DRY, LAKE, MUSHROOM, LAVA }
public record Peak(double x, double z, double radius, double lift) {}
public record Chamber(int id, double x, double y, double z, double rx, double ry, double rz, Kind kind) {
public int floorY() { return (int) Math.floor(y - ry * .62); }
public int fluidY() { return floorY() + (kind == Kind.LAKE ? 5 : kind == Kind.LAVA ? 3 : 0); }
public double horizontal(double px, double pz) {
double dx = (px - x) / rx, dz = (pz - z) / rz;
return dx * dx + dz * dz;
}
public double cut(double px, double py, double pz) {
double horizontal = horizontal(px, pz);
double irregular = 1 + .07 * Math.sin(px * .17 + pz * .11) * Math.cos(pz * .19 - py * .13);
if (py <= floorY()) return -1;
double vertical = (py - y) / ry;
return (irregular - horizontal - vertical * vertical) * .55;
}
}
public record Passage(Chamber from, Chamber to, double width, double phase) {
public double cut(double x, double y, double z) {
double dx = to.x - from.x, dz = to.z - from.z;
double t = Math.clamp(((x - from.x) * dx + (z - from.z) * dz) / (dx * dx + dz * dz), 0, 1);
double side = Math.sin(t * Math.PI * 2 + phase) * 9 * Math.sin(t * Math.PI);
double length = Math.sqrt(dx * dx + dz * dz);
double cx = from.x + t * dx - dz / length * side, cz = from.z + t * dz + dx / length * side;
double cy = from.y + (to.y - from.y) * t + Math.sin(t * Math.PI * 3 + phase) * 4 * Math.sin(t * Math.PI);
double a = (x - cx) / width, b = (z - cz) / width, c = (y - cy) / (width * .8);
return (1 - a * a - b * b - c * c) * .6;
}
}
public record Plan(long seed, double radius, List<Peak> peaks, List<Chamber> chambers, List<Passage> passages) {
public Plan { peaks = List.copyOf(peaks); chambers = List.copyOf(chambers); passages = List.copyOf(passages); }
public double lift(int x, int z) {
if (Math.hypot(x, z) < 64) return 0; // The arrival keeps its natural woodland scale.
double result = 0;
for (Peak peak : peaks) {
double d = Math.hypot(x - peak.x, z - peak.z) / peak.radius;
if (d < 1) {
double shape = Math.pow(1 - d, 1.35);
result = Math.max(result, peak.lift * shape);
}
}
double arrival = Math.clamp((Math.hypot(x,z)-64)/32,0,1);
return result * arrival * arrival * (3-2*arrival);
}
/** Monotonic vertical stretch: preserves cavities and prevents folded terrain. */
public int naturalY(int x, int y, int z) { return naturalY(y, lift(x,z)); }
public int naturalY(int y, double lift) {
if (y <= 150) return y;
return (int) Math.floor(150 + (y - 150) / (1 + lift / 95));
}
public Chamber retainingBasin(int x, int y, int z) {
for (Chamber c : chambers) if ((c.kind == Kind.LAKE || c.kind == Kind.LAVA)
&& c.horizontal(x,z) < 1.3 && y >= c.floorY()-3 && y <= c.fluidY()+2) return c;
return null;
}
public double excavation(int x, int y, int z) {
// Neighbouring dry rooms and slanting faults leave a natural membrane around a basin.
// This changes excavation only; no missing natural rock is manufactured.
var retained = retainingBasin(x,y,z);
if (retained != null) return retained.cut(x,y,z);
double cut = -1;
for (Chamber chamber : chambers) if (Math.abs(x - chamber.x) <= chamber.rx * 1.1
&& Math.abs(z - chamber.z) <= chamber.rz * 1.1 && Math.abs(y - chamber.y) <= chamber.ry * 1.1)
cut = Math.max(cut, chamber.cut(x, y, z));
for (Passage passage : passages) if (x >= Math.min(passage.from.x, passage.to.x) - 18
&& x <= Math.max(passage.from.x, passage.to.x) + 18
&& z >= Math.min(passage.from.z, passage.to.z) - 18 && z <= Math.max(passage.from.z, passage.to.z) + 18
&& y >= Math.min(passage.from.y, passage.to.y) - 12 && y <= Math.max(passage.from.y, passage.to.y) + 12)
cut = Math.max(cut, passage.cut(x, y, z));
return cut;
}
/** A small humid coastal sector, independent of any hut candidate or structure flag. */
public boolean wetland(int x, int y, int z) {
if (y < 145 || y > 248 || lift(x,z) > 14) return false;
double angle = (seed >>> 11) * 0x1.0p-53 * Math.PI * 2;
double cx = Math.cos(angle) * radius * .64, cz = Math.sin(angle) * radius * .64;
double dx = (x-cx) / (radius * .23), dz = (z-cz) / (radius * .20);
return dx*dx+dz*dz < 1 + .12*Math.sin(x*.07)*Math.cos(z*.09);
}
public Chamber chamberAt(int x, int y, int z) {
for (Chamber chamber : chambers) if (chamber.horizontal(x, z) < .92
&& y >= chamber.floorY() - 2 && y <= chamber.y + chamber.ry * .82) return chamber;
return null;
}
}
private Terrain25Shape() {}
public static Plan create(long seed, double radius) {
Random random = new Random(seed ^ 0xCA1C1725L);
List<Peak> peaks = new ArrayList<>();
double rotation = random.nextDouble() * Math.PI * 2;
for (int group = 0; group < 3; group++) {
double angle = rotation + group * Math.PI * 2 / 3 + (random.nextDouble() - .5) * .4;
double distance = radius * (.38 + random.nextDouble() * .15);
double gx = Math.cos(angle) * distance, gz = Math.sin(angle) * distance;
for (int p = 0; p < 5; p++) {
double offset = p == 0 ? 0 : 22 + random.nextDouble() * radius * .12;
double bearing = random.nextDouble() * Math.PI * 2;
peaks.add(new Peak(gx + Math.cos(bearing) * offset, gz + Math.sin(bearing) * offset,
radius * (.13 + random.nextDouble() * .08), 115 + random.nextDouble() * 90));
}
}
List<Chamber> chambers = new ArrayList<>();
List<Passage> passages = new ArrayList<>();
int groups = radius < 400 ? 4 : 5;
for (int group = 0; group < groups; group++) {
double angle = rotation + group * Math.PI * 2 / groups + .5;
double distance = radius * (.23 + random.nextDouble() * .25);
double cx = Math.cos(angle) * distance, cz = Math.sin(angle) * distance;
Kind kind = Kind.values()[group % 4];
double y = kind == Kind.LAVA ? 69 + random.nextInt(18) : 115 + random.nextInt(42);
Chamber first = new Chamber(chambers.size(), cx, y, cz, 24 + random.nextInt(14),
14 + random.nextInt(9), 22 + random.nextInt(15), kind);
chambers.add(first);
Chamber second = new Chamber(chambers.size(), cx + Math.cos(angle + 1) * 58,
y - 4 + random.nextInt(9), cz + Math.sin(angle + 1) * 58,
18 + random.nextInt(9), 12 + random.nextInt(7), 18 + random.nextInt(9), kind);
chambers.add(second);
passages.add(new Passage(first, second, kind == Kind.LAVA ? 3.5 : 4.5, random.nextDouble() * 6));
}
// A broad dry chamber exists independently of any village. Actual native structures
// still certify its natural floor and roof; this is never a structure-shaped box.
chambers.add(new Chamber(chambers.size(), Math.cos(rotation) * radius * .14, 151,
Math.sin(rotation) * radius * .14, Math.min(104, radius * .27), 31,
Math.min(94, radius * .25), Kind.DRY));
return new Plan(seed, radius, peaks, chambers, passages);
}
@FunctionalInterface public interface Sampler { float sample(int x,int y,int z); }
/** At most48 terrain proposals per basin/refuge. The shared intact field is sampled before any excavation. */
public static Plan fit(Plan proposed, Sampler sampler) {
var memo = new SignMemo(sampler);
List<Chamber> fitted = new ArrayList<>();
for (Chamber original : proposed.chambers) {
if (original.kind == Kind.DRY) { fitted.add(original); continue; }
Chamber chosen = null;
for (int attempt=0;attempt<48;attempt++) {
double angle = attempt * 2.399963229728653 + original.id * .7;
double distance = attempt==0?0:Math.sqrt(attempt/47.0)*76;
int x=(int)Math.round(original.x+Math.cos(angle)*distance);
int z=(int)Math.round(original.z+Math.sin(angle)*distance);
if (Math.hypot(x,z)>proposed.radius*.75)continue;
int bestBottom=-1,bestTop=-1,runBottom=-1;
for(int y=24;y<=286;y++) {
if(memo.sample(x,y,z)>0) {if(runBottom<0)runBottom=y;}
else if(runBottom>=0) {
if(y-runBottom>original.ry*1.65+9 && (bestBottom<0 || (original.kind==Kind.LAVA?runBottom<bestBottom:y-runBottom>bestTop-bestBottom))) {
bestBottom=runBottom;bestTop=y-1;
}
runBottom=-1;
}
}
if(bestBottom<0)continue;
int floor=(int)Math.round(original.kind==Kind.LAVA?bestBottom+8+(attempt%3)*6:bestBottom+(bestTop-bestBottom)*.28);
double cy=floor+original.ry*.62+.1;
Chamber candidate=new Chamber(original.id,x,cy,z,original.rx,original.ry,original.rz,original.kind);
if(candidate.y+candidate.ry>bestTop-3)continue;
if(fitted.stream().anyMatch(other -> fluidConflict(candidate,other)))continue;
if(!coarseFloor(memo,candidate))continue;
if((original.kind==Kind.LAKE || original.kind==Kind.LAVA) && !retainsNaturalFluid(memo,candidate))continue;
chosen=candidate;break;
}
fitted.add(chosen==null?original:chosen);
}
List<Passage> passages = proposed.passages.stream().map(p -> new Passage(fitted.get(p.from.id),fitted.get(p.to.id),p.width,p.phase)).toList();
return new Plan(proposed.seed,proposed.radius,proposed.peaks,fitted,passages);
}
/** Unlike-kind reservoirs retain at least six rock blocks between their filled envelopes. */
public static boolean fluidConflict(Chamber a,Chamber b) {
boolean opposite=(a.kind==Kind.LAKE && b.kind==Kind.LAVA)||(a.kind==Kind.LAVA && b.kind==Kind.LAKE);
return opposite && Math.abs(a.x-b.x)<a.rx+b.rx+6 && Math.abs(a.z-b.z)<a.rz+b.rz+6
&& a.floorY()<b.fluidY()+6 && b.floorY()<a.fluidY()+6;
}
private static boolean coarseFloor(Sampler sample,Chamber c) {
int floor=c.floorY(),roof=(int)(c.y+c.ry+2),ceilings=0;
for(int ix=-3;ix<=3;ix++)for(int iz=-3;iz<=3;iz++) {
double dx=ix/3.0,dz=iz/3.0;if(dx*dx+dz*dz>.85)continue;
int x=(int)Math.round(c.x+dx*c.rx),z=(int)Math.round(c.z+dz*c.rz);
if(sample.sample(x,floor,z)<=0 || sample.sample(x,floor-1,z)<=0)return false;
if(sample.sample(x,roof,z)>0)ceilings++;
}
return ceilings >= (c.kind == Kind.MUSHROOM ? 17 : 14);
}
private static boolean retainsNaturalFluid(Sampler sample,Chamber c) {
int floor=c.floorY(),water=c.fluidY();
for(int x=(int)(c.x-c.rx-2);x<=c.x+c.rx+2;x++)for(int z=(int)(c.z-c.rz-2);z<=c.z+c.rz+2;z++) {
if(c.cut(x,water,z)<=0)continue;
if(sample.sample(x,floor,z)<=0 || sample.sample(x,floor-1,z)<=0)return false;
for(int[] d:DIRECTIONS)for(int y=floor+1;y<=water;y++)
if(c.cut(x+d[0],y,z+d[1])<=0 && sample.sample(x+d[0],y,z+d[1])<=0)return false;
}
return true;
}
private static final int[][] DIRECTIONS={{1,0},{-1,0},{0,1},{0,-1}};
private static final class SignMemo implements Sampler {
private final Sampler source;
private final java.util.Map<Long,long[]> columns = new java.util.HashMap<>();
SignMemo(Sampler source){this.source=source;}
@Override public float sample(int x,int y,int z) {
if(y<0||y>=384)return -1;
long key=((long)x<<32)^(z&0xffffffffL);long[] bits=columns.computeIfAbsent(key,ignored->new long[12]);
int word=y>>>6;long mask=1L<<(y&63);
if((bits[word]&mask)==0){bits[word]|=mask;if(source.sample(x,y,z)>0)bits[word+6]|=mask;}
return (bits[word+6]&mask)!=0?1:-1;
}
}
/** Geological depth is measured in the intact column, never against an excavated cave floor. */
public static double depth(int y, int naturalBottom, int naturalTop) {
return Math.clamp((naturalTop - y) / (double) Math.max(1, naturalTop - naturalBottom), 0, 1);
}
}
@@ -23,12 +23,17 @@ public final class AerialPlanner24 {
}
public static AerialPlan24 forWorld(ServerLevel level, SanctuaryChunkGenerator generator) {
if (generator.rootGeneration() != 24) throw new IllegalArgumentException("Aerial planning requires generation24");
if ((generator.rootGeneration() != 24 && generator.rootGeneration() != 25)) throw new IllegalArgumentException("Aerial planning requires generation24");
if (!level.structureManager().shouldGenerateStructures() || !generator.experimentalStructures()) return AerialPlan24.EMPTY;
generator.prepareAerialSampling24(level);
return plan(level.getSeed(), generator.initialDiameter(), new Terrain() {
@Override public NoiseColumn column(int x, int z) { return generator.aerialBaseColumn24(x, z); }
@Override public boolean reserved(int x, int z, int minY, int maxY) {
if (generator.rootGeneration() == 25) {
var binding = generator.starterContext();
if (fr.koka.sanctuary.worldgen.Terrain25Runtime.protects(binding.generator(), binding.random(),
new net.minecraft.core.BlockPos(x, minY, z), 0, maxY - minY, 0)) return true;
}
return generator.intersectsExperimentalStructures(x, minY, z, x, maxY, z);
}
});
@@ -35,12 +35,22 @@ public final class AerialStore24 {
public record Stored(String token, AerialPlan24 plan) {}
/** Store lives beside sanctuary-world-v24, so creating an orphan manifest never looks like an initialized journal. */
public static Path directory(Path journalDirectory) { return journalDirectory.resolveSibling("sanctuary-aerial-v24"); }
public static Path directory(Path journalDirectory) {
return directory(journalDirectory, journalDirectory.getFileName().toString().equals("sanctuary-world-v25") ? 25 : 24);
}
public static Path directory(Path journalDirectory, int generation) {
requireGeneration(generation);
return journalDirectory.resolveSibling("sanctuary-aerial-v" + generation);
}
public static Stored create(Path directory, long seed, int diameter, Supplier<AerialPlan24> factory) throws IOException {
return create(directory, seed, diameter, 24, factory);
}
public static Stored create(Path directory, long seed, int diameter, int generation, Supplier<AerialPlan24> factory) throws IOException {
requireGeneration(generation);
AerialPlan24 plan = factory.get();
validateExtent(plan, diameter);
String encoded = encode(plan, seed, diameter).toString() + "\n";
String encoded = encode(plan, seed, diameter, generation).toString() + "\n";
byte[] bytes = encoded.getBytes(StandardCharsets.UTF_8);
if (bytes.length > MAX_BYTES) throw new IOException("Aerial layout exceeds its size limit");
String token = digest(bytes);
@@ -60,12 +70,16 @@ public final class AerialStore24 {
}
} finally { Files.deleteIfExists(temporary); }
}
Stored verified = load(directory, token, seed, diameter);
Stored verified = load(directory, token, seed, diameter, generation);
if (!verified.plan().sites().equals(plan.sites())) throw new IOException("Aerial manifest read-back differs from its plan");
return verified;
}
public static Stored load(Path directory, String token, long seed, int diameter) throws IOException {
return load(directory, token, seed, diameter, 24);
}
public static Stored load(Path directory, String token, long seed, int diameter, int generation) throws IOException {
requireGeneration(generation);
if (token == null || !token.matches("[0-9a-f]{64}")) throw new IOException("Invalid aerial manifest token");
Path file = directory.resolve(token + ".json");
if (!Files.isDirectory(directory, LinkOption.NOFOLLOW_LINKS) || !Files.isRegularFile(file, LinkOption.NOFOLLOW_LINKS)
@@ -77,7 +91,7 @@ public final class AerialStore24 {
JsonObject root = readJson(reader, 0).getAsJsonObject();
if (reader.peek() != JsonToken.END_DOCUMENT) throw new IOException("Trailing aerial manifest data");
fields(root, ROOT_FIELDS);
if (integer(root, "schema") != 1 || integer(root, "generation") != 24
if (integer(root, "schema") != 1 || integer(root, "generation") != generation
|| number(root, "world_seed").longValueExact() != seed || integer(root, "root_diameter") != diameter)
throw new IOException("Aerial manifest differs from its world contract");
var sites = new ArrayList<AerialPlan24.Site>();
@@ -103,9 +117,12 @@ public final class AerialStore24 {
if (Math.hypot(x, z) <= radius) throw new IOException("Aerial claim overlaps initial terrain ownership");
}
}
private static JsonObject encode(AerialPlan24 plan, long seed, int diameter) {
private static void requireGeneration(int generation) {
if (generation != 24 && generation != 25) throw new IllegalArgumentException("Unsupported aerial world generation");
}
private static JsonObject encode(AerialPlan24 plan, long seed, int diameter, int generation) {
var root = new JsonObject();
root.addProperty("schema", 1); root.addProperty("world_seed", seed); root.addProperty("root_diameter", diameter); root.addProperty("generation", 24);
root.addProperty("schema", 1); root.addProperty("world_seed", seed); root.addProperty("root_diameter", diameter); root.addProperty("generation", generation);
var sites = new JsonArray();
for (var s : plan.sites()) {
var site = new JsonObject(); site.addProperty("id", s.id()); site.addProperty("kind", s.kind().name());
@@ -22,7 +22,7 @@ public final class AerialStructure24 extends Structure {
}
public String family() { return family; }
@Override protected Optional<GenerationStub> findGenerationPoint(GenerationContext context) {
if (!(context.chunkGenerator() instanceof SanctuaryChunkGenerator generator) || generator.rootGeneration() != 24
if (!(context.chunkGenerator() instanceof SanctuaryChunkGenerator generator) || (generator.rootGeneration() != 24 && generator.rootGeneration() != 25)
|| !generator.experimentalStructures()) return Optional.empty();
var sites = generator.aerialPlan24().atStartChunk(context.chunkPos().x(), context.chunkPos().z()).stream()
.filter(site -> AerialStructures24.family(site).equals(family)).toList();
@@ -53,7 +53,7 @@ public final class AerialStructures24 {
ChunkGeneratorStructureState state, StructureManager manager, ChunkAccess chunk,
StructureTemplateManager templates, ResourceKey<Level> level) {
if (!Level.OVERWORLD.equals(level) || !manager.shouldGenerateStructures()
|| !(generator instanceof SanctuaryChunkGenerator sanctuary) || generator.rootGeneration() != 24
|| !(generator instanceof SanctuaryChunkGenerator sanctuary) || (generator.rootGeneration() != 24 && generator.rootGeneration() != 25)
|| !sanctuary.experimentalStructures()) return;
var sites = sanctuary.aerialPlan24().atStartChunk(chunk.getPos().x(), chunk.getPos().z());
if (sites.isEmpty()) return;
@@ -29,18 +29,18 @@ public final class LostCityStructure extends Structure {
throw new IllegalStateException("Lost City district exceeds native structure reference range");
}
return Optional.of(new GenerationStub(LostCityStructures.locator(district), pieces -> {
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)) && generator.cityReservation16(island) != null
&& ((generator.rootGeneration() != 21 && generator.rootGeneration() != 22 && (generator.rootGeneration() != 23 && generator.rootGeneration() != 24)) || generator.cityPlacement21(island).core().surface().districts().contains(district)))
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 || generator.rootGeneration() == 25)) && generator.cityReservation16(island) != null
&& ((generator.rootGeneration() != 21 && generator.rootGeneration() != 22 && (generator.rootGeneration() != 23 && generator.rootGeneration() != 24 && generator.rootGeneration() != 25)) || generator.cityPlacement21(island).core().surface().districts().contains(district)))
pieces.addPiece(new LostCityFoundationPiece16(generator.cityReservation16(island), district.seed()));
if (generator.rootGeneration() == 15) for (var support : generator.citySupports(island)) {
if (support.startChunkX() == district.startChunkX() && support.startChunkZ() == district.startChunkZ())
pieces.addPiece(new LostCitySupportPiece15(support));
}
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))) for (var support : generator.citySupports16(island)) {
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 || generator.rootGeneration() == 25))) for (var support : generator.citySupports16(island)) {
if (support.startChunkX() == district.startChunkX() && support.startChunkZ() == district.startChunkZ())
pieces.addPiece(new LostCitySupportPiece16(support));
}
district.parcels().forEach(parcel -> pieces.addPiece((generator.rootGeneration() == 21 || generator.rootGeneration() == 22 || (generator.rootGeneration() == 23 || generator.rootGeneration() == 24)) ? new LostCityPiece21(parcel) : generator.rootGeneration() == 20 ? new LostCityPiece20(parcel) : (generator.rootGeneration() == 16 || generator.rootGeneration() == 17 || generator.rootGeneration() == 19 || generator.rootGeneration() == 20 || generator.rootGeneration() == 21 || generator.rootGeneration() == 22 || (generator.rootGeneration() == 23 || generator.rootGeneration() == 24)) ? new LostCityPiece16(parcel) : generator.rootGeneration() == 15
district.parcels().forEach(parcel -> pieces.addPiece((generator.rootGeneration() == 21 || generator.rootGeneration() == 22 || (generator.rootGeneration() == 23 || generator.rootGeneration() == 24 || generator.rootGeneration() == 25)) ? new LostCityPiece21(parcel) : generator.rootGeneration() == 20 ? new LostCityPiece20(parcel) : (generator.rootGeneration() == 16 || generator.rootGeneration() == 17 || generator.rootGeneration() == 19 || generator.rootGeneration() == 20 || generator.rootGeneration() == 21 || generator.rootGeneration() == 22 || (generator.rootGeneration() == 23 || generator.rootGeneration() == 24 || generator.rootGeneration() == 25)) ? new LostCityPiece16(parcel) : generator.rootGeneration() == 15
? new LostCityPiece15(parcel) : new LostCityPiece(parcel)));
if (generator.rootGeneration() == 15) for (var network : generator.undergroundPlan(island).networks()) {
if (network.startChunkX() != district.startChunkX() || network.startChunkZ() != district.startChunkZ()) continue;
@@ -50,13 +50,13 @@ public final class LostCityStructure extends Structure {
network.modules().stream().filter(m -> m.kind() == LostCityUnderground15.Kind.ACCESS)
.forEach(m -> pieces.addPiece(new LostCityUndergroundPiece15(m)));
}
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))) for (var network : generator.undergroundPlan16(island).networks()) {
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 || generator.rootGeneration() == 25))) for (var network : generator.undergroundPlan16(island).networks()) {
if (network.startChunkX() != district.startChunkX() || network.startChunkZ() != district.startChunkZ()) continue;
// Surface comes first, rooms/corridors next, shafts last so every entrance remains open.
network.modules().stream().filter(m -> m.kind() != LostCityUnderground16.Kind.ACCESS)
.forEach(m -> pieces.addPiece((generator.rootGeneration() == 20 || generator.rootGeneration() == 21 || generator.rootGeneration() == 22 || (generator.rootGeneration() == 23 || generator.rootGeneration() == 24)) ? new LostCityUndergroundPiece20(m) : new LostCityUndergroundPiece16(m)));
.forEach(m -> pieces.addPiece((generator.rootGeneration() == 20 || generator.rootGeneration() == 21 || generator.rootGeneration() == 22 || (generator.rootGeneration() == 23 || generator.rootGeneration() == 24 || generator.rootGeneration() == 25)) ? new LostCityUndergroundPiece20(m) : new LostCityUndergroundPiece16(m)));
network.modules().stream().filter(m -> m.kind() == LostCityUnderground16.Kind.ACCESS)
.forEach(m -> pieces.addPiece((generator.rootGeneration() == 20 || generator.rootGeneration() == 21 || generator.rootGeneration() == 22 || (generator.rootGeneration() == 23 || generator.rootGeneration() == 24)) ? new LostCityUndergroundPiece20(m) : new LostCityUndergroundPiece16(m)));
.forEach(m -> pieces.addPiece((generator.rootGeneration() == 20 || generator.rootGeneration() == 21 || generator.rootGeneration() == 22 || (generator.rootGeneration() == 23 || generator.rootGeneration() == 24 || generator.rootGeneration() == 25)) ? new LostCityUndergroundPiece20(m) : new LostCityUndergroundPiece16(m)));
}
}));
}
@@ -14,9 +14,9 @@ public final class LostCityUndergroundStructure22 extends Structure {
@Override protected Optional<GenerationStub> findGenerationPoint(GenerationContext context) {
if (!(context.chunkGenerator() instanceof SanctuaryChunkGenerator generator)
|| (generator.rootGeneration() != 22 && (generator.rootGeneration() != 23 && generator.rootGeneration() != 24))) return Optional.empty();
|| (generator.rootGeneration() != 22 && (generator.rootGeneration() != 23 && generator.rootGeneration() != 24 && generator.rootGeneration() != 25))) return Optional.empty();
var island = generator.islandForChunk(context.chunkPos().x(), context.chunkPos().z());
if (island == null || !island.origin() || (island.generation() != 22 && (island.generation() != 23 && island.generation() != 24))) return Optional.empty();
if (island == null || !island.origin() || (island.generation() != 22 && (island.generation() != 23 && island.generation() != 24 && island.generation() != 25))) return Optional.empty();
var networks = generator.undergroundHall22().networks().stream()
.filter(n -> n.startChunkX() == context.chunkPos().x() && n.startChunkZ() == context.chunkPos().z()).toList();
if (networks.isEmpty()) return Optional.empty();
@@ -41,9 +41,9 @@ public final class LostCityUndergroundStructures22 {
ChunkGeneratorStructureState state, StructureManager manager, ChunkAccess chunk,
StructureTemplateManager templates, ResourceKey<Level> level) {
if (!Level.OVERWORLD.equals(level) || !manager.shouldGenerateStructures()
|| !(generator instanceof SanctuaryChunkGenerator sanctuary) || (generator.rootGeneration() != 22 && (generator.rootGeneration() != 23 && generator.rootGeneration() != 24))) return;
|| !(generator instanceof SanctuaryChunkGenerator sanctuary) || (generator.rootGeneration() != 22 && (generator.rootGeneration() != 23 && generator.rootGeneration() != 24 && generator.rootGeneration() != 25))) return;
var island = generator.islandForChunk(chunk.getPos().x(), chunk.getPos().z());
if (island == null || !island.origin() || (island.generation() != 22 && (island.generation() != 23 && island.generation() != 24))) return;
if (island == null || !island.origin() || (island.generation() != 22 && (island.generation() != 23 && island.generation() != 24 && island.generation() != 25))) return;
if (sanctuary.undergroundHall22().networks().stream().noneMatch(n -> n.startChunkX() == chunk.getPos().x()
&& n.startChunkZ() == chunk.getPos().z())) return;
Holder<Structure> holder = registries.lookupOrThrow(Registries.STRUCTURE).getOrThrow(KEY);
@@ -63,14 +63,14 @@ public final class LostCityUndergroundStructures22 {
BlockPos origin, int radius, boolean skipReferenced) {
if (!Level.OVERWORLD.equals(level.dimension())
|| !(level.getChunkSource().getGenerator() instanceof SanctuaryChunkGenerator generator)
|| (generator.rootGeneration() != 22 && (generator.rootGeneration() != 23 && generator.rootGeneration() != 24)) || !level.structureManager().shouldGenerateStructures()) return null;
|| (generator.rootGeneration() != 22 && (generator.rootGeneration() != 23 && generator.rootGeneration() != 24 && generator.rootGeneration() != 25)) || !level.structureManager().shouldGenerateStructures()) return null;
Holder<Structure> holder = level.registryAccess().lookupOrThrow(Registries.STRUCTURE).getOrThrow(KEY);
if (!structures.contains(holder)) return null;
var candidates = new ArrayList<LostCityUnderground16.Network>();
int originChunkX = origin.getX() >> 4, originChunkZ = origin.getZ() >> 4;
for (var network : generator.undergroundHall22().networks()) {
var island = generator.islandForChunk(network.startChunkX(), network.startChunkZ());
if (island == null || !island.origin() || (island.generation() != 22 && (island.generation() != 23 && island.generation() != 24)) || !island.status().equals("ready")) continue;
if (island == null || !island.origin() || (island.generation() != 22 && (island.generation() != 23 && island.generation() != 24 && island.generation() != 25)) || !island.status().equals("ready")) continue;
if (Math.abs((long) network.startChunkX() - originChunkX) <= radius
&& Math.abs((long) network.startChunkZ() - originChunkZ) <= radius) candidates.add(network);
}
@@ -19,7 +19,7 @@ public final class HeritageCommands19 {
var level = source.getLevel();
if (!Level.OVERWORLD.equals(level.dimension())
|| !(level.getChunkSource().getGenerator() instanceof SanctuaryChunkGenerator generator)
|| (generator.rootGeneration() != 19 && generator.rootGeneration() != 20 && generator.rootGeneration() != 21 && (generator.rootGeneration() != 22 && (generator.rootGeneration() != 23 && generator.rootGeneration() != 24))) || (generator.rootGeneration() != 22 && (generator.rootGeneration() != 23 && generator.rootGeneration() != 24)) && !generator.experimentalStructures()) {
|| (generator.rootGeneration() != 19 && generator.rootGeneration() != 20 && generator.rootGeneration() != 21 && (generator.rootGeneration() != 22 && (generator.rootGeneration() != 23 && generator.rootGeneration() != 24 && generator.rootGeneration() != 25))) || (generator.rootGeneration() != 22 && (generator.rootGeneration() != 23 && generator.rootGeneration() != 24 && generator.rootGeneration() != 25)) && !generator.experimentalStructures()) {
source.sendFailure(Component.translatable("sanctuary.heritage.unavailable"));
return 0;
}
@@ -28,20 +28,20 @@ public final class HeritageCommands19 {
for (var site : plan.sites()) source.sendSuccess(() -> Component.translatable("sanctuary.heritage.entry",
Component.translatable("sanctuary.heritage.kind." + site.kind().name().toLowerCase(Locale.ROOT)),
site.entry().x(), site.entry().y(), site.entry().z()), false);
if (generator.rootGeneration() == 21 || (generator.rootGeneration() == 22 || (generator.rootGeneration() == 23 || generator.rootGeneration() == 24))) for (var module : generator.transitPlan21().modules()) {
if (generator.rootGeneration() == 21 || (generator.rootGeneration() == 22 || (generator.rootGeneration() == 23 || generator.rootGeneration() == 24 || generator.rootGeneration() == 25))) for (var module : generator.transitPlan21().modules()) {
if (module.kind() != fr.koka.sanctuary.worldgen.transit.TransitPlan21.Kind.ACCESS) continue;
var at = fr.koka.sanctuary.worldgen.transit.TransitPiece21.locator(module);
source.sendSuccess(() -> Component.translatable("sanctuary.transit.entry", at.getX(), at.getY(), at.getZ()), false);
}
if ((generator.rootGeneration() == 22 || (generator.rootGeneration() == 23 || generator.rootGeneration() == 24))) for (var network : generator.undergroundHall22().networks()) {
if ((generator.rootGeneration() == 22 || (generator.rootGeneration() == 23 || generator.rootGeneration() == 24 || generator.rootGeneration() == 25))) for (var network : generator.undergroundHall22().networks()) {
var at = fr.koka.sanctuary.worldgen.city.LostCityUndergroundStructures22.locator(network);
source.sendSuccess(() -> Component.translatable("sanctuary.underground_hall.entry", at.getX(), at.getY(), at.getZ()), false);
}
if ((generator.rootGeneration() == 23 || generator.rootGeneration() == 24) && !generator.mineralSanctuary23().isEmpty()) {
if ((generator.rootGeneration() == 23 || generator.rootGeneration() == 24 || generator.rootGeneration() == 25) && !generator.mineralSanctuary23().isEmpty()) {
var at = generator.mineralSanctuary23().chamber().locator();
source.sendSuccess(() -> Component.translatable("sanctuary.mineral_sanctuary.entry", at.getX(), at.getY(), at.getZ()), false);
}
return plan.sites().size() + ((generator.rootGeneration() == 23 || generator.rootGeneration() == 24) && !generator.mineralSanctuary23().isEmpty() ? 1 : 0) + ((generator.rootGeneration() == 22 || (generator.rootGeneration() == 23 || generator.rootGeneration() == 24))
return plan.sites().size() + ((generator.rootGeneration() == 23 || generator.rootGeneration() == 24 || generator.rootGeneration() == 25) && !generator.mineralSanctuary23().isEmpty() ? 1 : 0) + ((generator.rootGeneration() == 22 || (generator.rootGeneration() == 23 || generator.rootGeneration() == 24 || generator.rootGeneration() == 25))
? (int) generator.transitPlan21().modules().stream().filter(m -> m.kind() == fr.koka.sanctuary.worldgen.transit.TransitPlan21.Kind.ACCESS).count()
+ generator.undergroundHall22().networks().size() : 0);
}))));
@@ -14,7 +14,7 @@ public final class HeritageStructure19 extends Structure {
@Override protected Optional<GenerationStub> findGenerationPoint(GenerationContext context) {
if (!(context.chunkGenerator() instanceof SanctuaryChunkGenerator generator)
|| (generator.rootGeneration() != 19 && generator.rootGeneration() != 20 && generator.rootGeneration() != 21 && (generator.rootGeneration() != 22 && (generator.rootGeneration() != 23 && generator.rootGeneration() != 24))) || !generator.experimentalStructures()) return Optional.empty();
|| (generator.rootGeneration() != 19 && generator.rootGeneration() != 20 && generator.rootGeneration() != 21 && (generator.rootGeneration() != 22 && (generator.rootGeneration() != 23 && generator.rootGeneration() != 24 && generator.rootGeneration() != 25))) || !generator.experimentalStructures()) return Optional.empty();
var island = generator.islandForChunk(context.chunkPos().x(), context.chunkPos().z());
if (island == null || !island.origin() || (island.generation() != generator.rootGeneration())) return Optional.empty();
var plan = generator.heritagePlan19();
@@ -23,9 +23,9 @@ public final class HeritageStructure19 extends Structure {
// Local route segments render before buildings. No piece needs a distant link endpoint loaded.
for (var segment : plan.segments())
if (segment.startChunkX() == x && segment.startChunkZ() == z)
pieces.add((generator.rootGeneration() == 21 || (generator.rootGeneration() == 22 || (generator.rootGeneration() == 23 || generator.rootGeneration() == 24))) ? new HeritagePiece21(segment) : generator.rootGeneration() == 20 ? new HeritagePiece20(segment) : new HeritagePiece19(segment));
pieces.add((generator.rootGeneration() == 21 || (generator.rootGeneration() == 22 || (generator.rootGeneration() == 23 || generator.rootGeneration() == 24 || generator.rootGeneration() == 25))) ? new HeritagePiece21(segment) : generator.rootGeneration() == 20 ? new HeritagePiece20(segment) : new HeritagePiece19(segment));
for (var site : plan.sites()) if (site.startChunkX() == x && site.startChunkZ() == z)
pieces.add((generator.rootGeneration() == 21 || (generator.rootGeneration() == 22 || (generator.rootGeneration() == 23 || generator.rootGeneration() == 24))) ? new HeritagePiece21(site) : generator.rootGeneration() == 20 ? new HeritagePiece20(site) : new HeritagePiece19(site));
pieces.add((generator.rootGeneration() == 21 || (generator.rootGeneration() == 22 || (generator.rootGeneration() == 23 || generator.rootGeneration() == 24 || generator.rootGeneration() == 25))) ? new HeritagePiece21(site) : generator.rootGeneration() == 20 ? new HeritagePiece20(site) : new HeritagePiece19(site));
if (pieces.isEmpty()) return Optional.empty();
for (var piece : pieces) {
var box = piece.getBoundingBox();
@@ -57,7 +57,7 @@ public final class HeritageStructures19 {
ChunkGeneratorStructureState state, StructureManager manager, ChunkAccess chunk,
StructureTemplateManager templates, ResourceKey<Level> level) {
if (!Level.OVERWORLD.equals(level) || !manager.shouldGenerateStructures()
|| !(generator instanceof SanctuaryChunkGenerator sanctuary) || (generator.rootGeneration() != 19 && generator.rootGeneration() != 20 && generator.rootGeneration() != 21 && (generator.rootGeneration() != 22 && (generator.rootGeneration() != 23 && generator.rootGeneration() != 24)))
|| !(generator instanceof SanctuaryChunkGenerator sanctuary) || (generator.rootGeneration() != 19 && generator.rootGeneration() != 20 && generator.rootGeneration() != 21 && (generator.rootGeneration() != 22 && (generator.rootGeneration() != 23 && generator.rootGeneration() != 24 && generator.rootGeneration() != 25)))
|| !sanctuary.experimentalStructures()) return;
var island = generator.islandForChunk(chunk.getPos().x(), chunk.getPos().z());
if (island == null || !island.origin() || island.generation() != generator.rootGeneration()) return;
@@ -79,7 +79,7 @@ public final class HeritageStructures19 {
BlockPos origin, int radius, boolean skipReferenced) {
if (!Level.OVERWORLD.equals(level.dimension())
|| !(level.getChunkSource().getGenerator() instanceof SanctuaryChunkGenerator generator)
|| (generator.rootGeneration() != 19 && generator.rootGeneration() != 20 && generator.rootGeneration() != 21 && (generator.rootGeneration() != 22 && (generator.rootGeneration() != 23 && generator.rootGeneration() != 24))) || !generator.experimentalStructures()
|| (generator.rootGeneration() != 19 && generator.rootGeneration() != 20 && generator.rootGeneration() != 21 && (generator.rootGeneration() != 22 && (generator.rootGeneration() != 23 && generator.rootGeneration() != 24 && generator.rootGeneration() != 25))) || !generator.experimentalStructures()
|| !level.structureManager().shouldGenerateStructures()) return null;
Holder<Structure> holder = level.registryAccess().lookupOrThrow(Registries.STRUCTURE).getOrThrow(KEY);
if (!structures.contains(holder)) return null;
@@ -21,12 +21,16 @@ import net.minecraft.world.level.storage.loot.LootTable;
/** Its complete immutable geometry is saved; a reload never consults the current planner. */
public final class MineralSanctuaryPiece23 extends StructurePiece {
private final MineralSanctuaryPlan23.Chamber chamber;
public MineralSanctuaryPiece23(MineralSanctuaryPlan23.Chamber chamber) {
private final int renderer;
public MineralSanctuaryPiece23(MineralSanctuaryPlan23.Chamber chamber) { this(chamber, 23); }
public MineralSanctuaryPiece23(MineralSanctuaryPlan23.Chamber chamber, int generation) {
super(MineralSanctuaryStructures23.PIECE, 0, chamber.bounds());
this.chamber = chamber; setOrientation(null);
this.chamber = chamber; renderer = generation == 25 ? 25 : 23; setOrientation(null);
}
public MineralSanctuaryPiece23(StructurePieceSerializationContext context, CompoundTag tag) {
super(MineralSanctuaryStructures23.PIECE, tag);
renderer = tag.getInt("SanctuaryMineralRenderer").orElse(23);
if (renderer != 23 && renderer != 25) throw new IllegalArgumentException("Unknown mineral renderer");
chamber = decode(tag.getCompound("SanctuaryMineral").orElseThrow());
if (!getBoundingBox().equals(chamber.bounds())) throw new IllegalArgumentException("Mineral piece bounding box mismatch");
setOrientation(null);
@@ -52,10 +56,11 @@ public final class MineralSanctuaryPiece23 extends StructurePiece {
@Override public BlockPos getLocatorPosition() { return chamber.locator(); }
@Override protected void addAdditionalSaveData(StructurePieceSerializationContext context, CompoundTag tag) {
tag.put("SanctuaryMineral", encode(chamber));
if (renderer == 25) tag.putInt("SanctuaryMineralRenderer", renderer);
}
@Override public void postProcess(WorldGenLevel level, StructureManager manager, ChunkGenerator generator,
RandomSource random, BoundingBox clip, ChunkPos chunk, BlockPos pivot) {
MineralSanctuaryStructures23.placing(() -> MineralSanctuaryRenderer23.render(chamber, new LostCityRenderer.BlockWriter() {
MineralSanctuaryStructures23.placing(() -> render(new LostCityRenderer.BlockWriter() {
@Override public void set(int x, int y, int z, BlockState state) {
var pos = new BlockPos(chamber.minX() + x, chamber.minY() + y, chamber.minZ() + z);
if (boundingBox.isInside(pos) && clip.isInside(pos)) placeBlock(level, state, pos.getX(), pos.getY(), pos.getZ(), clip);
@@ -63,7 +68,12 @@ public final class MineralSanctuaryPiece23 extends StructurePiece {
@Override public void chest(int x, int y, int z, ResourceKey<LootTable> loot, long seed) {
throw new IllegalStateException("Mineral sanctuary contains natural veins, not loot containers");
}
}, clip.minX() - chamber.minX(), clip.minZ() - chamber.minZ(),
clip.maxX() - chamber.minX(), clip.maxZ() - chamber.minZ()));
}, clip));
}
private void render(LostCityRenderer.BlockWriter writer, BoundingBox clip) {
int x0 = clip.minX() - chamber.minX(), z0 = clip.minZ() - chamber.minZ();
int x1 = clip.maxX() - chamber.minX(), z1 = clip.maxZ() - chamber.minZ();
if (renderer == 25) MineralSanctuaryRenderer25.render(chamber, writer, x0, z0, x1, z1);
else MineralSanctuaryRenderer23.render(chamber, writer, x0, z0, x1, z1);
}
}
@@ -0,0 +1,121 @@
package fr.koka.sanctuary.worldgen.mineral;
import fr.koka.sanctuary.worldgen.city.LostCityRenderer;
import net.minecraft.core.Direction;
import net.minecraft.world.level.block.StairBlock;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.state.BlockState;
/** Generation25 rock terraces with resolved corners and an inactive central blackstone frame. */
public final class MineralSanctuaryRenderer25 {
private MineralSanctuaryRenderer25() {}
public static void render(MineralSanctuaryPlan23.Chamber c, LostCityRenderer.BlockWriter out) {
render(c, out, 0, 0, c.width() - 1, c.depth() - 1);
}
/** Native chunk pieces render only their own columns; the saved absolute geometry stays identical. */
public static void render(MineralSanctuaryPlan23.Chamber c, LostCityRenderer.BlockWriter out,
int minX, int minZ, int maxX, int maxZ) {
for (int z = Math.max(0, minZ); z <= Math.min(c.depth() - 1, maxZ); z++)
for (int x = Math.max(0, minX); x <= Math.min(c.width() - 1, maxX); x++) {
int floor = c.floor(x, z), ceiling = c.ceiling(x, z);
double radius = c.radius(x, z);
boolean inside = radius <= 1, access = c.accessFootprint(x, z), pillar = c.pillar(x, z);
boolean fissure = c.portal(x, floor + 1, z);
for (int y = 0; y < c.height(); y++) {
boolean opening = fissure && y > floor && y <= floor + 5;
if (!inside && !opening && !(access && y <= floor)) continue;
boolean rock = y <= floor || y >= ceiling || radius > .95 || pillar;
if (opening) rock = false;
BlockState state = rock ? rock(c, x, y, z, floor, radius, pillar) : Blocks.AIR.defaultBlockState();
if (rock && y == floor) {
BlockState stair = stairAt(c, x, z);
if (stair != null) state = stair.setValue(StairBlock.SHAPE, shape(c, x, z, stair));
}
int lateral = c.facing() % 2 == 0 ? x - c.width() / 2 : z - c.depth() / 2;
int normal = c.facing() % 2 == 0 ? z - c.depth() / 2 : x - c.width() / 2;
int bottom = c.floor(c.width() / 2, c.depth() / 2);
if (normal == 0 && Math.abs(lateral) <= 3 && y >= bottom && y <= bottom + 8) {
state = Math.abs(lateral) == 3 || y == bottom || y == bottom + 8
? Blocks.BLACKSTONE.defaultBlockState() : Blocks.AIR.defaultBlockState();
}
out.set(x, y, z, state);
}
}
}
private static BlockState stairAt(MineralSanctuaryPlan23.Chamber c, int x, int z) {
if (x < 0 || x >= c.width() || z < 0 || z >= c.depth() || !c.footprint(x, z)
|| c.centralFloor(x, z) || c.pillar(x, z)) return null;
int floor = c.floor(x, z);
BlockState material = rock(c, x, floor, z, floor, c.radius(x, z), false);
if (!material.is(Blocks.STONE) && !material.is(Blocks.COBBLESTONE)) return null;
for (Direction low : new Direction[]{Direction.NORTH, Direction.EAST, Direction.SOUTH, Direction.WEST}) {
int nx = x + low.getStepX(), nz = z + low.getStepZ();
if (nx >= 0 && nx < c.width() && nz >= 0 && nz < c.depth() && c.footprint(nx, nz)
&& c.floor(nx, nz) == floor - 1) return
(material.is(Blocks.COBBLESTONE) ? Blocks.COBBLESTONE_STAIRS : Blocks.STONE_STAIRS)
.defaultBlockState().setValue(StairBlock.FACING, low.getOpposite());
}
return null;
}
private static net.minecraft.world.level.block.state.properties.StairsShape shape(
MineralSanctuaryPlan23.Chamber c, int x, int z, BlockState stair) {
var facing = stair.getValue(StairBlock.FACING);
for (boolean front : new boolean[]{true, false}) {
Direction step = front ? facing : facing.getOpposite();
int nx = x + step.getStepX(), nz = z + step.getStepZ();
BlockState other = stairAt(c, nx, nz);
if (other == null || c.floor(nx, nz) != c.floor(x, z)) continue;
Direction turn = other.getValue(StairBlock.FACING);
if (turn.getAxis() == facing.getAxis()) continue;
Direction side = front ? turn.getOpposite() : turn;
BlockState adjacent = stairAt(c, x + side.getStepX(), z + side.getStepZ());
if (adjacent != null && c.floor(x + side.getStepX(), z + side.getStepZ()) == c.floor(x, z)
&& adjacent.getValue(StairBlock.FACING) == facing) continue;
boolean left = turn == facing.getCounterClockWise();
return front
? (left ? net.minecraft.world.level.block.state.properties.StairsShape.OUTER_LEFT : net.minecraft.world.level.block.state.properties.StairsShape.OUTER_RIGHT)
: (left ? net.minecraft.world.level.block.state.properties.StairsShape.INNER_LEFT : net.minecraft.world.level.block.state.properties.StairsShape.INNER_RIGHT);
}
return net.minecraft.world.level.block.state.properties.StairsShape.STRAIGHT;
}
private static BlockState rock(MineralSanctuaryPlan23.Chamber c, int x, int y, int z, int floor, double radius, boolean pillar) {
// Each ore follows a coherent curved seam. The exposed floor and columns reveal a seam
// that continues into rock, instead of isolated reward blocks sprinkled through the air.
double phase = (c.seed() & 1023) * .006;
double seam = z + Math.sin(x * .065 + phase) * 4 + Math.sin(y * .19) * 2;
boolean seamRock = y <= floor + 2 || radius > .90 && y <= floor + 12 || pillar;
Block ore = null;
if (seamRock && Math.abs(seam - c.depth() * .29) < 1.8) ore = Blocks.COAL_ORE;
else if (seamRock && Math.abs(seam - c.depth() * .67) < 1.5) ore = Blocks.IRON_ORE;
else if (seamRock && Math.abs(x + Math.sin(z * .09 + phase) * 3 - c.width() * .64) < 1.4) ore = Blocks.COPPER_ORE;
if (ore != null) return ore.defaultBlockState();
Block rare = rareVein(c, x, y, z);
if (rare != null) return rare.defaultBlockState();
long pattern = mix(c.seed() ^ ((long) (x / 4) << 40) ^ ((long) (z / 4) << 20) ^ y / 3);
if (Math.floorMod(pattern, 13) < 2) return Blocks.COBBLESTONE.defaultBlockState();
if (c.minY() + y < 100 && Math.floorMod(pattern, 5) == 0) return Blocks.DEEPSLATE.defaultBlockState();
return switch (Math.floorMod(pattern, 29)) {
case 0, 1 -> Blocks.ANDESITE.defaultBlockState();
case 2 -> Blocks.DIORITE.defaultBlockState();
case 3 -> Blocks.GRANITE.defaultBlockState();
case 4, 5 -> Blocks.TUFF.defaultBlockState();
default -> Blocks.STONE.defaultBlockState();
};
}
/** Rare compact three-dimensional pockets depend on depth and seed, never a guaranteed reward. */
private static Block rareVein(MineralSanctuaryPlan23.Chamber c, int x, int y, int z) {
long roll = mix(c.seed() ^ 0x23DA7AL);
if (c.minY() >= 160 || (roll & 1) == 0) return null;
int vx = c.width() / 2 + (int) Math.floorMod(roll >>> 8, 37) - 18;
int vz = c.depth() / 2 + ((roll & 4) == 0 ? -26 : 26);
int vy = c.floor(vx, vz) - 1;
double dx = x - vx, dy = y - vy, dz = z - vz;
if (dx * dx / 5 + dy * dy / 3 + dz * dz / 4 > 1) return null;
if (c.minY() < 96 && Math.floorMod(roll >>> 16, 6) == 0) return Blocks.DIAMOND_ORE;
if (c.minY() < 128 && Math.floorMod(roll >>> 16, 3) == 0) return Blocks.REDSTONE_ORE;
if (Math.floorMod(roll >>> 20, 7) == 0) return Blocks.EMERALD_ORE;
return Blocks.GOLD_ORE;
}
private static long mix(long n) { n = (n ^ n >>> 30) * 0xbf58476d1ce4e5b9L; n = (n ^ n >>> 27) * 0x94d049bb133111ebL; return n ^ n >>> 31; }
}
@@ -11,14 +11,14 @@ public final class MineralSanctuaryStructure23 extends Structure {
public MineralSanctuaryStructure23(StructureSettings settings) { super(settings); }
@Override protected Optional<GenerationStub> findGenerationPoint(GenerationContext context) {
if (!(context.chunkGenerator() instanceof SanctuaryChunkGenerator generator)
|| (generator.rootGeneration() != 23 && generator.rootGeneration() != 24) || !generator.experimentalStructures()) return Optional.empty();
|| (generator.rootGeneration() != 23 && generator.rootGeneration() != 24 && generator.rootGeneration() != 25) || !generator.experimentalStructures()) return Optional.empty();
var island = generator.islandForChunk(context.chunkPos().x(), context.chunkPos().z());
if (island == null || !island.origin() || (island.generation() != 23 && island.generation() != 24)) return Optional.empty();
if (island == null || !island.origin() || (island.generation() != 23 && island.generation() != 24 && island.generation() != 25)) return Optional.empty();
var plan = generator.mineralSanctuary23();
if (plan.isEmpty()) return Optional.empty();
var c = plan.chamber();
if (c.startChunkX() != context.chunkPos().x() || c.startChunkZ() != context.chunkPos().z()) return Optional.empty();
return Optional.of(new GenerationStub(c.locator(), builder -> builder.addPiece(new MineralSanctuaryPiece23(c))));
return Optional.of(new GenerationStub(c.locator(), builder -> builder.addPiece(new MineralSanctuaryPiece23(c, generator.rootGeneration()))));
}
@Override public StructureType<?> type() { return MineralSanctuaryStructures23.TYPE; }
}
@@ -40,9 +40,9 @@ public final class MineralSanctuaryStructures23 {
ChunkGeneratorStructureState state, StructureManager manager, ChunkAccess chunk,
StructureTemplateManager templates, ResourceKey<Level> level) {
if (!Level.OVERWORLD.equals(level) || !manager.shouldGenerateStructures()
|| !(generator instanceof SanctuaryChunkGenerator sanctuary) || (generator.rootGeneration() != 23 && generator.rootGeneration() != 24) || !sanctuary.experimentalStructures()) return;
|| !(generator instanceof SanctuaryChunkGenerator sanctuary) || (generator.rootGeneration() != 23 && generator.rootGeneration() != 24 && generator.rootGeneration() != 25) || !sanctuary.experimentalStructures()) return;
var island = generator.islandForChunk(chunk.getPos().x(), chunk.getPos().z());
if (island == null || !island.origin() || (island.generation() != 23 && island.generation() != 24)) return;
if (island == null || !island.origin() || (island.generation() != 23 && island.generation() != 24 && island.generation() != 25)) return;
var plan = sanctuary.mineralSanctuary23();
if (plan.isEmpty() || plan.chamber().startChunkX() != chunk.getPos().x()
|| plan.chamber().startChunkZ() != chunk.getPos().z()) return;
@@ -63,14 +63,14 @@ public final class MineralSanctuaryStructures23 {
BlockPos origin, int radius, boolean skipReferenced) {
if (!Level.OVERWORLD.equals(level.dimension())
|| !(level.getChunkSource().getGenerator() instanceof SanctuaryChunkGenerator generator)
|| (generator.rootGeneration() != 23 && generator.rootGeneration() != 24) || !level.structureManager().shouldGenerateStructures()) return null;
|| (generator.rootGeneration() != 23 && generator.rootGeneration() != 24 && generator.rootGeneration() != 25) || !level.structureManager().shouldGenerateStructures()) return null;
Holder<Structure> holder = level.registryAccess().lookupOrThrow(Registries.STRUCTURE).getOrThrow(KEY);
if (!structures.contains(holder)) return null;
var plan = generator.mineralSanctuary23();
if (plan.isEmpty() || !generator.experimentalStructures()) return null;
var c = plan.chamber();
var island = generator.islandForChunk(c.startChunkX(), c.startChunkZ());
if (island == null || !island.origin() || (island.generation() != 23 && island.generation() != 24) || !island.status().equals("ready")) return null;
if (island == null || !island.origin() || (island.generation() != 23 && island.generation() != 24 && island.generation() != 25) || !island.status().equals("ready")) return null;
if (Math.abs((long) c.startChunkX() - (origin.getX() >> 4)) > radius
|| Math.abs((long) c.startChunkZ() - (origin.getZ() >> 4)) > radius) return null;
var chunk = level.getChunk(c.startChunkX(), c.startChunkZ(), ChunkStatus.STRUCTURE_STARTS, true);
@@ -13,7 +13,7 @@ public final class RuinsStructure17 extends Structure {
@Override protected Optional<GenerationStub> findGenerationPoint(GenerationContext context) {
if (!(context.chunkGenerator() instanceof SanctuaryChunkGenerator generator)
|| (generator.rootGeneration() != 17 && generator.rootGeneration() != 19 && generator.rootGeneration() != 20 && generator.rootGeneration() != 21 && (generator.rootGeneration() != 22 && (generator.rootGeneration() != 23 && generator.rootGeneration() != 24))) || !generator.experimentalStructures()) return Optional.empty();
|| (generator.rootGeneration() != 17 && generator.rootGeneration() != 19 && generator.rootGeneration() != 20 && generator.rootGeneration() != 21 && (generator.rootGeneration() != 22 && (generator.rootGeneration() != 23 && generator.rootGeneration() != 24 && generator.rootGeneration() != 25))) || !generator.experimentalStructures()) return Optional.empty();
var island = generator.islandForChunk(context.chunkPos().x(), context.chunkPos().z());
if (island == null || !island.origin() || island.generation() != generator.rootGeneration()) return Optional.empty();
var networks = generator.ruinsPlan17().networks().stream()
@@ -30,9 +30,9 @@ public final class RuinsStructure17 extends Structure {
return Optional.of(new GenerationStub(RuinsStructures17.locator(networks.getFirst()), pieces -> {
// Shafts come last so their surface hatches cannot be filled by a connected room.
networks.forEach(n -> n.rooms().stream().filter(r -> r.kind() != RuinsPlan17.Kind.ACCESS)
.forEach(r -> pieces.addPiece((generator.rootGeneration() == 20 || generator.rootGeneration() == 21 || (generator.rootGeneration() == 22 || (generator.rootGeneration() == 23 || generator.rootGeneration() == 24))) ? new RuinsPiece20(r) : new RuinsPiece17(r))));
.forEach(r -> pieces.addPiece((generator.rootGeneration() == 20 || generator.rootGeneration() == 21 || (generator.rootGeneration() == 22 || (generator.rootGeneration() == 23 || generator.rootGeneration() == 24 || generator.rootGeneration() == 25))) ? new RuinsPiece20(r) : new RuinsPiece17(r))));
networks.forEach(n -> n.rooms().stream().filter(r -> r.kind() == RuinsPlan17.Kind.ACCESS)
.forEach(r -> pieces.addPiece((generator.rootGeneration() == 20 || generator.rootGeneration() == 21 || (generator.rootGeneration() == 22 || (generator.rootGeneration() == 23 || generator.rootGeneration() == 24))) ? new RuinsPiece20(r) : new RuinsPiece17(r))));
.forEach(r -> pieces.addPiece((generator.rootGeneration() == 20 || generator.rootGeneration() == 21 || (generator.rootGeneration() == 22 || (generator.rootGeneration() == 23 || generator.rootGeneration() == 24 || generator.rootGeneration() == 25))) ? new RuinsPiece20(r) : new RuinsPiece17(r))));
}));
}
@Override public StructureType<?> type() { return RuinsStructures17.TYPE; }
@@ -55,7 +55,7 @@ public final class RuinsStructures17 {
ChunkGeneratorStructureState state, StructureManager manager, ChunkAccess chunk,
StructureTemplateManager templates, ResourceKey<Level> level) {
if (!Level.OVERWORLD.equals(level) || !manager.shouldGenerateStructures()
|| !(generator instanceof SanctuaryChunkGenerator sanctuary) || (generator.rootGeneration() != 17 && generator.rootGeneration() != 19 && generator.rootGeneration() != 20 && generator.rootGeneration() != 21 && (generator.rootGeneration() != 22 && (generator.rootGeneration() != 23 && generator.rootGeneration() != 24)))
|| !(generator instanceof SanctuaryChunkGenerator sanctuary) || (generator.rootGeneration() != 17 && generator.rootGeneration() != 19 && generator.rootGeneration() != 20 && generator.rootGeneration() != 21 && (generator.rootGeneration() != 22 && (generator.rootGeneration() != 23 && generator.rootGeneration() != 24 && generator.rootGeneration() != 25)))
|| !sanctuary.experimentalStructures()) return;
var island = generator.islandForChunk(chunk.getPos().x(), chunk.getPos().z());
if (island == null || !island.origin() || island.generation() != generator.rootGeneration()) return;
@@ -78,7 +78,7 @@ public final class RuinsStructures17 {
BlockPos origin, int radius, boolean skipReferenced) {
if (!Level.OVERWORLD.equals(level.dimension())
|| !(level.getChunkSource().getGenerator() instanceof SanctuaryChunkGenerator generator)
|| (generator.rootGeneration() != 17 && generator.rootGeneration() != 19 && generator.rootGeneration() != 20 && generator.rootGeneration() != 21 && (generator.rootGeneration() != 22 && (generator.rootGeneration() != 23 && generator.rootGeneration() != 24))) || !generator.experimentalStructures()
|| (generator.rootGeneration() != 17 && generator.rootGeneration() != 19 && generator.rootGeneration() != 20 && generator.rootGeneration() != 21 && (generator.rootGeneration() != 22 && (generator.rootGeneration() != 23 && generator.rootGeneration() != 24 && generator.rootGeneration() != 25))) || !generator.experimentalStructures()
|| !level.structureManager().shouldGenerateStructures()) return null;
Holder<Structure> holder = level.registryAccess().lookupOrThrow(Registries.STRUCTURE).getOrThrow(KEY);
if (!structures.contains(holder)) return null;
@@ -12,9 +12,9 @@ public final class TransitStructure21 extends Structure {
public TransitStructure21(StructureSettings settings) { super(settings); }
@Override protected Optional<GenerationStub> findGenerationPoint(GenerationContext context) {
if (!(context.chunkGenerator() instanceof SanctuaryChunkGenerator generator)
|| (generator.rootGeneration() != 21 && (generator.rootGeneration() != 22 && (generator.rootGeneration() != 23 && generator.rootGeneration() != 24))) || (generator.rootGeneration() != 22 && (generator.rootGeneration() != 23 && generator.rootGeneration() != 24)) && !generator.experimentalStructures()) return Optional.empty();
|| (generator.rootGeneration() != 21 && (generator.rootGeneration() != 22 && (generator.rootGeneration() != 23 && generator.rootGeneration() != 24 && generator.rootGeneration() != 25))) || (generator.rootGeneration() != 22 && (generator.rootGeneration() != 23 && generator.rootGeneration() != 24 && generator.rootGeneration() != 25)) && !generator.experimentalStructures()) return Optional.empty();
var island = generator.islandForChunk(context.chunkPos().x(), context.chunkPos().z());
if (island == null || !island.origin() || (island.generation() != 21 && (island.generation() != 22 && (island.generation() != 23 && island.generation() != 24)))) return Optional.empty();
if (island == null || !island.origin() || (island.generation() != 21 && (island.generation() != 22 && (island.generation() != 23 && island.generation() != 24 && island.generation() != 25)))) return Optional.empty();
var modules = generator.transitPlan21().modulesForChunk(context.chunkPos().x(), context.chunkPos().z());
if (modules.isEmpty()) return Optional.empty();
for (var module : modules) {
@@ -25,7 +25,7 @@ public final class TransitStructure21 extends Structure {
throw new IllegalStateException("Transit module exceeds local native reference range");
}
return Optional.of(new GenerationStub(TransitPiece21.locator(modules.getFirst()), pieces ->
modules.forEach(module -> pieces.addPiece((generator.rootGeneration() == 22 || (generator.rootGeneration() == 23 || generator.rootGeneration() == 24))
modules.forEach(module -> pieces.addPiece((generator.rootGeneration() == 22 || (generator.rootGeneration() == 23 || generator.rootGeneration() == 24 || generator.rootGeneration() == 25))
&& module.kind() == TransitPlan21.Kind.JUNCTION && Integer.bitCount(module.openings()) == 2
? new TransitPiece22(module) : new TransitPiece21(module)))));
}
@@ -46,10 +46,10 @@ public final class TransitStructures21 {
ChunkGeneratorStructureState state, StructureManager manager, ChunkAccess chunk,
StructureTemplateManager templates, ResourceKey<Level> level) {
if (!Level.OVERWORLD.equals(level) || !manager.shouldGenerateStructures()
|| !(generator instanceof SanctuaryChunkGenerator sanctuary) || (generator.rootGeneration() != 21 && (generator.rootGeneration() != 22 && (generator.rootGeneration() != 23 && generator.rootGeneration() != 24)))
|| (generator.rootGeneration() != 22 && (generator.rootGeneration() != 23 && generator.rootGeneration() != 24)) && !sanctuary.experimentalStructures()) return;
|| !(generator instanceof SanctuaryChunkGenerator sanctuary) || (generator.rootGeneration() != 21 && (generator.rootGeneration() != 22 && (generator.rootGeneration() != 23 && generator.rootGeneration() != 24 && generator.rootGeneration() != 25)))
|| (generator.rootGeneration() != 22 && (generator.rootGeneration() != 23 && generator.rootGeneration() != 24 && generator.rootGeneration() != 25)) && !sanctuary.experimentalStructures()) return;
var island = generator.islandForChunk(chunk.getPos().x(), chunk.getPos().z());
if (island == null || !island.origin() || (island.generation() != 21 && (island.generation() != 22 && (island.generation() != 23 && island.generation() != 24)))) return;
if (island == null || !island.origin() || (island.generation() != 21 && (island.generation() != 22 && (island.generation() != 23 && island.generation() != 24 && island.generation() != 25)))) return;
if (sanctuary.transitPlan21().modulesForChunk(chunk.getPos().x(), chunk.getPos().z()).isEmpty()) return;
Holder<Structure> holder = registries.lookupOrThrow(Registries.STRUCTURE).getOrThrow(KEY);
StructureStart previous = manager.getStartForStructure(holder.value(), chunk);
@@ -66,7 +66,7 @@ public final class TransitStructures21 {
BlockPos origin, int radius, boolean skipReferenced) {
if (!Level.OVERWORLD.equals(level.dimension())
|| !(level.getChunkSource().getGenerator() instanceof SanctuaryChunkGenerator generator)
|| (generator.rootGeneration() != 21 && (generator.rootGeneration() != 22 && (generator.rootGeneration() != 23 && generator.rootGeneration() != 24))) || (generator.rootGeneration() != 22 && (generator.rootGeneration() != 23 && generator.rootGeneration() != 24)) && !generator.experimentalStructures()
|| (generator.rootGeneration() != 21 && (generator.rootGeneration() != 22 && (generator.rootGeneration() != 23 && generator.rootGeneration() != 24 && generator.rootGeneration() != 25))) || (generator.rootGeneration() != 22 && (generator.rootGeneration() != 23 && generator.rootGeneration() != 24 && generator.rootGeneration() != 25)) && !generator.experimentalStructures()
|| !level.structureManager().shouldGenerateStructures()) return null;
Holder<Structure> holder = level.registryAccess().lookupOrThrow(Registries.STRUCTURE).getOrThrow(KEY);
if (!structures.contains(holder)) return null;
@@ -0,0 +1,55 @@
package fr.koka.sanctuary.worldgen.underground25;
import com.mojang.serialization.MapCodec;
import fr.koka.sanctuary.worldgen.SanctuaryChunkGenerator;
import fr.koka.sanctuary.worldgen.Terrain25Runtime;
import net.minecraft.core.BlockPos;
import net.minecraft.util.RandomSource;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.WorldGenLevel;
import net.minecraft.world.level.chunk.ChunkGenerator;
import net.minecraft.world.level.levelgen.feature.Feature;
import net.minecraft.world.level.levelgen.feature.MonsterRoomFeature;
import net.minecraft.world.level.levelgen.structure.BoundingBox;
/** Vanilla dungeon placement at generation25 altitudes, independently of the optional Sanctuary secrets. */
public final class MonsterRoom25Feature implements Feature {
public static final MapCodec<MonsterRoom25Feature> CODEC = MapCodec.unit(MonsterRoom25Feature::new);
@Override public MapCodec<MonsterRoom25Feature> codec() { return CODEC; }
@Override public boolean place(WorldGenLevel level, ChunkGenerator ignored, RandomSource random, BlockPos origin) {
if (!(level.getLevel().getChunkSource().getGenerator() instanceof SanctuaryChunkGenerator generator)
|| generator.rootGeneration() != 25 || !Level.OVERWORLD.equals(level.getLevel().dimension())
|| !level.getLevel().structureManager().shouldGenerateStructures()) return false;
var island = generator.islandAt(origin.getX(), origin.getZ());
if (island == null || !island.origin()) return false;
long seed = random.nextLong();
var dimensions = RandomSource.create(seed);
int rx = dimensions.nextInt(2) + 3, rz = dimensions.nextInt(2) + 3;
var box = new BoundingBox(origin.getX() - rx, origin.getY() - 1, origin.getZ() - rz,
origin.getX() + rx, origin.getY() + 4, origin.getZ() + rz);
if (box.minY() < 1 || box.maxY() >= level.getMaxY()
|| !island.contains(box.minX(), box.minZ()) || !island.contains(box.maxX(), box.maxZ())
|| generator.intersectsExperimentalStructures(box.minX(), box.minY(), box.minZ(), box.maxX(), box.maxY(), box.maxZ())) return false;
var noise = generator.delegate(island); var state = generator.randomState(island);
int openings = 0;
// Refuse the complete placement before the first write: a protected wall must not produce half a dungeon.
for (int z = box.minZ(); z <= box.maxZ(); z++) for (int x = box.minX(); x <= box.maxX(); x++) {
if (!level.getBlockState(new BlockPos(x, box.minY(), z)).isSolid()
|| !level.getBlockState(new BlockPos(x, box.maxY(), z)).isSolid()) return false;
for (int y = box.minY(); y <= box.maxY(); y++) {
var at = new BlockPos(x, y, z);
if (!level.getBlockState(at).getFluidState().isEmpty() || generator.protectsCityDecoration(at)
|| Terrain25Runtime.protects(noise, state, at, 1, 1, 1)
|| !level.ensureCanWrite(at)) return false;
}
if ((x == box.minX() || x == box.maxX() || z == box.minZ() || z == box.maxZ())
&& level.isEmptyBlock(new BlockPos(x, origin.getY(), z))
&& level.isEmptyBlock(new BlockPos(x, origin.getY() + 1, z))) openings++;
}
if (openings < 1 || openings > 5) return false;
final boolean[] placed = { false };
UndergroundPlacement25.placingBounds(box,
() -> placed[0] = new MonsterRoomFeature().place(level, generator, RandomSource.create(seed), origin));
return placed[0];
}
}
@@ -0,0 +1,78 @@
package fr.koka.sanctuary.worldgen.underground25;
import java.util.function.BiConsumer;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.DoorBlock;
import net.minecraft.world.level.block.Rotation;
import net.minecraft.world.level.block.StairBlock;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.block.state.properties.DoubleBlockHalf;
import net.minecraft.world.level.levelgen.structure.BoundingBox;
/** Pure geometry shared by actual placement and shape/support diagnostics. */
public final class UndergroundGeometry25 {
private UndergroundGeometry25() {}
public static int roofAt(BoundingBox box, int x, int z, boolean access) {
if (x < box.minX() || x > box.maxX() || z < box.minZ() || z > box.maxZ()) return -1;
if (box.getXSpan() <= 7 || box.getZSpan() <= 7 || access) return box.maxY();
double dx = Math.max(0, Math.max(box.minX() + 5 - x, x - (box.maxX() - 5))) / 5.0;
double dz = Math.max(0, Math.max(box.minZ() + 5 - z, z - (box.maxZ() - 5))) / 5.0;
double edge = dx * dx + dz * dz;
return edge > 1 ? -1 : box.maxY() - (int) Math.round(edge * 3);
}
public record Cabin(BoundingBox bounds, int floor, BlockPos entrance) {
public Cabin {
if (bounds.getXSpan() != 9 || bounds.getZSpan() != 9 || bounds.minY() != floor - 1 || bounds.maxY() != floor + 6)
throw new IllegalArgumentException("Miner cabin requires its complete 9x8x9 envelope");
entrance = entrance.immutable();
int x = entrance.getX() - bounds.minX(), z = entrance.getZ() - bounds.minZ();
if (entrance.getY() != floor || !((x == 4 && (z == 1 || z == 7)) || (z == 4 && (x == 1 || x == 7))))
throw new IllegalArgumentException("Miner entrance is not a supported door position");
}
public Rotation rotation() {
int x = entrance.getX() - bounds.minX(), z = entrance.getZ() - bounds.minZ();
return z == 1 ? Rotation.NONE : x == 7 ? Rotation.CLOCKWISE_90 : z == 7 ? Rotation.CLOCKWISE_180 : Rotation.COUNTERCLOCKWISE_90;
}
public BlockPos position(int x, int y, int z) {
int rx = x, rz = z;
switch (rotation()) {
case CLOCKWISE_90 -> { rx = 8 - z; rz = x; }
case CLOCKWISE_180 -> { rx = 8 - x; rz = 8 - z; }
case COUNTERCLOCKWISE_90 -> { rx = z; rz = 8 - x; }
default -> {}
}
return new BlockPos(bounds.minX() + rx, y, bounds.minZ() + rz);
}
public BlockPos chest() { return position(6, floor, 3); }
public void render(BiConsumer<BlockPos, BlockState> writer) {
int y0 = floor - 1;
for (int z = 0; z < 9; z++) for (int x = 0; x < 9; x++) for (int y = 0; y <= 7; y++) {
BlockState state = Blocks.CAVE_AIR.defaultBlockState();
if (y == 0) state = Blocks.COBBLESTONE.defaultBlockState();
if (y >= 1 && y <= 4 && x >= 1 && x <= 7 && z >= 1 && z <= 7 && (x == 1 || x == 7 || z == 1 || z == 7))
state = (y == 1 ? Blocks.STONE_BRICKS : Blocks.OAK_PLANKS).defaultBlockState();
if (y >= 1 && y <= 4 && (x == 1 || x == 7) && (z == 1 || z == 7)) state = Blocks.STRIPPED_OAK_LOG.defaultBlockState();
if (y == 4 && x >= 1 && x <= 7 && z >= 1 && z <= 7 && (x == 1 || x == 7 || z == 1 || z == 7)) state = Blocks.OAK_PLANKS.defaultBlockState();
int roof = 4 + Math.min(x, 8 - x);
if (roof <= 7 && y == roof) state = Blocks.OAK_STAIRS.defaultBlockState().setValue(StairBlock.FACING, x < 4 ? Direction.EAST : Direction.WEST);
if ((z == 1 || z == 7) && x >= 2 && x <= 6 && y >= 4 && y < roof) state = Blocks.OAK_PLANKS.defaultBlockState();
if (x == 4 && y == 7) state = Blocks.OAK_PLANKS.defaultBlockState();
if ((x == 1 || x == 7) && z >= 3 && z <= 4 && y == 3) state = Blocks.GLASS.defaultBlockState();
if (x == 4 && z == 1 && (y == 1 || y == 2)) state = Blocks.OAK_DOOR.defaultBlockState()
.setValue(DoorBlock.FACING, Direction.NORTH).setValue(DoorBlock.HALF, y == 1 ? DoubleBlockHalf.LOWER : DoubleBlockHalf.UPPER);
put(writer, x, y0 + y, z, state);
}
put(writer, 2, floor, 6, Blocks.CRAFTING_TABLE.defaultBlockState());
put(writer, 3, floor, 6, Blocks.FURNACE.defaultBlockState());
put(writer, 6, floor, 6, Blocks.BARREL.defaultBlockState());
put(writer, 6, floor + 1, 6, Blocks.LANTERN.defaultBlockState());
put(writer, 2, floor, 3, Blocks.OAK_STAIRS.defaultBlockState().setValue(StairBlock.FACING, Direction.WEST));
// The runtime creates this chest separately to attach native loot exactly once.
}
private void put(BiConsumer<BlockPos, BlockState> writer, int x, int y, int z, BlockState state) {
writer.accept(position(x, y, z), state.rotate(rotation()));
}
}
}
@@ -0,0 +1,125 @@
package fr.koka.sanctuary.worldgen.underground25;
import java.util.ArrayList;
import java.util.List;
import net.minecraft.core.BlockPos;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.nbt.ListTag;
import net.minecraft.util.RandomSource;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.StructureManager;
import net.minecraft.world.level.WorldGenLevel;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.Rotation;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.chunk.ChunkGenerator;
import net.minecraft.world.level.levelgen.feature.MonsterRoomFeature;
import net.minecraft.world.level.levelgen.structure.BoundingBox;
import net.minecraft.world.level.levelgen.structure.StructurePiece;
import net.minecraft.world.level.levelgen.structure.pieces.StructurePieceSerializationContext;
/** Saved cavity/raccord and the small miner shelter. Village buildings and mines remain native pieces. */
public final class UndergroundPiece25 extends StructurePiece {
public enum Kind { VILLAGE_CAVERN, ACCESS, MINER_CABIN, MONSTER_ROOM }
private final Kind kind;
private final int floorY;
private final long seed;
private final BlockPos entrance;
private final List<BoundingBox> volumes;
private boolean featureAttempted;
private boolean featurePlaced;
public UndergroundPiece25(Kind kind, int floorY, long seed, BlockPos entrance, List<BoundingBox> volumes) {
super(UndergroundStructures25.PIECE, 0, bounds(volumes));
this.kind = kind; this.floorY = floorY; this.seed = seed; this.entrance = entrance.immutable();
this.volumes = List.copyOf(volumes); setOrientation(null); validate();
}
public UndergroundPiece25(StructurePieceSerializationContext context, CompoundTag tag) {
super(UndergroundStructures25.PIECE, tag);
if (tag.getIntOr("Schema", 0) != 1) throw new IllegalArgumentException("Unknown underground25 piece schema");
kind = Kind.valueOf(tag.getString("Kind").orElseThrow()); floorY = tag.getInt("Floor").orElseThrow();
seed = tag.getLong("Seed").orElseThrow();
entrance = new BlockPos(tag.getInt("EntryX").orElseThrow(), tag.getInt("EntryY").orElseThrow(), tag.getInt("EntryZ").orElseThrow());
List<BoundingBox> decoded = new ArrayList<>();
ListTag list = tag.getList("Volumes").orElseThrow();
if (list.isEmpty() || list.size() > 512) throw new IllegalArgumentException("Invalid underground25 volume count");
for (int i = 0; i < list.size(); i++) {
var t = list.getCompound(i).orElseThrow();
decoded.add(new BoundingBox(t.getInt("x0").orElseThrow(), t.getInt("y0").orElseThrow(), t.getInt("z0").orElseThrow(),
t.getInt("x1").orElseThrow(), t.getInt("y1").orElseThrow(), t.getInt("z1").orElseThrow()));
}
volumes = List.copyOf(decoded);
featureAttempted = tag.getBooleanOr("FeatureAttempted", false);
featurePlaced = tag.getBooleanOr("FeaturePlaced", false);
setOrientation(null); validate();
if (!getBoundingBox().equals(bounds(volumes))) throw new IllegalArgumentException("Underground25 piece bounds mismatch");
}
private void validate() {
if (floorY < 3 || floorY > 345 || volumes.isEmpty() || volumes.size() > 512
|| getBoundingBox().getXSpan() > 256 || getBoundingBox().getZSpan() > 256
|| getBoundingBox().minY() < 1 || getBoundingBox().maxY() > 358)
throw new IllegalArgumentException("Invalid underground25 geometry");
if (kind == Kind.MINER_CABIN) cabinShape();
if (featurePlaced && (!featureAttempted || kind != Kind.MONSTER_ROOM))
throw new IllegalArgumentException("Invalid underground25 feature state");
}
public static BoundingBox bounds(List<BoundingBox> volumes) {
return BoundingBox.encapsulatingBoxes(volumes).orElseThrow();
}
public Kind kind() { return kind; }
public int floorY() { return floorY; }
public long seed() { return seed; }
public List<BoundingBox> volumes() { return volumes; }
public boolean featureAttempted() { return featureAttempted; }
public boolean featurePlaced() { return featurePlaced; }
@Override public BlockPos getLocatorPosition() { return entrance; }
public boolean projects(int x, int z) {
return kind == Kind.VILLAGE_CAVERN && volumes.stream().anyMatch(b -> x >= b.minX() && x <= b.maxX() && z >= b.minZ() && z <= b.maxZ());
}
public BlockState terrainOverride(int x, int y, int z) {
if (kind != Kind.VILLAGE_CAVERN && kind != Kind.ACCESS) return null;
if (!getBoundingBox().isInside(x, y, z)) return null;
int roof = -1;
for (var volume : volumes) roof = Math.max(roof, UndergroundGeometry25.roofAt(volume, x, z, kind == Kind.ACCESS));
if (roof < floorY + 2 || y < floorY - 2 || y > roof) return null;
if (y == floorY - 2) return Blocks.STONE.defaultBlockState();
if (y == floorY - 1) return (kind == Kind.ACCESS ? Blocks.STONE : Blocks.DIRT).defaultBlockState();
return Blocks.CAVE_AIR.defaultBlockState();
}
@Override protected void addAdditionalSaveData(StructurePieceSerializationContext context, CompoundTag tag) {
tag.putInt("Schema", 1); tag.putString("Kind", kind.name()); tag.putInt("Floor", floorY); tag.putLong("Seed", seed);
tag.putInt("EntryX", entrance.getX()); tag.putInt("EntryY", entrance.getY()); tag.putInt("EntryZ", entrance.getZ());
ListTag list = new ListTag();
for (var b : volumes) {
var t = new CompoundTag(); t.putInt("x0", b.minX()); t.putInt("y0", b.minY()); t.putInt("z0", b.minZ());
t.putInt("x1", b.maxX()); t.putInt("y1", b.maxY()); t.putInt("z1", b.maxZ()); list.add(t);
}
tag.put("Volumes", list); tag.putBoolean("FeatureAttempted", featureAttempted); tag.putBoolean("FeaturePlaced", featurePlaced);
}
@Override public void postProcess(WorldGenLevel level, StructureManager manager, ChunkGenerator generator,
RandomSource random, BoundingBox clip, ChunkPos chunk, BlockPos pivot) {
if (kind == Kind.MONSTER_ROOM) { monsterRoom(level, generator, clip); return; }
if (kind == Kind.MINER_CABIN) { cabin(level, clip); return; }
var b = getBoundingBox();
for (int z = Math.max(b.minZ(), clip.minZ()); z <= Math.min(b.maxZ(), clip.maxZ()); z++)
for (int x = Math.max(b.minX(), clip.minX()); x <= Math.min(b.maxX(), clip.maxX()); x++)
for (int y = b.minY(); y <= b.maxY(); y++) {
var block = terrainOverride(x, y, z);
if (block != null) placeBlock(level, block, x, y, z, clip);
}
}
private synchronized void monsterRoom(WorldGenLevel level, ChunkGenerator generator, BoundingBox clip) {
if (featureAttempted || !clip.isInside(entrance)) return;
// The exact vanilla feature owns its opening checks, one spawner, chest placement and mob distribution.
featureAttempted = true;
featurePlaced = new MonsterRoomFeature().place(level, generator, RandomSource.create(seed), entrance);
}
public UndergroundGeometry25.Cabin cabinShape() { return new UndergroundGeometry25.Cabin(getBoundingBox(), floorY, entrance); }
private void cabin(WorldGenLevel level, BoundingBox clip) {
cabinShape().render((pos, state) -> placeBlock(level, state, pos.getX(), pos.getY(), pos.getZ(), clip));
createChest(level, clip, RandomSource.create(seed), chestPosition(), UndergroundStructures25.MINER_LOOT, null);
}
public Rotation cabinRotation() { return cabinShape().rotation(); }
public BlockPos cabinPosition(int x, int y, int z) { return cabinShape().position(x, y, z); }
public BlockPos chestPosition() { return cabinShape().chest(); }
}
@@ -0,0 +1,60 @@
package fr.koka.sanctuary.worldgen.underground25;
import fr.koka.sanctuary.worldgen.SanctuaryChunkGenerator;
import java.util.List;
import net.minecraft.core.BlockPos;
import net.minecraft.core.registries.Registries;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.WorldGenLevel;
import net.minecraft.world.level.levelgen.structure.BoundingBox;
import net.minecraft.world.level.levelgen.structure.StructureStart;
/** Scope comes from saved pieces, so roads use the same underground floor after a partial-world reload. */
public final class UndergroundPlacement25 {
public record Context(List<BoundingBox> bounds, UndergroundPiece25 cavern) {}
private static final ThreadLocal<Context> CURRENT = new ThreadLocal<>();
private UndergroundPlacement25() {}
public static boolean isPlacing() { return CURRENT.get() != null; }
public static boolean mayWrite(BlockPos pos) {
var context = CURRENT.get();
return context != null && context.bounds().stream().anyMatch(box -> box.isInside(pos));
}
public static Integer projectedHeight(int x, int z) {
var context = CURRENT.get();
if (context == null || context.cavern() == null || !context.cavern().projects(x, z)) return null;
return context.cavern().floorY();
}
public static void placing(WorldGenLevel level, StructureStart start, Runnable action) {
if (!(level.getLevel().getChunkSource().getGenerator() instanceof SanctuaryChunkGenerator generator)
|| generator.rootGeneration() != 25 || !Level.OVERWORLD.equals(level.getLevel().dimension())) {
action.run(); return;
}
var structures = level.registryAccess().lookupOrThrow(Registries.STRUCTURE);
// Compare the persisted native identity and start chunk, never the in-memory preview object.
// This also recognizes a partially generated mineshaft or hut loaded from its own NBT.
var planned = UndergroundStructures25.plan(generator).starts().stream().filter(candidate ->
candidate.start().getChunkPos().equals(start.getChunkPos())
&& structures.getOrThrow(candidate.key()).value() == start.getStructure()).findFirst().orElse(null);
if (planned == null) { action.run(); return; }
UndergroundPiece25 cavern = null;
for (var piece : start.getPieces()) if (piece instanceof UndergroundPiece25 extra) {
if (extra.kind() == UndergroundPiece25.Kind.VILLAGE_CAVERN) cavern = extra;
}
var previous = CURRENT.get();
CURRENT.set(new Context(start.getPieces().stream().map(p -> writeBounds(planned.key().identifier().toString(), p.getBoundingBox())).toList(), cavern));
try { action.run(); } finally { if (previous == null) CURRENT.remove(); else CURRENT.set(previous); }
}
/** Native supports use the same vertical margin that previewAt already certified against other structures. */
public static BoundingBox writeBounds(String id, BoundingBox box) {
if (!id.equals("minecraft:mineshaft") && !id.equals("minecraft:swamp_hut")) return box;
return new BoundingBox(box.minX(), Math.max(1, box.minY() - 12), box.minZ(),
box.maxX(), Math.min(358, box.maxY() + 4), box.maxZ());
}
public static void placingRoom(UndergroundPiece25 room, Runnable action) {
placingBounds(room.getBoundingBox(), action);
}
public static void placingBounds(BoundingBox bounds, Runnable action) {
var previous = CURRENT.get(); CURRENT.set(new Context(List.of(bounds), null));
try { action.run(); } finally { if (previous == null) CURRENT.remove(); else CURRENT.set(previous); }
}
}
@@ -0,0 +1,44 @@
package fr.koka.sanctuary.worldgen.underground25;
import java.util.List;
import java.util.Map;
import net.minecraft.core.BlockPos;
import net.minecraft.resources.ResourceKey;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.levelgen.structure.BoundingBox;
import net.minecraft.world.level.levelgen.structure.Structure;
import net.minecraft.world.level.levelgen.structure.StructureStart;
/** Immutable admission result. Native pieces themselves acquire their normal one-shot placement flags. */
public record UndergroundPlan25(List<PlannedStart> starts, List<UndergroundPiece25> rooms, Map<String, Integer> diagnostics, int columns) {
public static final UndergroundPlan25 EMPTY = new UndergroundPlan25(List.of(), List.of(), Map.of(), 0);
public UndergroundPlan25 { starts = List.copyOf(starts); rooms = List.copyOf(rooms); diagnostics = Map.copyOf(diagnostics); }
public record PlannedStart(ResourceKey<Structure> key, StructureStart start, BlockPos entrance) {
public PlannedStart { entrance = entrance.immutable(); }
}
public boolean intersects(BoundingBox box) {
for (var planned : starts) for (var piece : planned.start().getPieces())
if (UndergroundPlacement25.writeBounds(planned.key().identifier().toString(), piece.getBoundingBox()).intersects(box)) return true;
for (var room : rooms) if (room.getBoundingBox().intersects(box)) return true;
return false;
}
public boolean protects(BlockPos pos) {
for (var planned : starts) for (var piece : planned.start().getPieces())
if (UndergroundPlacement25.writeBounds(planned.key().identifier().toString(), piece.getBoundingBox()).isInside(pos)) return true;
for (var room : rooms) if (room.getBoundingBox().isInside(pos)) return true;
return false;
}
/** Shared with final terrain sampling. A null value leaves natural terrain untouched. */
public BlockState terrainOverride(int x, int y, int z) {
for (var planned : starts) for (var piece : planned.start().getPieces())
if (piece instanceof UndergroundPiece25 extra) {
BlockState state = extra.terrainOverride(x, y, z);
if (state != null) return state;
}
return null;
}
public boolean carves(int x, int y, int z) {
var state = terrainOverride(x, y, z);
return state != null && state.isAir();
}
}
@@ -0,0 +1,389 @@
package fr.koka.sanctuary.worldgen.underground25;
import com.google.gson.JsonObject;
import com.mojang.serialization.JsonOps;
import fr.koka.sanctuary.SanctuaryMod;
import fr.koka.sanctuary.expansion.ExpansionChunkGenerator;
import fr.koka.sanctuary.expansion.ExpansionIsland;
import fr.koka.sanctuary.expansion.ExpansionStructures23;
import fr.koka.sanctuary.worldgen.SanctuaryChunkGenerator;
import fr.koka.sanctuary.worldgen.Terrain25Runtime;
import fr.koka.sanctuary.worldgen.Terrain25Shape;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Holder;
import net.minecraft.core.registries.Registries;
import net.minecraft.resources.Identifier;
import net.minecraft.resources.RegistryOps;
import net.minecraft.resources.ResourceKey;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.util.RandomSource;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.LevelHeightAccessor;
import net.minecraft.world.level.NoiseColumn;
import net.minecraft.world.level.biome.Biomes;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.chunk.PalettedContainerFactory;
import net.minecraft.world.level.chunk.ProtoChunk;
import net.minecraft.world.level.chunk.UpgradeData;
import net.minecraft.world.level.levelgen.Heightmap;
import net.minecraft.world.level.levelgen.RandomState;
import net.minecraft.world.level.levelgen.densityfunction.SamplerContext;
import net.minecraft.world.level.levelgen.structure.BoundingBox;
import net.minecraft.world.level.levelgen.structure.PoolElementStructurePiece;
import net.minecraft.world.level.levelgen.structure.Structure;
import net.minecraft.world.level.levelgen.structure.StructurePiece;
import net.minecraft.world.level.levelgen.structure.StructureStart;
import net.minecraft.world.level.levelgen.structure.pieces.PiecesContainer;
/** Bounded read-only admission over final natural terrain. Large native graphs are refused, never truncated. */
public final class UndergroundPlanner25 {
public static final int MAX_COLUMNS = 16_000;
public static final int MAX_VILLAGE_ATTEMPTS = 18;
private UndergroundPlanner25() {}
public static UndergroundPlan25 plan(ServerLevel level, SanctuaryChunkGenerator generator) {
var planner = new Planner(level, generator);
try { planner.village(); } catch (BudgetExceeded ex) { planner.refuse("village:terrain-budget"); }
planner.nextStage();
try { planner.mine(); } catch (BudgetExceeded ex) { planner.refuse("mine:terrain-budget"); }
planner.nextStage();
try { planner.hut(); } catch (BudgetExceeded ex) { planner.refuse("hut:terrain-budget"); }
planner.nextStage();
try { planner.rooms(); } catch (BudgetExceeded ex) { planner.refuse("rooms:terrain-budget"); }
return new UndergroundPlan25(planner.accepted, planner.roomFeatures, planner.diagnostics, planner.columnsUsed + planner.terrain.columns.size());
}
private static final class BudgetExceeded extends RuntimeException {}
private static final class Terrain {
final ServerLevel level;
final SanctuaryChunkGenerator generator;
final ExpansionIsland root;
final Map<Long, NoiseColumn> columns = new HashMap<>();
Terrain(ServerLevel level, SanctuaryChunkGenerator generator, ExpansionIsland root) { this.level = level; this.generator = generator; this.root = root; }
NoiseColumn column(int x, int z) {
long key = ((long) x << 32) ^ (z & 0xffffffffL);
var result = columns.get(key);
if (result == null) {
if (columns.size() >= MAX_COLUMNS) throw new BudgetExceeded();
result = generator.getBaseColumn(x, z, level, generator.randomState(root)); columns.put(key, result);
}
return result;
}
BlockState at(int x, int y, int z) { return column(x, z).getBlock(y); }
boolean solid(int x, int y, int z) { var state = at(x, y, z); return state.isSolid() && state.getFluidState().isEmpty(); }
boolean air(int x, int y, int z) { return at(x, y, z).isAir(); }
boolean protectedAt(int x, int y, int z) {
return !at(x, y, z).getFluidState().isEmpty()
|| Terrain25Runtime.protects(generator.delegate(root), generator.randomState(root), new BlockPos(x, y, z), 1, 3, 2);
}
int top(int x, int z) {
var col = column(x, z);
for (int y = 358; y > 0; y--) if (!col.getBlock(y).isAir()) return y;
return 0;
}
}
private static final class Planner {
final ServerLevel level;
final SanctuaryChunkGenerator generator;
final ExpansionIsland root;
Terrain terrain;
int columnsUsed;
final Terrain25Shape.Plan shape;
final List<UndergroundPlan25.PlannedStart> accepted = new ArrayList<>();
final List<UndergroundPiece25> roomFeatures = new ArrayList<>();
final Map<String, Integer> diagnostics = new LinkedHashMap<>();
final long deadline;
Planner(ServerLevel level, SanctuaryChunkGenerator generator) {
this.level = level; this.generator = generator;
root = generator.source().islands().stream().filter(ExpansionIsland::origin).findFirst().orElseThrow();
terrain = new Terrain(level, generator, root);
shape = Terrain25Runtime.shape(generator.delegate(root), generator.randomState(root));
deadline = Long.MAX_VALUE;
}
void refuse(String reason) { diagnostics.merge(reason, 1, Integer::sum); }
void checkBudget() { /* Bounds are counts, never elapsed time: identical results on every host. */ }
void nextStage() { columnsUsed += terrain.columns.size(); terrain = new Terrain(level, generator, root); }
boolean conflict(BoundingBox b) {
if (b.minY() < 3 || b.maxY() > 357 || !root.contains(b.minX(), b.minZ()) || !root.contains(b.maxX(), b.maxZ())) return true;
if (generator.intersectsExperimentalStructures(b.minX(), b.minY(), b.minZ(), b.maxX(), b.maxY(), b.maxZ())) return true;
for (var a : accepted) for (var p : a.start().getPieces())
if (UndergroundPlacement25.writeBounds(a.key().identifier().toString(), p.getBoundingBox()).intersects(b)) return true;
for (var room : roomFeatures) if (room.getBoundingBox().intersects(b)) return true;
return false;
}
Holder.Reference<Structure> holder(ResourceKey<Structure> key) { return level.registryAccess().lookupOrThrow(Registries.STRUCTURE).getOrThrow(key); }
void add(ResourceKey<Structure> key, StructureStart start, BlockPos entrance) {
accepted.add(new UndergroundPlan25.PlannedStart(key, start, entrance)); refuse("accepted:" + key.identifier());
}
void village() {
int attempts = 0;
var chambers = shape.chambers().stream().filter(c -> c.kind() == Terrain25Shape.Kind.DRY)
.sorted(Comparator.comparingDouble((Terrain25Shape.Chamber c) -> c.rx() * c.rz()).reversed()).toList();
for (var chamber : chambers) for (int offset = 0; offset < 3; offset++) {
if (attempts++ >= MAX_VILLAGE_ATTEMPTS) { refuse("village:attempt-budget"); return; }
checkBudget();
int x = (int) Math.round(chamber.x()) + (offset == 1 ? 16 : offset == 2 ? -16 : 0);
int z = (int) Math.round(chamber.z()); int floor = chamber.floorY() + 1;
var chunk = new ChunkPos(x >> 4, z >> 4);
StructureStart nativeStart = villageStart(chunk, floor);
if (!nativeStart.isValid()) { refuse("village:native-refused"); continue; }
long houses = nativeStart.getPieces().stream().filter(p -> p instanceof PoolElementStructurePiece pool
&& pool.getElement().toString().contains("/houses/")).count();
if (houses < 6) { refuse("village:not-extended"); continue; }
boolean deepConflict = false;
BlockPos deepAt = null; String deepReason = null;
deepPieces:
for (var piece : nativeStart.getPieces()) {
var box = piece.getBoundingBox();
if (conflict(box)) { deepConflict = true; deepAt = box.getCenter(); deepReason = "reserved-volume"; break; }
// Wells and other native sub-pieces can extend below the common road floor.
if (box.minY() < floor - 2) for (int pz = box.minZ(); pz <= box.maxZ(); pz += 2)
for (int px = box.minX(); px <= box.maxX(); px += 2) {
if (!terrain.solid(px, box.minY() - 1, pz)) {
deepConflict = true; deepAt = new BlockPos(px, box.minY() - 1, pz); deepReason = "missing-deep-support"; break deepPieces;
}
for (int py = box.minY() - 1; py < floor - 2; py++)
if (terrain.protectedAt(px, py, pz)) {
deepConflict = true; deepAt = new BlockPos(px, py, pz); deepReason = "protected-deep-volume"; break deepPieces;
}
}
}
if (deepConflict) {
SanctuaryMod.LOGGER.info("Underground25 village chamber {} start {} floor {} refused {} at {}", chamber.id(), chunk, floor, deepReason, deepAt);
refuse("village:native-deep-piece-conflict"); continue;
}
List<BoundingBox> volumes = new ArrayList<>();
for (var p : nativeStart.getPieces()) {
var b = p.getBoundingBox();
volumes.add(new BoundingBox(b.minX() - 6, floor - 2, b.minZ() - 6,
b.maxX() + 6, Math.max(floor + 10, b.maxY() + 5), b.maxZ() + 6));
}
String rejection = certifyVillage(volumes, floor);
if (rejection != null) { refuse("village:" + rejection); continue; }
var streets = nativeStart.getPieces().stream().filter(p -> p instanceof PoolElementStructurePiece pool
&& pool.getElement().toString().contains("/streets/")).toList();
var buildings = nativeStart.getPieces().stream().filter(p -> !streets.contains(p)).toList();
var access = access(streets, floor, volumes, buildings);
if (access == null) { refuse("village:no-natural-access"); continue; }
volumes.addAll(access.volumes());
var complete = UndergroundPiece25.bounds(volumes);
if (complete.minX() < chunk.getMinBlockX() - 128 || complete.maxX() > chunk.getMaxBlockX() + 128
|| complete.minZ() < chunk.getMinBlockZ() - 128 || complete.maxZ() > chunk.getMaxBlockZ() + 128
|| complete.getXSpan() > 256 || complete.getZSpan() > 256) {
refuse("village:native-reference-range"); continue;
}
var cavity = new UndergroundPiece25(UndergroundPiece25.Kind.VILLAGE_CAVERN, floor,
mix(root.seed() ^ chunk.pack()), access.entrance(), volumes);
List<StructurePiece> pieces = new ArrayList<>(); pieces.add(cavity); pieces.addAll(nativeStart.getPieces());
var start = new StructureStart(holder(UndergroundStructures25.VILLAGE).value(), chunk, 0, new PiecesContainer(pieces));
add(UndergroundStructures25.VILLAGE, start, access.entrance()); return;
}
refuse("village:no-fitting-cavern");
}
StructureStart villageStart(ChunkPos chunk, int floor) {
var ops = RegistryOps.create(JsonOps.INSTANCE, level.registryAccess());
Structure original = level.registryAccess().lookupOrThrow(Registries.STRUCTURE).getValue(Identifier.withDefaultNamespace("village_plains"));
JsonObject json = Structure.DIRECT_CODEC.encodeStart(ops, original).getOrThrow().getAsJsonObject().deepCopy();
json.addProperty("start_pool", "sanctuary:underground25/zombie_plains_starts");
json.addProperty("size", 6); json.addProperty("max_distance_from_center", 80);
json.addProperty("terrain_adaptation", "none"); json.remove("project_start_to_heightmap");
var height = new JsonObject(); height.addProperty("absolute", floor); json.add("start_height", height);
var structure = Structure.DIRECT_CODEC.parse(ops, json).getOrThrow();
var random = generator.randomState(root);
var view = new FloorView(generator, level, floor);
var proto = new ProtoChunk(chunk, UpgradeData.EMPTY, level, PalettedContainerFactory.create(level.registryAccess()), null);
var start = structure.generate(holder(UndergroundStructures25.VILLAGE), level.dimension(), level.registryAccess(), view,
generator.getBiomeSource(), random.createClimateSampler(SamplerContext.builder().enableCaches().build()), random,
level.getStructureTemplateManager(), root.seed(), chunk, 0, proto, b -> !b.is(Biomes.DEEP_DARK));
if (!start.isValid()) return start;
return new StructureStart(holder(UndergroundStructures25.VILLAGE).value(), chunk, 0, new PiecesContainer(start.getPieces()));
}
String certifyVillage(List<BoundingBox> volumes, int floor) {
int count = 0, naturallyOpen = 0, roof = 0;
for (var b : volumes) {
checkBudget(); if (conflict(b)) return "protected-or-outside";
for (int z = b.minZ() + 3; z <= b.maxZ() - 2; z += 4) for (int x = b.minX() + 3; x <= b.maxX() - 2; x += 4) {
if (!terrain.solid(x, floor - 1, z) || !terrain.solid(x, floor - 2, z)) {
SanctuaryMod.LOGGER.info("Underground25 village floor {} unsupported at {},{}: surface {}, below {}",
floor, x, z, terrain.at(x, floor - 1, z), terrain.at(x, floor - 2, z));
return "unsupported-floor";
}
count++; if (terrain.air(x, floor + 2, z) && terrain.air(x, floor + 6, z)) naturallyOpen++;
if (terrain.solid(x, b.maxY() + 2, z) || terrain.solid(x, b.maxY() + 5, z)) roof++;
for (int y = floor - 1; y <= b.maxY() + 2; y += 3) if (terrain.protectedAt(x, y, z)) return "protected-water";
}
}
if (count == 0 || naturallyOpen * 100 < count * 60) return "cavern-too-small";
if (roof * 100 < count * 55) return "insufficient-rock-cover";
return null;
}
record Access(BlockPos entrance, List<BoundingBox> volumes, int dx, int dz) {}
Access access(List<StructurePiece> pieces, int floor, List<BoundingBox> occupied) {
return access(pieces, floor, occupied, List.of());
}
Access access(List<StructurePiece> pieces, int floor, List<BoundingBox> occupied, List<StructurePiece> obstacles) {
// Begin at real rooms/streets; a route is accepted only when it reaches supported natural cave air.
var origins = pieces.stream().sorted(Comparator.comparingInt(p -> p.getBoundingBox().getXSpan() * p.getBoundingBox().getZSpan())).limit(18).toList();
for (var piece : origins) for (int facing = 0; facing < 4; facing++) {
checkBudget(); var b = piece.getBoundingBox();
int dx = facing == 0 ? 1 : facing == 2 ? -1 : 0, dz = facing == 1 ? 1 : facing == 3 ? -1 : 0;
int x0 = dx > 0 ? b.maxX() : dx < 0 ? b.minX() : b.getCenter().getX();
int z0 = dz > 0 ? b.maxZ() : dz < 0 ? b.minZ() : b.getCenter().getZ();
List<BoundingBox> route = new ArrayList<>(); int open = 0;
for (int step = 0; step <= 40; step++) {
int x = x0 + dx * step, z = z0 + dz * step;
boolean outside = occupied.stream().noneMatch(v -> x >= v.minX() && x <= v.maxX() && z >= v.minZ() && z <= v.maxZ());
var slice = new BoundingBox(x - Math.abs(dz), floor - 2, z - Math.abs(dx), x + Math.abs(dz), floor + 3, z + Math.abs(dx));
if (outside && conflict(slice)) break;
if (obstacles.stream().anyMatch(p -> p.getBoundingBox().intersects(slice))) break;
boolean pass = true, natural = true;
for (int side = -1; side <= 1; side++) {
int sx = x - dz * side, sz = z + dx * side;
if (!terrain.solid(sx, floor - 2, sz)) { pass = false; break; }
for (int y = floor - 1; y <= floor + 3; y++) if (terrain.protectedAt(sx, y, sz)) pass = false;
natural &= terrain.solid(sx, floor - 1, sz) && terrain.air(sx, floor, sz) && terrain.air(sx, floor + 1, sz);
}
if (!pass) break;
route.add(slice); open = outside && natural ? open + 1 : 0;
if (open >= 3 && Math.hypot(x, z) >= 48) return new Access(new BlockPos(x, floor, z), List.copyOf(route), dx, dz);
}
}
return null;
}
void mine() {
int attempts = 0;
for (var candidate : ExpansionStructures23.candidates(root)) {
if (attempts++ >= 16) break; checkBudget();
var nativeMine = ExpansionStructures23.previewAt(level, root, "minecraft:mineshaft", candidate, deadline);
if (nativeMine == null) { refuse("mine:native-refused"); continue; }
if (nativeMine.start().getPieces().stream().anyMatch(p -> conflict(p.getBoundingBox()))) { refuse("mine:protected"); continue; }
var minePieces = nativeMine.start().getPieces();
// The mine itself is useful even if no safe attached shelter can be found.
var key = nativeMine.structure().unwrapKey().orElseThrow();
UndergroundPiece25 cabin = null, approach = null;
// Prefer corridors already close to a natural chamber instead of whichever pieces the recursive native graph emitted first.
for (var piece : minePieces.stream().filter(p -> p.getBoundingBox().getYSpan() <= 4)
.sorted(Comparator.comparingDouble(this::caveDistance)).limit(32).toList()) {
int floor = piece.getBoundingBox().minY();
var access = access(List.of(piece), floor, minePieces.stream().map(StructurePiece::getBoundingBox).toList());
if (access == null) continue;
int x = access.entrance().getX(), z = access.entrance().getZ();
int ox = x - (access.dx() < 0 ? 7 : access.dx() > 0 ? 1 : 4);
int oz = z - (access.dz() < 0 ? 7 : access.dz() > 0 ? 1 : 4);
var box = new BoundingBox(ox, floor - 1, oz, ox + 8, floor + 6, oz + 8);
if (conflict(box) || minePieces.stream().anyMatch(p -> p.getBoundingBox().intersects(box))) continue;
boolean supported = true; int open = 0, cells = 0;
for (int bz = box.minZ(); bz <= box.maxZ(); bz += 2) for (int bx = box.minX(); bx <= box.maxX(); bx += 2) {
supported &= terrain.solid(bx, floor - 1, bz);
for (int y = floor; y <= floor + 6; y++) { cells++; if (terrain.air(bx, y, bz)) open++; if (terrain.protectedAt(bx, y, bz)) supported = false; }
}
if (!supported || open * 100 < cells * 65) continue;
cabin = new UndergroundPiece25(UndergroundPiece25.Kind.MINER_CABIN, floor, mix(root.seed() ^ candidate.roll()),
new BlockPos(x, floor, z), List.of(box));
approach = new UndergroundPiece25(UndergroundPiece25.Kind.ACCESS, floor, root.seed(), access.entrance(), access.volumes());
break;
}
add(key, nativeMine.start(), nativeMine.start().getPieces().getFirst().getLocatorPosition());
if (cabin != null) {
var position = new ChunkPos(cabin.getLocatorPosition().getX() >> 4, cabin.getLocatorPosition().getZ() >> 4);
var start = new StructureStart(holder(UndergroundStructures25.MINER_CABIN).value(), position, 0, new PiecesContainer(List.of(approach, cabin)));
add(UndergroundStructures25.MINER_CABIN, start, cabin.getLocatorPosition());
} else refuse("cabin:no-accessible-mine-shelter");
return;
}
refuse("mine:no-fitting-native-start");
}
double caveDistance(StructurePiece piece) {
var center = piece.getBoundingBox().getCenter(); double nearest = Double.POSITIVE_INFINITY;
for (var chamber : shape.chambers()) if (chamber.kind() == Terrain25Shape.Kind.DRY) {
double dx = Math.max(0, Math.abs(center.getX() - chamber.x()) - chamber.rx() * .7);
double dz = Math.max(0, Math.abs(center.getZ() - chamber.z()) - chamber.rz() * .7);
double dy = piece.getBoundingBox().minY() - chamber.floorY() - 1;
nearest = Math.min(nearest, dx * dx + dz * dz + 9 * dy * dy);
}
return nearest;
}
void hut() {
int attempts = 0;
var candidates = new LinkedHashMap<Long, ExpansionStructures23.Candidate>();
// Real wetland micro-biome, sampled before the native biome predicate, rather than a forced valid flag.
int range = root.terrainRadius() >> 4;
for (int cz = -range; cz <= range; cz++) for (int cx = -range; cx <= range; cx++) {
int x = (cx << 4) + 8, z = (cz << 4) + 8;
if (shape.wetland(x, 192, z)) {
var chunk = new ChunkPos(cx, cz);
candidates.put(chunk.pack(), new ExpansionStructures23.Candidate(chunk, mix(root.seed() ^ chunk.pack())));
}
}
for (var candidate : ExpansionStructures23.candidates(root)) candidates.putIfAbsent(candidate.chunk().pack(), candidate);
for (var candidate : candidates.values()) {
checkBudget();
int x = candidate.chunk().getMiddleBlockX(), z = candidate.chunk().getMiddleBlockZ();
if (Math.hypot(x, z) < 64) continue;
boolean water = false;
for (int dz = -12; dz <= 12 && !water; dz += 6) for (int dx = -12; dx <= 12; dx += 6) {
int top = terrain.top(x + dx, z + dz);
if (!terrain.at(x + dx, top, z + dz).getFluidState().isEmpty()) { water = true; break; }
}
if (!water) continue;
if (attempts++ >= 24) break;
var hut = ExpansionStructures23.previewAt(level, root, "minecraft:swamp_hut", candidate, deadline);
if (hut == null || conflict(hut.start().getBoundingBox())) { refuse("hut:shore-refused"); continue; }
add(hut.structure().unwrapKey().orElseThrow(), hut.start(), hut.start().getPieces().getFirst().getLocatorPosition()); return;
}
refuse("hut:no-native-marsh-shore");
}
void rooms() {
int admitted = 0, tries = 0;
// A vanilla room needs a low natural doorway and solid roof. Search beside actual passages,
// where this geometry can occur, rather than against the tall open wall of a great cavern.
for (var passage : shape.passages()) {
if (passage.from().kind() != Terrain25Shape.Kind.DRY || passage.to().kind() != Terrain25Shape.Kind.DRY) continue;
double dx = passage.to().x() - passage.from().x(), dz = passage.to().z() - passage.from().z();
double length = Math.hypot(dx, dz);
if (length < 1) continue;
for (int step = 2; step <= 8; step++) for (int side : new int[]{-1, 1}) for (int distance = 2; distance <= 5; distance++) for (int height = -1; height <= 1; height++) {
if (admitted >= 3 || tries++ >= 336) { if (admitted == 0) refuse("rooms:no-native-doorway"); return; }
checkBudget();
double t = step / 10.0;
double bend = Math.sin(t * Math.PI * 2 + passage.phase()) * 9 * Math.sin(t * Math.PI);
double offset = bend + side * (passage.width() + distance);
int x = (int) Math.round(passage.from().x() + t * dx - dz / length * offset);
int z = (int) Math.round(passage.from().z() + t * dz + dx / length * offset);
int y = (int) Math.floor(passage.from().y() + (passage.to().y() - passage.from().y()) * t
+ Math.sin(t * Math.PI * 3 + passage.phase()) * 4 * Math.sin(t * Math.PI)) - 1 + height;
long seed = mix(root.seed() ^ ((long) x << 32) ^ z ^ 0xD025L);
var random = RandomSource.create(seed); int rx = random.nextInt(2) + 3, rz = random.nextInt(2) + 3;
var box = new BoundingBox(x - rx, y - 1, z - rz, x + rx, y + 4, z + rz);
if (conflict(box)) continue;
if (!monsterRoomFits(terrain, new BlockPos(x, y, z), seed)) continue;
var piece = new UndergroundPiece25(UndergroundPiece25.Kind.MONSTER_ROOM, y, seed, new BlockPos(x, y, z), List.of(box));
roomFeatures.add(piece); refuse("accepted:monster_room_feature"); admitted++;
}
}
if (admitted == 0) refuse("rooms:no-native-doorway");
}
}
private static boolean monsterRoomFits(Terrain terrain, BlockPos origin, long seed) {
var random = RandomSource.create(seed); int rx = random.nextInt(2) + 3, rz = random.nextInt(2) + 3, openings = 0;
for (int dz = -rz; dz <= rz; dz++) for (int dx = -rx; dx <= rx; dx++) {
int x = origin.getX() + dx, z = origin.getZ() + dz, y = origin.getY();
if (!terrain.solid(x, y - 1, z) || !terrain.solid(x, y + 4, z)) return false;
for (int dy = -1; dy <= 4; dy++) if (terrain.protectedAt(x, y + dy, z)) return false;
if ((Math.abs(dx) == rx || Math.abs(dz) == rz) && terrain.air(x, y, z) && terrain.air(x, y + 1, z)) openings++;
}
return openings >= 1 && openings <= 5;
}
private static final class FloorView extends ExpansionChunkGenerator {
final SanctuaryChunkGenerator host; final int floor;
FloorView(SanctuaryChunkGenerator host, ServerLevel level, int floor) { super(host.source(), host.initialDiameter(), true); this.host = host; this.floor = floor; this.registries = level.registryAccess(); }
@Override public int getBaseHeight(int x, int z, Heightmap.Types type, LevelHeightAccessor height, RandomState state) { return floor; }
@Override public NoiseColumn getBaseColumn(int x, int z, LevelHeightAccessor height, RandomState state) { return host.getBaseColumn(x, z, height, state); }
@Override public RandomState randomState(ExpansionIsland island) { return host.randomState(island); }
}
private static long mix(long value) { value = (value ^ (value >>> 30)) * 0xbf58476d1ce4e5b9L; value = (value ^ (value >>> 27)) * 0x94d049bb133111ebL; return value ^ (value >>> 31); }
}
@@ -0,0 +1,14 @@
package fr.koka.sanctuary.worldgen.underground25;
import com.mojang.serialization.MapCodec;
import java.util.Optional;
import net.minecraft.world.level.levelgen.structure.Structure;
import net.minecraft.world.level.levelgen.structure.StructureType;
/** Registry identity for admitted initial-world sites. No random-spread placement or dimension-wide injection. */
public final class UndergroundStructure25 extends Structure {
public static final MapCodec<UndergroundStructure25> CODEC = simpleCodec(UndergroundStructure25::new);
public UndergroundStructure25(StructureSettings settings) { super(settings); }
@Override protected Optional<GenerationStub> findGenerationPoint(GenerationContext context) { return Optional.empty(); }
@Override public StructureType<?> type() { return UndergroundStructures25.TYPE; }
}
@@ -0,0 +1,80 @@
package fr.koka.sanctuary.worldgen.underground25;
import fr.koka.sanctuary.SanctuaryMod;
import fr.koka.sanctuary.expansion.ExpansionChunkGenerator;
import fr.koka.sanctuary.worldgen.SanctuaryChunkGenerator;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.WeakHashMap;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Registry;
import net.minecraft.core.RegistryAccess;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.core.registries.Registries;
import net.minecraft.resources.ResourceKey;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.StructureManager;
import net.minecraft.world.level.chunk.ChunkAccess;
import net.minecraft.world.level.chunk.ChunkGeneratorStructureState;
import net.minecraft.world.level.levelgen.structure.Structure;
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;
import net.minecraft.world.level.storage.loot.LootTable;
/** Initial generation25 only. No injected structure sets, overwritten vanilla pools, or old-world retrofit. */
public final class UndergroundStructures25 {
public static final ResourceKey<Structure> VILLAGE = key("underground_abandoned_village");
public static final ResourceKey<Structure> MINER_CABIN = key("miner_cabin");
public static final List<ResourceKey<Structure>> KEYS = List.of(VILLAGE, MINER_CABIN);
public static final ResourceKey<LootTable> MINER_LOOT = ResourceKey.create(Registries.LOOT_TABLE, SanctuaryMod.id("chests/miner_cabin25"));
public static final StructureType<UndergroundStructure25> TYPE = Registry.register(BuiltInRegistries.STRUCTURE_TYPE,
SanctuaryMod.id("underground_site_v25"), () -> UndergroundStructure25.CODEC);
public static final StructurePieceType PIECE = Registry.register(BuiltInRegistries.STRUCTURE_PIECE,
SanctuaryMod.id("underground_piece_v25"), UndergroundPiece25::new);
private static final Map<SanctuaryChunkGenerator, UndergroundPlan25> PLANS = Collections.synchronizedMap(new WeakHashMap<>());
private UndergroundStructures25() {}
private static ResourceKey<Structure> key(String id) { return ResourceKey.create(Registries.STRUCTURE, SanctuaryMod.id(id)); }
public static void register() {
Registry.register(BuiltInRegistries.FEATURE_TYPE, SanctuaryMod.id("monster_room25"), MonsterRoom25Feature.CODEC);
}
public static UndergroundPlan25 plan(SanctuaryChunkGenerator generator) { return PLANS.getOrDefault(generator, UndergroundPlan25.EMPTY); }
public static boolean isPlacing() { return UndergroundPlacement25.isPlacing(); }
public static boolean mayWrite(BlockPos pos) { return UndergroundPlacement25.mayWrite(pos); }
/** Called once after terrain, core secrets and aerial reservations are available, before requesting new chunks. */
public static synchronized void prepare(ServerLevel level) {
if (!(level.getChunkSource().getGenerator() instanceof SanctuaryChunkGenerator generator)
|| generator.rootGeneration() != 25 || !Level.OVERWORLD.equals(level.dimension()) || PLANS.containsKey(generator)) return;
if (!level.structureManager().shouldGenerateStructures() || !generator.experimentalStructures()) {
PLANS.put(generator, UndergroundPlan25.EMPTY); return;
}
long start = System.nanoTime();
// During preparation plan() remains EMPTY, so final-column sampling cannot recursively apply this plan.
var result = UndergroundPlanner25.plan(level, generator);
PLANS.put(generator, result);
SanctuaryMod.LOGGER.info("Underground25 seed {}: {} conditional starts, {} columns, {} ms; diagnostics {}",
level.getSeed(), result.starts().size(), result.columns(), (System.nanoTime() - start) / 1_000_000, result.diagnostics());
}
public static void generate(ExpansionChunkGenerator host, RegistryAccess registries, ChunkGeneratorStructureState state,
StructureManager manager, ChunkAccess chunk, StructureTemplateManager templates, ResourceKey<Level> dimension) {
if (!(host instanceof SanctuaryChunkGenerator generator) || generator.rootGeneration() != 25
|| !Level.OVERWORLD.equals(dimension) || !manager.shouldGenerateStructures() || !generator.experimentalStructures()) return;
for (var planned : plan(generator).starts()) if (planned.start().getChunkPos().equals(chunk.getPos())) {
var existing = manager.getStartForStructure(planned.start().getStructure(), chunk);
if (existing != null && existing.isValid()) continue;
manager.setStartForStructure(planned.start().getStructure(), planned.start(), chunk);
}
}
/** These are vanilla features, not structure starts. FEATURES executes once per new origin chunk. */
public static void decorate(net.minecraft.world.level.WorldGenLevel level, ChunkAccess chunk) {
if (!(level.getLevel().getChunkSource().getGenerator() instanceof SanctuaryChunkGenerator generator)
|| generator.rootGeneration() != 25 || !level.getLevel().structureManager().shouldGenerateStructures()
|| !generator.experimentalStructures() || !Level.OVERWORLD.equals(level.getLevel().dimension())) return;
for (var room : plan(generator).rooms()) if (new net.minecraft.world.level.ChunkPos(room.getLocatorPosition().getX() >> 4, room.getLocatorPosition().getZ() >> 4).equals(chunk.getPos())) {
UndergroundPlacement25.placingRoom(room, () -> room.postProcess(level, level.getLevel().structureManager(), generator,
net.minecraft.util.RandomSource.create(room.seed()), room.getBoundingBox(), chunk.getPos(), room.getLocatorPosition()));
}
}
}
@@ -226,6 +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.terrain25_hint": "Calcite peaks, natural depths and aerial sites. New oceans contain a monument when structures are enabled.",
"sanctuary.world_options.aerial24_hint": "Secrets, abandoned routes and five aerial sites. 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",
@@ -237,5 +238,7 @@
"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",
"structure.sanctuary.underground_abandoned_village": "Underground abandoned village",
"structure.sanctuary.miner_cabin": "Miner’s cabin"
}
@@ -226,6 +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.terrain25_hint": "Massifs de calcite, profondeurs naturelles et lieux suspendus. Les nouveaux océans accueillent un monument avec les structures activées.",
"sanctuary.world_options.aerial24_hint": "Secrets, voies abandonnées et cinq lieux 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",
@@ -237,5 +238,7 @@
"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",
"structure.sanctuary.underground_abandoned_village": "Village souterrain abandonné",
"structure.sanctuary.miner_cabin": "Cabane du mineur"
}
@@ -0,0 +1,134 @@
{
"type": "minecraft:chest",
"pools": [
{
"rolls": {
"type": "minecraft:uniform",
"min": 4,
"max": 7
},
"entries": [
{
"type": "minecraft:item",
"name": "minecraft:coal",
"weight": 7,
"functions": [
{
"function": "minecraft:set_count",
"count": {
"type": "minecraft:uniform",
"min": 8,
"max": 20
}
}
]
},
{
"type": "minecraft:item",
"name": "minecraft:iron_ingot",
"weight": 5,
"functions": [
{
"function": "minecraft:set_count",
"count": {
"type": "minecraft:uniform",
"min": 3,
"max": 8
}
}
]
},
{
"type": "minecraft:item",
"name": "minecraft:gold_ingot",
"weight": 3,
"functions": [
{
"function": "minecraft:set_count",
"count": {
"type": "minecraft:uniform",
"min": 2,
"max": 5
}
}
]
},
{
"type": "minecraft:item",
"name": "minecraft:rail",
"weight": 5,
"functions": [
{
"function": "minecraft:set_count",
"count": {
"type": "minecraft:uniform",
"min": 8,
"max": 20
}
}
]
},
{
"type": "minecraft:item",
"name": "minecraft:bread",
"weight": 4,
"functions": [
{
"function": "minecraft:set_count",
"count": {
"type": "minecraft:uniform",
"min": 3,
"max": 7
}
}
]
}
]
},
{
"rolls": 1,
"entries": [
{
"type": "minecraft:item",
"name": "minecraft:diamond",
"weight": 3,
"functions": [
{
"function": "minecraft:set_count",
"count": {
"type": "minecraft:uniform",
"min": 1,
"max": 3
}
}
]
},
{
"type": "minecraft:item",
"name": "minecraft:golden_apple",
"weight": 2
},
{
"type": "minecraft:item",
"name": "minecraft:diamond_pickaxe",
"weight": 1
},
{
"type": "minecraft:item",
"name": "minecraft:emerald",
"weight": 3,
"functions": [
{
"function": "minecraft:set_count",
"count": {
"type": "minecraft:uniform",
"min": 2,
"max": 5
}
}
]
}
]
}
]
}
@@ -0,0 +1,156 @@
{
"attributes": {
"minecraft:audio/background_music": {
"default": {
"max_delay": 24000,
"min_delay": 12000,
"sound": "minecraft:music.overworld.sparse_jungle"
}
},
"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:chicken",
"count": 4,
"weight": 10
},
{
"type": "minecraft:wolf",
"count": {
"type": "minecraft:uniform",
"max_inclusive": 4,
"min_inclusive": 2
},
"weight": 8
}
],
"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": 100
},
{
"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
}
],
"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": "#79a6ff"
},
"carvers": [],
"downfall": 0.95,
"effects": {
"water_color": "#3f76e4",
"grass_color": "#88b96b",
"foliage_color": "#79aa61",
"dry_foliage_color": "#b9a766"
},
"features": [
[],
[],
[],
[
"sanctuary:terrain25_monster_room",
"sanctuary:terrain25_monster_room_deep"
],
[],
[],
[],
[],
[],
[
"minecraft:glow_lichen",
"minecraft:brown_mushroom_normal",
"minecraft:red_mushroom_normal",
"sanctuary:terrain25_groves",
"sanctuary:population_decorations",
"sanctuary:population_shore_sugar_cane",
"sanctuary:terrain25_refuge"
],
[
"minecraft:freeze_top_layer"
]
],
"has_precipitation": true,
"temperature": 0.8
}
@@ -0,0 +1,151 @@
{
"attributes": {
"minecraft:audio/background_music": {
"default": {
"max_delay": 24000,
"min_delay": 12000,
"sound": "minecraft:music.overworld.forest"
}
},
"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
}
],
"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": 100
},
{
"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
}
],
"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": "#79a6ff"
},
"carvers": [],
"downfall": 0.7,
"effects": {
"water_color": "#3f76e4",
"grass_color": "#88b96b",
"foliage_color": "#79aa61",
"dry_foliage_color": "#b9a766"
},
"features": [
[],
[],
[],
[
"sanctuary:terrain25_monster_room",
"sanctuary:terrain25_monster_room_deep"
],
[],
[],
[],
[],
[],
[
"minecraft:glow_lichen",
"minecraft:forest_flowers",
"minecraft:wildflowers_birch_forest",
"minecraft:trees_birch",
"minecraft:patch_bush",
"minecraft:flower_default",
"minecraft:patch_grass_forest",
"minecraft:brown_mushroom_normal",
"minecraft:red_mushroom_normal",
"minecraft:patch_pumpkin",
"minecraft:patch_sugar_cane",
"minecraft:patch_firefly_bush_near_water",
"sanctuary:terrain25_groves",
"sanctuary:population_decorations",
"sanctuary:population_shore_sugar_cane",
"sanctuary:terrain25_refuge"
],
[
"minecraft:freeze_top_layer"
]
],
"has_precipitation": true,
"temperature": 0.7
}
@@ -0,0 +1,153 @@
{
"attributes": {
"minecraft:audio/background_music": {
"default": {
"max_delay": 24000,
"min_delay": 12000,
"sound": "minecraft:music.overworld.meadow"
}
},
"minecraft:gameplay/natural_mob_spawns": {
"argument": {
"spawn_costs": {},
"spawns_by_category": {
"ambient": [
{
"type": "minecraft:bat",
"count": 8,
"weight": 10
}
],
"creature": [
{
"type": "minecraft:donkey",
"count": {
"type": "minecraft:uniform",
"max_inclusive": 2,
"min_inclusive": 1
},
"weight": 1
},
{
"type": "minecraft:rabbit",
"count": {
"type": "minecraft:uniform",
"max_inclusive": 6,
"min_inclusive": 2
},
"weight": 2
},
{
"type": "minecraft:sheep",
"count": {
"type": "minecraft:uniform",
"max_inclusive": 4,
"min_inclusive": 2
},
"weight": 2
}
],
"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": 100
},
{
"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
}
],
"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": "#79a6ff"
},
"carvers": [],
"downfall": 0.75,
"effects": {
"water_color": "#3f76e4",
"grass_color": "#88b96b",
"foliage_color": "#79aa61",
"dry_foliage_color": "#b9a766"
},
"features": [
[],
[],
[],
[
"sanctuary:terrain25_monster_room",
"sanctuary:terrain25_monster_room_deep"
],
[],
[],
[],
[],
[],
[
"minecraft:glow_lichen",
"minecraft:patch_tall_grass_2",
"minecraft:patch_grass_meadow",
"minecraft:flower_meadow",
"minecraft:trees_meadow",
"minecraft:wildflowers_meadow",
"minecraft:flower_flower_forest",
"sanctuary:terrain25_groves",
"sanctuary:population_decorations",
"sanctuary:population_shore_sugar_cane",
"sanctuary:terrain25_refuge"
],
[
"minecraft:freeze_top_layer"
]
],
"has_precipitation": true,
"temperature": 0.7
}
@@ -0,0 +1,142 @@
{
"attributes": {
"minecraft:audio/background_music": {
"default": {
"max_delay": 24000,
"min_delay": 12000,
"sound": "minecraft:music.overworld.forest"
}
},
"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
}
],
"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": 100
},
{
"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
}
],
"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": "#79a6ff"
},
"carvers": [],
"downfall": 0.9,
"effects": {
"water_color": "#3f76e4",
"grass_color": "#88b96b",
"foliage_color": "#79aa61",
"dry_foliage_color": "#b9a766"
},
"features": [
[],
[],
[],
[
"sanctuary:terrain25_monster_room",
"sanctuary:terrain25_monster_room_deep"
],
[],
[],
[],
[],
[],
[
"minecraft:glow_lichen",
"minecraft:brown_mushroom_normal",
"minecraft:red_mushroom_normal",
"sanctuary:terrain25_groves",
"sanctuary:population_decorations",
"sanctuary:population_shore_sugar_cane",
"sanctuary:terrain25_refuge"
],
[
"minecraft:freeze_top_layer"
]
],
"has_precipitation": true,
"temperature": 0.7
}
@@ -0,0 +1,150 @@
{
"attributes": {
"minecraft:audio/background_music": {
"default": {
"max_delay": 24000,
"min_delay": 12000,
"sound": "minecraft:music.overworld.dripstone_caves"
}
},
"minecraft:gameplay/natural_mob_spawns": {
"argument": {
"spawn_costs": {},
"spawns_by_category": {
"ambient": [
{
"type": "minecraft:bat",
"count": 8,
"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": 100
},
{
"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:drowned",
"count": 4,
"weight": 95
}
],
"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"
},
"carvers": [],
"downfall": 0.4,
"effects": {
"water_color": "#3f76e4"
},
"features": [
[],
[
"minecraft:lake_lava_underground",
"minecraft:lake_lava_surface"
],
[
"minecraft:amethyst_geode",
"minecraft:large_dripstone"
],
[
"sanctuary:terrain25_monster_room",
"sanctuary:terrain25_monster_room_deep"
],
[],
[],
[
"minecraft:ore_dirt",
"minecraft:ore_gravel",
"minecraft:ore_granite_upper",
"minecraft:ore_granite_lower",
"minecraft:ore_diorite_upper",
"minecraft:ore_diorite_lower",
"minecraft:ore_andesite_upper",
"minecraft:ore_andesite_lower",
"minecraft:ore_tuff",
"minecraft:underwater_magma",
"minecraft:disk_sand",
"minecraft:disk_clay",
"minecraft:disk_gravel"
],
[
"minecraft:dripstone_cluster",
"minecraft:pointed_dripstone"
],
[
"minecraft:spring_water",
"minecraft:spring_lava"
],
[
"minecraft:glow_lichen",
"minecraft:patch_tall_grass_2",
"minecraft:trees_plains",
"minecraft:flower_plains",
"minecraft:patch_grass_plain",
"minecraft:brown_mushroom_normal",
"minecraft:red_mushroom_normal",
"minecraft:patch_pumpkin",
"sanctuary:terrain25_refuge"
],
[
"minecraft:freeze_top_layer"
]
],
"has_precipitation": true,
"temperature": 0.8
}
@@ -0,0 +1,152 @@
{
"attributes": {
"minecraft:audio/background_music": {
"default": {
"max_delay": 24000,
"min_delay": 12000,
"sound": "minecraft:music.overworld.forest"
}
},
"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:wolf",
"count": 4,
"weight": 5
}
],
"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": 100
},
{
"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
}
],
"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": "#79a6ff"
},
"carvers": [],
"downfall": 0.4,
"effects": {
"water_color": "#3f76e4",
"grass_color": "#88b96b",
"foliage_color": "#79aa61",
"dry_foliage_color": "#b9a766"
},
"features": [
[],
[],
[],
[
"sanctuary:terrain25_monster_room",
"sanctuary:terrain25_monster_room_deep"
],
[],
[],
[],
[],
[],
[
"minecraft:glow_lichen",
"minecraft:forest_flowers",
"sanctuary:rift_sparse_trees",
"minecraft:patch_bush",
"minecraft:flower_default",
"minecraft:patch_grass_forest",
"minecraft:brown_mushroom_normal",
"minecraft:red_mushroom_normal",
"sanctuary:terrain25_groves",
"sanctuary:population_decorations",
"sanctuary:population_shore_sugar_cane",
"sanctuary:terrain25_refuge"
],
[
"minecraft:freeze_top_layer"
]
],
"has_precipitation": true,
"temperature": 0.75
}
@@ -0,0 +1,161 @@
{
"attributes": {
"minecraft:audio/background_music": {
"default": {
"max_delay": 24000,
"min_delay": 12000,
"sound": "minecraft:music.overworld.lush_caves"
}
},
"minecraft:gameplay/natural_mob_spawns": {
"argument": {
"spawn_costs": {},
"spawns_by_category": {
"ambient": [
{
"type": "minecraft:bat",
"count": 8,
"weight": 10
}
],
"axolotls": [
{
"type": "minecraft:axolotl",
"count": {
"type": "minecraft:uniform",
"max_inclusive": 6,
"min_inclusive": 4
},
"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": 100
},
{
"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
}
],
"underground_water_creature": [
{
"type": "minecraft:glow_squid",
"count": {
"type": "minecraft:uniform",
"max_inclusive": 6,
"min_inclusive": 4
},
"weight": 10
}
],
"water_ambient": [
{
"type": "minecraft:tropical_fish",
"count": 8,
"weight": 25
}
]
}
},
"modifier": "overlay"
},
"minecraft:visual/sky_color": "#7ba4ff"
},
"carvers": [],
"downfall": 0.5,
"effects": {
"water_color": "#3f76e4"
},
"features": [
[],
[
"minecraft:lake_lava_underground",
"minecraft:lake_lava_surface"
],
[
"minecraft:amethyst_geode"
],
[
"sanctuary:terrain25_monster_room",
"sanctuary:terrain25_monster_room_deep"
],
[],
[],
[
"minecraft:ore_dirt",
"minecraft:ore_gravel",
"minecraft:ore_granite_upper",
"minecraft:ore_granite_lower",
"minecraft:ore_diorite_upper",
"minecraft:ore_diorite_lower",
"minecraft:ore_andesite_upper",
"minecraft:ore_andesite_lower",
"minecraft:ore_tuff",
"minecraft:underwater_magma",
"minecraft:ore_clay",
"minecraft:disk_sand",
"minecraft:disk_clay",
"minecraft:disk_gravel"
],
[],
[
"minecraft:spring_water",
"minecraft:spring_lava"
],
[
"minecraft:glow_lichen",
"minecraft:patch_tall_grass_2",
"minecraft:lush_caves_ceiling_vegetation",
"minecraft:cave_vines",
"minecraft:lush_caves_clay",
"minecraft:lush_caves_vegetation",
"minecraft:rooted_azalea_tree",
"minecraft:spore_blossom",
"minecraft:classic_vines_cave_feature",
"sanctuary:terrain25_refuge"
],
[
"minecraft:freeze_top_layer"
]
],
"has_precipitation": true,
"temperature": 0.5
}
@@ -0,0 +1,54 @@
{
"attributes": {
"minecraft:audio/background_music": {
"default": {
"max_delay": 24000,
"min_delay": 12000,
"sound": "minecraft:music.overworld.forest"
}
},
"minecraft:gameplay/natural_mob_spawns": {
"argument": {
"spawn_costs": {},
"spawns_by_category": {
"ambient": [],
"creature": [],
"monster": []
}
},
"modifier": "overlay"
},
"minecraft:visual/sky_color": "#79a6ff"
},
"carvers": [],
"downfall": 0.9,
"effects": {
"water_color": "#3f76e4",
"grass_color": "#88b96b",
"foliage_color": "#79aa61",
"dry_foliage_color": "#b9a766"
},
"features": [
[],
[],
[],
[
"sanctuary:terrain25_monster_room",
"sanctuary:terrain25_monster_room_deep"
],
[],
[],
[],
[],
[],
[
"minecraft:glow_lichen",
"sanctuary:terrain25_refuge"
],
[
"minecraft:freeze_top_layer"
]
],
"has_precipitation": true,
"temperature": 0.7
}
@@ -0,0 +1,155 @@
{
"attributes": {
"minecraft:audio/background_music": {
"default": {
"max_delay": 24000,
"min_delay": 12000,
"sound": "minecraft:music.overworld.forest"
}
},
"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:wolf",
"count": 4,
"weight": 5
}
],
"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": 100
},
{
"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
}
],
"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": "#79a6ff"
},
"carvers": [],
"downfall": 0.8,
"effects": {
"water_color": "#3f76e4",
"grass_color": "#88b96b",
"foliage_color": "#79aa61",
"dry_foliage_color": "#b9a766"
},
"features": [
[],
[],
[],
[
"sanctuary:terrain25_monster_room",
"sanctuary:terrain25_monster_room_deep"
],
[],
[],
[],
[],
[],
[
"minecraft:glow_lichen",
"minecraft:forest_flowers",
"minecraft:trees_birch_and_oak_leaf_litter",
"minecraft:patch_bush",
"minecraft:flower_default",
"minecraft:patch_grass_forest",
"minecraft:brown_mushroom_normal",
"minecraft:red_mushroom_normal",
"minecraft:patch_pumpkin",
"minecraft:patch_sugar_cane",
"minecraft:patch_firefly_bush_near_water",
"sanctuary:terrain25_groves",
"sanctuary:population_decorations",
"sanctuary:population_shore_sugar_cane",
"sanctuary:terrain25_refuge"
],
[
"minecraft:freeze_top_layer"
]
],
"has_precipitation": true,
"temperature": 0.7
}
@@ -0,0 +1,147 @@
{
"attributes": {
"minecraft:audio/background_music": {
"default": {
"max_delay": 24000,
"min_delay": 12000,
"sound": "minecraft:music.overworld.meadow"
}
},
"minecraft:gameplay/natural_mob_spawns": {
"argument": {
"spawn_costs": {},
"spawns_by_category": {
"ambient": [
{
"type": "minecraft:bat",
"count": 8,
"weight": 10
}
],
"creature": [
{
"type": "minecraft:donkey",
"count": {
"type": "minecraft:uniform",
"max_inclusive": 2,
"min_inclusive": 1
},
"weight": 1
},
{
"type": "minecraft:rabbit",
"count": {
"type": "minecraft:uniform",
"max_inclusive": 6,
"min_inclusive": 2
},
"weight": 2
},
{
"type": "minecraft:sheep",
"count": {
"type": "minecraft:uniform",
"max_inclusive": 4,
"min_inclusive": 2
},
"weight": 2
}
],
"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": 100
},
{
"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
}
],
"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": "#79a6ff"
},
"carvers": [],
"downfall": 0.65,
"effects": {
"water_color": "#3f76e4",
"grass_color": "#88b96b",
"foliage_color": "#79aa61",
"dry_foliage_color": "#b9a766"
},
"features": [
[],
[],
[],
[
"sanctuary:terrain25_monster_room",
"sanctuary:terrain25_monster_room_deep"
],
[],
[],
[],
[],
[],
[
"minecraft:glow_lichen",
"sanctuary:terrain25_groves",
"sanctuary:population_decorations",
"sanctuary:population_shore_sugar_cane",
"sanctuary:terrain25_refuge"
],
[
"minecraft:freeze_top_layer"
]
],
"has_precipitation": true,
"temperature": 0.7
}
@@ -0,0 +1,137 @@
{
"attributes": {
"minecraft:audio/background_music": {
"default": {
"max_delay": 24000,
"min_delay": 12000,
"sound": "minecraft:music.overworld.sulfur_caves"
}
},
"minecraft:gameplay/natural_mob_spawns": {
"argument": {
"spawn_costs": {},
"spawns_by_category": {
"ambient": [
{
"type": "minecraft:bat",
"count": 8,
"weight": 10
}
],
"monster": [
{
"type": "minecraft:sulfur_cube",
"count": {
"type": "minecraft:uniform",
"max_inclusive": 4,
"min_inclusive": 2
},
"weight": 100
},
{
"type": "minecraft:creeper",
"count": 2,
"weight": 50
},
{
"type": "minecraft:skeleton",
"count": 2,
"weight": 50
},
{
"type": "minecraft:slime",
"count": 1,
"weight": 25
},
{
"type": "minecraft:cave_spider",
"count": 1,
"weight": 20
},
{
"type": "minecraft:zombie",
"count": 2,
"weight": 50
},
{
"type": "minecraft:enderman",
"count": 1,
"weight": 10
},
{
"type": "minecraft:witch",
"count": 1,
"weight": 1
},
{
"type": "minecraft:zombie_villager",
"count": 1,
"weight": 5
}
]
}
},
"modifier": "overlay"
},
"minecraft:visual/fog_color": "#8cb831",
"minecraft:visual/sky_color": "#78a7ff",
"minecraft:visual/water_fog_color": "#17543c"
},
"carvers": [],
"downfall": 0.4,
"effects": {
"grass_color": "#aba64f",
"water_color": "#34bf89"
},
"features": [
[],
[
"minecraft:lake_lava_underground",
"minecraft:lake_lava_surface",
"minecraft:rooted_sulfur_spring",
"minecraft:sulfur_pool"
],
[
"minecraft:amethyst_geode"
],
[
"sanctuary:terrain25_monster_room",
"sanctuary:terrain25_monster_room_deep"
],
[],
[],
[
"minecraft:ore_dirt",
"minecraft:ore_gravel",
"minecraft:ore_granite_upper",
"minecraft:ore_granite_lower",
"minecraft:ore_diorite_upper",
"minecraft:ore_diorite_lower",
"minecraft:ore_andesite_upper",
"minecraft:ore_andesite_lower",
"minecraft:ore_tuff",
"minecraft:underwater_magma",
"minecraft:disk_sand",
"minecraft:disk_clay",
"minecraft:disk_gravel"
],
[
"minecraft:sulfur_spike_cluster",
"minecraft:sulfur_spike"
],
[
"minecraft:spring_water",
"minecraft:spring_lava"
],
[
"minecraft:glow_lichen",
"minecraft:patch_tall_grass_2",
"sanctuary:terrain25_refuge"
],
[
"minecraft:freeze_top_layer"
]
],
"has_precipitation": true,
"temperature": 0.8
}
@@ -0,0 +1,198 @@
{
"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": [
[],
[
"minecraft:lake_lava_underground",
"minecraft:lake_lava_surface"
],
[
"minecraft:amethyst_geode"
],
[
"minecraft:fossil_upper",
"minecraft:fossil_lower",
"sanctuary:terrain25_monster_room",
"sanctuary:terrain25_monster_room_deep"
],
[],
[],
[
"minecraft:ore_dirt",
"minecraft:ore_gravel",
"minecraft:ore_granite_upper",
"minecraft:ore_granite_lower",
"minecraft:ore_diorite_upper",
"minecraft:ore_diorite_lower",
"minecraft:ore_andesite_upper",
"minecraft:ore_andesite_lower",
"minecraft:ore_tuff",
"minecraft:underwater_magma",
"minecraft:disk_clay"
],
[],
[
"minecraft:spring_water",
"minecraft:spring_lava"
],
[
"minecraft:glow_lichen",
"minecraft:trees_swamp",
"minecraft:flower_swamp",
"minecraft:patch_grass_normal",
"minecraft:patch_dead_bush",
"minecraft:patch_waterlily",
"minecraft:brown_mushroom_swamp",
"minecraft:red_mushroom_swamp",
"minecraft:brown_mushroom_normal",
"minecraft:red_mushroom_normal",
"minecraft:patch_sugar_cane_swamp",
"minecraft:patch_pumpkin",
"minecraft:patch_firefly_bush_swamp",
"minecraft:patch_firefly_bush_near_water_swamp",
"minecraft:seagrass_swamp",
"sanctuary:terrain25_refuge"
],
[
"minecraft:freeze_top_layer"
]
],
"has_precipitation": true,
"temperature": 0.8
}
@@ -0,0 +1,53 @@
{
"type": "sanctuary:terrain25",
"terrain": {
"type": "minecraft:squeeze",
"input": {
"type": "minecraft:interpolated",
"cell_size_xz": 8,
"cell_size_y": 4,
"input": {
"type": "minecraft:mul",
"left": 0.64,
"right": "sanctuary:floating_archipelago_384"
}
}
},
"distortion": {
"type": "minecraft:noise",
"noise": "sanctuary:island_edge",
"xz_scale": 1.0,
"y_scale": 0.0
},
"sculpt": {
"type": "minecraft:noise",
"noise": "sanctuary:island_sculpt",
"xz_scale": 1.0,
"y_scale": 1.0
},
"detail": {
"type": "minecraft:noise",
"noise": "sanctuary:island_detail",
"xz_scale": 1.0,
"y_scale": 1.0
},
"underside": {
"type": "minecraft:noise",
"noise": "sanctuary:island_underside",
"xz_scale": 1.0,
"y_scale": 1.0
},
"players": 10,
"rift_warp": {
"type": "minecraft:noise",
"noise": "sanctuary:rift_warp",
"xz_scale": 1.0,
"y_scale": 1.0
},
"rift_detail": {
"type": "minecraft:noise",
"noise": "sanctuary:rift_detail",
"xz_scale": 1.0,
"y_scale": 1.0
}
}
@@ -0,0 +1,53 @@
{
"type": "sanctuary:terrain25",
"terrain": {
"type": "minecraft:squeeze",
"input": {
"type": "minecraft:interpolated",
"cell_size_xz": 8,
"cell_size_y": 4,
"input": {
"type": "minecraft:mul",
"left": 0.64,
"right": "sanctuary:floating_archipelago_384"
}
}
},
"distortion": {
"type": "minecraft:noise",
"noise": "sanctuary:island_edge",
"xz_scale": 1.0,
"y_scale": 0.0
},
"sculpt": {
"type": "minecraft:noise",
"noise": "sanctuary:island_sculpt",
"xz_scale": 1.0,
"y_scale": 1.0
},
"detail": {
"type": "minecraft:noise",
"noise": "sanctuary:island_detail",
"xz_scale": 1.0,
"y_scale": 1.0
},
"underside": {
"type": "minecraft:noise",
"noise": "sanctuary:island_underside",
"xz_scale": 1.0,
"y_scale": 1.0
},
"players": 20,
"rift_warp": {
"type": "minecraft:noise",
"noise": "sanctuary:rift_warp",
"xz_scale": 1.0,
"y_scale": 1.0
},
"rift_detail": {
"type": "minecraft:noise",
"noise": "sanctuary:rift_detail",
"xz_scale": 1.0,
"y_scale": 1.0
}
}
@@ -0,0 +1,53 @@
{
"type": "sanctuary:terrain25",
"terrain": {
"type": "minecraft:squeeze",
"input": {
"type": "minecraft:interpolated",
"cell_size_xz": 8,
"cell_size_y": 4,
"input": {
"type": "minecraft:mul",
"left": 0.64,
"right": "sanctuary:floating_archipelago_384"
}
}
},
"distortion": {
"type": "minecraft:noise",
"noise": "sanctuary:island_edge",
"xz_scale": 1.0,
"y_scale": 0.0
},
"sculpt": {
"type": "minecraft:noise",
"noise": "sanctuary:island_sculpt",
"xz_scale": 1.0,
"y_scale": 1.0
},
"detail": {
"type": "minecraft:noise",
"noise": "sanctuary:island_detail",
"xz_scale": 1.0,
"y_scale": 1.0
},
"underside": {
"type": "minecraft:noise",
"noise": "sanctuary:island_underside",
"xz_scale": 1.0,
"y_scale": 1.0
},
"players": 5,
"rift_warp": {
"type": "minecraft:noise",
"noise": "sanctuary:rift_warp",
"xz_scale": 1.0,
"y_scale": 1.0
},
"rift_detail": {
"type": "minecraft:noise",
"noise": "sanctuary:rift_detail",
"xz_scale": 1.0,
"y_scale": 1.0
}
}
@@ -0,0 +1,287 @@
{
"type": "minecraft:sequence",
"sequence": [
{
"type": "minecraft:condition",
"if_true": {
"type": "minecraft:biome",
"biome_is": "sanctuary:terrain25_sulfur_depths"
},
"then_run": "minecraft:overworld/sulfur_cave_bands"
},
{
"type": "minecraft:condition",
"if_true": {
"type": "minecraft:biome",
"biome_is": "sanctuary:terrain25_sulfur_depths"
},
"then_run": "sanctuary:population_sulfur_strata"
},
{
"type": "minecraft:condition",
"if_true": {
"type": "minecraft:not",
"invert": {
"type": "minecraft:y_above",
"add_stone_depth": false,
"anchor": {
"absolute": 136
},
"surface_depth_multiplier": 0
}
},
"then_run": "sanctuary:population_deep_strata"
},
{
"type": "minecraft:condition",
"if_true": {
"type": "minecraft:not",
"invert": {
"type": "minecraft:y_above",
"add_stone_depth": false,
"anchor": {
"absolute": 152
},
"surface_depth_multiplier": 0
}
},
"then_run": {
"type": "minecraft:condition",
"if_true": {
"type": "minecraft:noise_threshold",
"noise": "sanctuary:population_depth_boundary",
"min_threshold": -1000,
"max_threshold": 0.45,
"is_3d": true
},
"then_run": "sanctuary:population_deep_strata"
}
},
{
"type": "minecraft:condition",
"if_true": {
"type": "minecraft:not",
"invert": {
"type": "minecraft:y_above",
"add_stone_depth": false,
"anchor": {
"absolute": 168
},
"surface_depth_multiplier": 0
}
},
"then_run": {
"type": "minecraft:condition",
"if_true": {
"type": "minecraft:noise_threshold",
"noise": "sanctuary:population_depth_boundary",
"min_threshold": -1000,
"max_threshold": -0.45,
"is_3d": true
},
"then_run": "sanctuary:population_deep_strata"
}
},
{
"type": "minecraft:condition",
"if_true": {
"type": "minecraft:biome",
"biome_is": "sanctuary:terrain25_lush_caves"
},
"then_run": "sanctuary:population_humid_strata"
},
{
"type": "minecraft:condition",
"if_true": {
"type": "minecraft:biome",
"biome_is": "sanctuary:terrain25_dark_grove"
},
"then_run": {
"type": "minecraft:sequence",
"sequence": [
{
"type": "minecraft:condition",
"if_true": {
"type": "minecraft:not",
"invert": {
"type": "minecraft:y_above",
"add_stone_depth": false,
"anchor": {
"absolute": 192
},
"surface_depth_multiplier": 0
}
},
"then_run": "sanctuary:population_humid_strata"
},
{
"type": "minecraft:condition",
"if_true": {
"type": "minecraft:not",
"invert": {
"type": "minecraft:y_above",
"add_stone_depth": false,
"anchor": {
"absolute": 208
},
"surface_depth_multiplier": 0
}
},
"then_run": {
"type": "minecraft:condition",
"if_true": {
"type": "minecraft:noise_threshold",
"noise": "sanctuary:population_depth_boundary",
"min_threshold": -1000,
"max_threshold": 0.45,
"is_3d": true
},
"then_run": "sanctuary:population_humid_strata"
}
},
{
"type": "minecraft:condition",
"if_true": {
"type": "minecraft:not",
"invert": {
"type": "minecraft:y_above",
"add_stone_depth": false,
"anchor": {
"absolute": 224
},
"surface_depth_multiplier": 0
}
},
"then_run": {
"type": "minecraft:condition",
"if_true": {
"type": "minecraft:noise_threshold",
"noise": "sanctuary:population_depth_boundary",
"min_threshold": -1000,
"max_threshold": -0.45,
"is_3d": true
},
"then_run": "sanctuary:population_humid_strata"
}
}
]
}
},
{
"type": "minecraft:condition",
"if_true": {
"type": "minecraft:biome",
"biome_is": "sanctuary:terrain25_bamboo_grove"
},
"then_run": {
"type": "minecraft:sequence",
"sequence": [
{
"type": "minecraft:condition",
"if_true": {
"type": "minecraft:not",
"invert": {
"type": "minecraft:y_above",
"add_stone_depth": false,
"anchor": {
"absolute": 192
},
"surface_depth_multiplier": 0
}
},
"then_run": "sanctuary:population_humid_strata"
},
{
"type": "minecraft:condition",
"if_true": {
"type": "minecraft:not",
"invert": {
"type": "minecraft:y_above",
"add_stone_depth": false,
"anchor": {
"absolute": 208
},
"surface_depth_multiplier": 0
}
},
"then_run": {
"type": "minecraft:condition",
"if_true": {
"type": "minecraft:noise_threshold",
"noise": "sanctuary:population_depth_boundary",
"min_threshold": -1000,
"max_threshold": 0.45,
"is_3d": true
},
"then_run": "sanctuary:population_humid_strata"
}
},
{
"type": "minecraft:condition",
"if_true": {
"type": "minecraft:not",
"invert": {
"type": "minecraft:y_above",
"add_stone_depth": false,
"anchor": {
"absolute": 224
},
"surface_depth_multiplier": 0
}
},
"then_run": {
"type": "minecraft:condition",
"if_true": {
"type": "minecraft:noise_threshold",
"noise": "sanctuary:population_depth_boundary",
"min_threshold": -1000,
"max_threshold": -0.45,
"is_3d": true
},
"then_run": "sanctuary:population_humid_strata"
}
}
]
}
},
{
"type": "minecraft:condition",
"if_true": {
"type": "minecraft:biome",
"biome_is": "sanctuary:terrain25_rocky_heath"
},
"then_run": "sanctuary:population_upper_strata"
},
{
"type": "minecraft:condition",
"if_true": {
"type": "minecraft:noise_threshold",
"noise": "sanctuary:population_exposure",
"min_threshold": 0.85,
"max_threshold": 1000,
"is_3d": false
},
"then_run": "sanctuary:population_upper_strata"
},
{
"type": "minecraft:condition",
"if_true": {
"type": "minecraft:biome",
"biome_is": "sanctuary:terrain25_dry_woodland"
},
"then_run": {
"type": "minecraft:condition",
"if_true": {
"type": "minecraft:noise_threshold",
"noise": "sanctuary:population_exposure",
"min_threshold": 0.15,
"max_threshold": 1000,
"is_3d": false
},
"then_run": "sanctuary:population_upper_strata"
}
},
"sanctuary:starter_island",
"sanctuary:population_upper_strata"
]
}
@@ -0,0 +1,23 @@
{
"default_block": "minecraft:stone",
"default_fluid": "minecraft:air",
"disable_mob_generation": false,
"legacy_random_source": true,
"material_rule": "sanctuary:terrain25_island",
"noise": {
"height": 384,
"min_y": 0
},
"noise_router": {
"chunk_surface_level": 0.0,
"continents": 0.0,
"depth": 0.0,
"erosion": 0.0,
"final_density": "sanctuary:terrain25_10",
"ridges": 0.0,
"temperature": "sanctuary:temperate_variation",
"vegetation": "sanctuary:temperate_moisture"
},
"sea_level": -64,
"spawn_target": []
}
@@ -0,0 +1,23 @@
{
"default_block": "minecraft:stone",
"default_fluid": "minecraft:air",
"disable_mob_generation": false,
"legacy_random_source": true,
"material_rule": "sanctuary:terrain25_island",
"noise": {
"height": 384,
"min_y": 0
},
"noise_router": {
"chunk_surface_level": 0.0,
"continents": 0.0,
"depth": 0.0,
"erosion": 0.0,
"final_density": "sanctuary:terrain25_20",
"ridges": 0.0,
"temperature": "sanctuary:temperate_variation",
"vegetation": "sanctuary:temperate_moisture"
},
"sea_level": -64,
"spawn_target": []
}
@@ -0,0 +1,23 @@
{
"default_block": "minecraft:stone",
"default_fluid": "minecraft:air",
"disable_mob_generation": false,
"legacy_random_source": true,
"material_rule": "sanctuary:terrain25_island",
"noise": {
"height": 384,
"min_y": 0
},
"noise_router": {
"chunk_surface_level": 0.0,
"continents": 0.0,
"depth": 0.0,
"erosion": 0.0,
"final_density": "sanctuary:terrain25_5",
"ridges": 0.0,
"temperature": "sanctuary:temperate_variation",
"vegetation": "sanctuary:temperate_moisture"
},
"sea_level": -64,
"spawn_target": []
}
@@ -0,0 +1,6 @@
{
"feature": {
"type": "sanctuary:terrain25_groves"
},
"placement": []
}
@@ -0,0 +1,29 @@
{
"feature": {
"type": "sanctuary:monster_room25"
},
"placement": [
{
"type": "minecraft:count",
"count": 10
},
{
"type": "minecraft:in_square"
},
{
"type": "minecraft:height_range",
"height": {
"type": "minecraft:uniform",
"max_inclusive": {
"below_top": 0
},
"min_inclusive": {
"absolute": 0
}
}
},
{
"type": "minecraft:biome"
}
]
}
@@ -0,0 +1,29 @@
{
"feature": {
"type": "sanctuary:monster_room25"
},
"placement": [
{
"type": "minecraft:count",
"count": 4
},
{
"type": "minecraft:in_square"
},
{
"type": "minecraft:height_range",
"height": {
"type": "minecraft:uniform",
"max_inclusive": {
"absolute": 180
},
"min_inclusive": {
"absolute": 70
}
}
},
{
"type": "minecraft:biome"
}
]
}
@@ -0,0 +1,6 @@
{
"feature": {
"type": "sanctuary:terrain25_refuge"
},
"placement": []
}
@@ -0,0 +1,7 @@
{
"type": "sanctuary:underground_site_v25",
"biomes": "#minecraft:is_overworld",
"step": "top_layer_modification",
"terrain_adaptation": "none",
"spawn_overrides": {}
}

Some files were not shown because too many files have changed in this diff Show More