Compare commits

..
5 Commits
Author SHA1 Message Date
koka 83c377acc1 Validate beta.144 water, unloaded boundaries and ocean seams
Build Sanctuary / build (push) Waiting to run
2026-09-18 18:42:50 +02:00
koka 7eef9454d4 Implement natural pixel water for beta.144; final performance validation pending 2026-09-18 14:44:07 +02:00
koka 580b2414da Document beta.143 publication and Prism synchronization
Build Sanctuary / build (push) Canceled after 0s
2026-09-18 12:44:49 +02:00
koka 97ff2b54cb Slow seasonal snow pacing in beta.143
Build Sanctuary / build (push) Canceled after 0s
2026-09-18 12:41:27 +02:00
koka 59346d7f0a Document verified beta.142 publication and Prism synchronization
Build Sanctuary / build (push) Canceled after 0s
2026-09-18 05:08:09 +02:00
35 changed files with 1032 additions and 47 deletions
+24 -5
View File
@@ -20,11 +20,20 @@ La [vision complète](docs/vision.md) conserve les intentions ; le
L’économie, les claims et les dimensions décrits dans la vision restent
à implémenter.
La version **beta.142** utilise Vulkan comme moteur graphique par défaut,
stabilise les rafraîchissements du relief PBR et règle son intensité initiale
à 80 %. Les lectures de textures et calculs de reflets sont sécurisés contre
les résultats indéfinis. Le [ticket](docs/pbr-refresh-beta142.md) distingue les
corrections vérifiées du bruit signalé en mouvement sur le pilote de l'utilisateur.
La version **beta.144** ajoute une option d**eau naturelle pixélisée** :
couleur du biome, trois profils selon le fond, vert olive d’étang et visibilité
adaptée en immersion.
Les océans gardent leur couleur native et le profil clair.
[Contrat et vérifications](docs/natural-water-beta144.md). Livraison en cours.
La version **beta.143** garde le sampler natif de précipitation pour la neige
saisonnière, mais ralentit son rythme : l'accumulation est espacée par un
facteur 12 et la fonte par le facteur temps réel 72. Les protections existantes
restent inchangées : neige construite, biomes froids, hiver, profils neigeux
et météo Vanilla. Le [ticket](docs/realtime-snow-melt-beta143.md) documente le
contrat et les vérifications.
La beta.143 est publiée et synchronisée dans linstance Sanctuary Beta ;
les 923 fichiers personnels suivis sont conservés.
La [livraison précédente](docs/clay-workshop-integration-beta110.md) réunit déjà
latelier, les œufs et la neige saisonnière dans linstance **Sanctuary Beta**.
Les [synchronisations antérieures](docs/git-sync-beta092.md) restent documentées.
@@ -35,6 +44,16 @@ La [carte native beta.005](docs/atlas-beta005.md) prend le relais après
l’étude de Xaero et des alternatives libres.
## beta.143 — Neige saisonnière en temps réel
Accumulation ralentie par un facteur 12 et fonte saisonnière ralentie par le
facteur naturel 72, tout en conservant le sampler natif des colonnes de
précipitation. Deux blocs de neige fondent en environ 2,7 jours réels en été,
5,4 au printemps et 11 en automne.
[Contrat et vérifications](docs/realtime-snow-melt-beta143.md) ·
[Pack normal](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.143/Sanctuary-beta.143.mrpack) ·
[Pack de test](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.143/Sanctuary-Test-beta.143.mrpack).
## beta.142 — Vulkan par défaut et stabilité PBR
Moteur natif Vulkan prioritaire, relief conservé pendant les reconstructions,
+189
View File
@@ -0,0 +1,189 @@
# WATER-144 — Eau naturelle pixélisée
Branche `codex/water-beta144`, socle beta.143 publiée (`97ff2b5`) et suivi
`580b241`. Minecraft 26.3, Java 25, dépendances conservées.
## Contrat
**Options → Shaders → Eau naturelle** est activée par défaut avec Sanctuary.
Lintensité (0100 %, défaut 100 %) agit sur la surface et limmersion.
Linterrupteur ou 0 % retrouve le rendu précédent. Les préférences existantes
restent conservées ; un moteur externe suspend leffet comme les autres effets
natifs Sanctuary.
La couleur native du biome reste la base, y compris les variantes docéans.
Les océans (`minecraft:is_ocean`) imposent le profil clair.
Hors océan, le premier support situé au maximum huit blocs sous la cellule
observée choisit le profil :
| Profil | Fond | Mélange olive #788448 | Alpha natif | Distance de brume native |
| --- | --- | ---: | ---: | ---: |
| Claire | Pierre, matériaux construits, inconnus | 0 % | ×0,55 | ×1 |
| Légèrement teintée | Sable, sable rouge, gravier, argile | 10 % | ×0,80 | ×0,70 |
| Étang | Terre, terre stérile/enracinée, herbe, podzol, mycélium, boue, racines de palétuvier boueuses | 85 % | ×1 | ×0,40 |
Leau et les plantes aquatiques sans collision sont traversées. Un support
immergé reste un fond ; absence de support, air ou données manquantes donnent
une eau claire. La référence visuelle fournie pendant limplémentation demande un véritable
vert olive légèrement jaunâtre : le profil étang passe donc de 45 % à 85 %
de mélange, tout en laissant les eaux claires et océaniques natives.
Les surfaces profondes sans fond dans la limite restent donc
claires. Les profils se mélangent aux sommets sur un bloc, sans filtrer les
textures. Aux bords océaniques, les sommets partagés restent clairs et la
transition occupe le bloc voisin hors océan. Leau sous un toit suit son fond,
pas l’éclairage du bâtiment.
La ressource client `assets/sanctuary/water_materials.json` accepte deux listes
didentifiants de blocs, `light` et `pond`, remplaçables par un resource pack.
Les blocs absents des listes sont clairs. Une ressource invalide est signalée
et reprend les familles embarquées. Le template personnel inclut cette ressource.
## Rendu et coût
Modification des seules couleurs/alpha des sommets de `FluidRenderer`, sans
passe écran ni duplication de maillage. Textures, UV, animation, éclairage
et tri des transparences restent natifs. Les calculs de profils sont différés
jusqu’à l’émission dun sommet visible, puis mutualisés dans la région compilée.
Un test de palette élimine les sections sans eau avant toute copie de biomes.
Les états de blocs viennent des copies natives ; les appartenances océaniques
sont capturées avant lenvoi au worker. La présence du terrain est vérifiée dans le cache client avec chargement
désactivé, sans utiliser `ClientLevel.hasChunk` (toujours vrai). Aucun
chargement de chunk nest demandé par le classificateur.
Un changement de bloc invalide la zone dépendante, huit blocs au-dessus et
un bloc horizontalement. Les reconstructions natives recopient leurs données ;
leffet ne déclenche aucune reconstruction pour une lumière mobile.
Un changement doption reconstruit seulement les sections chargées contenant de
leau ; les caches des autres effets restent disponibles. Le cache temporaire
disparaît avec sa compilation, sans historique de monde.
Sous leau, le profil est échantillonné au maximum une fois par tick, avec
transition de 0,5 seconde. La couleur et les distances environnementales sont
adaptées dans le rendu natif du brouillard. La teinte finale est appliquée après
la normalisation de la vision aquatique, pour éviter de transformer lolive en
jaune fluorescent ; la luminosité issue des modificateurs natifs de vision
et le bonus du compagnon calmar sont conservés. Lave et brume atmosphérique ne changent
pas. Il ny a pas de simulation dabsorption volumétrique ni de reflet ajouté.
## Validation native
Les essais utilisent un monde plat jetable, graine 122, avec bassins sur quartz,
sable et boue, support du sable, plantes aquatiques, toit, frontière de sections
et changement de biome océan. Une copie personnelle de ressource remplace
réellement les familles de matériaux puis est désactivée.
Les suites graphiques finales OpenGL et Vulkan sont réussies le 18 septembre
2026 (`WATER144_PASS` et `WATER144_EXTRAS_PASS`). Elles couvrent surface,
immersion, fond modifié, priorité océan, toit, plantes, cascades, lave inchangée,
blocs gorgés deau, préférences FR/EN et rechargement des ressources/chunks.
Les sommets partagés aux limites océaniques et le repli clair lorsque le cache
client ne contient pas le terrain font lobjet dassertions supplémentaires.
Aucun autre client de test concurrent na été détecté pendant ces séries.
Deux passages ciblés supplémentaires (`WATER144_TEXTURE_PASS`), Vulkan en
1 min 18 s et OpenGL en 1 min 4 s, chargent de vraies textures personnelles
32×32 à deux images animées pour leau calme et courante. Grille, animation
visible et retour aux textures natives sont vérifiés après désactivation.
Les fixtures, leurs alpha et les captures restent dans les preuves locales.
### Protocole de performance
MacBookPro17,1, Apple M1, 8 Gio, macOS 26.5.1 (25F80), Java 25,
fenêtre logique 854×480, cible Retina **1708×960 pixels**, distance de rendu 6 chunks,
ombres 32 blocs / 16 pixels, PBR 50 %, autres effets activés.
VSync désactivée, limite 260 (illimitée), limitation AFK désactivée.
Le mod de test vérifie le limiteur effectif et journalise le mode de présentation.
Pendant les échantillons uniquement, il omet lattente artificielle de
`FramerateLimiter.limitDisplayFPS`, afin de mesurer sans temporisation
applicative. Cette sonde reste absente du JAR de production. Les temps couvrent
la frame native, présentation comprise, et ne sont pas des temps GPU isolés.
Pour chaque scène : OFF/ON/ON/OFF/ON/OFF/OFF/ON, dix secondes par passage,
avec stabilisation des sections puis quatre secondes de chauffe entre passages.
Le déplacement avance de 120 blocs et laisse les sections se compiler
naturellement, sans vidange artificielle des caches des autres effets.
La scène de déplacement prolonge le bassin hors de la vue initiale : son terrain
est préparé côté serveur de test, puis découvert normalement par le client.
Le vérificateur exige des compilations réelles dans chaque passage en mouvement.
Les moyennes des quatre médianes et P95 par état sont comparées avec les seuils
+5 % et +2 ms. Les temps de compilation de sections sont consignés également.
Les essais courts initiaux, les exécutions concurrentes et les vérifications
incomplètes du limiteur restent conservés comme preuves de développement ;
ils ne fondent pas ces résultats. Les séries finales ont été exécutées après
la fin du chantier SSR. Les variations négatives sont du bruit de mesure,
pas une promesse daccélération. Vulkan reste proche de 120 images/s dans
cette chaîne de présentation, malgré le mode `IMMEDIATE` et labsence de
limiteur applicatif : les conclusions sont limitées à la machine et aux
conditions ci-dessus.
| Moteur / scène | Médiane OFF → ON (ms) | Variation | P95 OFF → ON (ms) | Δ P95 (ms) |
| --- | --- | ---: | --- | ---: |
| vulkan / mixed | 8.069 → 8.092 | +0.29 % | 9.091 → 9.101 | +0.010 |
| vulkan / large | 8.078 → 8.062 | -0.20 % | 9.126 → 9.114 | -0.012 |
| vulkan / moving | 8.123 → 8.103 | -0.24 % | 9.103 → 9.080 | -0.023 |
| opengl / mixed | 4.545 → 4.407 | -3.02 % | 7.080 → 6.733 | -0.347 |
| opengl / large | 4.879 → 5.110 | +4.73 % | 7.519 → 7.767 | +0.248 |
| opengl / moving | 6.817 → 6.338 | -7.02 % | 10.618 → 9.759 | -0.859 |
Les six comparaisons respectent les seuils : médiane ≤ +5 %, P95 ≤ +2 ms.
Le cas limitant est la grande surface OpenGL (+4,73 %, +0,248 ms).
Les compilations de sections sont mesurées sur le parcours en mouvement :
| Moteur | Sections OFF / ON | Médiane OFF → ON (ms) | P95 OFF → ON (ms) |
| --- | ---: | --- | --- |
| vulkan | 222 / 220 | 1.889 → 2.186 | 4.307 → 4.898 |
| opengl | 222 / 220 | 1.528 → 1.540 | 5.492 → 5.227 |
Pour reproduire, avec Java 25 et un environnement graphique disponible :
```sh
./gradlew :sanctuary:runClientGameTest -PsanctuaryClientTests=true \
-PsanctuaryPixelShadows122ClientTests=true -PsanctuaryWater144ClientTests=true \
-PsanctuaryClientNoVsync=true -PsanctuaryQuickTests=true \
-PsanctuaryClientGraphicsBackend=vulkan > build/water144-vulkan.log 2>&1
python3 scripts/check_water_performance.py build/water144-vulkan.log
```
Répéter avec `opengl`, sans autre client graphique simultané. Le vérificateur
refuse les séries incomplètes et les dépassements ; il écrit le reçu détaillé
près du journal. Le marqueur `WATER144_PASS` atteste les contrôles fonctionnels.
### Assemblage et publication
Le GameTest serveur dédié reste exclu conformément au refus antérieur de son
EULA. La commande de livraison est `./gradlew check build assemblePack
assembleTestPack -x :sanctuary:runGameTest` ; la suite complète non exclue
nest pas déclarée réussie. Les contrôles natifs de leau utilisent le serveur
intégré dun monde jetable. Aucun essai Windows nest revendiqué.
Lassemblage avec lexclusion ci-dessus a réussi en 2 min 33 s (126 tâches).
`python3 scripts/pack.py check` réussit. Les cinq archives passent le contrôle
ZIP ; les deux MRpack embarquent exactement le même JAR Sanctuary et déclarent
Fabric API 0.160.5+26.3 avec lempreinte du manifeste source. Lexport MRpack est
exécuté depuis chaque dossier de staging packwiz. Les dépendances imbriquées
Demeure et JEI sont vérifiées par lassemblage natif du dépôt.
La release, le canal et linstance ne sont pas encore mis à jour.
Les preuves détaillées restent dans les dossiers ignorés :
`build/water144-*.log`, `build/water144-*-performance.json`,
`build/water144-*-isolation.json` et `build/water144-evidence/`.
## Artefacts vérifiés
| Artefact | Octets | SHA-256 |
| --- | ---: | --- |
| `sanctuary-beta.144.jar` | 11343257 | `1ddbe941f3262da8a1497ea5df4a992e260b80ae95b341623dd68f8f9729ae88` |
| `Sanctuary-beta.144.mrpack` | 10483196 | `d5ef5a781f5ee796117210d1f6afc96172461315faee34097323beff541ce6f0` |
| `Sanctuary-Test-beta.144.mrpack` | 10502119 | `e7cc32ac19ae98ebbe7a9aac3dc08decdf09396c8175b597f87051399ff3812e` |
| `Sanctuary-Template-beta.144.zip` | 4354678 | `e510c0040dc9053492715ac91381b7dbc5a6f7c7defbf81e7502d8e18dbb0ee6` |
| `Sanctuary-Prism-auto-update.zip` | 92423 | `c0920823f40d709c14e074d8d949f2cc3681c24140bf86f21c3be345cfc3e1f6` |
## Captures comparatives
Pièces jointes de la release, issues du client Vulkan de validation :
[OFF](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.144/Sanctuary-beta.144-water-off.png),
[ON](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.144/Sanctuary-beta.144-water-on.png),
[profil étang en proximité](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.144/Sanctuary-beta.144-water-pond.png).
Les captures sont exportées à 854×480 ; la cible de présentation mesurée est
1708×960. Le fichier public `Sanctuary-beta.144-SHA256SUMS.txt` inclut les cinq
livrables et ces trois captures. Les journaux et échantillons détaillés restent
locaux, dans `build/water144-evidence/` et les reçus `build/water144-*.json`.
+35
View File
@@ -14,6 +14,41 @@ JAR Sanctuary sont des pièces jointes de releases Gitea. Aucun JAR de mod ou
dépendance téléchargée n'entre dans l'historique Git. Le canal Beta est distinct
de l'ancien pack 26.2.
## beta.144 — Eau naturelle pixélisée (préparation)
Trois profils de couleur/transparence selon le fond, océans clairs et brume
aquatique adaptée. Le [ticket de livraison](natural-water-beta144.md) décrit
la teinte olive retenue, les contrôles et le protocole de performance.
La publication et la synchronisation attendent la validation finale isolée.
Les textures restent à beta.121 ; Minecraft 26.3 et les dépendances sont conservés.
## beta.143 — Neige saisonnière en temps réel
Accumulation espacée par un facteur 12 et fonte par le facteur temps réel 72,
avec le sampler natif de précipitation conservé. Protections inchangées pour
la neige construite, les biomes froids, l'hiver, les profils neigeux et la
météo Vanilla. [Contrat et vérifications](realtime-snow-melt-beta143.md).
[Release beta.143](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.143)
publiée ; canal `bafff5e9fcdd95c5dd184953a34ada805e455797`. Deux synchronisations
isolées puis deux dans l'instance Sanctuary Beta réussies. Les 923 fichiers
personnels suivis gardent leurs hashes. Aucun monde personnel ouvert ;
sauvegarde préalable `sanctuary-backups/before-beta.143/`. Archives vérifiées
dans `sanctuary-beta/build/`.
## beta.142 — Vulkan par défaut et stabilité PBR
Vulkan natif prioritaire, rafraîchissements PBR stables et intensité initiale
80 %, calculs de reflets bornés. [Contrat et vérifications](pbr-refresh-beta142.md).
[Release beta.142](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.142)
publiée ; canal `d661260e2bbabb0326c86d6cc543daf986979596`. Deux synchronisations isolées
puis deux dans l'instance Sanctuary Beta réussies. Les 923 fichiers personnels
suivis gardent leurs hashes. Aucun monde personnel ouvert ; sauvegarde préalable
`sanctuary-backups/before-beta.142/`. Archives vérifiées dans `sanctuary-beta/build/`.
Un choix OpenGL explicite reste conservé : sélectionner Vulkan dans les réglages
vidéo et redémarrer pour changer ce choix. La capture Windows de bruit en
mouvement reste à reconfirmer sur son pilote ; les contrôles OpenGL/Vulkan Mac
et la correction du domaine numérique du reflet sont documentés dans le ticket.
## beta.141 — Direction des lumières pour les normales PBR
Directions locales mises en cache, sources portées, confiance réduite lorsque
+19
View File
@@ -152,3 +152,22 @@ SHA-256 du JAR : `0bc2da9df2ef004e6dc78932cfde7b31b7717c63c83ceed789d151177c41bc
- `Sanctuary-Test-beta.142.mrpack` : 10483672 octets ; SHA-256 `d56b81a4aea6649a40a3c49dedee904989943a518ad0ae7c520b808dd0f9b9a7`.
Reçu détaillé ignoré : `build/shader142-artifact.json`.
## Livraison réalisée
[Release beta.142](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.142)
publiée depuis `ea151dd5df8c7c5abd0107656f40c723efbf2ebe`. Canal stable :
`d661260e2bbabb0326c86d6cc543daf986979596`. Archives normale/Test, ZIP d'amorçage Prism et template
publiés avec vérification de leurs téléchargements ; tag et artefacts immuables.
Deux synchronisations isolées puis deux synchronisations de l'instance
**Sanctuary Beta** réussies. Les 923 fichiers personnels suivis ont conservé
leurs hashes, notamment mondes, réglages et packs personnels. Aucun monde
personnel ouvert. Sauvegarde préalable : `sanctuary-backups/before-beta.142/`.
L'instance locale conserve son réglage graphique natif « default », qui démarre
désormais avec Vulkan prioritaire. Un Windows explicitement réglé sur OpenGL
doit sélectionner Vulkan dans les réglages vidéo et redémarrer.
Archives locales également dans `sanctuary-beta/build/`. Le checkout de travail
historique modifié `sanctuary-beta` n'a pas été réinitialisé ; seules ces archives
ignorées y ont été déposées. Sources sur `codex/pbr-refresh-beta142`.
+68
View File
@@ -0,0 +1,68 @@
# SNOW-143 — Neige saisonnière en temps réel
Branche `codex/realtime-snow-melt-beta143`, socle beta.142. Minecraft 26.3 /
Java 25. Livraison beta.143.
## Comportement
L'accumulation et la fonte saisonnière conservent le sampler natif de
précipitation `ServerLevel.tickPrecipitation`. Sanctuary ne scanne donc toujours
pas les chunks et ne force aucun chargement de terrain. Le changement porte
seulement sur la probabilité des mutations de neige.
Pendant une chute de neige, Sanctuary ajoute désormais une couche une fois sur
12 échantillons natifs en moyenne. La neige reste visible pendant une session,
mais les colonnes ne montent plus instantanément. À `randomTickSpeed = 3`, une
couche apparaît en moyenne après environ 40 minutes de neige continue.
Pour la fonte, chaque tirage saisonnier est multiplié par `72`, le rapport
entre une journée réelle de 24 heures et une journée Minecraft de 20 minutes.
Une couche fond désormais en moyenne en 4 h 05 en été,
8 h 10 au printemps et 16 h 25 en automne. Avec le plafond actuel de deux blocs,
une colonne météo suivie met environ 2,7 jours réels à disparaître en été,
5,4 jours au printemps et 11 jours en automne. L'hiver ne fond toujours pas.
Les règles de protection restent celles de beta.109 : seule la neige déposée
par la météo Sanctuary depuis l'ajout du suivi `sanctuary:natural_snow_v1`
peut fondre. La neige construite, l'ancienne neige non suivie, les couches
manuelles, les biomes froids, les profils neigeux et la météo Vanilla restent
préservés. Aucun format de sauvegarde ne change.
## Vérifications
Vérifié le 18 septembre 2026 sur macOS, Java 25.
- `./gradlew :sanctuary:runClientGameTest -PsanctuaryClientTests=true
-PsanctuaryMelt109ClientTests=true -PsanctuaryClientNoVsync=true
-PsanctuaryQuickTests=true` : réussi en 57 s, avec le marqueur
`MELT109_NATIVE_PASS`. Le test couvre l'accumulation beta.108 ralentie,
la fonte couche par couche, la provenance persistante, les saisons, la
gamerule, la météo Vanilla, les profils neigeux, les biomes froids et le
rechargement de monde.
- `./gradlew check build assemblePack assembleTestPack -x :sanctuary:runGameTest` :
réussi en 2 min 31 s, 126 tâches. Le GameTest serveur dédié reste exclu
conformément au refus antérieur de son EULA ; les contrôles natifs utilisent
le serveur intégré.
- `python3 scripts/pack.py check` : versions, icône et index packwiz vérifiés.
- Exports MRpack produits avec `packwiz modrinth export`, intégrité ZIP vérifiée.
Les deux MRpack embarquent exactement le JAR Sanctuary construit.
| Artefact | Taille | SHA-256 |
| --- | ---: | --- |
| `sanctuary-beta.143.jar` | 11 323 665 octets | `fd4b663ac2b2a106624c42b09997db229cc14d6c39d44ae83079bc6aa9d39a08` |
| `Sanctuary-beta.143.mrpack` | 10 464 931 octets | `e55caaa41c5a3a2666ff5415846e18f44d504ac34b698f3d974091cfb52364c5` |
| `Sanctuary-Test-beta.143.mrpack` | 10 483 858 octets | `b14bca04c4a48771209d0f7e234ff9ef6b800eb236fb969a6f0dfcee5620dafc` |
| `Sanctuary-Template-beta.143.zip` | 4 354 153 octets | `071fd8da67d1d0224b89889a5fcbc4cd269673dddb7a52f89aa78802f154605a` |
## Publication
[Release beta.143](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.143)
publiée depuis le commit `97ff2b54cb63afe8fdd88bfa8f2ebb25dbe3b86d`.
Le canal packwiz stable pointe sur
`bafff5e9fcdd95c5dd184953a34ada805e455797`.
Deux synchronisations isolées, puis deux synchronisations dans l'instance
Sanctuary Beta ont réussi. Les 923 fichiers personnels suivis gardent leurs
hashes ; aucun monde personnel n'a été ouvert. Sauvegarde préalable :
`sanctuary-backups/before-beta.143/`.
+6
View File
@@ -107,3 +107,9 @@ Les contrôles ne revendiquent pas un essai avec Iris ou un autre moteur externe
Les ombres portées pixélisées sont livrées dans [beta.127](shader-beta127.md),
intégrées sur la beta.126. Leur préparation locale portait un numéro beta.122
non publié ; la release beta.122 existante n'est pas remplacée.
## beta.144 — Eau naturelle
Le [ticket WATER-144](natural-water-beta144.md) ajoute les profils clair,
légèrement teinté et étang au rendu natif des fluides et à leur brouillard.
La couleur du biome et les textures pixélisées restent la base.
+2 -2
View File
@@ -9,8 +9,8 @@ loom_version=1.17.20
fabric_api_version=0.160.5+26.3
# Release counter: beta.001, .002, .003, ... (see docs/versioning.md).
mod_version=beta.142
pack_version=beta.142
mod_version=beta.144
pack_version=beta.144
resource_pack_version=beta.121
maven_group=fr.koka.sanctuary
jei_version=30.32.0-sanctuary.3
+3
View File
@@ -34,6 +34,9 @@ if (clientRenderTests) {
if (improvedTransparency.isPresent()) {
systemProperty('sanctuary.test.improvedTransparency', improvedTransparency.get())
}
systemProperty('sanctuary.test.water144', providers.gradleProperty('sanctuaryWater144ClientTests').getOrElse('false'))
systemProperty('sanctuary.test.water144ExtrasOnly', providers.gradleProperty('sanctuaryWater144ExtrasOnly').getOrElse('false'))
systemProperty('sanctuary.test.water144PerfOnly', providers.gradleProperty('sanctuaryWater144PerfOnly').getOrElse('false'))
systemProperty('sanctuary.test.pbr142', providers.gradleProperty('sanctuaryPbr142ClientTests').getOrElse('false'))
systemProperty('sanctuary.test.directional141Only', providers.gradleProperty('sanctuaryDirectional141Only').getOrElse('false'))
systemProperty('sanctuary.test.directional141', providers.gradleProperty('sanctuaryDirectional141ClientTests').getOrElse('false'))
@@ -12,6 +12,7 @@ import java.time.Instant;
import java.time.ZoneOffset;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.BooleanSupplier;
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
import net.fabricmc.fabric.api.client.gametest.v1.FabricClientGameTest;
import net.fabricmc.fabric.api.client.gametest.v1.context.ClientGameTestContext;
@@ -50,24 +51,29 @@ public final class Melt109ClientChecks implements FabricClientGameTest {
for(int x=-15;x<=15;x++)for(int z=-15;z<=15;z++)l.setBlockAndUpdate(new BlockPos(x,79,z),Blocks.STONE.defaultBlockState());
clock(l,"2026-07-10T12:00:00Z",SeasonalWeather.Season.SUMMER);
check(l.getGameRules().get(SeasonalSnowMelt.ENABLED),"Melt enabled by default");
check(SeasonalSnowMelt.REALTIME_SCALE==72,"Real-time snow melt scale is one real day per 72 native days");
check(SeasonalSnowMelt.chance(SeasonalWeather.Season.SUMMER)==72
&&SeasonalSnowMelt.chance(SeasonalWeather.Season.SPRING)==144
&&SeasonalSnowMelt.chance(SeasonalWeather.Season.AUTUMN)==288
&&SeasonalSnowMelt.chance(SeasonalWeather.Season.WINTER)==0,"Seasonal melt chances include the 72x real-time scale");
deposit(l,AT,16);check(l.getBlockState(AT.above()).is(Blocks.SNOW_BLOCK),"Default two-block accumulation retained");
clear(l);step(l,AT,1);
check(layers(l,AT.above())==7&&layers(l,AT)==8,"Summer melts only one top layer");
step(l,AT,7);check(l.getBlockState(AT.above()).isAir()&&layers(l,AT)==8,"Top block melts completely before the lower block");
step(l,AT,8);check(l.getBlockState(AT).isAir()&&!tracked(l,AT),"Natural column vanishes without water; receipt removed");
clear(l);
until(l,AT,5000,()->layers(l,AT.above())==7&&layers(l,AT)==8,"Summer melts only one top layer");
until(l,AT,20000,()->l.getBlockState(AT.above()).isAir()&&layers(l,AT)==8,"Top block melts completely before the lower block");
until(l,AT,20000,()->l.getBlockState(AT).isAir()&&!tracked(l,AT),"Natural column vanishes without water; receipt removed");
var built=new BlockPos(7,80,4);l.setBlockAndUpdate(built,Blocks.SNOW_BLOCK.defaultBlockState());step(l,built,64);check(layers(l,built)==8,"Untracked snow block preserved");
var old=new BlockPos(8,80,4);snow(l,old,5);step(l,old,64);check(layers(l,old)==5,"Untracked old layers preserved");
var base=new BlockPos(9,80,4);snow(l,base,3);deposit(l,base,5);clear(l);step(l,base,16);check(layers(l,base)==3&&!tracked(l,base),"Weather above manual layers melts only back to its base");
var atop=new BlockPos(9,80,6);l.setBlockAndUpdate(atop,Blocks.SNOW_BLOCK.defaultBlockState());deposit(l,atop,8);clear(l);step(l,atop,16);check(layers(l,atop)==8&&l.getBlockState(atop.above()).isAir(),"Weather on a snow construction melts; construction stays");
var base=new BlockPos(9,80,4);snow(l,base,3);deposit(l,base,5);clear(l);until(l,base,20000,()->layers(l,base)==3&&!tracked(l,base),"Weather above manual layers melts only back to its base");
var atop=new BlockPos(9,80,6);l.setBlockAndUpdate(atop,Blocks.SNOW_BLOCK.defaultBlockState());deposit(l,atop,8);clear(l);until(l,atop,20000,()->layers(l,atop)==8&&l.getBlockState(atop.above()).isAir(),"Weather on a snow construction melts; construction stays");
var replaced=new BlockPos(10,80,4);deposit(l,replaced,8);l.setBlockAndUpdate(replaced,Blocks.AIR.defaultBlockState());l.setBlockAndUpdate(replaced,Blocks.SNOW_BLOCK.defaultBlockState());clear(l);step(l,replaced,32);check(layers(l,replaced)==8&&!tracked(l,replaced),"Break and replacement cannot inherit melt permission");
var thickened=new BlockPos(10,80,6);deposit(l,thickened,2);snow(l,thickened,6);clear(l);step(l,thickened,32);check(layers(l,thickened)==6&&!tracked(l,thickened),"Manual layer edit relinquishes natural provenance");
var capped=new BlockPos(12,80,6);deposit(l,capped,8);snow(l,capped.above(),1);clear(l);step(l,capped,32);check(layers(l,capped)==8&&layers(l,capped.above())==1,"Manual thin snow cap protects its natural support");
var roof=new BlockPos(11,80,4);deposit(l,roof,8);l.setBlockAndUpdate(roof.above(3),Blocks.STONE.defaultBlockState());clear(l);step(l,roof,32);check(layers(l,roof)==8,"Covered snow remains untouched");l.setBlockAndUpdate(roof.above(3),Blocks.AIR.defaultBlockState());step(l,roof,1);check(layers(l,roof)==7,"Uncovered natural snow resumes melt");
var roof=new BlockPos(11,80,4);deposit(l,roof,8);l.setBlockAndUpdate(roof.above(3),Blocks.STONE.defaultBlockState());clear(l);step(l,roof,32);check(layers(l,roof)==8,"Covered snow remains untouched");l.setBlockAndUpdate(roof.above(3),Blocks.AIR.defaultBlockState());until(l,roof,10000,()->layers(l,roof)==7,"Uncovered natural snow resumes melt");
command(l,"gamerule sanctuary:seasonal_snow_melt false");step(l,roof,32);check(layers(l,roof)==7,"Operator disables melting");command(l,"gamerule sanctuary:seasonal_snow_melt true");
clock(l,"2026-01-10T12:00:00Z",SeasonalWeather.Season.WINTER);clear(l);step(l,roof,64);check(layers(l,roof)==7,"Winter never melts even with clear weather");
clock(l,"2026-04-10T12:00:00Z",SeasonalWeather.Season.SPRING);clear(l);step(l,roof,64);check(l.getBlockState(roof).isAir(),"Spring progressively melts");
deposit(l,roof,8);clock(l,"2026-10-10T12:00:00Z",SeasonalWeather.Season.AUTUMN);clear(l);step(l,roof,128);check(l.getBlockState(roof).isAir(),"Autumn progressively melts");
clock(l,"2026-04-10T12:00:00Z",SeasonalWeather.Season.SPRING);clear(l);until(l,roof,30000,()->l.getBlockState(roof).isAir(),"Spring progressively melts");
deposit(l,roof,8);clock(l,"2026-10-10T12:00:00Z",SeasonalWeather.Season.AUTUMN);clear(l);until(l,roof,50000,()->l.getBlockState(roof).isAir(),"Autumn progressively melts");
deposit(l,roof,8);clock(l,"2026-07-10T12:00:00Z",SeasonalWeather.Season.SUMMER);
command(l,"sanctuary weather snow");l.getWeatherData().setRaining(false);l.setRainLevel(0);step(l,roof,64);check(layers(l,roof)==8,"Forced summer snowfall blocks melting");
command(l,"sanctuary weather snow_showers");l.getWeatherData().setRaining(false);l.setRainLevel(0);step(l,roof,64);check(layers(l,roof)==8,"Pauses in snow showers also block melting");
@@ -84,7 +90,7 @@ public final class Melt109ClientChecks implements FabricClientGameTest {
var l=server.overworld();clock(l,"2026-07-10T12:00:00Z",SeasonalWeather.Season.SUMMER);clear(l);
check(!l.getGameRules().get(SeasonalSnowMelt.ENABLED),"Melt rule persists across restart");
check(layers(l,PERSIST)==8&&layers(l,PERSIST.above())==8&&tracked(l,PERSIST)&&tracked(l,PERSIST.above()),"Chunk attachment survives native save/reload");
command(l,"gamerule sanctuary:seasonal_snow_melt true");step(l,PERSIST,16);
command(l,"gamerule sanctuary:seasonal_snow_melt true");until(l,PERSIST,30000,()->l.getBlockState(PERSIST).isAir()&&l.getBlockState(PERSIST.above()).isAir()&&!tracked(l,PERSIST), "Loaded natural snow melts and clears provenance");
check(l.getBlockState(PERSIST).isAir()&&l.getBlockState(PERSIST.above()).isAir()&&!tracked(l,PERSIST),"Loaded natural snow melts and clears provenance: base="+l.getBlockState(PERSIST)+", top="+l.getBlockState(PERSIST.above())+", sky="+l.canSeeSky(PERSIST.above(2))+", active="+WeatherService.active(l)+", biome="+l.getBiome(PERSIST)+", season="+WeatherService.season(server));
check(layers(l,new BlockPos(7,80,4))==8&&layers(l,new BlockPos(9,80,4))==3,"Built and preexisting snow preserved across restart");
});
@@ -92,13 +98,15 @@ public final class Melt109ClientChecks implements FabricClientGameTest {
System.out.println("MELT109_NATIVE_PASS: accumulation regression, top-down melt, seasons, profiles, cold biomes, constructions, edit invalidation, gamerule, native chunk save/reload");
}finally{intro.set(false);}
}
private static void deposit(ServerLevel l,BlockPos pos,int count){l.getGameRules().set(GameRules.MAX_SNOW_ACCUMULATION_HEIGHT,1,l.getServer());command(l,"sanctuary weather snow");l.getWeatherData().setRaining(true);l.setRainLevel(1);step(l,pos,count);l.getGameRules().set(GameRules.MAX_SNOW_ACCUMULATION_HEIGHT,0,l.getServer());}
private static void deposit(ServerLevel l,BlockPos pos,int count){l.getGameRules().set(GameRules.MAX_SNOW_ACCUMULATION_HEIGHT,1,l.getServer());command(l,"sanctuary weather snow");l.getWeatherData().setRaining(true);l.setRainLevel(1);int target=depth(l,pos)+count;until(l,pos,Math.max(1000,count*200),()->depth(l,pos)>=target,"Weather snow reaches depth "+target+" at "+pos);l.getGameRules().set(GameRules.MAX_SNOW_ACCUMULATION_HEIGHT,0,l.getServer());}
private static void clear(ServerLevel l){command(l,"sanctuary weather clear");l.getWeatherData().setRaining(false);l.setRainLevel(0);}
private static void clock(ServerLevel l,String time,SeasonalWeather.Season expected){try{var field=RealtimeService.Session.class.getDeclaredField("clock");field.setAccessible(true);field.set(RealtimeService.session(l.getServer()),Clock.fixed(Instant.parse(time),ZoneOffset.UTC));check(WeatherService.season(l.getServer())==expected,"Test calendar: "+expected);}catch(ReflectiveOperationException e){throw new AssertionError(e);}}
private static boolean tracked(ServerLevel l,BlockPos p){var m=l.getChunkAt(p).getAttached(SeasonalSnowMelt.NATURAL);return m!=null&&m.containsKey(Long.toString(p.asLong()));}
private static void snow(ServerLevel l,BlockPos p,int layers){l.setBlockAndUpdate(p,Blocks.SNOW.defaultBlockState().setValue(SnowLayerBlock.LAYERS,layers));}
private static int layers(ServerLevel l,BlockPos p){return SeasonalSnowMelt.layers(l.getBlockState(p));}
private static int depth(ServerLevel l,BlockPos p){int total=0;for(int i=0;i<32;i++){int layers=layers(l,p.above(i));if(layers==0)break;total+=layers;}return total;}
private static void step(ServerLevel l,BlockPos p,int n){for(int i=0;i<n;i++)l.tickPrecipitation(p);}
private static void until(ServerLevel l,BlockPos p,int max,BooleanSupplier condition,String message){for(int i=0;i<max&&!condition.getAsBoolean();i++)l.tickPrecipitation(p);check(condition.getAsBoolean(),message);}
private static void command(ServerLevel l,String s){l.getServer().getCommands().performPrefixedCommand(l.getServer().createCommandSourceStack(),s);}
private static KeyEvent enter(){return new KeyEvent(InputConstants.KEY_RETURN,0,0);}
private static void check(boolean ok,String message){if(!ok)throw new AssertionError(message);}
@@ -6,6 +6,7 @@ import fr.koka.sanctuary.progression.SanctuaryGameplay;
import fr.koka.sanctuary.weather.SnowAccumulation;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.BooleanSupplier;
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
import net.fabricmc.fabric.api.client.gametest.v1.FabricClientGameTest;
import net.fabricmc.fabric.api.client.gametest.v1.context.ClientGameTestContext;
@@ -45,42 +46,45 @@ public final class Snow108ClientChecks implements FabricClientGameTest {
if(enabled) {
command(l,"sanctuary weather snow");l.setRainLevel(1);
check(l.getGameRules().get(SnowAccumulation.MAX_HEIGHT)==2,"Default two blocks");
for(int i=1;i<=7;i++){l.tickPrecipitation(AT);check(layers(l,AT)==i,"One layer per precipitation event: "+i);}
check(SnowAccumulation.REALTIME_SCALE==12,"Snow accumulation uses the 12x real-time arrival scale");
for(int i=1;i<=7;i++){int expected=i;until(l,AT,1000,()->layers(l,AT)==expected,"Layer "+i+" arrives through the native sampler");}
var pig=EntityTypes.PIG.create(l,EntitySpawnReason.COMMAND);pig.setNoAi(true);pig.setNoGravity(true);pig.setPos(4.5,80.75,4.5);l.addFreshEntity(pig);
l.tickPrecipitation(AT);check(l.getBlockState(AT).is(Blocks.SNOW_BLOCK),"Eighth layer becomes solid snow block");
until(l,AT,1000,()->l.getBlockState(AT).is(Blocks.SNOW_BLOCK),"Eighth layer becomes solid snow block");
check(l.getBlockState(AT).getCollisionShape(l,AT,CollisionContext.empty()).bounds().maxY==1,"Full block collision");
check(pig.getY()>=81,"Growth lifts a standing entity");pig.discard();
l.tickPrecipitation(AT);check(layers(l,AT.above())==1,"New layer begins above solid snow");
for(int i=0;i<30;i++)l.tickPrecipitation(AT);
until(l,AT,1000,()->layers(l,AT.above())==1,"New layer begins above solid snow");
until(l,AT,2000,()->l.getBlockState(AT.above()).is(Blocks.SNOW_BLOCK),"Default cap fills a second block");
check(l.getBlockState(AT.above()).is(Blocks.SNOW_BLOCK)&&l.getBlockState(AT.above(2)).isAir(),"Default cap at two blocks");
command(l,"gamerule sanctuary:snow_accumulation_height 4");
for(int i=0;i<30;i++)l.tickPrecipitation(AT);
until(l,AT,5000,()->l.getBlockState(AT.above(3)).is(Blocks.SNOW_BLOCK),"Raised cap fills the fourth block");
check(l.getBlockState(AT.above(3)).is(Blocks.SNOW_BLOCK)&&l.getBlockState(AT.above(4)).isAir(),"Operator raises cap to four blocks");
command(l,"gamerule sanctuary:snow_accumulation_height -1");
for(int i=0;i<16;i++)l.tickPrecipitation(AT);
until(l,AT,3000,()->l.getBlockState(AT.above(5)).is(Blocks.SNOW_BLOCK),"Unlimited mode fills beyond the cap");
check(l.getBlockState(AT.above(5)).is(Blocks.SNOW_BLOCK),"Unlimited mode continues upward");
var fresh=new BlockPos(-5,80,-5);command(l,"gamerule sanctuary:snow_accumulation_height 0");l.tickPrecipitation(fresh);check(l.getBlockState(fresh).isAir(),"Sanctuary zero stops deposits");
command(l,"gamerule sanctuary:snow_accumulation_height 2");l.getGameRules().set(GameRules.MAX_SNOW_ACCUMULATION_HEIGHT,0,server);l.tickPrecipitation(fresh);check(l.getBlockState(fresh).isAir(),"Native zero remains authoritative");l.getGameRules().set(GameRules.MAX_SNOW_ACCUMULATION_HEIGHT,1,server);
l.setRainLevel(0);l.getWeatherData().setRaining(false);l.tickPrecipitation(fresh);check(l.getBlockState(fresh).isAir(),"No accumulation between showers");
command(l,"sanctuary weather snow_showers");l.setRainLevel(1);l.getWeatherData().setRaining(true);l.tickPrecipitation(fresh);check(layers(l,fresh)==1,"Snow showers accumulate while falling");
var flower=new BlockPos(-6,80,-6);l.setBlockAndUpdate(flower.below(),Blocks.GRASS_BLOCK.defaultBlockState());l.setBlockAndUpdate(flower,Blocks.DANDELION.defaultBlockState());l.tickPrecipitation(flower);check(l.getBlockState(flower).is(Blocks.DANDELION),"Plant preserved");
var water=new BlockPos(-7,79,-7);l.setBlockAndUpdate(water,Blocks.WATER.defaultBlockState());l.tickPrecipitation(water);check(l.getBlockState(water).is(Blocks.WATER)&&l.getBlockState(water.above()).isAir(),"Open water not filled with snow");
var roof=new BlockPos(7,83,-7);l.setBlockAndUpdate(roof,Blocks.STONE.defaultBlockState());l.tickPrecipitation(roof);check(layers(l,roof.above())==1&&l.getBlockState(roof.below()).isAir(),"Snow on roof, not indoors");
var fresh=new BlockPos(-5,80,-5);command(l,"gamerule sanctuary:snow_accumulation_height 0");step(l,fresh,200);check(l.getBlockState(fresh).isAir(),"Sanctuary zero stops deposits");
command(l,"gamerule sanctuary:snow_accumulation_height 2");l.getGameRules().set(GameRules.MAX_SNOW_ACCUMULATION_HEIGHT,0,server);step(l,fresh,200);check(l.getBlockState(fresh).isAir(),"Native zero remains authoritative");l.getGameRules().set(GameRules.MAX_SNOW_ACCUMULATION_HEIGHT,1,server);
l.setRainLevel(0);l.getWeatherData().setRaining(false);step(l,fresh,200);check(l.getBlockState(fresh).isAir(),"No accumulation between showers");
command(l,"sanctuary weather snow_showers");l.setRainLevel(1);l.getWeatherData().setRaining(true);until(l,fresh,1000,()->layers(l,fresh)==1,"Snow showers accumulate while falling");
var flower=new BlockPos(-6,80,-6);l.setBlockAndUpdate(flower.below(),Blocks.GRASS_BLOCK.defaultBlockState());l.setBlockAndUpdate(flower,Blocks.DANDELION.defaultBlockState());step(l,flower,200);check(l.getBlockState(flower).is(Blocks.DANDELION),"Plant preserved");
var water=new BlockPos(-7,79,-7);l.setBlockAndUpdate(water,Blocks.WATER.defaultBlockState());step(l,water,200);check(l.getBlockState(water).is(Blocks.WATER)&&l.getBlockState(water.above()).isAir(),"Open water not filled with snow");
var roof=new BlockPos(7,83,-7);l.setBlockAndUpdate(roof,Blocks.STONE.defaultBlockState());until(l,roof,1000,()->layers(l,roof.above())==1,"Snow on roof arrives");check(layers(l,roof.above())==1&&l.getBlockState(roof.below()).isAir(),"Snow on roof, not indoors");
var cauldron=new BlockPos(8,80,-8);l.setBlockAndUpdate(cauldron,Blocks.CAULDRON.defaultBlockState());for(int i=0;i<200;i++)l.tickPrecipitation(cauldron);check(l.getBlockState(cauldron).is(Blocks.POWDER_SNOW_CAULDRON),"Native cauldron snow filling preserved");
command(l,"sanctuary weather rain");l.setRainLevel(1);var rainy=new BlockPos(10,80,0);l.tickPrecipitation(rainy);check(l.getBlockState(rainy).isAir(),"Temperate rain is not snow");
command(l,"sanctuary weather vanilla");cold(l);for(int i=0;i<8;i++)l.tickPrecipitation(new BlockPos(0,80,0));check(l.getBlockState(new BlockPos(0,80,0)).is(Blocks.SNOW_BLOCK),"Cold biome also accumulates with vanilla weather");
command(l,"sanctuary weather vanilla");cold(l);until(l,new BlockPos(0,80,0),2000,()->l.getBlockState(new BlockPos(0,80,0)).is(Blocks.SNOW_BLOCK),"Cold biome also accumulates with vanilla weather");
var light=new BlockPos(11,80,11);l.setBlockAndUpdate(light.below(),Blocks.GLOWSTONE.defaultBlockState());command(l,"sanctuary weather snow");l.setRainLevel(1);
} else {
cold(l);for(int i=0;i<40;i++)l.tickPrecipitation(AT);check(layers(l,AT)==1&&l.getBlockState(AT.above()).isAir(),"Non-Sanctuary world keeps native one-layer limit");
}
});
if(enabled){c.waitTicks(8);world.getServer().runOnServer(s->{var l=s.overworld();var light=new BlockPos(11,80,11);check(l.getBrightness(LightLayer.BLOCK,light)>=10,"Test light propagated");for(int i=0;i<16;i++)l.tickPrecipitation(light);check(l.getBlockState(light).isAir(),"Bright surfaces do not accumulate");});}
if(enabled){c.waitTicks(8);world.getServer().runOnServer(s->{var l=s.overworld();var light=new BlockPos(11,80,11);check(l.getBrightness(LightLayer.BLOCK,light)>=10,"Test light propagated");step(l,light,400);check(l.getBlockState(light).isAir(),"Bright surfaces do not accumulate");});}
}
System.out.println("SNOW108_NATIVE_PASS: layers, solid blocks, vertical stacking, entity lift, caps and commands, weather, light, supports, cauldron and vanilla world");
}finally{intro.set(false);}
}
private static void cold(ServerLevel l){command(l,"fillbiome -8 76 -8 8 88 8 minecraft:snowy_plains");l.getWeatherData().setRaining(true);l.setRainLevel(1);}
private static int layers(ServerLevel l,BlockPos p){var s=l.getBlockState(p);return s.is(Blocks.SNOW)?s.getValue(SnowLayerBlock.LAYERS):0;}
private static void step(ServerLevel l,BlockPos p,int n){for(int i=0;i<n;i++)l.tickPrecipitation(p);}
private static void until(ServerLevel l,BlockPos p,int max,BooleanSupplier condition,String message){for(int i=0;i<max&&!condition.getAsBoolean();i++)l.tickPrecipitation(p);check(condition.getAsBoolean(),message);}
private static void command(ServerLevel l,String s){l.getServer().getCommands().performPrefixedCommand(l.getServer().createCommandSourceStack(),s);}
private static KeyEvent enter(){return new KeyEvent(InputConstants.KEY_RETURN,0,0);}
private static void check(boolean ok,String message){if(!ok)throw new AssertionError(message);}
@@ -151,6 +151,7 @@ public final class PixelShadows122ClientChecks implements FabricClientGameTest {
c.runOnClient(m -> check(VanillaLight.active(), "Native Sanctuary shader is active without an external engine"));
float morning = c.computeOnClient(m -> m.gameRenderer.gameRenderState().levelRenderState.skyRenderState.sunAngle);
if(Boolean.getBoolean("sanctuary.test.water144")){Water144ClientChecks.run(c,server);return;}
if(coloredOnly){if(Boolean.getBoolean("sanctuary.test.directional141Only")){DirectionalLights141ClientChecks.run(c,server);return;}ColoredLights139ClientChecks.run(c,server);if(Boolean.getBoolean("sanctuary.test.colored140"))ColoredLights140ClientChecks.run(c,server);if(Boolean.getBoolean("sanctuary.test.directional141"))DirectionalLights141ClientChecks.run(c,server);return;}
if(sunshaftOnly){Sunshafts137ClientChecks.run(c,server);return;}
if(closeupOnly){SsgiCloseup135ClientChecks.run(c,server);Pbr134ClientChecks.run(c,server);return;}
@@ -568,7 +569,7 @@ public final class PixelShadows122ClientChecks implements FabricClientGameTest {
}
static Frame capture(ClientGameTestContext c, String name) {
if (name != null) c.takeScreenshot("sanctuary-beta142-shader-" + name);
if (name != null) c.takeScreenshot((Boolean.getBoolean("sanctuary.test.water144")?"sanctuary-beta144-water-":"sanctuary-beta142-shader-") + name);
var result = new CompletableFuture<Frame>();
c.runOnClient(m -> Screenshot.takeScreenshot(m.gameRenderer.mainRenderTarget(), image -> {
try (image) {
@@ -0,0 +1,215 @@
package fr.koka.sanctuary.client.shader;
import static fr.koka.sanctuary.client.shader.PixelShadows122ClientChecks.*;
import fr.koka.sanctuary.gametest.Water144Metrics;
import net.fabricmc.fabric.api.client.gametest.v1.context.*;
import net.minecraft.core.BlockPos;
import net.minecraft.network.chat.Component;
import net.minecraft.world.level.block.Blocks;
import java.nio.file.*;
import java.util.*;
final class Water144ClientChecks {
static void run(ClientGameTestContext c,TestServerContext server){
c.runOnClient(m->{
VanillaLight.setEnabled(true);VanillaLight.setNaturalWater(true);VanillaLight.setWaterIntensity(100);
VanillaLight.setPbr(false);VanillaLight.setSsgi(false);VanillaLight.setBloom(false);VanillaLight.setOreEmission(false);
VanillaLight.setPixelShadows(false);VanillaLight.setColoredLights(false);VanillaLight.setSunshafts(false);VanillaLight.setLensFlare(false);VanillaLight.setEdgeHighlights(false);
});
server.runOnServer(s->{
command(s,"fill 47 94 -1 96 95 33 quartz_block");
command(s,"fill 47 96 -1 47 100 33 glass");command(s,"fill 96 96 -1 96 100 33 glass");
command(s,"fill 47 96 -1 96 100 -1 glass");command(s,"fill 47 96 33 96 100 33 glass");
command(s,"fill 64 95 0 79 95 32 sand");command(s,"fill 80 95 0 95 95 32 mud");
command(s,"fill 48 96 0 95 99 32 water");
command(s,"setblock 84 96 8 seagrass");
for(int x=50;x<95;x+=4)command(s,"setblock "+x+" 96 20 gold_block");
});
camera(c,server,72,125,16,70);c.waitTicks(50);settle(c);
if(Boolean.getBoolean("sanctuary.test.water144ExtrasOnly")){
resourceOverride(c);
camera(c,server,88.5,96.2,8.5,70);look(c,server,0,0);c.waitTicks(20);capture(c,"pond-final-fog");
var green=c.computeOnClient(m->new org.joml.Vector4f(m.gameRenderer.gameRenderState().levelRenderState.cameraRenderState.fogData.color));
enable(c,false);var blue=c.computeOnClient(m->new org.joml.Vector4f(m.gameRenderer.gameRenderState().levelRenderState.cameraRenderState.fogData.color));capture(c,"native-final-fog");
check(green.y>green.z&&green.y<Math.max(blue.x,Math.max(blue.y,blue.z))*.8,"Olive fog stays muted after native vision adaptation");
enable(c,true);extras(c,server);return;
}
if(!Boolean.getBoolean("sanctuary.test.water144PerfOnly")){
c.runOnClient(m->{
check(NaturalWater.sample(m.level,56.5,99.1,8.5).equals(NaturalWater.CLEAR),"Constructed bed clear");
check(NaturalWater.sample(m.level,72.5,99.1,8.5).equals(NaturalWater.LIGHT),"Sand lightly tinted");
check(NaturalWater.sample(m.level,88.5,99.1,8.5).equals(NaturalWater.POND),"Mud pond");
check(NaturalWater.sample(m.level,84.5,99.1,8.5).equals(NaturalWater.POND),"Seagrass is not a floor");
check(NaturalWater.sample(m.level,88.5,110.1,8.5).equals(NaturalWater.CLEAR),"No bed in bounded search uses clear");
var unloaded=new BlockPos(1_000_016,96,1_000_016);
check(!NaturalWater.loaded(m.level,unloaded.getX()>>4,unloaded.getZ()>>4),"Fixture region is unloaded");
var missing=new NaturalWater.Region(m.level,62500,5,62500);
check(missing.corner(null,unloaded,0,0).equals(NaturalWater.CLEAR),"Unavailable biome snapshot forces clear without reading missing terrain");
check(!NaturalWater.loaded(m.level,unloaded.getX()>>4,unloaded.getZ()>>4),"Classification does not load chunks");
var boundary=NaturalWater.sample(m.level,80,99.1,8.5);check(boundary.tint()>.1&&boundary.tint()<.85,"Section boundary mixes profiles");
});
enable(c,false);var off=capture(c,"surface-off");enable(c,true);var on=capture(c,"surface-on");
check(difference(off,on)>100000,"Visible surface colour and opacity change");
c.runOnClient(m->VanillaLight.setWaterIntensity(0));settle(c);var zero=capture(c,"surface-zero");
check(difference(off,zero)<difference(off,on)/4,"Zero restores native water (animated texture tolerance)");
c.runOnClient(m->VanillaLight.setWaterIntensity(100));settle(c);
camera(c,server,88.5,103,4.5,70);look(c,server,0,28);capture(c,"pond-closeup");
camera(c,server,72,125,16,70);
server.runOnServer(s->command(s,"fill 80 95 0 95 95 32 quartz_block"));c.waitTicks(30);settle(c);var changed=capture(c,"bed-changed");
check(difference(changed,on)>100000,"Changing bed four blocks below surface refreshes water");
server.runOnServer(s->command(s,"fill 80 95 0 95 95 32 mud"));c.waitTicks(25);settle(c);
server.runOnServer(s->command(s,"fill 80 103 0 95 103 12 stone"));c.waitTicks(25);settle(c);
c.runOnClient(m->check(NaturalWater.sample(m.level,88.5,99.1,8.5).equals(NaturalWater.POND),"Roof does not replace floor rule"));capture(c,"covered-pond");
server.runOnServer(s->command(s,"fill 80 103 0 95 103 12 air"));c.waitTicks(20);
for(int x:new int[]{56,72,88}){
camera(c,server,x+.5,96.2,8.5,70);look(c,server,0,0);c.waitTicks(20);capture(c,"immersed-"+x);
c.runOnClient(m->{check(m.gameRenderer.mainCamera().getFluidInCamera()==net.minecraft.world.level.material.FogType.WATER,"Camera immersed");
float expected=x==56?1:x==72?.7f:.4f;check(Math.abs(NaturalWater.fogProfile().fog()-expected)<.001,"Immersion uses corresponding profile");});
}
var pondColor=c.computeOnClient(m->new org.joml.Vector4f(m.gameRenderer.gameRenderState().levelRenderState.cameraRenderState.fogData.color));
check(pondColor.y>pondColor.z&&pondColor.y<.8f,"Olive fog avoids native neon normalization");
float pondFog=c.computeOnClient(m->m.gameRenderer.gameRenderState().levelRenderState.cameraRenderState.fogData.environmentalEnd);
enable(c,false);float nativeFog=c.computeOnClient(m->m.gameRenderer.gameRenderState().levelRenderState.cameraRenderState.fogData.environmentalEnd);
check(Math.abs(pondFog/nativeFog-.4)<.01,"Rendered underwater fog distance matches pond profile");capture(c,"immersed-native");enable(c,true);
camera(c,server,72,125,16,70);
server.runOnServer(s->command(s,"fillbiome 80 88 0 95 104 32 minecraft:ocean"));c.waitTicks(35);settle(c);
c.runOnClient(m->{
check(NaturalWater.sample(m.level,88.5,99.1,8.5).equals(NaturalWater.CLEAR),"Ocean overrides muddy bed");
boolean found=false;
for(int x=72;x<89;x++){
var p=new BlockPos(x,99,8);
if(!NaturalWater.ocean(m.level,p)&&NaturalWater.ocean(m.level,p.east())){
var left=new NaturalWater.Region(m.level,(x>>4)-1,5,-1);
var right=new NaturalWater.Region(m.level,((x+1)>>4)-1,5,-1);
check(left.corner(null,p,1,0).equals(right.corner(null,p.east(),0,0)),"Ocean edge emits identical shared vertices");
found=true;break;
}
}
check(found,"Native ocean boundary exercised");
});capture(c,"ocean-over-mud");
server.runOnServer(s->command(s,"fillbiome 80 88 0 95 104 32 minecraft:plains"));c.waitTicks(30);
var reload=c.computeOnClient(net.minecraft.client.Minecraft::reloadResourcePacks);c.waitFor(m->reload.isDone()&&m.gui.overlay()==null,1200);reload.join();settle(c);capture(c,"resources-reloaded");
resourceOverride(c);
camera(c,server,400,120,16,70);c.waitTicks(30);camera(c,server,72,125,16,70);settle(c);
c.runOnClient(m->{check(NaturalWater.sample(m.level,88.5,99.1,8.5).equals(NaturalWater.POND),"Reloaded chunks keep correct profile");
VanillaLight.setNaturalWater(false);VanillaLight.setWaterIntensity(37);forgetPreferences();check(!VanillaLight.naturalWater()&&VanillaLight.waterIntensity()==37,"Water preferences persist");
VanillaLight.setNaturalWater(true);VanillaLight.setWaterIntensity(100);
for(String locale:new String[]{"fr_fr","en_us"}){m.getLanguageManager().setSelected(locale);m.getLanguageManager().onResourceManagerReload(m.getResourceManager());
for(String key:new String[]{"natural_water","natural_water_help","water_intensity","water_intensity_help","water_intensity_value"}){String full="sanctuary.shaders."+key;check(!Component.translatable(full,100).getString().equals(full),"FR/EN "+full);}}
});settle(c);
}
// All effects enabled; fixed inherited test quality (shadows 32 blocks, PBR 50%) for A/B comparison.
c.runOnClient(m->{VanillaLight.setPbr(true);VanillaLight.setSsgi(true);VanillaLight.setBloom(true);VanillaLight.setOreEmission(true);
VanillaLight.setPixelShadows(true);VanillaLight.setColoredLights(true);VanillaLight.setSunshafts(true);VanillaLight.setLensFlare(true);VanillaLight.setEdgeHighlights(true);});
c.waitTicks(80);capture(c,"combined");
benchmark(c,server);
extras(c,server);
System.out.println("WATER144_PASS surface, immersion, beds, ocean, section seam, roof, plants, invalidation, reload, preferences, FR/EN and combined rendering");
}
private static void enable(ClientGameTestContext c,boolean value){c.runOnClient(m->VanillaLight.setNaturalWater(value));settle(c);}
private static void settle(ClientGameTestContext c){c.waitTicks(8);c.waitFor(m->m.levelRenderer.hasRenderedAllSections(),1200);c.waitTicks(20);}
private static long difference(Frame a,Frame b){long value=0;for(int i=0;i<a.pixels().length;i++)value+=channelDifference(a.pixels()[i],b.pixels()[i]);return value;}
private static void extras(ClientGameTestContext c,TestServerContext server){
server.runOnServer(s->{
for(int x:new int[]{100,108}){
command(s,"fill "+x+" 95 0 "+(x+4)+" 104 16 glass");
command(s,"fill "+(x+1)+" 96 1 "+(x+3)+" 103 15 air");
command(s,"fill "+(x+1)+" 95 1 "+(x+3)+" 95 15 mud");
}
command(s,"setblock 102 102 2 water");command(s,"setblock 110 102 2 lava");
command(s,"setblock 56 99 8 oak_slab[waterlogged=true]");
});
c.waitTicks(80);camera(c,server,106,103,25,70);look(c,server,179,20);settle(c);
c.runOnClient(m->{
var flow=m.level.getFluidState(new BlockPos(102,101,2));check(flow.is(net.minecraft.tags.FluidTags.WATER)&&!flow.isSource(),"Real falling water is rendered");
check(m.level.getFluidState(new BlockPos(110,101,2)).is(net.minecraft.tags.FluidTags.LAVA),"Lava cascade remains lava");
check(m.level.getBlockState(new BlockPos(56,99,8)).getValue(net.minecraft.world.level.block.state.properties.BlockStateProperties.WATERLOGGED),"Waterlogged slab preserved");
});
enable(c,false);var off=capture(c,"cascades-off");enable(c,true);var on=capture(c,"cascades-on");
check(difference(off,on)>1000,"Waterfall responds to water option");
int lava=0,stable=0;for(int i=0;i<off.pixels().length;i++){int a=off.pixels()[i];int r=a>>16&255,g=a>>8&255,b=a&255;
if(r>160&&r>g*1.3&&b<90){lava++;if(channelDifference(a,on.pixels()[i])<70)stable++;}}
check(lava>100&&stable>lava*.8,"Lava colours remain native (animation tolerance)");
camera(c,server,56.5,102,8.5,70);capture(c,"waterlogged-slab");
System.out.println("WATER144_EXTRAS_PASS falling water, unchanged lava and waterlogged slab");
}
private static net.minecraft.client.renderer.texture.SpriteContents waterSprite(net.minecraft.client.Minecraft m,String name){
return m.getAtlasManager().getAtlasOrThrow(net.minecraft.data.AtlasIds.BLOCKS)
.getSprite(net.minecraft.resources.Identifier.withDefaultNamespace("block/"+name)).contents();
}
private static void resourceOverride(ClientGameTestContext c){
int originalWidth=c.computeOnClient(m->waterSprite(m,"water_still").width());
int originalFrames=c.computeOnClient(m->waterSprite(m,"water_still").getUniqueFrames().size());
var directory=c.computeOnClient(m->m.getResourcePackDirectory().resolve("water144-fixture"));
try{
Files.createDirectories(directory.resolve("assets/sanctuary"));
var mod=net.fabricmc.loader.api.FabricLoader.getInstance().getModContainer("sanctuary").orElseThrow();
Files.copy(mod.findPath("resourcepacks/template/pack.mcmeta").orElseThrow(),directory.resolve("pack.mcmeta"),StandardCopyOption.REPLACE_EXISTING);
var textures=directory.resolve("assets/minecraft/textures/block");Files.createDirectories(textures);
try(var pixels=new com.mojang.blaze3d.platform.NativeImage(32,64,false)){
for(int y=0;y<64;y++)for(int x=0;x<32;x++)pixels.setPixel(x,y,((x/4+y/4+y/32)&1)==0?0xd0ffffff:0x80404040);
for(String name:new String[]{"water_still","water_flow"}){
pixels.writeToFile(textures.resolve(name+".png"));
Files.writeString(textures.resolve(name+".png.mcmeta"),"{\"animation\":{\"frametime\":4,\"frames\":[0,1]}}");
}
}
Files.writeString(directory.resolve("assets/sanctuary/water_materials.json"),"{\"light\":[],\"pond\":[\"minecraft:quartz_block\"]}");
}catch(Exception e){throw new AssertionError(e);}
c.runOnClient(m->{var repo=m.getResourcePackRepository();repo.reload();check(repo.addPack("file/water144-fixture"),"Custom materials pack discovered");});
var future=c.computeOnClient(net.minecraft.client.Minecraft::reloadResourcePacks);c.waitFor(m->future.isDone()&&m.gui.overlay()==null,1200);future.join();settle(c);
c.runOnClient(m->{check(NaturalWater.sample(m.level,56.5,99.1,8.5).equals(NaturalWater.POND),"Resource pack replaces material family");
check(NaturalWater.sample(m.level,88.5,99.1,8.5).equals(NaturalWater.CLEAR),"Replacement removes bundled assignment");});
c.runOnClient(m->{for(String name:new String[]{"water_still","water_flow"}){
var sprite=waterSprite(m,name);check(sprite.width()==32&&sprite.height()==32&&sprite.getUniqueFrames().size()==2,"Custom pixel grid and animation loaded: "+name);
}});
var first=capture(c,"custom-materials");long animated=0;
for(int i=0;i<8;i++){c.waitTicks(1);animated=Math.max(animated,difference(first,capture(c,"custom-animation-"+i)));}
check(animated>100000,"Custom water animation remains visible");
c.runOnClient(m->m.getResourcePackRepository().removePack("file/water144-fixture"));
var restore=c.computeOnClient(net.minecraft.client.Minecraft::reloadResourcePacks);c.waitFor(m->restore.isDone()&&m.gui.overlay()==null,1200);restore.join();settle(c);
c.runOnClient(m->{
check(NaturalWater.sample(m.level,88.5,99.1,8.5).equals(NaturalWater.POND),"Disabling custom pack restores defaults");
check(waterSprite(m,"water_still").width()==originalWidth&&waterSprite(m,"water_still").getUniqueFrames().size()==originalFrames,"Native water texture restored");
});
System.out.println("WATER144_TEXTURE_PASS custom 32px still/flow textures, alpha, two animation frames and restoration");
}
private static void benchmark(ClientGameTestContext c,TestServerContext s){
int oldLimit=c.computeOnClient(m->m.options.framerateLimit().get());
var oldInactivity=c.computeOnClient(m->m.options.inactivityFpsLimit().get());
c.runOnClient(m->{m.options.framerateLimit().set(net.minecraft.client.Options.UNLIMITED_FRAMERATE_CUTOFF);m.options.inactivityFpsLimit().set(net.minecraft.client.InactivityFpsLimit.MINIMIZED);m.getFramerateLimitTracker().setFramerateLimit(net.minecraft.client.Options.UNLIMITED_FRAMERATE_CUTOFF);});
var results=new LinkedHashMap<String,Object>();
try {
for(String scene:new String[]{"mixed","large","moving"}){
if(scene.equals("large")){s.runOnServer(server->{command(server,"fill 31 94 -17 112 100 -17 glass");command(server,"fill 31 94 48 112 100 48 glass");
command(server,"fill 31 94 -17 31 100 48 glass");command(server,"fill 112 94 -17 112 100 48 glass");
command(server,"fill 32 93 -16 111 93 47 quartz_block");command(server,"fill 32 94 -16 111 94 47 sand");command(server,"fill 32 95 -16 111 99 47 water");});c.waitTicks(50);}
if(scene.equals("moving")){
// Test fixture only: prepare water beyond the initial client view so movement really compiles terrain.
s.runOnServer(server->{
command(server,"forceload add 112 -17 288 48");
for(int x=7;x<=18;x++)for(int z=-2;z<=3;z++)server.overworld().getChunk(x,z);
command(server,"fill 112 94 -17 288 100 -17 glass");command(server,"fill 112 94 48 288 100 48 glass");
command(server,"fill 288 94 -17 288 100 48 glass");
for(int x=112;x<288;x+=16){
command(server,"fill "+x+" 93 -16 "+(x+15)+" 93 47 quartz_block");
command(server,"fill "+x+" 94 -16 "+(x+15)+" 94 47 sand");
command(server,"fill "+x+" 95 -16 "+(x+15)+" 99 47 water");
}
});c.waitTicks(80);
}
camera(c,s,72,125,16,70);settle(c);
for(boolean enabled:new boolean[]{false,true,true,false,true,false,false,true}){
camera(c,s,72,125,16,70);enable(c,enabled);c.waitTicks(80);c.runOnClient(m->{
check(m.getFramerateLimitTracker().getFramerateLimit()==260&&!m.options.enableVsync().get(),"Uncapped native limiter and VSync disabled");
System.out.println("WATER144_CONDITIONS limit="+m.getFramerateLimitTracker().getFramerateLimit()+" throttle="+m.getFramerateLimitTracker().getThrottleReason()+" renderLimit="+m.gameRenderer.gameRenderState().framerateLimit+" surface="+m.windowSurface().currentConfiguration()+" limiterBypass=true");
Water144Metrics.start();});
if(scene.equals("moving")){
for(int i=0;i<200;i++){final int step=i;c.runOnClient(m->{m.player.setPos(48+step*.6,125,16);m.player.setDeltaMovement(net.minecraft.world.phys.Vec3.ZERO);});c.waitTicks(1);}
}else c.waitTicks(200);
var values=c.computeOnClient(m->Water144Metrics.stop());String key=scene+"-"+enabled+"-"+results.size();results.put(key,values);
System.out.println("WATER144_PERF "+key+" "+Arrays.toString(values));
}
}
try{Files.writeString(Path.of("water144-performance.json"),new com.google.gson.GsonBuilder().setPrettyPrinting().create().toJson(results));}catch(Exception e){throw new AssertionError(e);}
}finally{c.runOnClient(m->{m.options.framerateLimit().set(oldLimit);m.getFramerateLimitTracker().setFramerateLimit(oldLimit);m.options.inactivityFpsLimit().set(oldInactivity);});}
}
}
@@ -0,0 +1,11 @@
package fr.koka.sanctuary.gametest;
import java.util.*;
public final class Water144Metrics {
public static volatile boolean recording;
private static final List<Long> frames=new ArrayList<>(),sections=new ArrayList<>();
public static synchronized void frame(long ns){if(recording)frames.add(ns);}
public static synchronized void section(long ns){if(recording)sections.add(ns);}
public static synchronized void start(){frames.clear();sections.clear();recording=true;}
public static synchronized double[] stop(){recording=false;return new double[]{frames.size(),quantile(frames,.5),quantile(frames,.95),sections.size(),quantile(sections,.5),quantile(sections,.95)};}
private static double quantile(List<Long> values,double q){if(values.isEmpty())return 0;var sorted=values.stream().mapToLong(Long::longValue).sorted().toArray();return sorted[Math.min(sorted.length-1,(int)(sorted.length*q))]/1e6;}
}
@@ -0,0 +1,17 @@
package fr.koka.sanctuary.gametest.mixin;
import fr.koka.sanctuary.gametest.Water144Metrics;
import com.llamalad7.mixinextras.injector.wrapoperation.*;
import net.minecraft.client.Minecraft;
import org.spongepowered.asm.mixin.*;
import org.spongepowered.asm.mixin.injection.*;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
@Mixin(Minecraft.class)
abstract class Water144FrameProbeMixin {
@Unique private long sanctuary$start;
// Benchmark only: the extracted frame state can retain a 120 FPS limit despite the option/tracker.
@WrapOperation(method="renderFrame",at=@At(value="INVOKE",target="Lnet/minecraft/client/FramerateLimiter;limitDisplayFPS(I)V"))
private void sanctuary$uncapped(int fps,Operation<Void> original){if(!Water144Metrics.recording)original.call(fps);}
@Inject(method="renderFrame",at=@At("HEAD")) private void start(boolean tick,CallbackInfo ci){if(Water144Metrics.recording)sanctuary$start=System.nanoTime();}
@Inject(method="renderFrame",at=@At("RETURN")) private void end(boolean tick,CallbackInfo ci){if(Water144Metrics.recording)Water144Metrics.frame(System.nanoTime()-sanctuary$start);}
}
@@ -0,0 +1,17 @@
package fr.koka.sanctuary.gametest.mixin;
import fr.koka.sanctuary.gametest.Water144Metrics;
import com.llamalad7.mixinextras.injector.wrapmethod.WrapMethod;
import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
import net.minecraft.client.renderer.chunk.*;
import net.minecraft.client.renderer.SectionBufferBuilderPack;
import net.minecraft.core.SectionPos;
import com.mojang.blaze3d.vertex.VertexSorting;
import org.spongepowered.asm.mixin.Mixin;
@Mixin(SectionCompiler.class)
abstract class Water144SectionProbeMixin {
@WrapMethod(method="compile")
private SectionCompiler.Results compile(SectionPos pos,RenderSectionRegion region,VertexSorting sorting,SectionBufferBuilderPack buffers,Operation<SectionCompiler.Results> original){
boolean record=Water144Metrics.recording;long start=record?System.nanoTime():0;
try{return original.call(pos,region,sorting,buffers);}finally{if(record)Water144Metrics.section(System.nanoTime()-start);}
}
}
@@ -19,6 +19,8 @@
"Atlas026TextureProbeMixin",
"Inventory013MerchantAccess",
"Dynamic030TerrainProbeMixin",
"Star040RenderProbeMixin"
"Star040RenderProbeMixin",
"Water144FrameProbeMixin",
"Water144SectionProbeMixin"
]
}
@@ -18,6 +18,7 @@ public final class SanctuaryKnowledgeClient implements ClientModInitializer {
public static void registerClient() {
SanctuaryResourcePack.register();
fr.koka.sanctuary.client.shader.NaturalWater.register();
VortexCloudRenderer.register();
fr.koka.sanctuary.client.shader.PixelShadows.register();
fr.koka.sanctuary.client.shader.EdgeHighlights.register();
@@ -0,0 +1,172 @@
package fr.koka.sanctuary.client.shader;
import com.google.gson.*;
import fr.koka.sanctuary.SanctuaryMod;
import java.util.*;
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
import net.fabricmc.fabric.api.resource.v1.ResourceLoader;
import net.minecraft.client.Minecraft;
import net.minecraft.client.multiplayer.ClientLevel;
import net.minecraft.client.renderer.block.BlockAndTintGetter;
import net.minecraft.core.BlockPos;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.resources.Identifier;
import net.minecraft.server.packs.PackType;
import net.minecraft.server.packs.resources.ResourceManagerReloadListener;
import net.minecraft.tags.BiomeTags;
import net.minecraft.tags.FluidTags;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.state.BlockState;
/** Client-only water materials; no world writes, per-frame terrain scan or additional render pass. */
public final class NaturalWater {
public static final int DEPTH=8;
public record Profile(float tint,float alpha,float fog) {
public Profile mix(Profile b,float t){if(this==b||t==0)return this;if(t==1)return b;return new Profile(tint+(b.tint-tint)*t,alpha+(b.alpha-alpha)*t,fog+(b.fog-fog)*t);}
}
public static final Profile CLEAR=new Profile(0,.55f,1),LIGHT=new Profile(.10f,.80f,.70f),POND=new Profile(.85f,1,.40f);
private static final Profile[] PROFILES={CLEAR,LIGHT,POND};
private static volatile Map<Block,Integer> materials=Map.of();
private static volatile float strength;
private static ClientLevel previousLevel;
private static Profile fogFrom=CLEAR,fogTarget=CLEAR;
private static long fogChanged;
private static boolean wasUnderwater;
private NaturalWater(){}
public static void register(){
ResourceLoader.get(PackType.CLIENT_RESOURCES).registerReloadListener(Identifier.parse("sanctuary:water_materials"),
(ResourceManagerReloadListener) manager->{
try(var reader=manager.getResourceOrThrow(Identifier.parse("sanctuary:water_materials.json")).openAsReader()){
var root=JsonParser.parseReader(reader).getAsJsonObject();var next=new HashMap<Block,Integer>();
for(var entry:Map.of("light",1,"pond",2).entrySet()) for(var id:root.getAsJsonArray(entry.getKey())){
var key=Identifier.parse(id.getAsString());
if(!BuiltInRegistries.BLOCK.containsKey(key))throw new IllegalArgumentException("Unknown water material "+key);
if(next.put(BuiltInRegistries.BLOCK.getValue(key),entry.getValue())!=null)throw new IllegalArgumentException("Duplicate water material "+key);
}
materials=Map.copyOf(next);
}catch(Exception e){SanctuaryMod.LOGGER.warn("Invalid water materials; using bundled defaults",e);materials=defaults();}
});
ClientTickEvents.END_CLIENT_TICK.register(client->{
float next=VanillaLight.active()&&VanillaLight.naturalWater()?VanillaLight.waterIntensity()/100f:0;
if(previousLevel!=client.level||strength!=next){previousLevel=client.level;strength=next;wasUnderwater=false;
if(client.level!=null)invalidateWaterSections(client);}
if(client.level==null||next==0){wasUnderwater=false;return;}
var camera=client.gameRenderer.mainCamera();
if(camera.getFluidInCamera()!=net.minecraft.world.level.material.FogType.WATER){wasUnderwater=false;return;}
var pos=camera.position();var target=sample(client.level,pos.x,pos.y,pos.z);
if(!wasUnderwater){fogFrom=fogTarget=target;fogChanged=System.nanoTime();wasUnderwater=true;}
else if(!target.equals(fogTarget)){fogFrom=fogProfile();fogTarget=target;fogChanged=System.nanoTime();}
});
}
/** Preference changes only rebuild loaded water meshes, retaining unrelated shader caches. */
private static void invalidateWaterSections(Minecraft client){
var level=client.level;
var center=BlockPos.containing(client.gameRenderer.mainCamera().position());
int radius=client.options.getEffectiveRenderDistance()+1;
for(int z=(center.getZ()>>4)-radius;z<=(center.getZ()>>4)+radius;z++)
for(int x=(center.getX()>>4)-radius;x<=(center.getX()>>4)+radius;x++){
var chunk=level.getChunkSource().getChunk(x,z,net.minecraft.world.level.chunk.status.ChunkStatus.FULL,false);
if(chunk==null)continue;
var sections=chunk.getSections();
for(int i=0;i<sections.length;i++)if(!sections[i].hasOnlyAir()
&&sections[i].getStates().maybeHas(state->state.getFluidState().is(FluidTags.WATER)))
client.levelExtractor.setSectionDirty(x,level.getSectionYFromSectionIndex(i),z);
}
}
private static Map<Block,Integer> defaults(){
var result=new HashMap<Block,Integer>();
for(String name:new String[]{"sand","red_sand","gravel","clay"})result.put(BuiltInRegistries.BLOCK.getValue(Identifier.withDefaultNamespace(name)),1);
for(String name:new String[]{"dirt","coarse_dirt","rooted_dirt","grass_block","podzol","mycelium","mud","muddy_mangrove_roots"})result.put(BuiltInRegistries.BLOCK.getValue(Identifier.withDefaultNamespace(name)),2);
return Map.copyOf(result);
}
public static float strength(){return strength;}
public static Profile fogProfile(){return fogFrom.mix(fogTarget,Math.clamp((System.nanoTime()-fogChanged)/500_000_000f,0,1));}
// ClientLevel.hasChunk always returns true; inspect the cache without requesting a chunk.
public static boolean loaded(ClientLevel level,int x,int z){
return level.getChunkSource().getChunk(x,z,net.minecraft.world.level.chunk.status.ChunkStatus.FULL,false)!=null;
}
public static boolean ocean(ClientLevel level,BlockPos pos){return loaded(level,pos.getX()>>4,pos.getZ()>>4)&&level.getBiome(pos).is(BiomeTags.IS_OCEAN);}
private static int classify(java.util.function.Function<BlockPos,BlockState> read,BlockPos pos,Map<Block,Integer> table){
var cursor=pos.mutable();
for(int i=1;i<=DEPTH;i++){
cursor.set(pos.getX(),pos.getY()-i,pos.getZ());var state=read.apply(cursor);
if(state==null||state.isAir())return 0;
if(state.isSolid()||!state.getCollisionShape(net.minecraft.world.level.EmptyBlockGetter.INSTANCE,cursor).isEmpty()){
// Plants have no collision; a waterlogged solid remains a basin floor.
return table.getOrDefault(state.getBlock(),0);
}
if(!state.getFluidState().is(FluidTags.WATER))return 0;
}
return 0;
}
public static Profile sample(ClientLevel level,double x,double y,double z){
var pos=BlockPos.containing(x,y,z);if(ocean(level,pos))return CLEAR;
int bx=(int)Math.floor(x-.5),bz=(int)Math.floor(z-.5),by=pos.getY();
float dx=(float)(x-.5-bx),dz=(float)(z-.5-bz);
return live(level,bx,by,bz).mix(live(level,bx+1,by,bz),dx).mix(live(level,bx,by,bz+1).mix(live(level,bx+1,by,bz+1),dx),dz);
}
private static Profile live(ClientLevel l,int x,int y,int z){
var p=new BlockPos(x,y,z);if(!loaded(l,x>>4,z>>4)||ocean(l,p))return CLEAR;
return PROFILES[classify(l::getBlockState,p,materials)];
}
/** Owned by a single native section compilation, with ocean data captured before worker submission. */
public static final class Region {
private final int ox,oy,oz;
private final net.minecraft.world.level.biome.BiomeManager biomes;
private byte[] oceanKinds;
private byte[] profiles;
private final Map<Block,Integer> table=materials;
public final float intensity=strength;
public Region(ClientLevel level,int minX,int minY,int minZ){
ox=(minX+1)*16;oy=(minY+1)*16;oz=(minZ+1)*16;
// Copy quart holders, then keep the native seeded biome zoom on the immutable copy.
// Sampling only quart centres would misclassify the jagged edges of ocean biomes.
var copied=new java.util.ArrayList<net.minecraft.core.Holder<net.minecraft.world.level.biome.Biome>>(216);
int qx=(ox>>2)-1,qy=(oy>>2)-1,qz=(oz>>2)-1;
for(int y=0;y<6;y++)for(int z=0;z<6;z++)for(int x=0;x<6;x++)
copied.add(loaded(level,(qx+x)>>2,(qz+z)>>2)?level.getBiomeManager().getNoiseBiomeAtQuart(qx+x,qy+y,qz+z):null);
biomes=level.getBiomeManager().withDifferentSource((x,y,z)->copied.get(((y-qy)*6+z-qz)*6+x-qx));
}
// A missing biome snapshot also forces clear water at unloaded region edges.
public boolean ocean(int x,int y,int z){
if(oceanKinds==null)oceanKinds=new byte[18*16*18];
int key=((y-oy)*18+z-oz+1)*18+x-ox+1;
int code=oceanKinds[key];if(code==0){var biome=biomes.getBiome(x,y,z);code=biome==null||biome.is(BiomeTags.IS_OCEAN)?2:1;oceanKinds[key]=(byte)code;}
return code==2;
}
private Profile profile(BlockAndTintGetter region,int x,int y,int z){
if(x<ox-1||x>ox+16||y<oy||y>oy+15||z<oz-1||z>oz+16)return CLEAR;
if(ocean(x,y,z))return CLEAR;
int key=((y-oy)*18+z-oz+1)*18+x-ox+1;
if(profiles==null)profiles=new byte[18*16*18];
int code=profiles[key];if(code==0){code=1+classify(region::getBlockState,new BlockPos(x,y,z),table);profiles[key]=(byte)code;}
return PROFILES[code-1];
}
public Profile corner(BlockAndTintGetter region,BlockPos p,int dx,int dz){
if(ocean(p.getX(),p.getY(),p.getZ()))return CLEAR;
int x=p.getX()+dx-1,y=p.getY(),z=p.getZ()+dz-1;
// Both cells sharing an ocean edge must emit the same clear vertex.
// The one-block transition stays on the non-ocean side, preserving ocean priority.
if(ocean(x,y,z)||ocean(x+1,y,z)||ocean(x,y,z+1)||ocean(x+1,y,z+1))return CLEAR;
return profile(region,x,y,z).mix(profile(region,x+1,y,z),.5f).mix(profile(region,x,y,z+1).mix(profile(region,x+1,y,z+1),.5f),.5f);
}
}
public interface RegionAccess { Region sanctuary$waterRegion(); }
public static int color(int color,int base,Profile profile,float intensity){
float tint=profile.tint*intensity;
int r=color>>16&255,g=color>>8&255,b=color&255;
// Restore directional face shading after tinting, including custom biome/resource-pack colours.
float shade=(r+g+b)/(float)Math.max(1,(base>>16&255)+(base>>8&255)+(base&255));
int rr=Math.clamp(Math.round(r*(1-tint)+120*shade*tint),0,255);
int gg=Math.clamp(Math.round(g*(1-tint)+132*shade*tint),0,255);
int bb=Math.clamp(Math.round(b*(1-tint)+72*shade*tint),0,255);
int a=Math.round((color>>>24)*(1+(profile.alpha-1)*intensity));
return a<<24|rr<<16|gg<<8|bb;
}
public static void dirty(BlockPos pos){
if(strength==0)return;
var renderer=Minecraft.getInstance().levelExtractor;
renderer.setSectionRangeDirty((pos.getX()-1)>>4,pos.getY()>>4,(pos.getZ()-1)>>4,
(pos.getX()+1)>>4,(pos.getY()+DEPTH)>>4,(pos.getZ()+1)>>4);
}
}
@@ -14,6 +14,9 @@ public final class ShaderOptionsScreen extends OptionsSubScreen {
list.addBig(OptionInstance.createBoolean("sanctuary.shaders.vanilla_light",OptionInstance.cachedConstantTooltip(
Component.translatable(VanillaLight.externalEngine()?"sanctuary.shaders.external":"sanctuary.shaders.help")),
VanillaLight.enabled(),VanillaLight::setEnabled));
list.addBig(OptionInstance.createBoolean("sanctuary.shaders.natural_water",OptionInstance.cachedConstantTooltip(
Component.translatable("sanctuary.shaders.natural_water_help")),VanillaLight.naturalWater(),VanillaLight::setNaturalWater));
slider("water_intensity",VanillaLight.waterIntensity(),VanillaLight::setWaterIntensity);
list.addBig(new OptionInstance<Integer>("sanctuary.shaders.saturation",OptionInstance.cachedConstantTooltip(
Component.translatable("sanctuary.shaders.saturation_help")),(caption,value)->Component.translatable("sanctuary.shaders.saturation_value",value),
new OptionInstance.IntRange(0,200),VanillaLight.saturation(),VanillaLight::setSaturation));
@@ -16,6 +16,8 @@ public final class VanillaLight {
private VanillaLight() { }
private static final class Preferences {
boolean enabled=true;
boolean naturalWater=true;
int waterIntensity=100;
int saturation=142;
boolean pixelShadows=true;
boolean bloom=true;
@@ -58,6 +60,7 @@ public final class VanillaLight {
preferences.oreIntensity=Math.clamp(preferences.oreIntensity,0,100);
preferences.edgeIntensity=Math.clamp(preferences.edgeIntensity,0,100);
preferences.edgeDistance=normalizeShadowDistance(preferences.edgeDistance);
preferences.waterIntensity=Math.clamp(preferences.waterIntensity,0,100);
preferences.saturation=Math.clamp(preferences.saturation,0,200);
preferences.shadowPixels=normalizePrecision(preferences.shadowPixels);
preferences.shadowDistance=normalizeShadowDistance(preferences.shadowDistance);
@@ -66,6 +69,10 @@ public final class VanillaLight {
}
public static boolean externalEngine(){return EXTERNAL_ENGINE;}
public static boolean active(){return ProgressionClient.active()&&!EXTERNAL_ENGINE&&enabled();}
public static boolean naturalWater(){enabled();return preferences.naturalWater;}
public static int waterIntensity(){enabled();return preferences.waterIntensity;}
public static void setNaturalWater(boolean value){enabled();preferences.naturalWater=value;save();}
public static void setWaterIntensity(int value){enabled();preferences.waterIntensity=Math.clamp(value,0,100);save();}
public static int saturation(){enabled();return preferences.saturation;}
public static boolean edgeHighlights(){enabled();return preferences.edgeHighlights;}
public static int edgeDistance(){enabled();return preferences.edgeDistance;}
@@ -0,0 +1,48 @@
package fr.koka.sanctuary.mixin.client;
import com.llamalad7.mixinextras.injector.wrapmethod.WrapMethod;
import com.llamalad7.mixinextras.injector.wrapoperation.*;
import com.mojang.blaze3d.vertex.VertexConsumer;
import fr.koka.sanctuary.client.shader.NaturalWater;
import net.minecraft.client.color.block.BlockTintSource;
import net.minecraft.client.renderer.block.*;
import net.minecraft.core.BlockPos;
import net.minecraft.tags.FluidTags;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.material.FluidState;
import org.spongepowered.asm.mixin.*;
import org.spongepowered.asm.mixin.injection.At;
/** Native renderer instances belong to one section compiler. Context is restored even on failure. */
@Mixin(FluidRenderer.class)
abstract class NaturalWaterFluidMixin {
@Unique private NaturalWater.Profile[] sanctuary$corners;
@Unique private NaturalWater.Region sanctuary$snapshot;
@Unique private BlockAndTintGetter sanctuary$region;
@Unique private BlockPos sanctuary$pos;
@Unique private float sanctuary$strength;
@Unique private int sanctuary$x,sanctuary$z,sanctuary$base;
@WrapMethod(method="tesselate")
private void sanctuary$water(BlockAndTintGetter region,BlockPos pos,FluidRenderer.Output output,BlockState state,FluidState fluid,Operation<Void> original){
var previous=sanctuary$snapshot;
try{
sanctuary$corners=null;sanctuary$snapshot=null;sanctuary$base=-1;
if(fluid.is(FluidTags.WATER)&&region instanceof NaturalWater.RegionAccess access){
var snapshot=access.sanctuary$waterRegion();
if(snapshot!=null){
sanctuary$strength=snapshot.intensity;sanctuary$x=pos.getX()&15;sanctuary$z=pos.getZ()&15;
sanctuary$snapshot=snapshot;sanctuary$region=region;sanctuary$pos=pos;
}
}
original.call(region,pos,output,state,fluid);
}finally{sanctuary$snapshot=previous;sanctuary$corners=null;}
}
@WrapOperation(method="tesselate",at=@At(value="INVOKE",target="Lnet/minecraft/client/color/block/BlockTintSource;colorInWorld(Lnet/minecraft/world/level/block/state/BlockState;Lnet/minecraft/client/renderer/block/BlockAndTintGetter;Lnet/minecraft/core/BlockPos;)I"))
private int sanctuary$base(BlockTintSource source,BlockState state,BlockAndTintGetter region,BlockPos pos,Operation<Integer> original){return sanctuary$base=original.call(source,state,region,pos);}
@WrapOperation(method="vertex",at=@At(value="INVOKE",target="Lcom/mojang/blaze3d/vertex/VertexConsumer;addVertex(FFFIFFIIFFF)V"))
private void sanctuary$vertex(VertexConsumer consumer,float x,float y,float z,int color,float u,float v,int overlay,int light,float nx,float ny,float nz,Operation<Void> original){
if(sanctuary$snapshot!=null){
if(sanctuary$corners==null){var s=sanctuary$snapshot;var r=sanctuary$region;var p=sanctuary$pos;
sanctuary$corners=new NaturalWater.Profile[]{s.corner(r,p,0,0),s.corner(r,p,1,0),s.corner(r,p,0,1),s.corner(r,p,1,1)};}
int corner=(x-sanctuary$x>.5f?1:0)+(z-sanctuary$z>.5f?2:0);color=NaturalWater.color(color,sanctuary$base,sanctuary$corners[corner],sanctuary$strength);}
original.call(consumer,x,y,z,color,u,v,overlay,light,nx,ny,nz);
}
}
@@ -0,0 +1,24 @@
package fr.koka.sanctuary.mixin.client;
import fr.koka.sanctuary.client.shader.NaturalWater;
import net.minecraft.client.Camera;
import net.minecraft.client.multiplayer.ClientLevel;
import net.minecraft.client.renderer.fog.FogRenderer;
import net.minecraft.world.level.material.FogType;
import org.joml.Vector4f;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.*;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
@Mixin(FogRenderer.class)
abstract class NaturalWaterFogColorMixin {
@Inject(method="computeFogColor",at=@At("RETURN"))
private void sanctuary$waterColor(Camera camera,float ticks,ClientLevel level,int distance,float darken,Vector4f color,CallbackInfo ci){
if(NaturalWater.strength()==0||camera.getFluidInCamera()!=FogType.WATER)return;
float amount=NaturalWater.strength()*NaturalWater.fogProfile().tint();if(amount==0)return;
// Apply after native water-vision normalization: normalizing olive to max=1 turns it neon yellow.
// Retain native darkness/vision brightness, including a fully black blinded environment.
float brightness=Math.max(color.x,Math.max(color.y,color.z));
color.x+=(120/255f*brightness-color.x)*amount;
color.y+=(132/255f*brightness-color.y)*amount;
color.z+=(72/255f*brightness-color.z)*amount;
}
}
@@ -0,0 +1,19 @@
package fr.koka.sanctuary.mixin.client;
import fr.koka.sanctuary.client.shader.NaturalWater;
import net.minecraft.client.Camera;
import net.minecraft.client.DeltaTracker;
import net.minecraft.client.multiplayer.ClientLevel;
import net.minecraft.client.renderer.fog.FogData;
import net.minecraft.client.renderer.fog.environment.WaterFogEnvironment;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.*;
import org.spongepowered.asm.mixin.injection.callback.*;
@Mixin(WaterFogEnvironment.class)
abstract class NaturalWaterFogMixin {
@Inject(method="setupFog",at=@At("TAIL"))
private void sanctuary$fog(FogData fog,Camera camera,ClientLevel level,float distance,DeltaTracker delta,CallbackInfo ci){
float amount=NaturalWater.strength();if(amount==0)return;
float factor=1+(NaturalWater.fogProfile().fog()-1)*amount;
fog.environmentalStart*=factor;fog.environmentalEnd*=factor;fog.skyEnd*=factor;fog.cloudEnd*=factor;
}
}
@@ -0,0 +1,23 @@
package fr.koka.sanctuary.mixin.client;
import fr.koka.sanctuary.client.shader.NaturalWater;
import net.minecraft.client.multiplayer.ClientLevel;
import net.minecraft.client.renderer.chunk.RenderSectionRegion;
import org.spongepowered.asm.mixin.*;
import org.spongepowered.asm.mixin.injection.*;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
@Mixin(RenderSectionRegion.class)
abstract class NaturalWaterRegionMixin implements NaturalWater.RegionAccess {
@Shadow @Final private ClientLevel level;
@Shadow @Final private net.minecraft.client.renderer.chunk.SectionCopy[] sections;
@Shadow @Final private int minSectionX,minSectionY,minSectionZ;
@Unique private NaturalWater.Region sanctuary$water;
@Inject(method="<init>",at=@At("RETURN"))
private void sanctuary$snapshot(CallbackInfo ci){
if(NaturalWater.strength()==0)return;
// Native 3×3×3 region, centre index 13. Palette rejection avoids 216 biome reads for dry terrain.
var states=((NaturalWaterSectionCopyAccess)sections[13]).sanctuary$states();
if(states!=null&&states.maybeHas(state->state.getFluidState().is(net.minecraft.tags.FluidTags.WATER)))
sanctuary$water=new NaturalWater.Region(level,minSectionX,minSectionY,minSectionZ);
}
public NaturalWater.Region sanctuary$waterRegion(){return sanctuary$water;}
}
@@ -0,0 +1,10 @@
package fr.koka.sanctuary.mixin.client;
import net.minecraft.client.renderer.chunk.SectionCopy;
import net.minecraft.world.level.chunk.PalettedContainer;
import net.minecraft.world.level.block.state.BlockState;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.gen.Accessor;
@Mixin(SectionCopy.class)
interface NaturalWaterSectionCopyAccess {
@Accessor("section") PalettedContainer<BlockState> sanctuary$states();
}
@@ -17,6 +17,7 @@ import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
abstract class PixelShadowDirtyMixin {
@Inject(method="blockChanged", at=@At("HEAD"))
private void sanctuary$dirty(BlockPos pos, int flags, CallbackInfo ci) {
fr.koka.sanctuary.client.shader.NaturalWater.dirty(pos);
PixelShadows.dirtyBlock(pos); ProceduralPbr.dirtyBlock(pos); Bloom.dirtyBlock(pos); ColoredLights.dirtyBlock(pos);
}
@Inject(method="allChanged", at=@At("HEAD"))
@@ -21,6 +21,9 @@ import net.minecraft.world.level.levelgen.Heightmap;
/** Melt only known weather deposits, using the existing precipitation column sampler. */
public final class SeasonalSnowMelt {
/** One real day lasts 72 native Minecraft days. Keep the native sampler, slow only its melt chance. */
public static final int REALTIME_SCALE = 72;
public static final GameRule<Boolean> ENABLED = GameRuleBuilder.forBoolean(true)
.category(GameRuleCategory.UPDATES).buildAndRegister(SanctuaryMod.id("seasonal_snow_melt"));
@@ -77,12 +80,7 @@ public final class SeasonalSnowMelt {
var chunk = level.getChunkAt(column);
var entries = chunk.getAttached(NATURAL);
if (entries == null || entries.isEmpty()) return;
int chance = switch (WeatherService.season(level.getServer())) {
case WINTER -> 0;
case SPRING -> 2;
case SUMMER -> 1;
case AUTUMN -> 4;
};
int chance = chance(WeatherService.season(level.getServer()));
if (chance == 0 || level.getRandom().nextInt(chance) != 0) return;
var pos = level.getHeightmapPos(Heightmap.Types.MOTION_BLOCKING, column);
// Thin layers are above the heightmap; a full snow block is just below it.
@@ -103,5 +101,15 @@ public final class SeasonalSnowMelt {
if (level.setBlockAndUpdate(pos, after)) remember(level, pos, entry.base, after);
}
public static int chance(SeasonalWeather.Season season) {
int seasonal = switch (season) {
case WINTER -> 0;
case SPRING -> 2;
case SUMMER -> 1;
case AUTUMN -> 4;
};
return seasonal == 0 ? 0 : seasonal * REALTIME_SCALE;
}
private static String key(BlockPos pos) { return Long.toString(pos.asLong()); }
}
@@ -16,8 +16,11 @@ import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.gamerules.GameRule;
import net.minecraft.world.level.gamerules.GameRuleCategory;
/** One real layer per native precipitation tick; no additional chunk or entity scan. */
/** Weather snow accumulation, using the native precipitation sampler without chunk scans. */
public final class SnowAccumulation {
/** Snowfall should remain visible during a session, but no longer stack instantly. */
public static final int REALTIME_SCALE = 12;
public static final GameRule<Integer> MAX_HEIGHT = GameRuleBuilder.forInteger(2)
.range(-1, 128).category(GameRuleCategory.UPDATES)
.buildAndRegister(SanctuaryMod.id("snow_accumulation_height"));
@@ -38,7 +41,7 @@ public final class SnowAccumulation {
/** Return whether vanilla should perform its own deposit. */
public static boolean replaceNative(ServerLevel level, BlockPos pos, boolean canSnow) {
if (!canSnow || !ENABLED.contains(level.getServer())) return canSnow;
if (canSnow) deposit(level, pos);
if (level.getRandom().nextInt(REALTIME_SCALE) == 0) deposit(level, pos);
return false;
}
@@ -2319,5 +2319,10 @@
"sanctuary.shaders.colored_light_distance_value": "Colored lights: %s blocks",
"sanctuary.shaders.colored_light_distance_help": "View distance: 8 to 64 blocks. Longer distances use more memory and computation. Smooth range fade; each source keeps its native reach.",
"sanctuary.shaders.warm_torches": "Warm torches",
"sanctuary.shaders.warm_torches_help": "Off: ordinary torches keep the native color temperature. On: orange tint. Other sources retain their own colors."
"sanctuary.shaders.warm_torches_help": "Off: ordinary torches keep the native color temperature. On: orange tint. Other sources retain their own colors.",
"sanctuary.shaders.natural_water": "Natural water",
"sanctuary.shaders.natural_water_help": "Biome colour, clarity from the bed. Clear oceans. Also changes underwater visibility.",
"sanctuary.shaders.water_intensity": "Water intensity",
"sanctuary.shaders.water_intensity_help": "At 0%, keeps native rendering. At 100%, applies all three profiles.",
"sanctuary.shaders.water_intensity_value": "Water intensity: %s%%"
}
@@ -2319,5 +2319,10 @@
"sanctuary.shaders.colored_light_distance_value": "Lumières colorées : %s blocs",
"sanctuary.shaders.colored_light_distance_help": "Distance d'affichage : 8 à 64 blocs. Les grandes distances utilisent plus de mémoire et de calcul. Fondu aux limites ; la portée de chaque source reste native.",
"sanctuary.shaders.warm_torches": "Torches chaudes",
"sanctuary.shaders.warm_torches_help": "Désactivé : les torches ordinaires conservent la température de couleur native. Activé : teinte orangée. Les autres sources gardent leurs propres couleurs."
"sanctuary.shaders.warm_torches_help": "Désactivé : les torches ordinaires conservent la température de couleur native. Activé : teinte orangée. Les autres sources gardent leurs propres couleurs.",
"sanctuary.shaders.natural_water": "Eau naturelle",
"sanctuary.shaders.natural_water_help": "Couleur du biome, clarté selon le fond. Océans clairs. Modifie aussi la visibilité sous leau.",
"sanctuary.shaders.water_intensity": "Intensité de leau",
"sanctuary.shaders.water_intensity_help": "À 0 %, conserve le rendu natif. À 100 %, applique les trois profils.",
"sanctuary.shaders.water_intensity_value": "Intensité de leau: %s%%"
}
@@ -0,0 +1,4 @@
{
"light": ["minecraft:sand", "minecraft:red_sand", "minecraft:gravel", "minecraft:clay"],
"pond": ["minecraft:dirt", "minecraft:coarse_dirt", "minecraft:rooted_dirt", "minecraft:grass_block", "minecraft:podzol", "minecraft:mycelium", "minecraft:mud", "minecraft:muddy_mangrove_roots"]
}
@@ -278,7 +278,12 @@
"client.PixelShadowRendererMixin",
"client.PixelShadowLevelMixin",
"client.PixelShadowDirtyMixin",
"client.PreferredGraphicsApiMixin"
"client.PreferredGraphicsApiMixin",
"client.NaturalWaterRegionMixin",
"client.NaturalWaterFluidMixin",
"client.NaturalWaterFogMixin",
"client.NaturalWaterFogColorMixin",
"client.NaturalWaterSectionCopyAccess"
],
"injectors": {
"defaultRequire": 1
+1 -1
View File
@@ -1,6 +1,6 @@
name = "Sanctuary"
author = "KOKA99CAB"
version = "beta.142"
version = "beta.144"
pack-format = "packwiz:1.1.0"
[index]
+30
View File
@@ -0,0 +1,30 @@
#!/usr/bin/env python3
"""Check the eight alternating water benchmark passages per scene.
Usage: python3 scripts/check_water_performance.py build/water144-vulkan.log
Writes the detailed receipt beside the log, and rejects incomplete/over-budget runs.
The caller must also verify that no other graphics test ran during measurement.
"""
import json,re,sys
from pathlib import Path
log_path=Path(sys.argv[1])
backend=log_path.stem
log=log_path.read_text()
rows={key:json.loads(data) for key,data in re.findall(r"WATER144_PERF (\S+) (\[[^\n]+\])",log)}
assert len(rows)==24, ("Incomplete measurements",len(rows))
assert log.count("WATER144_CONDITIONS limit=260 throttle=NONE")==24, "Measurements must be uncapped"
result={"backend":backend,"samples":rows,"scenes":{}}
for scene in ("mixed","large","moving"):
off=[v for k,v in rows.items() if k.startswith(scene+"-false-")]
on=[v for k,v in rows.items() if k.startswith(scene+"-true-")]
assert len(off)==len(on)==4 and all(v[0]>50 for v in off+on)
if scene=="moving":
assert all(v[3]>0 for v in off+on), "Moving scene must compile terrain sections"
median_off=sum(v[1] for v in off)/len(off);median_on=sum(v[1] for v in on)/len(on)
p95_off=sum(v[2] for v in off)/len(off);p95_on=sum(v[2] for v in on)/len(on)
value={"offMedianMs":median_off,"onMedianMs":median_on,"medianIncreasePercent":100*(median_on/median_off-1),"offP95Ms":p95_off,"onP95Ms":p95_on,"p95IncreaseMs":p95_on-p95_off}
value["passed"]=value["medianIncreasePercent"]<=5 and value["p95IncreaseMs"]<=2
result["scenes"][scene]=value
log_path.with_name(log_path.stem+"-performance.json").write_text(json.dumps(result,indent=2)+"\n")
print(json.dumps(result["scenes"],indent=2))
assert all(v["passed"] for v in result["scenes"].values()), "Performance acceptance not met"
+3
View File
@@ -8,3 +8,6 @@ ajoute les ombres portées pixélisées optionnelles, configurables depuis
Ce dossier reste réservé aux futurs packs externes. Leur ajout au manifeste
packwiz nécessitera version, URL, hash, licence et compatibilité vérifiée pour
la version exacte du jeu et du moteur Iris.
La beta.144 ajoute l[eau naturelle](../docs/natural-water-beta144.md) dans
le rendu natif : profils selon le fond, océans clairs et brume sous-marine.