Compare commits

...
Author SHA1 Message Date
koka 580b2414da Document beta.143 publication and Prism synchronization
Build Sanctuary / build (push) Waiting to run
2026-09-18 12:44:49 +02:00
koka 97ff2b54cb Slow seasonal snow pacing in beta.143
Build Sanctuary / build (push) Waiting to run
2026-09-18 12:41:27 +02:00
koka 59346d7f0a Document verified beta.142 publication and Prism synchronization
Build Sanctuary / build (push) Waiting to run
2026-09-18 05:08:09 +02:00
koka ea151dd5df Prefer native Vulkan and stabilize PBR lighting in beta.142
Build Sanctuary / build (push) Waiting to run
2026-09-18 05:05:49 +02:00
25 changed files with 508 additions and 83 deletions
+25 -6
View File
@@ -20,12 +20,13 @@ 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.141** donne une direction approximative aux lumières locales
pour orienter les normales et les reflets PBR. Les blocs posés utilisent une
estimation en cache ; les objets portés réutilisent leur position. Les sources
opposées adoucissent le relief, avec les fondus existants et sans nouveau bouton.
Le [ticket](docs/directional-lights-beta141.md) décrit le rendu et les vérifications.
La beta.141 est publiée et synchronisée dans linstance Sanctuary Beta ;
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**.
@@ -37,6 +38,24 @@ 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,
calculs de reflets bornés et intensité initiale 80 %. Choix personnels conservés.
[Contrat et vérifications](docs/pbr-refresh-beta142.md) ·
[Pack normal](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.142/Sanctuary-beta.142.mrpack) ·
[Pack de test](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.142/Sanctuary-Test-beta.142.mrpack).
## beta.141 — Direction locale et normales PBR
Relief et reflets orientés par les lumières locales. Cache compact, confiance
+27
View File
@@ -14,6 +14,33 @@ 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.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
+173
View File
@@ -0,0 +1,173 @@
# PBR-142 — Stabilité du relief et Vulkan par défaut
Socle beta.141 `7ce01c4`, branche `codex/pbr-refresh-beta142`.
Minecraft 26.3 / Java 25. Livraison beta.142.
## Contrat
Corriger le clignotement lors de la reconstruction des sections PBR : conserver
le rendu précédent jusqu'à ce que le remplaçant soit disponible et éviter les
reconstructions dues seulement aux lumières dynamiques. Une invalidation pendant
une reconstruction doit être conservée, sans annuler indéfiniment le travail.
Le PBR passe à 80 % par défaut, valeur confirmée par l'utilisateur ; les choix
explicitement enregistrés restent conservés.
La capture `2026-09-18_04.19.34.png` montre des points très clairs, plus fins
que les pixels des textures, sur des parois et un plafond de grotte. L'utilisateur
confirme OpenGL et leur disparition quand seul le PBR est coupé. Examiner
les lectures de textures et les calculs aux angles rasants séparément du cache.
Tests natifs prévus : recalcul de section sous observation, invalidations
répétées, remplacement atomique, suppression réelle de blocs, lumière portée,
cache et préférences ; OpenGL et Vulkan. Pas de monde personnel ouvert.
## Précision du signalement et implémentation
L'utilisateur précise ensuite que le bruit apparaît **en mouvement**, et demande
le passage à Vulkan. Il ne faut donc pas attribuer les points de sa capture au
seul clignotement de reconstruction : ce sont deux vérifications distinctes.
- Les sections PBR publiées restent affichées jusqu'au remplacement de leur
maillage. Une nouvelle invalidation durant la construction est mémorisée ;
elle n'annule plus continuellement le travail. Le budget permanent reste
24 Mio, avec temporairement l'ancien et le nouveau maillage d'une section.
- Les invalidations de lumière dynamique ne régénèrent plus la géométrie PBR.
Les changements de blocs et de lumière du ciel restent pris en compte.
- Lectures explicites des texels de profondeur/scène et du niveau de matériau,
gradients de texture calculés avant les rejets de fragments. Les produits
scalaires des reflets sont bornés, la puissance entière de Fresnel est
explicite, les résultats non finis ne remplacent pas la scène native.
Aucun flou ajouté aux textures.
- Le choix graphique natif « Par défaut » devient « Vulkan (par défaut) »
(FR/EN) et essaie Vulkan en premier. Minecraft conserve son repli OpenGL si
Vulkan ne peut pas être initialisé. Un choix **OpenGL explicite** reste
disponible ; pour une installation ainsi réglée, sélectionner Vulkan dans
les réglages vidéo puis redémarrer. Aucun moteur externe ni VulkanMod ajouté.
- 80 % est la nouvelle intensité PBR initiale. Les valeurs déjà enregistrées
explicitement restent conservées.
Le code Minecraft 26.3 a été inspecté : `PreferredGraphicsApi.VULKAN` essaie
Vulkan puis OpenGL ; le choix natif `DEFAULT` essayait auparavant OpenGL d'abord.
Tous les effets Sanctuary utilisent déjà l'API graphique native. Le seul hook
OpenGL direct est le correctif spécifique de lecture des panoramas dans
`GlCommandEncoder` ; il ne remplace pas la voie Vulkan native.
Référence pour les lectures après contrôle divergent :
[documentation Khronos des samplers](https://wikis.khronos.org/opengl/Sampler_%28GLSL%29).
La correction rend ces lectures explicites ; elle ne constitue pas à elle seule
une reproduction du défaut sur le pilote Windows de l'utilisateur.
## Observations de diagnostic
Le détecteur de points blancs isolés (minimum RGB 170, au moins cinq voisins
sur huit sous 120) compte 669 points sur l'image utilisateur et zéro sur la
première grotte de contrôle. Il s'agit de scènes, résolutions et pilotes différents,
pas d'un avant/après reproduisant exactement le bug utilisateur. Les rendus
natifs OpenGL de contrôle étaient déjà exempts de ce grain avant les nouvelles
protections du shader ; le signalement Windows reste donc à reconfirmer là-bas.
Le test de rafraîchissement conserve les mêmes maillages publiés durant douze
invalidations consécutives, compare l'image et vérifie que des remplacements
aboutissent. Le test de torche alterne quatre fois les torches normales/des âmes
sans reconstruction des normales. Le contrôle de mouvement presse réellement
Avancer, maintient la torche et le PBR actifs et vérifie un déplacement d'au moins
un bloc avant de vérifier les 24 captures. Les angles -30°, 0° et 35° sont aussi
contrôlés, ainsi que les effets combinés et les préférences.
### Domaine invalide du reflet de la torche en main
Pour la lumière du joueur en vue subjective, la source est la caméra : le
vecteur lumière et le vecteur de vue sont parallèles. L'ancien
`pow(1 - max(dot(v,h),0),5)` n'avait aucune borne supérieure pour ce produit
scalaire. Un calcul reproductible Float32 (100 000 positions, graine 142)
donne 19 991 bases légèrement négatives, jusqu'à -2,3841858e-7, contre zéro
après clamp. C'est une reproduction numérique d'un **domaine de calcul invalide**,
pas une reproduction des pixels produits par le pilote utilisateur.
GLSL 3.30 ne définit pas `pow` pour une base négative, même avec un exposant
entier : [spécification Khronos, fonctions exponentielles](https://registry.khronos.org/OpenGL/specs/gl/GLSLangSpec.3.30.pdf).
Le correctif borne les cosinus à [0,1] et remplace cette puissance par des
multiplications. La torche alignée sur la caméra ne dépend donc plus du traitement
de cette entrée invalide par le pilote. Reçu numérique ignoré :
`build/pbr142-fresnel-domain.json`.
### Mise à jour des fixtures antérieures
La suite Vulkan complète s'est arrêtée au contrôle SSGI de coin : l'oracle
ne trouvait plus de pierre neutre (`rows=0`), car les lumières colorées ajoutées
depuis beta.139/140 teintaient la pierre avec la lave. Le test de proximité SSGI
et le test PBR solaire isolent désormais leur effet en coupant cette autre passe.
Aucun seuil n'a été diminué et aucun changement de rendu SSGI n'est livré.
Le sous-ensemble en échec est relancé séparément ; les étapes précédentes
(ombres, CTM, bloom, lens flare, déplacement SSGI) avaient réussi.
## Vérifications natives
Sur Mac Apple M1, Java 25 et Minecraft 26.3 :
- OpenGL : suite couleurs/directions/rafraîchissement en 3 min 36 s, puis
déplacement continu avec torche active en 1 min 36 s. Zéro point blanc isolé
sur les 24 images de mouvement et les trois angles de grotte.
- Vulkan/MoltenVK 1.4.2 : sélection automatique sans argument de moteur,
couleurs/directions/rafraîchissement en 3 min 47 s. Dernier test continu avec
torche active en 1 min 37 s, zéro point blanc isolé sur les 24 images.
- Ombrages lune/midi/feuilles, distances et précisions, stabilité temps réel,
CTM, bloom (minerais, torche, lampe, lave, soleil), émissifs indépendants et
lens flare validés sous Vulkan dans la suite d'effets. Le déplacement SSGI
sur douze blocs passe également. La suite s'arrête ensuite sur l'oracle
ancien de couleur de pierre décrit ci-dessus : ne pas présenter ce lancement
de 7 min 15 s comme une réussite complète.
- Reprise ciblée après isolation de cet oracle : SSGI près des surfaces puis
PBR extérieur réussis sous Vulkan en 2 min 55 s. Proximité 35/100, haute
résolution, léger mouvement, toit ajouté/retiré, reload, resize, zéro/OFF et
effets combinés. Gain SSGI au coin test : 6,01 sur 99 lignes à 35 %.
- Les nouvelles valeurs initiales et les choix enregistrés sont testés, dont
une intensité PBR explicitement enregistrée à 50 qui reste à 50.
Les premiers essais du nouveau contrôle de mouvement ont permis de corriger
son entrée clavier : `pressKey` ne maintient pas une touche enfoncée. Le test
final utilise `holdKey` puis `releaseKey`, vérifie le déplacement réel et refuse
la disparition de la lumière portée ou du PBR pendant les captures.
Les jeux de test utilisent uniquement des mondes neufs et ignorés. La capture
et le pilote Windows utilisateur n'ont pas été reproduits à l'identique ; leur
retest reste ouvert malgré les corrections numériques et les contrôles natifs.
## Construction et archives
`./gradlew check build assemblePack assembleTestPack -x :sanctuary:runGameTest`
réussit en **2 min 38 s**, 126 tâches. Le GameTest serveur dédié reste exclu
selon le refus EULA antérieur ; les suites clientes ci-dessus ont réellement
utilisé les deux moteurs natifs. `git diff --check` passe.
Les archives normale/Test embarquent le même JAR, leurs versions/index/hashes
sont vérifiés, leurs sources correspondent aux fichiers courants. Le template
contient exactement les ressources attendues. Les onze entrées de production
modifiées par rapport à beta.141 sont limitées au PBR, au choix graphique et
aux traductions ; les ressources de jeu antérieures sont conservées.
SHA-256 du JAR : `0bc2da9df2ef004e6dc78932cfde7b31b7717c63c83ceed789d151177c41bc1a`.
- `Sanctuary-beta.142.mrpack` : 10464746 octets ; SHA-256 `dba4fd8e1e3b20136faa1c1de3494df3cb250c87de199c04e07883c786ca48ee`.
- `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/`.
+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.141
pack_version=beta.141
mod_version=beta.143
pack_version=beta.143
resource_pack_version=beta.121
maven_group=fr.koka.sanctuary
jei_version=30.32.0-sanctuary.3
+1
View File
@@ -34,6 +34,7 @@ if (clientRenderTests) {
if (improvedTransparency.isPresent()) {
systemProperty('sanctuary.test.improvedTransparency', improvedTransparency.get())
}
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'))
systemProperty('sanctuary.test.colored140', providers.gradleProperty('sanctuaryColored140ClientTests').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);}
@@ -20,6 +20,7 @@ final class DirectionalLights141ClientChecks {
int left=c.computeOnClient(m->ColoredLights.directionSample(PROBE));check(dx(left)<-.9&&Math.abs(dy(left))<.02&&Math.abs(dz(left))<.02,"Cached arrival points toward the left source");
var relief=capture(c,"direction-left-pbr-on");long delta=difference(flat,relief);check(delta>1000,"Local light visibly affects stone normals without skylight: "+delta);
int builds=c.computeOnClient(m->ColoredLights.builds());c.waitTicks(25);check(c.computeOnClient(m->ColoredLights.builds())==builds,"Static directions reuse the cache");unchanged(relief,capture(c,null),"Static local PBR is stable");
if(Boolean.getBoolean("sanctuary.test.pbr142"))PbrRefresh142ClientChecks.refresh(c,server,relief);
server.runOnServer(s->{s.overworld().setBlockAndUpdate(LEFT,Blocks.AIR.defaultBlockState());s.overworld().setBlockAndUpdate(RIGHT,Blocks.TORCH.defaultBlockState());});c.waitTicks(40);settle(c);
int right=c.computeOnClient(m->ColoredLights.directionSample(PROBE));check(dx(right)>.9,"Moving the source reverses arrival direction");capture(c,"direction-right-pbr-on");
server.runOnServer(s->s.overworld().setBlockAndUpdate(LEFT,Blocks.TORCH.defaultBlockState()));c.waitTicks(40);settle(c);
@@ -33,11 +34,13 @@ final class DirectionalLights141ClientChecks {
c.waitTicks(45);settle(c);capture(c,"direction-held-pbr-on");
c.runOnClient(m->VanillaLight.setPbr(false));c.waitTicks(35);settle(c);var heldFlat=capture(c,"direction-held-pbr-off");
c.runOnClient(m->VanillaLight.setPbr(true));c.waitTicks(35);settle(c);var heldRelief=capture(c,"direction-held-pbr-on-final");
if(Boolean.getBoolean("sanctuary.test.pbr142"))PbrRefresh142ClientChecks.held(c,server);
long heldDelta=difference(heldFlat,heldRelief);check(heldDelta>1000&&c.computeOnClient(m->ColoredLights.dynamicCount())==1,"Carried light drives local PBR: "+heldDelta);
server.runOnServer(s->s.getPlayerList().getPlayers().getFirst().setItemSlot(net.minecraft.world.entity.EquipmentSlot.MAINHAND,net.minecraft.world.item.ItemStack.EMPTY));c.waitTicks(35);
c.runOnClient(m->VanillaLight.setPbr(false));c.waitTicks(35);settle(c);check(c.computeOnClient(m->ColoredLights.directionSample(LEFT))==0,"PBR OFF releases directional field");
c.runOnClient(m->{VanillaLight.setColoredLights(false);VanillaLight.setPbr(true);});c.waitTicks(25);capture(c,"direction-colored-off");
check(!c.computeOnClient(m->ColoredLights.allocated()),"Color OFF releases local field while PBR keeps its solar path");
if(Boolean.getBoolean("sanctuary.test.pbr142"))PbrRefresh142ClientChecks.cave(c,server);
System.out.println("DIRECTIONAL141_PASS localPbrDelta="+delta+" native normals, left/right, opposing sources, wall, held light, stable cache, OFF");
}
private static double dx(int c){return ((c>>16&255)-128)/127.;}
@@ -13,7 +13,7 @@ import java.util.Arrays;
public final class Pbr134ClientChecks implements FabricClientGameTest {
public void runTest(ClientGameTestContext c){new PixelShadows122ClientChecks(false,false,true).runTest(c);}
static void run(ClientGameTestContext c,TestServerContext server){
c.runOnClient(m->{preferences();VanillaLight.setSunshafts(false);VanillaLight.setLensFlare(false);materials();VanillaLight.setEnabled(true);VanillaLight.setSaturation(100);VanillaLight.setPbr(false);VanillaLight.setSsgi(false);VanillaLight.setBloom(false);VanillaLight.setOreEmission(false);VanillaLight.setEdgeHighlights(false);VanillaLight.setPixelShadows(false);});
c.runOnClient(m->{preferences();VanillaLight.setSunshafts(false);VanillaLight.setLensFlare(false);materials();VanillaLight.setEnabled(true);VanillaLight.setSaturation(100);VanillaLight.setColoredLights(false);VanillaLight.setPbr(false);VanillaLight.setSsgi(false);VanillaLight.setBloom(false);VanillaLight.setOreEmission(false);VanillaLight.setEdgeHighlights(false);VanillaLight.setPixelShadows(false);});
server.runOnServer(s->{command(s,"time set 1000");for(int x=-5;x<=24;x++)for(int z=-5;z<=24;z++){
s.overworld().setBlockAndUpdate(new BlockPos(x,64,z),(x<10?Blocks.BRICKS:x<17?Blocks.OAK_PLANKS:Blocks.IRON_BLOCK).defaultBlockState());
for(int y=65;y<=102;y++)s.overworld().setBlockAndUpdate(new BlockPos(x,y,z),Blocks.AIR.defaultBlockState());
@@ -61,7 +61,7 @@ public final class Pbr134ClientChecks implements FabricClientGameTest {
}
private static void preferences(){try{
var p=FabricLoader.getInstance().getConfigDir().resolve("sanctuary-shaders.json");Files.writeString(p,"{\"ssgi\":true,\"ssgiIntensity\":35,\"bloom\":false}");forgetPreferences();
check(VanillaLight.pbr()&&VanillaLight.pbrIntensity()==50&&VanillaLight.ssgi()&&!VanillaLight.bloom(),"Missing PBR setting defaults ON while preserving explicit old choices");
check(VanillaLight.pbr()&&VanillaLight.pbrIntensity()==80&&VanillaLight.ssgi()&&!VanillaLight.bloom(),"Missing PBR setting defaults ON while preserving explicit old choices");
for(int n:new int[]{-1,0,50,100,101}){VanillaLight.setPbr(true);VanillaLight.setPbrIntensity(n);forgetPreferences();check(VanillaLight.pbr()&&VanillaLight.pbrIntensity()==Math.clamp(n,0,100),"PBR preference persistence/clamp");}
VanillaLight.setPbrIntensity(50);
}catch(Exception e){throw new AssertionError(e);}}
@@ -0,0 +1,63 @@
package fr.koka.sanctuary.client.shader;
import static fr.koka.sanctuary.client.shader.PixelShadows122ClientChecks.*;
import net.fabricmc.fabric.api.client.gametest.v1.context.*;
import net.minecraft.core.BlockPos;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.item.*;
import net.minecraft.world.entity.EquipmentSlot;
final class PbrRefresh142ClientChecks {
static void refresh(ClientGameTestContext c,TestServerContext s,Frame reference){
int entries=c.computeOnClient(m->ProceduralPbr.terrainEntries()),builds=c.computeOnClient(m->ProceduralPbr.terrainBuilds());check(entries>0,"PBR fixture has visible sections");
for(int i=0;i<12;i++){
c.runOnClient(m->{for(var section:m.levelRenderer.visibleSections()){var p=section.getRenderOrigin();ProceduralPbr.dirtySection(p.getX()>>4,p.getY()>>4,p.getZ()>>4);}
check(ProceduralPbr.terrainEntries()==entries,"Invalidation retains every published PBR section");});
c.waitTicks(1);unchanged(reference,capture(c,null),"PBR remains visible throughout repeated refreshes");
}
ready(c);check(c.computeOnClient(m->ProceduralPbr.terrainBuilds())>builds,"Replacement meshes actually published");unchanged(reference,capture(c,"refresh-stable"),"Replacement preserves the rendered normals");
System.out.println("PBR142_REFRESH_PASS entries="+entries+" repeated invalidation, retained rendering, completed replacements");
}
static void held(ClientGameTestContext c,TestServerContext s){
ready(c);int builds=c.computeOnClient(m->ProceduralPbr.terrainBuilds());
for(var item:new Item[]{Items.SOUL_TORCH,Items.TORCH,Items.SOUL_TORCH,Items.TORCH}){
s.runOnServer(server->server.getPlayerList().getPlayers().getFirst().setItemSlot(EquipmentSlot.MAINHAND,new ItemStack(item)));c.waitTicks(25);
}
check(c.computeOnClient(m->ProceduralPbr.terrainBuilds())==builds,"Carried light updates do not rebuild the normal mesh");
System.out.println("PBR142_HELD_PASS native dynamic relighting preserves the PBR geometry cache");
}
static void cave(ClientGameTestContext c,TestServerContext s){
c.runOnClient(m->{VanillaLight.setColoredLights(true);VanillaLight.setPbr(false);VanillaLight.setPbrIntensity(80);VanillaLight.setColoredLightDistance(16);});
camera(c,s,0.5,65,2.5,70);look(c,s,0,-12);c.waitTicks(30);
s.runOnServer(server->{for(int x=-5;x<=5;x++)for(int z=-1;z<=18;z++)for(int y=64;y<=70;y++)
server.overworld().setBlockAndUpdate(new BlockPos(x,y,z),(x==-5||x==5||y==64||y==70||z==-1||z==18)?Blocks.DEEPSLATE.defaultBlockState():Blocks.AIR.defaultBlockState());
for(int y=65;y<=69;y++)server.overworld().setBlockAndUpdate(new BlockPos(-2,y,9),Blocks.DEEPSLATE.defaultBlockState());
for(int z=5;z<14;z++)server.overworld().setBlockAndUpdate(new BlockPos(3,67,z),Blocks.DEEPSLATE.defaultBlockState());
server.overworld().setBlockAndUpdate(new BlockPos(3,68,8),Blocks.WATER.defaultBlockState());
server.getPlayerList().getPlayers().getFirst().setItemSlot(EquipmentSlot.MAINHAND,new ItemStack(Items.TORCH));
});c.waitTicks(100);var off=capture(c,"cave-pbr-off");c.runOnClient(m->VanillaLight.setPbr(true));ready(c);var on=capture(c,"cave-pbr-80");
check(specks(on)<=specks(off)+4,"PBR does not introduce isolated white points in the cave");
System.out.println("PBR142_CAVE_PIXELS off="+specks(off)+" on="+specks(on));
// Capture while the camera is actually travelling, before settling caches.
s.runOnServer(server->{var player=server.getPlayerList().getPlayers().getFirst();player.getAbilities().flying=true;player.onUpdateAbilities();});c.waitTicks(10);
var start=c.computeOnClient(m->m.player.position());
c.getInput().holdKey(o->o.keyUp);
int maximumSpecks=0;
try {for(int frame=0;frame<24;frame++){
c.waitTicks(1);c.runOnClient(m->check(ColoredLights.dynamicCount()>0&&ProceduralPbr.applied(),"Carried light and PBR remain active during motion"));var moving=capture(c,frame%8==0?"cave-moving-"+frame:null);
maximumSpecks=Math.max(maximumSpecks,specks(moving));
}} finally {c.getInput().releaseKey(o->o.keyUp);}
check(c.computeOnClient(m->m.player.position().distanceToSqr(start))>1.0,"Camera really travels at least one block");
check(maximumSpecks<=4,"Moving camera has no isolated white grain: "+maximumSpecks);
System.out.println("PBR142_MOVEMENT_PASS frames=24 maxSpecks="+maximumSpecks);
camera(c,s,0.5,65,2.5,70);
for(float pitch:new float[]{-30,0,35}){look(c,s,0,pitch);ready(c);var frame=capture(c,"cave-angle-"+(int)pitch);check(specks(frame)<=4,"Grazing-angle PBR has no white grain");System.out.println("PBR142_CAVE_ANGLE "+pitch+" specks="+specks(frame));}
c.runOnClient(m->{VanillaLight.setSsgi(true);VanillaLight.setBloom(true);VanillaLight.setPixelShadows(true);VanillaLight.setEdgeHighlights(true);VanillaLight.setSunshafts(true);});c.waitTicks(40);ready(c);capture(c,"cave-combined");
System.out.println("PBR142_PASS refresh, dynamic cache, defaults, cave and grazing-angle captures");
}
private static int specks(Frame f){int count=0,w=f.width();for(int y=2;y<f.height()-2;y++)for(int x=2;x<w-2;x++){
int i=y*w+x,p=f.pixels()[i];int value=Math.min(p>>16&255,Math.min(p>>8&255,p&255));if(value<170)continue;
int dark=0;for(int dy=-1;dy<=1;dy++)for(int dx=-1;dx<=1;dx++){if(dx==0&&dy==0)continue;int q=f.pixels()[i+dy*w+dx];if(Math.max(q>>16&255,Math.max(q>>8&255,q&255))<120)dark++;}if(dark>=5)count++;
}return count;}
private static void ready(ClientGameTestContext c){c.waitTicks(14);c.waitFor(m->ProceduralPbr.ready(),2400);c.waitTicks(8);}
}
@@ -54,7 +54,9 @@ public final class PixelShadows122ClientChecks implements FabricClientGameTest {
public void runTest(ClientGameTestContext c) {
c.runOnClient(m -> {
var info = com.mojang.blaze3d.systems.RenderSystem.getDevice().getDeviceInfo();
String expected = System.getProperty("sanctuary.test.graphicsBackend", "");
String expected = System.getProperty("sanctuary.test.graphicsBackend", "vulkan");
check(net.minecraft.client.PreferredGraphicsApi.DEFAULT.getBackendsToTry()[0] instanceof com.mojang.renderpearl.backend.vulkan.VulkanBackend, "Default tries native Vulkan first");
check(net.minecraft.client.PreferredGraphicsApi.OPENGL.getBackendsToTry()[0] instanceof com.mojang.renderpearl.backend.opengl.GlBackend, "Explicit OpenGL remains available");
check(expected.isEmpty() || info.backendName().equalsIgnoreCase(expected),
"Requested graphics backend must not silently fall back: " + expected + " / " + info.backendName());
System.out.println("SHADER139_BACKEND " + info.backendName() + " / " + info.name()
@@ -281,7 +283,7 @@ public final class PixelShadows122ClientChecks implements FabricClientGameTest {
});
});
c.waitTicks(3);
c.takeScreenshot("sanctuary-beta141-shader-options-" + locale);
c.takeScreenshot("sanctuary-beta142-shader-options-" + locale);
}
c.runOnClient(m -> m.gui.setScreen(null));
}
@@ -450,11 +452,12 @@ public final class PixelShadows122ClientChecks implements FabricClientGameTest {
var path=FabricLoader.getInstance().getConfigDir().resolve("sanctuary-shaders.json");
Files.deleteIfExists(path);forgetPreferences();
check(VanillaLight.bloom()&&VanillaLight.bloomIntensity()==20&&VanillaLight.pbr()&&VanillaLight.ssgi(),"Fresh defaults: bloom20, PBR and SSGI ON");
check(VanillaLight.pbrIntensity()==50&&VanillaLight.ssgiIntensity()==35,"Existing effect strengths retained");
check(VanillaLight.pbrIntensity()==80&&VanillaLight.ssgiIntensity()==35,"PBR default 80, SSGI strength retained");
Files.writeString(path,"{\"pbr\":false,\"ssgi\":false,\"bloomIntensity\":73}");forgetPreferences();
check(!VanillaLight.pbr()&&!VanillaLight.ssgi()&&VanillaLight.bloomIntensity()==73,"Explicit personal toggles and intensity survive changed defaults");
Files.writeString(path,"{\"bloom\":false}");forgetPreferences();
check(!VanillaLight.bloom()&&VanillaLight.bloomIntensity()==20&&VanillaLight.pbr()&&VanillaLight.ssgi(),"Missing fields get new defaults without replacing explicit values");
Files.writeString(path,"{\"pbrIntensity\":50}");forgetPreferences();check(VanillaLight.pbrIntensity()==50,"Explicit PBR 50 is preserved");
System.out.println("SHADER135_DEFAULTS_PASS fresh, partial and personal preferences");
} catch(IOException e){throw new AssertionError(e);}
}
@@ -565,7 +568,7 @@ public final class PixelShadows122ClientChecks implements FabricClientGameTest {
}
static Frame capture(ClientGameTestContext c, String name) {
if (name != null) c.takeScreenshot("sanctuary-beta141-shader-" + name);
if (name != null) c.takeScreenshot("sanctuary-beta142-shader-" + name);
var result = new CompletableFuture<Frame>();
c.runOnClient(m -> Screenshot.takeScreenshot(m.gameRenderer.mainRenderTarget(), image -> {
try (image) {
@@ -13,7 +13,7 @@ import java.nio.file.Files;
public final class Ssgi132ClientChecks implements FabricClientGameTest {
public void runTest(ClientGameTestContext c){new PixelShadows122ClientChecks(false,true).runTest(c);}
static void run(ClientGameTestContext c,TestServerContext server){
c.runOnClient(m->{preferences();VanillaLight.setSunshafts(false);VanillaLight.setLensFlare(false);VanillaLight.setPbr(false);VanillaLight.setEnabled(true);VanillaLight.setSaturation(100);VanillaLight.setSsgi(false);VanillaLight.setBloom(false);VanillaLight.setOreEmission(false);VanillaLight.setPixelShadows(false);VanillaLight.setEdgeHighlights(false);});
c.runOnClient(m->{preferences();VanillaLight.setSunshafts(false);VanillaLight.setLensFlare(false);VanillaLight.setPbr(false);VanillaLight.setEnabled(true);VanillaLight.setSaturation(100);VanillaLight.setColoredLights(false);VanillaLight.setSsgi(false);VanillaLight.setBloom(false);VanillaLight.setOreEmission(false);VanillaLight.setPixelShadows(false);VanillaLight.setEdgeHighlights(false);});
server.runOnServer(s->{command(s,"time set 6000");for(int x=-5;x<=24;x++)for(int z=-5;z<=24;z++){
s.overworld().setBlockAndUpdate(new BlockPos(x,64,z),Blocks.CONCRETE.white().defaultBlockState());
for(int y=65;y<=102;y++)s.overworld().setBlockAndUpdate(new BlockPos(x,y,z),Blocks.AIR.defaultBlockState());
@@ -10,7 +10,7 @@ import net.minecraft.world.level.block.Blocks;
public final class SsgiCloseup135ClientChecks implements net.fabricmc.fabric.api.client.gametest.v1.FabricClientGameTest {
public void runTest(ClientGameTestContext c){new PixelShadows122ClientChecks(false,false,false,true).runTest(c);}
static void run(ClientGameTestContext c,TestServerContext server){
c.runOnClient(m->{VanillaLight.setEnabled(true);VanillaLight.setSaturation(100);VanillaLight.setSsgi(false);VanillaLight.setPbr(false);VanillaLight.setBloom(false);VanillaLight.setOreEmission(false);VanillaLight.setPixelShadows(false);VanillaLight.setEdgeHighlights(false);});
c.runOnClient(m->{VanillaLight.setEnabled(true);VanillaLight.setSaturation(100);VanillaLight.setColoredLights(false);VanillaLight.setSsgi(false);VanillaLight.setPbr(false);VanillaLight.setBloom(false);VanillaLight.setOreEmission(false);VanillaLight.setPixelShadows(false);VanillaLight.setEdgeHighlights(false);});
server.runOnServer(s->{
// Minecraft 26.3 rejects setting a clock to its existing value.
command(s,"time add 1");command(s,"time set 6000");
@@ -30,6 +30,7 @@ final class PbrTerrain implements AutoCloseable {
final PbrMaterials materials=new PbrMaterials();
private Build building;
private long bytes;
private int completed;
private Object models,level;
private List<Section> visible=List.of();
record Entry(BlockPos origin,GpuBuffer vertices,int indices){}
@@ -45,7 +46,11 @@ final class PbrTerrain implements AutoCloseable {
if(chunk==null)continue;
var storage=chunk.getSection(mc.level.getSectionIndex(pos.getY()));
var section=sections.get(key);
if(section==null||section.storage!=storage){if(section!=null)remove(section);section=new Section(key,pos.immutable(),storage);sections.put(key,section);}
if(section==null||section.storage!=storage){
Entry retained=section==null?null:section.entry;
if(section!=null){section.entry=null;remove(section);}
section=new Section(key,pos.immutable(),storage);section.entry=retained;sections.put(key,section);
}
section.seen=frame;active.add(section);
}
visible=active;
@@ -56,23 +61,30 @@ final class PbrTerrain implements AutoCloseable {
if(started++>=4)break;
Section next=active.stream().filter(s->s.dirty).min(Comparator.comparingDouble(s->s.origin.distToCenterSqr(camera))).orElse(null);
if(next==null)break;
if(!next.storage.maybeHas(materials::candidate)){next.dirty=false;continue;}
if(!next.storage.maybeHas(materials::candidate)){releaseEntry(next);next.dirty=false;continue;}
building=new Build(next);
}
if(!advance(building,deadline))break;
var done=building;building=null;
try(done){
done.section.dirty=false;
done.section.dirty=done.section.revision!=done.revision;
Entry replacement=null;
if(done.quads>0&&!done.overflow){
try(var mesh=done.out.buildOrThrow()){
long size=mesh.vertexBuffer().remaining();
if(bytes+size<=MAX_BYTES){
long replaced=done.section.entry==null?0:done.section.entry.vertices.size();
if(bytes-replaced+size<=MAX_BYTES){
var buffer=RenderSystem.getDevice().createBuffer(()->"Sanctuary PBR surfaces",GpuBuffer.USAGE_VERTEX|GpuBuffer.USAGE_COPY_DST,mesh.vertexBuffer());
int count=mesh.drawState().indexCount();RenderSystem.getSequentialBuffer(PrimitiveTopology.QUADS).getBuffer(count);
done.section.entry=new Entry(done.section.origin,buffer,count);bytes+=buffer.size();
replacement=new Entry(done.section.origin,buffer,count);
}
}
}
// Publish atomically. A dirty section stays visible while its new
// geometry is built, including when light updates arrive mid-build.
releaseEntry(done.section);done.section.entry=replacement;
if(replacement!=null)bytes+=replacement.vertices.size();
completed++;
}
}
return active.stream().map(s->s.entry).filter(Objects::nonNull).toList();
@@ -118,18 +130,22 @@ final class PbrTerrain implements AutoCloseable {
void dirtySection(int x,int y,int z){dirty(sections.get(SectionPos.asLong(x,y,z)));}
void dirty(BlockPos pos){for(int x=(pos.getX()-1)>>4;x<=(pos.getX()+1)>>4;x++)for(int y=(pos.getY()-1)>>4;y<=(pos.getY()+1)>>4;y++)for(int z=(pos.getZ()-1)>>4;z<=(pos.getZ()+1)>>4;z++)dirty(sections.get(SectionPos.asLong(x,y,z)));}
void dirtyChunk(int x,int z){for(var section:sections.values())if(Math.abs((section.origin.getX()>>4)-x)<=1&&Math.abs((section.origin.getZ()>>4)-z)<=1)dirty(section);}
private void dirty(Section section){if(section!=null){remove(section);section.dirty=true;}}
private void remove(Section section){if(building!=null&&building.section==section){building.close();building=null;}if(section.entry!=null){bytes-=section.entry.vertices.size();section.entry.vertices.close();section.entry=null;}}
private void dirty(Section section){if(section!=null){section.revision++;section.dirty=true;}}
private void releaseEntry(Section section){if(section.entry!=null){bytes-=section.entry.vertices.size();section.entry.vertices.close();section.entry=null;}}
private void remove(Section section){if(building!=null&&building.section==section){building.close();building=null;}releaseEntry(section);}
int entryCount(){return (int)visible.stream().filter(s->s.entry!=null).count();}
int completed(){return completed;}
boolean building(){return building!=null;}
boolean settled(){return building==null&&visible.stream().noneMatch(s->s.dirty);}
public void close(){if(building!=null){building.close();building=null;}for(var section:sections.values())remove(section);sections.clear();visible=List.of();materials.close();models=level=null;bytes=0;}
private static final class Section{
final long key;final BlockPos origin;final LevelChunkSection storage;long seen;boolean dirty=true;Entry entry;
final long key;final BlockPos origin;final LevelChunkSection storage;long seen,revision;boolean dirty=true;Entry entry;
Section(long key,BlockPos origin,LevelChunkSection storage){this.key=key;this.origin=origin;this.storage=storage;}
}
private static final class Build implements AutoCloseable{
final Section section;final ByteBufferBuilder buffer=new ByteBufferBuilder(65536);final BufferBuilder out=new BufferBuilder(buffer,PrimitiveTopology.QUADS,FORMAT);
final Section section;final long revision;final ByteBufferBuilder buffer=new ByteBufferBuilder(65536);final BufferBuilder out=new BufferBuilder(buffer,PrimitiveTopology.QUADS,FORMAT);
int cursor,quads;boolean overflow;
Build(Section section){this.section=section;}
Build(Section section){this.section=section;revision=section.revision;}
public void close(){buffer.close();}
}
}
@@ -45,7 +45,7 @@ public final class ProceduralPbr {
public static void captureProjection(Matrix4f matrix){projection.set(matrix);}
public static void dirtyBlock(BlockPos pos){TERRAIN.dirty(pos);}
public static void dirtyChunk(int x,int z){TERRAIN.dirtyChunk(x,z);}
public static void dirtySection(int x,int y,int z){TERRAIN.dirtySection(x,y,z);}
public static void dirtySection(int x,int y,int z){if(!fr.koka.sanctuary.client.DynamicLightClient.invalidatingSections())TERRAIN.dirtySection(x,y,z);}
public static void render(){
applied=false;var mc=Minecraft.getInstance();
if(!VanillaLight.active()||!VanillaLight.pbr()||VanillaLight.pbrIntensity()==0||mc.level==null){release();return;}
@@ -93,5 +93,8 @@ public final class ProceduralPbr {
public static void release(){TERRAIN.close();if(source!=null){source.destroyBuffers();source=null;}if(scene!=null){scene.close();scene=null;}applied=false;}
static boolean applied(){return applied;}static boolean ready(){return TERRAIN.settled();}
static boolean allocated(){return source!=null;}static int renderedFrames(){return frames;}
static int terrainEntries(){return TERRAIN.entryCount();}
static int terrainBuilds(){return TERRAIN.completed();}
static boolean terrainBuilding(){return TERRAIN.building();}
static int materialCount(){return TERRAIN.materials.count();}static int analyses(){return TERRAIN.materials.analyses();}
}
@@ -24,7 +24,7 @@ public final class VanillaLight {
boolean lensFlare=true;
int lensFlareIntensity=30;
boolean pbr=true;
int pbrIntensity=50;
int pbrIntensity=80;
boolean ssgi=true;
boolean coloredLights=true;
boolean warmTorches=false;
@@ -0,0 +1,24 @@
package fr.koka.sanctuary.mixin.client;
import com.mojang.renderpearl.api.device.GpuBackend;
import net.minecraft.client.PreferredGraphicsApi;
import net.minecraft.network.chat.Component;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
/** Sanctuary's default uses native Vulkan with Minecraft's OpenGL fallback. */
@Mixin(PreferredGraphicsApi.class)
abstract class PreferredGraphicsApiMixin {
@Inject(method="getBackendsToTry", at=@At("HEAD"), cancellable=true)
private void sanctuary$vulkanDefault(CallbackInfoReturnable<GpuBackend[]> ci) {
if ((Object)this == PreferredGraphicsApi.DEFAULT)
ci.setReturnValue(PreferredGraphicsApi.VULKAN.getBackendsToTry());
}
@Inject(method="caption", at=@At("HEAD"), cancellable=true)
private void sanctuary$defaultCaption(CallbackInfoReturnable<Component> ci) {
if ((Object)this == PreferredGraphicsApi.DEFAULT)
ci.setReturnValue(Component.translatable("sanctuary.graphics.default_vulkan"));
}
}
@@ -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;
}
@@ -1,4 +1,5 @@
{
"sanctuary.graphics.default_vulkan": "Vulkan (default)",
"sanctuary.statuary.clay_ready": "Clay: original model colors.",
"sanctuary.weather.selected": "Real Time weather: %s for %s (%s). The daily forecast resumes at midnight.",
"sanctuary.weather.overcast": "Overcast",
@@ -2295,7 +2296,7 @@
"sanctuary.shaders.ssgi_intensity_help": "Strength of diffuse color bounce. Zero stops rendering and frees its resources. Does not change world light.",
"sanctuary.shaders.ssgi_intensity_value": "SSGI: %s%%",
"sanctuary.shaders.pbr": "PBR · material relief",
"sanctuary.shaders.pbr_help": "Experimental: procedural block normals and roughness, sun, moon and local light highlights. Enable Colored lights for local direction. Range 32 blocks. ON by default.",
"sanctuary.shaders.pbr_help": "Experimental: procedural block normals and roughness, sun, moon and local light highlights. Enable Colored lights for local direction. Range 32 blocks. ON at 80% by default.",
"sanctuary.shaders.pbr_intensity": "PBR strength",
"sanctuary.shaders.pbr_intensity_help": "Material relief and highlights. Zero stops rendering and frees the cache. Original textures remain unchanged.",
"sanctuary.shaders.pbr_intensity_value": "PBR: %s%%",
@@ -1,4 +1,5 @@
{
"sanctuary.graphics.default_vulkan": "Vulkan (par défaut)",
"sanctuary.statuary.clay_ready": "Argile : couleurs originales du modèle.",
"sanctuary.weather.selected": "Météo Real Time : %s pour la journée du %s (%s). Le tirage quotidien reprend à minuit.",
"sanctuary.weather.overcast": "Ciel gris",
@@ -2295,7 +2296,7 @@
"sanctuary.shaders.ssgi_intensity_help": "Force de la teinte diffusée. Zéro coupe le calcul et libère ses ressources. Aucun changement de lumière du monde.",
"sanctuary.shaders.ssgi_intensity_value": "SSGI : %s %%",
"sanctuary.shaders.pbr": "PBR · relief des matériaux",
"sanctuary.shaders.pbr_help": "Expérimental : normales procédurales et rugosité des blocs, reflets du soleil, de la lune et des lumières locales. Activer Lumières colorées pour la direction locale. Portée 32 blocs. Activé par défaut.",
"sanctuary.shaders.pbr_help": "Expérimental : normales procédurales et rugosité des blocs, reflets du soleil, de la lune et des lumières locales. Activer Lumières colorées pour la direction locale. Portée 32 blocs. Activé par défaut à 80 %.",
"sanctuary.shaders.pbr_intensity": "Intensité du PBR",
"sanctuary.shaders.pbr_intensity_help": "Relief et reflets des matériaux. Zéro coupe le calcul et libère le cache. Ne modifie pas les textures originales.",
"sanctuary.shaders.pbr_intensity_value": "PBR : %s %%",
@@ -48,7 +48,7 @@ float visibility(vec3 p,vec3 n){
vec3 q=light.xyz/light.w;q.xy=q.xy*0.5+0.5;
if(DistanceClipFog.y<0.5)q.z=q.z*0.5+0.5;
if(any(lessThan(q,vec3(0)))||any(greaterThan(q,vec3(1))))return 1.0;
return q.z>texture(SunDepth,q.xy).r+0.00015?0.0:1.0;
return q.z>textureLod(SunDepth,q.xy,0.0).r+0.00015?0.0:1.0;
}
// Cached direction moments; filtering moments before normalization avoids unstable
// switches between nearby/opposed sources. Alpha stores the light response.
@@ -88,12 +88,14 @@ vec4 localArrival(vec3 p,vec3 normal){
vec3 localShine(vec3 n,vec3 v,vec3 l,vec4 material,vec3 albedo){
vec3 halfVector=l+v;float size=dot(halfVector,halfVector);if(size<.00001)return vec3(0);
vec3 h=halfVector*inversesqrt(size);
float nl=max(dot(n,l),0.0),nv=max(dot(n,v),.001),nh=max(dot(n,h),0.0),vh=max(dot(v,h),0.0);
float nl=clamp(dot(n,l),0.0,1.0),nv=clamp(dot(n,v),.001,1.0),nh=clamp(dot(n,h),0.0,1.0),vh=clamp(dot(v,h),0.0,1.0);
float roughness=max(material.b,.25),alpha=roughness*roughness,a2=alpha*alpha;
float denom=nh*nh*(a2-1.0)+1.0,distribution=a2/max(3.14159265*denom*denom,.00001);
float k=(roughness+1.0)*(roughness+1.0)/8.0;
float geometry=nv/(nv*(1.0-k)+k)*nl/max(nl*(1.0-k)+k,.00001);
vec3 f0=mix(vec3(.04),albedo,material.a),fresnel=f0+(1.0-f0)*pow(1.0-vh,5.0);
// Explicit integer power stays defined even at round-off limits on OpenGL drivers.
float grazing=1.0-vh,grazing2=grazing*grazing;
vec3 f0=mix(vec3(.04),albedo,material.a),fresnel=f0+(1.0-f0)*(grazing2*grazing2*grazing);
return distribution*geometry*fresnel/max(4.0*nv*nl,.0001)*nl;
}
void main(){
@@ -102,27 +104,23 @@ void main(){
vec2 tx=dFdx(blockUv),ty=dFdy(blockUv);
float det=tx.x*ty.y-tx.y*ty.x;
vec3 baseNormal=normalize(cross(dx,dy));if(dot(baseNormal,-relativePosition)<0.0)baseNormal=-baseNormal;
vec2 uv=gl_FragCoord.xy/vec2(textureSize(WorldDepth,0));float depth=texture(WorldDepth,uv).r;
vec2 uv=gl_FragCoord.xy/vec2(textureSize(WorldDepth,0));float depth=texelFetch(WorldDepth,ivec2(gl_FragCoord.xy),0).r;
vec4 projected=PbrInverseVP*vec4(uv*2.0-1.0,PbrParams.y>0.5?depth:depth*2.0-1.0,1.0);
vec3 visible=projected.xyz/projected.w;
if(depth<=0.0||abs(length(visible)-length(relativePosition))>0.035||length(relativePosition)>=PbrParams.w||abs(det)<1e-12)discard;
vec4 texel=texture(BlockAtlas,blockUv);if(texel.a<0.99)discard;
// Keep atlas derivatives valid after per-fragment depth/alpha rejection.
vec4 texel=textureGrad(BlockAtlas,blockUv,tx,ty);if(texel.a<0.99)discard;
vec3 tangent=normalize((dx*ty.y-dy*tx.y)/det),bitangent=normalize((dy*tx.x-dx*ty.x)/det);
vec4 material=texture(MaterialAtlas,maskUv);
vec4 material=textureLod(MaterialAtlas,maskUv,0.0);
// RG stores signed height derivatives; B roughness, A metalness, not transparency.
vec2 slope=(material.rg*255.0-128.0)/127.0*PbrParams.x*2.0;
vec3 n=normalize(baseNormal+tangent*slope.x+bitangent*slope.y);
vec3 l=normalize(PbrSun.xyz),v=normalize(-relativePosition),h=normalize(l+v);
float nl=max(dot(n,l),0.0),nv=max(dot(n,v),0.001),nh=max(dot(n,h),0.0),vh=max(dot(v,h),0.0);
float roughness=max(material.b,0.25),alpha=roughness*roughness,a2=alpha*alpha;
float denom=nh*nh*(a2-1.0)+1.0;
float distribution=a2/max(3.14159265*denom*denom,0.00001);
float k=(roughness+1.0)*(roughness+1.0)/8.0;
float geometry=nv/(nv*(1.0-k)+k)*nl/max(nl*(1.0-k)+k,0.00001);
// Reject degenerate/interpolated geometry without replacing the native scene with NaNs.
if(any(isnan(n))||any(isinf(n))||dot(relativePosition,relativePosition)<.000001)discard;
vec3 l=normalize(PbrSun.xyz),v=normalize(-relativePosition);
float nl=clamp(dot(n,l),0.0,1.0);
vec3 albedo=texel.rgb*tint.rgb;albedo*=albedo;
vec3 f0=mix(vec3(0.04),albedo,material.a);
vec3 fresnel=f0+(1.0-f0)*pow(1.0-vh,5.0);
vec3 specular=distribution*geometry*fresnel/max(4.0*nv*nl,0.0001)*nl;
vec3 specular=localShine(n,v,l,material,albedo);
float fog=max(clamp((max(length(relativePosition.xz),abs(relativePosition.y))-PbrFog.x)/max(PbrFog.y-PbrFog.x,.001),0.0,1.0),
clamp((length(relativePosition)-PbrFog.z)/max(PbrFog.w-PbrFog.z,.001),0.0,1.0));
float amount=PbrSun.w*pow(tint.a,8.0)*visibility(relativePosition,baseNormal)*(1.0-fog*PbrEnvironment.x)
@@ -130,7 +128,7 @@ void main(){
vec4 arrival=localArrival(relativePosition,baseNormal);
float localAmount=arrival.w*(1.0-fog*PbrEnvironment.x)*(1.0-smoothstep(PbrParams.w*.75,PbrParams.w,length(relativePosition)));
if(amount<=0.0001&&localAmount<=0.0001)discard;
vec3 scene=texture(SceneColor,uv).rgb;vec3 linear=scene*scene;
vec3 scene=texelFetch(SceneColor,ivec2(gl_FragCoord.xy),0).rgb;vec3 linear=scene*scene;
// Native diffuse light already exists. Apply only the perturbed-normal difference, then add GGX.
float relief=(nl-max(dot(baseNormal,l),0.0))*.7;
vec3 lit=clamp(linear*(1.0+relief*amount),0.0,1.0);
@@ -140,5 +138,6 @@ void main(){
}
vec3 shine=min((specular*amount+localShine(n,v,arrival.xyz,material,albedo)*localAmount)*PbrParams.x*3.0,vec3(0.5));
lit+=(1.0-lit)*(1.0-exp(-shine));
if(any(isnan(lit))||any(isinf(lit)))discard;
fragColor=vec4(sqrt(clamp(lit,0.0,1.0)),1.0);
}
@@ -277,7 +277,8 @@
"client.SculptureParticlesMixin",
"client.PixelShadowRendererMixin",
"client.PixelShadowLevelMixin",
"client.PixelShadowDirtyMixin"
"client.PixelShadowDirtyMixin",
"client.PreferredGraphicsApiMixin"
],
"injectors": {
"defaultRequire": 1
+1 -1
View File
@@ -1,6 +1,6 @@
name = "Sanctuary"
author = "KOKA99CAB"
version = "beta.141"
version = "beta.143"
pack-format = "packwiz:1.1.0"
[index]