Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8de569bbf1 | ||
|
|
580b2414da | ||
|
|
97ff2b54cb | ||
|
|
59346d7f0a | ||
|
|
ea151dd5df | ||
|
|
7ce01c47f2 |
@@ -1,5 +1,12 @@
|
||||
# Changelog
|
||||
|
||||
## beta.144 — Code d’accès à la création
|
||||
|
||||
- Hello World demande le code `SANC-XXXX-XXXX` quand le serveur a l’URL et le jeton du site.
|
||||
- Le client ne contacte pas le site. Le serveur vérifie, puis consomme le code au moment de créer l’habitant.
|
||||
- Le lien Discord est enregistré à part. Une création interrompue après `redeemed` peut se terminer sans nouveau code si le compte est encore connu.
|
||||
- [Contrat](docs/access-code-beta144.md).
|
||||
|
||||
## beta.110 — Atelier d’argile inclus
|
||||
|
||||
- Une seule livraison réunit l’atelier d’argile, les statuaires et l’import GLB de beta.106 avec les œufs et la neige saisonnière de beta.107 à beta.109.
|
||||
|
||||
@@ -20,11 +20,14 @@ 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 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 l’instance 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à
|
||||
l’atelier, les œufs et la neige saisonnière dans l’instance **Sanctuary Beta**.
|
||||
Les [synchronisations antérieures](docs/git-sync-beta092.md) restent documentées.
|
||||
@@ -35,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
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
# beta.144 — Code d’accès à la création
|
||||
|
||||
Le site accepte la candidature, whitelist le pseudo et envoie un code `SANC-XXXX-XXXX`.
|
||||
Hello World demande ce code avant de créer le personnage. Le client ne parle pas
|
||||
au site. Seul le serveur appelle l’API, avec le jeton `SANCTUARY_API_TOKEN`.
|
||||
|
||||
Sans ces réglages, Hello World reste celui de beta.143 : biographie, couleur et
|
||||
familier, sans code. Dès que l’URL et le jeton sont présents, un nouveau
|
||||
personnage exige un code reconnu. Un habitant déjà enregistré entre sans écran.
|
||||
|
||||
## Réglage serveur
|
||||
|
||||
Variables d’environnement, ou fichier `config/sanctuary/access.json` si une
|
||||
variable manque :
|
||||
|
||||
```json
|
||||
{"appUrl":"https://exemple.sanctuary","token":"..."}
|
||||
```
|
||||
|
||||
`appUrl` est l’origine du site, sans barre finale. Le serveur appelle
|
||||
`POST {appUrl}/api/v1/access-codes/verify` puis `redeem`. Le jeton n’est pas
|
||||
écrit dans les logs, le client ou le pack. Une URL ou un jeton illisible ferme
|
||||
la création : aucun personnage n’est inventé hors ligne.
|
||||
|
||||
## Parcours
|
||||
|
||||
1. La whitelist laisse passer le pseudo. L’écran s’ouvre tant que l’UUID n’a pas
|
||||
d’habitant.
|
||||
2. Le joueur saisit le code. L’affichage du champ et de l’exemple utilise
|
||||
l’alphabet galactique standard (`minecraft:alt`, la police de la table
|
||||
d’enchantement). La valeur envoyée reste le texte tapé. Un format complet
|
||||
déclenche `verify`, pas chaque frappe. « Code reconnu » n’écrit rien.
|
||||
3. Confirmer envoie le code de la session, la biographie, la couleur et le
|
||||
familier. Le pseudo envoyé au site est celui de la session.
|
||||
4. `redeem` répond `redeemed` avec `discord_id` : le lien est enregistré, puis
|
||||
l’habitant est créé. Les connexions suivantes sautent l’écran.
|
||||
5. Erreur, site injoignable ou jeton refusé : le joueur reste sur Hello World.
|
||||
|
||||
Une déconnexion avant `redeemed` ne consomme pas le code. Si le site a répondu
|
||||
`redeemed` et que l’écriture de l’habitant est interrompue, le lien Discord
|
||||
reste dans `data/sanctuary-access.json` (`pending`, schéma 1, graine du monde).
|
||||
La confirmation suivante termine le personnage sans rappeler `redeem`.
|
||||
|
||||
Si le code est déjà consommé et que le site ne renvoie plus `discord_id`, le
|
||||
joueur reste bloqué avec un message de reprise. Le site doit alors renvoyer
|
||||
`minecraft_username` et `discord_id` pour le même pseudo. Le mod accepte cette
|
||||
réponse, que `valid` soit vrai ou faux, et finit la création.
|
||||
|
||||
Le registre des habitants ne change pas. Le Discord est un fichier à part.
|
||||
Un fichier illisible est conservé et refuse l’accueil.
|
||||
|
||||
## Limites
|
||||
|
||||
Le gel dans le monde n’est pas ajouté : Hello World reste avant l’entrée, comme
|
||||
aujourd’hui. Inventaire, commandes et dimensions ne sont pas accessibles tant
|
||||
que l’habitant n’existe pas. La whitelist RCON reste celle du site. Un pseudo
|
||||
ajouté à la main, sans code, voit l’écran sans pouvoir le valider.
|
||||
@@ -114,3 +114,14 @@ SHA-256 du JAR : `660f29c3fa77fee4de6a8db5673c61619985804494f70e961998da36f396d0
|
||||
|
||||
- Sanctuary-beta.141.mrpack : `08df23dac9e98fbfa2935f747345c38244bddc3a3d9371c6416f251e85a7eb19`.
|
||||
- Sanctuary-Test-beta.141.mrpack : `a300ef5bfff7b6990c9f5c2760e9c3a23d7a27aeb10b8f04d29db9a7409fa0e1`.
|
||||
|
||||
## Livraison
|
||||
|
||||
[Release beta.141](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.141)
|
||||
publiée sur le commit source `93ed5574a4f77bd3a083b254498f4994efe31179` ; canal stable
|
||||
`7e6c84400f80db24bf5204cd21d628194ae9f0f7` vérifié après publication.
|
||||
Deux synchronisations isolées puis deux dans la même instance Sanctuary Beta
|
||||
réussissent. Les **923 fichiers personnels** suivis gardent leurs hashes.
|
||||
Sauvegarde préalable : `sanctuary-backups/before-beta.141/`. Aucun monde personnel
|
||||
ouvert. Reçus : `build/shader141-isolated.json` et `build/shader141-prism.json`.
|
||||
Archives normale/Test/template et comparaison copiées dans `sanctuary-beta/build/`.
|
||||
|
||||
@@ -14,6 +14,44 @@ 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
|
||||
les sources s'opposent et fondus. Activer PBR et Lumières colorées ; portée PBR
|
||||
de 32 blocs. [Contrat et vérifications](directional-lights-beta141.md).
|
||||
[Release beta.141](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.141)
|
||||
publiée ; canal `7e6c84400f80db24bf5204cd21d628194ae9f0f7`. 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.141/`. Archives vérifiées dans `sanctuary-beta/build/`.
|
||||
|
||||
## beta.140 — Lumières portées et fondus
|
||||
|
||||
Couleurs des objets en main et sur la tête, distances 8/16/32/64 et fondus
|
||||
|
||||
@@ -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`.
|
||||
@@ -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
@@ -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.144
|
||||
pack_version=beta.144
|
||||
resource_pack_version=beta.121
|
||||
maven_group=fr.koka.sanctuary
|
||||
jei_version=30.32.0-sanctuary.3
|
||||
|
||||
@@ -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'))
|
||||
@@ -1136,6 +1137,14 @@ tasks.register('intro031Smoke', JavaExec) {
|
||||
}
|
||||
tasks.named('check') { dependsOn('intro031Smoke') }
|
||||
|
||||
tasks.register('access144Smoke', JavaExec) {
|
||||
group = 'verification'
|
||||
dependsOn('testClasses')
|
||||
classpath = sourceSets.test.runtimeClasspath
|
||||
mainClass = 'fr.koka.sanctuary.progression.AccessCodeSmoke'
|
||||
}
|
||||
tasks.named('check') { dependsOn('access144Smoke') }
|
||||
|
||||
if (providers.gradleProperty('sanctuaryQuickTests').map { it.toBoolean() }.getOrElse(false)) {
|
||||
dependencies { gametestRuntimeOnly project(':sanctuary-test') }
|
||||
}
|
||||
|
||||
+19
-11
@@ -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);}
|
||||
|
||||
+19
-15
@@ -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);}
|
||||
|
||||
+3
@@ -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.;}
|
||||
|
||||
+2
-2
@@ -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);}}
|
||||
|
||||
+63
@@ -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);}
|
||||
}
|
||||
+7
-4
@@ -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) {
|
||||
|
||||
+1
-1
@@ -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());
|
||||
|
||||
+1
-1
@@ -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");
|
||||
|
||||
@@ -12,13 +12,20 @@ import net.minecraft.client.gui.screens.Screen;
|
||||
import net.minecraft.client.renderer.RenderPipelines;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.network.chat.FontDescription;
|
||||
import net.minecraft.network.chat.Style;
|
||||
import net.minecraft.util.FormattedCharSequence;
|
||||
import net.minecraft.ChatFormatting;
|
||||
|
||||
/** Configuration-stage identity and starter slots. Closing cancels connection, never the saved offer. */
|
||||
public final class HelloWorldScreen extends Screen {
|
||||
private static final FontDescription GALACTIC = new FontDescription.Resource(Identifier.withDefaultNamespace("alt"));
|
||||
private static final Style GALACTIC_STYLE = Style.EMPTY.withFont(GALACTIC);
|
||||
private ProgressionPayloads.Hello offer;private final Runnable disconnect;
|
||||
private EditBox bio,familiarName;private int color=-1;private Button submit;private boolean waiting,identityLocked;
|
||||
private String draft="",starter="",nameDraft="";private List<StarterCompanions.Choice> starterChoices=List.of();
|
||||
private String draft="",starter="",nameDraft="",codeDraft="",codeStatus="",lastChecked="";
|
||||
private boolean accessRequired,accessPending,admitting;private EditBox codeBox;
|
||||
private List<StarterCompanions.Choice> starterChoices=List.of();
|
||||
private final List<Button> colors=new ArrayList<>(),starters=new ArrayList<>();
|
||||
private int panelX,panelY,panelWidth,bioLabelY,colorLabelY,starterLabelY,nameLabelY;private boolean compact;
|
||||
public HelloWorldScreen(ProgressionPayloads.Hello offer,Runnable disconnect){super(ProgressionService.label("hello_title"));this.offer=offer;this.disconnect=disconnect;}
|
||||
@@ -29,18 +36,29 @@ public final class HelloWorldScreen extends Screen {
|
||||
if(identityLocked){draft=welcome.bio();color=offer.colors().getFirst();}
|
||||
if(starterChoices.stream().noneMatch(c->c.mob().equals(starter)))starter="";
|
||||
}
|
||||
public void accept(StarterPayloads.Welcome welcome){if(bio!=null)draft=bio.getValue();load(welcome);waiting=false;if(!offer.colors().contains(color))color=-1;rebuildWidgets();}
|
||||
public void accept(ProgressionPayloads.Hello offer){this.offer=offer;waiting=false;if(bio!=null)draft=bio.getValue();if(!offer.colors().contains(color))color=-1;rebuildWidgets();}
|
||||
public void prepareAccess(boolean pending){accessRequired=true;accessPending=pending;if(pending)codeStatus="code_recognized";}
|
||||
public void requireAccess(boolean pending){boolean changed=!accessRequired||accessPending!=pending;prepareAccess(pending);if(changed&&minecraft!=null&&minecraft.gui.screen()==this){keepDrafts();rebuildWidgets();}}
|
||||
public void acceptCode(String code,String reason){if(admitting)return;if(!AccessCodes.normalize(code).equals(AccessCodes.normalize(codeDraft))){if(AccessCodes.validFormat(AccessCodes.normalize(codeDraft)))sendCheck(AccessCodes.normalize(codeDraft));return;}codeStatus=reason;waiting=false;updateSelection();}
|
||||
public void accept(StarterPayloads.Welcome welcome){keepDrafts();load(welcome);waiting=false;admitting=false;noteCodeError();if(!offer.colors().contains(color))color=-1;rebuildWidgets();}
|
||||
public void accept(ProgressionPayloads.Hello offer){keepDrafts();this.offer=offer;waiting=false;admitting=false;noteCodeError();if(!offer.colors().contains(color))color=-1;rebuildWidgets();}
|
||||
private void keepDrafts(){if(bio!=null)draft=bio.getValue();if(codeBox!=null)codeDraft=codeBox.getValue();if(familiarName!=null)nameDraft=familiarName.getValue();}
|
||||
private void noteCodeError(){if(offer.error().startsWith("code_"))codeStatus=offer.error();}
|
||||
@Override protected void init(){
|
||||
boolean naming=!starterChoices.isEmpty();compact=naming&&height<278;
|
||||
panelWidth=Math.min(520,width-20);panelX=(width-panelWidth)/2;panelY=Math.max(0,(height-(naming&&!compact?270:238))/2);colors.clear();starters.clear();familiarName=null;
|
||||
bioLabelY=panelY+(compact?37:44);colorLabelY=panelY+(compact?71:81);starterLabelY=panelY+(compact?145:177);nameLabelY=starterLabelY+36;
|
||||
boolean naming=!starterChoices.isEmpty();int shift=accessRequired?36:0;compact=naming&&height<278+shift;
|
||||
panelWidth=Math.min(520,width-20);panelX=(width-panelWidth)/2;panelY=Math.max(0,(height-((naming&&!compact?270:238)+shift))/2);colors.clear();starters.clear();familiarName=null;codeBox=null;
|
||||
bioLabelY=panelY+(compact?37:44)+shift;colorLabelY=panelY+(compact?71:81)+shift;starterLabelY=panelY+(compact?145:177)+shift;nameLabelY=starterLabelY+36;
|
||||
var skin=addRenderableWidget(new PlayerSkinWidget(64,145,minecraft.getEntityModels(),minecraft.getSkinManager().createLookup(minecraft.getGameProfile(),false)));skin.setPosition(panelX,panelY+35);
|
||||
int right=panelX+72,w=panelWidth-72;
|
||||
bio=addRenderableWidget(new EditBox(font,right,panelY+(compact?48:57),w,18,ProgressionService.label("bio")));bio.setMaxLength(256);bio.setHint(ProgressionService.label("bio_hint"));bio.setValue(draft);bio.setResponder(value->draft=value);
|
||||
if(accessRequired){
|
||||
codeBox=addRenderableWidget(new EditBox(font,right,panelY+(compact?48:57),w,18,ProgressionService.label("code")));
|
||||
codeBox.setMaxLength(32);codeBox.setHint(Component.translatable("sanctuary.progression.code_hint").withStyle(style -> style.withFont(GALACTIC)));codeBox.setValue(codeDraft);
|
||||
codeBox.addFormatter((text,offset) -> FormattedCharSequence.forward(text, GALACTIC_STYLE));
|
||||
codeBox.setResponder(value->{codeDraft=value;if(accessPending){updateSelection();return;}String normalized=AccessCodes.normalize(value);if(!AccessCodes.validFormat(normalized)){codeStatus=normalized.isEmpty()?"":"code_malformed";lastChecked="";updateSelection();return;}if(!normalized.equals(lastChecked))sendCheck(normalized);else updateSelection();});
|
||||
}
|
||||
bio=addRenderableWidget(new EditBox(font,right,panelY+(compact?48:57)+shift,w,18,ProgressionService.label("bio")));bio.setMaxLength(256);bio.setHint(ProgressionService.label("bio_hint"));bio.setValue(draft);bio.setResponder(value->draft=value);
|
||||
int cell=w/4;
|
||||
for(int i=0;i<offer.colors().size();i++){
|
||||
final int rgb=offer.colors().get(i);int rowHeight=compact?12:16;Button button=addRenderableWidget(new ColorButton(right+(i%4)*cell,panelY+(compact?82:94)+(i/4)*rowHeight,cell-2,rowHeight,rgb,i+1,b->{color=rgb;updateSelection();}));
|
||||
final int rgb=offer.colors().get(i);int rowHeight=compact?12:16;Button button=addRenderableWidget(new ColorButton(right+(i%4)*cell,panelY+(compact?82:94)+shift+(i/4)*rowHeight,cell-2,rowHeight,rgb,i+1,b->{color=rgb;updateSelection();}));
|
||||
button.setTooltip(Tooltip.create(InhabitantColorNames.label(rgb)));colors.add(button);
|
||||
}
|
||||
for(int i=0;i<starterChoices.size();i++)starters.add(addRenderableWidget(new StarterSlot(right+i*28,starterLabelY+11,starterChoices.get(i))));
|
||||
@@ -49,10 +67,16 @@ public final class HelloWorldScreen extends Screen {
|
||||
familiarName.setMaxLength(StarterCompanions.NAME_LIMIT);familiarName.setHint(Component.translatable("sanctuary.starter.name_hint"));familiarName.setValue(nameDraft);
|
||||
familiarName.setResponder(value->{nameDraft=value;updateSelection();});
|
||||
}
|
||||
int actionsY=panelY+(naming&&!compact?248:216);
|
||||
int actionsY=panelY+(naming&&!compact?248:216)+shift;
|
||||
addRenderableWidget(Button.builder(ProgressionService.label("cancel"),b->onClose()).bounds(panelX,actionsY,90,20).build());
|
||||
submit=addRenderableWidget(Button.builder(ProgressionService.label("enter"),b->{
|
||||
if(waiting||color<0||(!starterChoices.isEmpty()&&(starter.isEmpty()||!StarterCompanions.validName(nameDraft))))return;
|
||||
if(accessRequired){
|
||||
if(!(accessPending||"code_recognized".equals(codeStatus)))return;
|
||||
admitting=true;waiting=true;updateSelection();
|
||||
ClientConfigurationNetworking.send(new AccessPayloads.Admit(bio.getValue(),color,starterChoices.isEmpty()?"":starter,nameDraft,accessPending||codeBox==null?"":codeBox.getValue()));
|
||||
return;
|
||||
}
|
||||
if(!starterChoices.isEmpty()&&!nameDraft.isBlank()&&!ClientConfigurationNetworking.canSend(StarterPayloads.NamedCreate.TYPE)){
|
||||
offer=new ProgressionPayloads.Hello(offer.name(),offer.colors(),"starter_name_version_required");return;
|
||||
}
|
||||
@@ -62,9 +86,12 @@ public final class HelloWorldScreen extends Screen {
|
||||
else ClientConfigurationNetworking.send(new StarterPayloads.Create(bio.getValue(),color,starter));
|
||||
}).bounds(panelX+96,actionsY,panelWidth-96,20).build());updateSelection();
|
||||
}
|
||||
private void sendCheck(String normalized){lastChecked=normalized;codeStatus="code_checking";updateSelection();if(ClientConfigurationNetworking.canSend(AccessPayloads.Check.TYPE))ClientConfigurationNetworking.send(new AccessPayloads.Check(normalized));}
|
||||
private void updateSelection(){
|
||||
if(submit!=null)submit.active=!waiting&&color>=0&&(starterChoices.isEmpty()||(!starter.isEmpty()&&StarterCompanions.validName(nameDraft)));
|
||||
boolean codeOk=!accessRequired||accessPending||"code_recognized".equals(codeStatus);
|
||||
if(submit!=null)submit.active=!waiting&&color>=0&&(starterChoices.isEmpty()||(!starter.isEmpty()&&StarterCompanions.validName(nameDraft)))&&codeOk;
|
||||
if(familiarName!=null)familiarName.setEditable(!waiting);
|
||||
if(codeBox!=null)codeBox.setEditable(!waiting&&!accessPending);
|
||||
if(bio!=null)bio.setEditable(!waiting&&!identityLocked);colors.forEach(b->b.active=!waiting&&!identityLocked);starters.forEach(b->b.active=!waiting);
|
||||
}
|
||||
@Override public void extractBackground(GuiGraphicsExtractor g,int mx,int my,float a){
|
||||
@@ -73,13 +100,19 @@ public final class HelloWorldScreen extends Screen {
|
||||
@Override public void extractRenderState(GuiGraphicsExtractor g,int mx,int my,float a){
|
||||
super.extractRenderState(g,mx,my,a);g.centeredText(font,title,width/2,panelY+7,0xFFFFFFFF);
|
||||
g.text(font,Component.literal(offer.name()).withStyle(s->color<0?s:s.withColor(color)),panelX+72,panelY+(compact?24:29),0xFFFFFFFF);
|
||||
if(accessRequired){
|
||||
String codeKey=codeStatus.isEmpty()?"code":codeStatus;
|
||||
int codeColor="code_recognized".equals(codeStatus)?0xFF88CC88:codeStatus.isEmpty()?0xFFBBBBBB:0xFFFFFFAA;
|
||||
g.text(font,ProgressionService.label(codeKey),panelX+72,panelY+(compact?37:44),codeColor);
|
||||
}
|
||||
g.text(font,ProgressionService.label("bio"),panelX+72,bioLabelY,0xFFBBBBBB);g.text(font,ProgressionService.label("choose_color"),panelX+72,colorLabelY,0xFFBBBBBB);
|
||||
String status=waiting?"joining":offer.error();
|
||||
String shown=offer.error().startsWith("code_")?"":offer.error();
|
||||
String status=waiting?"joining":shown;
|
||||
if(!starterChoices.isEmpty()){
|
||||
g.text(font,status.isEmpty()?Component.translatable("sanctuary.starter.slots"):ProgressionService.label(status),panelX+72,starterLabelY,status.isEmpty()?0xFFBBBBBB:0xFFFFFFAA);
|
||||
g.text(font,Component.translatable("sanctuary.starter.name"),panelX+72,nameLabelY,0xFFBBBBBB);
|
||||
}
|
||||
else if(!status.isEmpty())g.centeredText(font,ProgressionService.label(status),width/2,panelY+204,0xFFFFFFAA);
|
||||
else if(!status.isEmpty())g.centeredText(font,ProgressionService.label(status),width/2,panelY+204+(accessRequired?36:0),0xFFFFFFAA);
|
||||
}
|
||||
private final class ColorButton extends Button.Plain {
|
||||
ColorButton(int x,int y,int w,int h,int rgb,int number,OnPress press){super(x,y,w,h,InhabitantColorNames.label(rgb).copy().withStyle(s->s.withColor(rgb)),press,ignored->ProgressionService.label("color_number",number).copy().append(": ").append(InhabitantColorNames.label(rgb)));setOverrideRenderHighlightedSprite(()->color==rgb||isHoveredOrFocused());}
|
||||
|
||||
@@ -11,6 +11,7 @@ import net.minecraft.client.gui.screens.Screen;
|
||||
|
||||
public final class ProgressionClient {
|
||||
public static ProgressRecord progress;
|
||||
private static boolean accessGate,accessPending;
|
||||
public static boolean operator;
|
||||
public static InhabitantStore.Identity identity;
|
||||
public static ProgressionConfig config=ProgressionConfig.DEFAULT;
|
||||
@@ -38,14 +39,21 @@ public final class ProgressionClient {
|
||||
CycleClient.register();
|
||||
EntityRendererRegistry.register(SanctuarySitting.SEAT,NoopRenderer::new);
|
||||
ClientConfigurationNetworking.registerGlobalReceiver(ProgressionPayloads.InventoryLayout.TYPE,(payload,context) -> {});
|
||||
ClientConfigurationNetworking.registerGlobalReceiver(AccessPayloads.Gate.TYPE,(payload,context)->context.client().execute(()->{
|
||||
accessGate=payload.required();accessPending=payload.pending();
|
||||
if(payload.required()&&context.client().gui.screen() instanceof HelloWorldScreen screen)screen.requireAccess(payload.pending());
|
||||
}));
|
||||
ClientConfigurationNetworking.registerGlobalReceiver(AccessPayloads.Status.TYPE,(payload,context)->context.client().execute(()->{
|
||||
if(context.client().gui.screen() instanceof HelloWorldScreen screen)screen.acceptCode(payload.code(),payload.reason());
|
||||
}));
|
||||
ClientConfigurationNetworking.registerGlobalReceiver(StarterPayloads.Welcome.TYPE,(payload,context)->context.client().execute(()->{
|
||||
if(context.client().gui.screen() instanceof HelloWorldScreen screen)screen.accept(payload);
|
||||
else context.client().gui.setScreen(new HelloWorldScreen(payload,()->context.responseSender().disconnect(ProgressionService.label("cancelled"))));
|
||||
else showHello(context.client(),new HelloWorldScreen(payload,()->context.responseSender().disconnect(ProgressionService.label("cancelled"))));
|
||||
}));
|
||||
ClientConfigurationNetworking.registerGlobalReceiver(ProgressionPayloads.Hello.TYPE,(payload,context) ->
|
||||
context.client().execute(() -> {
|
||||
if (context.client().gui.screen() instanceof HelloWorldScreen screen) screen.accept(payload);
|
||||
else context.client().gui.setScreen(new HelloWorldScreen(payload,
|
||||
else showHello(context.client(),new HelloWorldScreen(payload,
|
||||
() -> context.responseSender().disconnect(ProgressionService.label("cancelled"))));
|
||||
}));
|
||||
ClientPlayNetworking.registerGlobalReceiver(ProgressionPayloads.Snapshot.TYPE,(payload,context) ->
|
||||
@@ -91,9 +99,11 @@ public final class ProgressionClient {
|
||||
if (active() && ClientPlayNetworking.canSend(ProgressionPayloads.Action.TYPE))
|
||||
ClientPlayNetworking.send(new ProgressionPayloads.Action(target,progress.revision()));
|
||||
}
|
||||
private static void showHello(Minecraft client,HelloWorldScreen screen){if(accessGate)screen.prepareAccess(accessPending);client.gui.setScreen(screen);}
|
||||
private static void reset(Minecraft client) {
|
||||
if (client.player != null) ProgressEffects.clear(client.player);
|
||||
identity=null;progress=null;operator=false;config=ProgressionConfig.DEFAULT;names=MobNamesConfig.DEFAULT;
|
||||
accessGate=false;accessPending=false;
|
||||
wasSneaking=false;pendingSit=false;lastSneak=-100;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
+24
@@ -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"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
package fr.koka.sanctuary.progression;
|
||||
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParser;
|
||||
import java.util.Locale;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/** Site access-code contract. The client never calls the site; only these decisions run on the server. */
|
||||
public final class AccessCodes {
|
||||
private static final Pattern CODE = Pattern.compile("SANC-[A-Z0-9]{4}-[A-Z0-9]{4}");
|
||||
private static final Pattern USER = Pattern.compile("[A-Za-z0-9_]{3,16}");
|
||||
private static final Pattern DISCORD = Pattern.compile("[0-9]{1,20}");
|
||||
private AccessCodes() { }
|
||||
public record Result(boolean valid, String reason, String minecraftUsername, String discordId) {
|
||||
public static Result of(boolean valid, String reason, String username, String discordId) {
|
||||
return new Result(valid, reason == null ? "" : reason, username == null ? "" : username, discordId == null ? "" : discordId);
|
||||
}
|
||||
public static Result unreachable() { return of(false, "unreachable", "", ""); }
|
||||
public static Result unauthorized() { return of(false, "unauthorized", "", ""); }
|
||||
public static Result unconfigured() { return of(false, "unconfigured", "", ""); }
|
||||
}
|
||||
public record Redeem(boolean accepted, String reason, String minecraftUsername, String discordId) {
|
||||
public static Redeem accept(String username, String discordId) { return new Redeem(true, "code_recognized", username, discordId); }
|
||||
public static Redeem reject(String reason) { return new Redeem(false, reason, "", ""); }
|
||||
}
|
||||
public static String normalize(String code) { return code == null ? "" : code.trim().toUpperCase(Locale.ROOT); }
|
||||
public static boolean validFormat(String normalized) { return normalized != null && CODE.matcher(normalized).matches(); }
|
||||
public static boolean validUsername(String name) { return name != null && USER.matcher(name).matches(); }
|
||||
public static boolean validDiscord(String id) { return id != null && DISCORD.matcher(id).matches(); }
|
||||
public static boolean sameUser(String returned, String session) {
|
||||
return validUsername(session) && returned != null && returned.equalsIgnoreCase(session) && validUsername(returned);
|
||||
}
|
||||
public static Result parse(int status, String body) {
|
||||
if (status == 401) return Result.unauthorized();
|
||||
if (status == 429 || status < 200 || status >= 500) return Result.unreachable();
|
||||
try {
|
||||
var object = JsonParser.parseString(body).getAsJsonObject();
|
||||
if (!object.has("valid") || !object.has("reason")) return Result.unreachable();
|
||||
return Result.of(object.get("valid").getAsBoolean(), text(object, "reason"), text(object, "minecraft_username"), text(object, "discord_id"));
|
||||
} catch (RuntimeException error) { return Result.unreachable(); }
|
||||
}
|
||||
public static String verifyKey(Result result, String username) {
|
||||
if (result == null) return "code_unreachable";
|
||||
return switch (result.reason()) {
|
||||
case "unconfigured" -> "code_unconfigured";
|
||||
case "unauthorized" -> "code_unauthorized";
|
||||
case "unreachable" -> "code_unreachable";
|
||||
case "unused" -> result.valid() && sameUser(result.minecraftUsername(), username) && validDiscord(result.discordId()) ? "code_recognized" : "code_invalid";
|
||||
case "invalid" -> "code_invalid";
|
||||
case "already_used" -> "code_used";
|
||||
default -> "code_invalid";
|
||||
};
|
||||
}
|
||||
/** Redeem commits the code. An already used code is accepted only when the site still returns the same account. */
|
||||
public static Redeem redeem(Result result, String username) {
|
||||
if (result == null) return Redeem.reject("code_unreachable");
|
||||
return switch (result.reason()) {
|
||||
case "unconfigured" -> Redeem.reject("code_unconfigured");
|
||||
case "unauthorized" -> Redeem.reject("code_unauthorized");
|
||||
case "unreachable" -> Redeem.reject("code_unreachable");
|
||||
case "redeemed", "already_used" -> account(result, username);
|
||||
case "invalid" -> Redeem.reject("code_invalid");
|
||||
default -> Redeem.reject("code_invalid");
|
||||
};
|
||||
}
|
||||
private static Redeem account(Result result, String username) {
|
||||
if (!result.minecraftUsername().isEmpty() && !result.minecraftUsername().equalsIgnoreCase(username))
|
||||
return Redeem.reject("code_mismatch");
|
||||
if (!sameUser(result.minecraftUsername(), username) || !validDiscord(result.discordId()))
|
||||
return Redeem.reject("code_replay");
|
||||
return Redeem.accept(result.minecraftUsername(), result.discordId());
|
||||
}
|
||||
private static String text(JsonObject object, String key) {
|
||||
if (!object.has(key) || object.get(key).isJsonNull() || !object.get(key).isJsonPrimitive()) return "";
|
||||
return object.get(key).getAsString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
package fr.koka.sanctuary.progression;
|
||||
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParser;
|
||||
import java.net.URI;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.time.Duration;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/** Server-only site access. The token stays in the environment or the server config, never in the client. */
|
||||
public final class AccessConfig {
|
||||
private static final Logger LOG = LoggerFactory.getLogger("Sanctuary/Access");
|
||||
private static final HttpClient HTTP = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(5)).followRedirects(HttpClient.Redirect.NEVER).build();
|
||||
public interface Directory {
|
||||
AccessCodes.Result verify(String username, String code);
|
||||
AccessCodes.Result redeem(String username, String code);
|
||||
}
|
||||
private static volatile boolean enabled;
|
||||
private static volatile Directory directory = new Disabled();
|
||||
private AccessConfig() { }
|
||||
public static boolean enabled() { return enabled; }
|
||||
public static Directory directory() { return directory; }
|
||||
public static void load(Path configDir) {
|
||||
String url = env("SANCTUARY_APP_URL");
|
||||
String token = env("SANCTUARY_API_TOKEN");
|
||||
if (url.isEmpty() || token.isEmpty()) {
|
||||
var file = readFile(configDir.resolve("access.json"));
|
||||
if (url.isEmpty()) url = file.url;
|
||||
if (token.isEmpty()) token = file.token;
|
||||
}
|
||||
if (url.isEmpty() && token.isEmpty()) {
|
||||
enabled = false; directory = new Disabled();
|
||||
LOG.info("Sanctuary access codes are off. Set SANCTUARY_APP_URL and SANCTUARY_API_TOKEN to require a code.");
|
||||
return;
|
||||
}
|
||||
URI uri;
|
||||
try { uri = URI.create(url.endsWith("/") ? url.substring(0, url.length() - 1) : url); }
|
||||
catch (IllegalArgumentException error) { failClosed("Sanctuary access URL is invalid. Character creation stays closed."); return; }
|
||||
if (uri.getUserInfo() != null || uri.getRawQuery() != null || uri.getHost() == null) {
|
||||
failClosed("Sanctuary access URL must be a bare https origin. Character creation stays closed."); return;
|
||||
}
|
||||
String scheme = uri.getScheme();
|
||||
if (!"https".equalsIgnoreCase(scheme) && !"http".equalsIgnoreCase(scheme)) {
|
||||
failClosed("Sanctuary access URL must use https. Character creation stays closed."); return;
|
||||
}
|
||||
if (token.isEmpty()) { failClosed("Sanctuary access token is missing. Character creation stays closed."); return; }
|
||||
if ("http".equalsIgnoreCase(scheme)) LOG.warn("Sanctuary access URL is not HTTPS. Use HTTPS outside a local site.");
|
||||
enabled = true;
|
||||
directory = new HttpDirectory(uri, token);
|
||||
LOG.info("Sanctuary access codes are required before character creation.");
|
||||
}
|
||||
private static void failClosed(String message) {
|
||||
enabled = true; directory = new Disabled(); LOG.error(message);
|
||||
}
|
||||
private static String env(String key) {
|
||||
String value = System.getenv(key);
|
||||
return value == null ? "" : value.trim();
|
||||
}
|
||||
private record FileConfig(String url, String token) { }
|
||||
private static FileConfig readFile(Path file) {
|
||||
if (!Files.isRegularFile(file)) return new FileConfig("", "");
|
||||
try {
|
||||
var object = JsonParser.parseString(Files.readString(file, StandardCharsets.UTF_8)).getAsJsonObject();
|
||||
return new FileConfig(text(object, "appUrl"), text(object, "token"));
|
||||
} catch (Exception error) {
|
||||
LOG.error("Sanctuary access config cannot be read. Character creation stays closed.");
|
||||
return new FileConfig("http://invalid", "");
|
||||
}
|
||||
}
|
||||
private static String text(JsonObject object, String key) {
|
||||
if (!object.has(key) || object.get(key).isJsonNull()) return "";
|
||||
return object.get(key).getAsString().trim();
|
||||
}
|
||||
private static final class Disabled implements Directory {
|
||||
public AccessCodes.Result verify(String username, String code) { return AccessCodes.Result.unconfigured(); }
|
||||
public AccessCodes.Result redeem(String username, String code) { return AccessCodes.Result.unconfigured(); }
|
||||
}
|
||||
private static final class HttpDirectory implements Directory {
|
||||
private final URI origin;
|
||||
private final String token;
|
||||
HttpDirectory(URI origin, String token) { this.origin = origin; this.token = token; }
|
||||
public AccessCodes.Result verify(String username, String code) { return post("/api/v1/access-codes/verify", username, code); }
|
||||
public AccessCodes.Result redeem(String username, String code) { return post("/api/v1/access-codes/redeem", username, code); }
|
||||
private AccessCodes.Result post(String path, String username, String code) {
|
||||
var body = new JsonObject();
|
||||
body.addProperty("code", code);
|
||||
body.addProperty("minecraft_username", username);
|
||||
var request = HttpRequest.newBuilder(URI.create(origin.toString() + path)).timeout(Duration.ofSeconds(10))
|
||||
.header("Authorization", "Bearer " + token).header("Content-Type", "application/json").header("Accept", "application/json")
|
||||
.POST(HttpRequest.BodyPublishers.ofString(body.toString())).build();
|
||||
try {
|
||||
var response = HTTP.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));
|
||||
if (response.statusCode() == 401) LOG.warn("Sanctuary access token was rejected");
|
||||
return AccessCodes.parse(response.statusCode(), response.body());
|
||||
} catch (Exception error) {
|
||||
LOG.warn("Sanctuary access endpoint is unreachable ({})", error.getClass().getSimpleName());
|
||||
return AccessCodes.Result.unreachable();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
package fr.koka.sanctuary.progression;
|
||||
|
||||
import com.google.gson.GsonBuilder;
|
||||
import com.google.gson.JsonObject;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Path;
|
||||
import java.util.TreeMap;
|
||||
import java.util.UUID;
|
||||
|
||||
/** Discord binding for an inhabitant. Pending entries finish a creation interrupted after the site consumed the code. */
|
||||
public final class AccessLedger {
|
||||
private static final int LIMIT = 1024 * 1024;
|
||||
private final Path file;
|
||||
private final long seed;
|
||||
private byte[] previous;
|
||||
private final TreeMap<UUID, Binding> pending = new TreeMap<>();
|
||||
private final TreeMap<UUID, Binding> bound = new TreeMap<>();
|
||||
private boolean failed;
|
||||
public record Binding(String name, String discordId) { }
|
||||
public AccessLedger(Path file, long seed) throws IOException {
|
||||
this.file = file; this.seed = seed;
|
||||
previous = AtomicJson.read(file, LIMIT);
|
||||
if (previous == null) return;
|
||||
var root = AtomicJson.parse(new String(previous, StandardCharsets.UTF_8)).getAsJsonObject();
|
||||
AtomicJson.fields(root, "schema", "seed", "pending", "bound");
|
||||
if (root.get("schema").getAsInt() != 1 || root.get("seed").getAsLong() != seed)
|
||||
throw new IllegalArgumentException("Invalid access ledger");
|
||||
read(root.getAsJsonObject("pending"), pending);
|
||||
read(root.getAsJsonObject("bound"), bound);
|
||||
}
|
||||
public Binding known(UUID id) {
|
||||
var open = pending.get(id);
|
||||
return open != null ? open : bound.get(id);
|
||||
}
|
||||
public void remember(UUID id, String name, String discordId) throws IOException {
|
||||
if (failed) throw new IOException("Access ledger failed earlier");
|
||||
if (!AccessCodes.validUsername(name) || !AccessCodes.validDiscord(discordId))
|
||||
throw new IllegalArgumentException("Invalid access binding");
|
||||
if (bound.containsKey(id)) return;
|
||||
pending.put(id, new Binding(name, discordId));
|
||||
save();
|
||||
}
|
||||
public void commit(UUID id) throws IOException {
|
||||
if (failed) throw new IOException("Access ledger failed earlier");
|
||||
var binding = pending.remove(id);
|
||||
if (binding == null) return;
|
||||
bound.put(id, binding);
|
||||
save();
|
||||
}
|
||||
private static void read(JsonObject object, TreeMap<UUID, Binding> into) {
|
||||
for (var entry : object.entrySet()) {
|
||||
AtomicJson.fields(entry.getValue(), "name", "discordId");
|
||||
UUID id = UUID.fromString(entry.getKey());
|
||||
var value = entry.getValue().getAsJsonObject();
|
||||
String name = value.get("name").getAsString();
|
||||
String discordId = value.get("discordId").getAsString();
|
||||
if (!AccessCodes.validUsername(name) || !AccessCodes.validDiscord(discordId))
|
||||
throw new IllegalArgumentException("Invalid access binding");
|
||||
if (into.put(id, new Binding(name, discordId)) != null) throw new IllegalArgumentException("Duplicate access binding");
|
||||
}
|
||||
}
|
||||
private void save() throws IOException {
|
||||
var root = new JsonObject();
|
||||
root.addProperty("schema", 1);
|
||||
root.addProperty("seed", seed);
|
||||
root.add("pending", write(pending));
|
||||
root.add("bound", write(bound));
|
||||
try {
|
||||
previous = AtomicJson.write(file, previous, new GsonBuilder().setPrettyPrinting().create().toJson(root), LIMIT);
|
||||
} catch (IOException | RuntimeException error) { failed = true; throw error; }
|
||||
}
|
||||
private static JsonObject write(TreeMap<UUID, Binding> map) {
|
||||
var object = new JsonObject();
|
||||
map.forEach((id, binding) -> {
|
||||
var value = new JsonObject();
|
||||
value.addProperty("name", binding.name);
|
||||
value.addProperty("discordId", binding.discordId);
|
||||
object.add(id.toString(), value);
|
||||
});
|
||||
return object;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package fr.koka.sanctuary.progression;
|
||||
|
||||
import fr.koka.sanctuary.SanctuaryMod;
|
||||
import net.minecraft.network.FriendlyByteBuf;
|
||||
import net.minecraft.network.codec.StreamCodec;
|
||||
import net.minecraft.network.protocol.common.custom.CustomPacketPayload;
|
||||
|
||||
/** Additive Hello World handshake. Existing create packets stay unchanged when access codes are off. */
|
||||
public final class AccessPayloads {
|
||||
private AccessPayloads() { }
|
||||
public record Gate(boolean required, boolean pending) implements CustomPacketPayload {
|
||||
public static final Type<Gate> TYPE = new Type<>(SanctuaryMod.id("access_gate_v1"));
|
||||
public static final StreamCodec<FriendlyByteBuf, Gate> CODEC = CustomPacketPayload.codec(
|
||||
(p, b) -> { b.writeBoolean(p.required); b.writeBoolean(p.pending); },
|
||||
b -> new Gate(b.readBoolean(), b.readBoolean()));
|
||||
public Type<Gate> type() { return TYPE; }
|
||||
}
|
||||
public record Check(String code) implements CustomPacketPayload {
|
||||
public static final Type<Check> TYPE = new Type<>(SanctuaryMod.id("access_check_v1"));
|
||||
public static final StreamCodec<FriendlyByteBuf, Check> CODEC = CustomPacketPayload.codec(
|
||||
(p, b) -> b.writeUtf(p.code, 32), b -> new Check(b.readUtf(32)));
|
||||
public Type<Check> type() { return TYPE; }
|
||||
}
|
||||
public record Status(String code, String reason) implements CustomPacketPayload {
|
||||
public static final Type<Status> TYPE = new Type<>(SanctuaryMod.id("access_status_v1"));
|
||||
public static final StreamCodec<FriendlyByteBuf, Status> CODEC = CustomPacketPayload.codec(
|
||||
(p, b) -> { b.writeUtf(p.code, 32); b.writeUtf(p.reason, 64); },
|
||||
b -> new Status(b.readUtf(32), b.readUtf(64)));
|
||||
public Type<Status> type() { return TYPE; }
|
||||
}
|
||||
public record Admit(String bio, int color, String mob, String name, String code) implements CustomPacketPayload {
|
||||
public static final Type<Admit> TYPE = new Type<>(SanctuaryMod.id("access_admit_v1"));
|
||||
public static final StreamCodec<FriendlyByteBuf, Admit> CODEC = CustomPacketPayload.codec(
|
||||
(p, b) -> { b.writeUtf(p.bio, 256); b.writeInt(p.color); b.writeUtf(p.mob, 64); b.writeUtf(p.name, StarterCompanions.NAME_LIMIT); b.writeUtf(p.code, 32); },
|
||||
b -> new Admit(b.readUtf(256), b.readInt(), b.readUtf(64), b.readUtf(StarterCompanions.NAME_LIMIT), b.readUtf(32)));
|
||||
public Type<Admit> type() { return TYPE; }
|
||||
}
|
||||
}
|
||||
+113
-1
@@ -50,6 +50,7 @@ public final class ProgressionService {
|
||||
public StarterCompanions.Store starters;
|
||||
public CycleConfig cycleConfig=CycleConfig.DEFAULT;
|
||||
public TutorialConfig tutorialConfig=TutorialConfig.DEFAULT;
|
||||
public AccessLedger access;
|
||||
Session(InhabitantStore inhabitants, ProgressionConfig config, MobNamesConfig names) {
|
||||
this.inhabitants=inhabitants; this.config=config; this.names=names;
|
||||
}
|
||||
@@ -61,6 +62,10 @@ public final class ProgressionService {
|
||||
PayloadTypeRegistry.clientboundConfiguration().register(StarterPayloads.Welcome.TYPE,StarterPayloads.Welcome.CODEC);
|
||||
PayloadTypeRegistry.serverboundConfiguration().register(StarterPayloads.Create.TYPE,StarterPayloads.Create.CODEC);
|
||||
PayloadTypeRegistry.serverboundConfiguration().register(StarterPayloads.NamedCreate.TYPE,StarterPayloads.NamedCreate.CODEC);
|
||||
PayloadTypeRegistry.clientboundConfiguration().register(AccessPayloads.Gate.TYPE,AccessPayloads.Gate.CODEC);
|
||||
PayloadTypeRegistry.clientboundConfiguration().register(AccessPayloads.Status.TYPE,AccessPayloads.Status.CODEC);
|
||||
PayloadTypeRegistry.serverboundConfiguration().register(AccessPayloads.Check.TYPE,AccessPayloads.Check.CODEC);
|
||||
PayloadTypeRegistry.serverboundConfiguration().register(AccessPayloads.Admit.TYPE,AccessPayloads.Admit.CODEC);
|
||||
SanctuarySitting.register();
|
||||
fr.koka.sanctuary.inventory.HotbarService.register();
|
||||
fr.koka.sanctuary.inventory.InventorySortService.register();
|
||||
@@ -87,6 +92,9 @@ public final class ProgressionService {
|
||||
});
|
||||
ServerConfigurationConnectionEvents.CONFIGURE.register((handler,server) -> {
|
||||
if (session(server) == null) return;
|
||||
if (session(server).access != null && !ServerConfigurationNetworking.canSend(handler,AccessPayloads.Gate.TYPE)) {
|
||||
handler.disconnect(label("access_version_required")); return;
|
||||
}
|
||||
if (!ServerConfigurationNetworking.canSend(handler,ProgressionPayloads.Hello.TYPE)) {
|
||||
handler.disconnect(label("client_required")); return;
|
||||
}
|
||||
@@ -147,6 +155,12 @@ public final class ProgressionService {
|
||||
ServerConfigurationNetworking.registerGlobalReceiver(StarterPayloads.NamedCreate.TYPE,(payload,context)->context.server().execute(()->{
|
||||
HelloTask task=PENDING.get(context.packetListener());if(task!=null)task.createWithStarter(new StarterPayloads.Create(payload.bio(),payload.color(),payload.mob()),payload.name());
|
||||
}));
|
||||
ServerConfigurationNetworking.registerGlobalReceiver(AccessPayloads.Check.TYPE,(payload,context)->context.server().execute(()->{
|
||||
HelloTask task=PENDING.get(context.packetListener());if(task!=null)task.check(payload.code());
|
||||
}));
|
||||
ServerConfigurationNetworking.registerGlobalReceiver(AccessPayloads.Admit.TYPE,(payload,context)->context.server().execute(()->{
|
||||
HelloTask task=PENDING.get(context.packetListener());if(task!=null)task.admit(payload);
|
||||
}));
|
||||
ServerConfigurationNetworking.registerGlobalReceiver(ProgressionPayloads.IntroFinished.TYPE, (payload,context) ->
|
||||
context.server().execute(() -> {
|
||||
HelloTask task = PENDING.get(context.packetListener());
|
||||
@@ -194,6 +208,7 @@ public final class ProgressionService {
|
||||
private static void start(MinecraftServer server) {
|
||||
if (!SanctuaryGameplay.supported(server)) return;
|
||||
try {
|
||||
AccessConfig.load(CONFIG);
|
||||
var config=ProgressionConfig.load(CONFIG.resolve("progression.json"));
|
||||
var names=MobNamesConfig.load(CONFIG.resolve("mob-names.json"));
|
||||
Path world=server.getWorldPath(LevelResource.ROOT);
|
||||
@@ -210,6 +225,7 @@ public final class ProgressionService {
|
||||
session.cycleConfig=CycleConfig.load(CONFIG.resolve("cycle-factions.json"));
|
||||
session.tutorialConfig=TutorialConfig.load(CONFIG.resolve("tutorial.json"));
|
||||
session.cycles=new CycleStore(world.resolve("data/sanctuary-cycle-factions.json"),server.overworld().getSeed());
|
||||
if (AccessConfig.enabled()) session.access=new AccessLedger(world.resolve("data/sanctuary-access.json"),server.overworld().getSeed());
|
||||
for (var event:session.cycles.events()) {
|
||||
if (inhabitants.get(event.actor())==null || (!event.target().isEmpty() && inhabitants.get(UUID.fromString(event.target()))==null))
|
||||
throw new java.io.IOException("Cycle journal references a missing inhabitant");
|
||||
@@ -233,7 +249,10 @@ public final class ProgressionService {
|
||||
private static final Type TYPE=new Type("sanctuary:hello_world");
|
||||
private final ServerConfigurationPacketListenerImpl handler;
|
||||
private final MinecraftServer server;
|
||||
private boolean awaitingIntro,awaitingStarter;
|
||||
private boolean awaitingIntro,awaitingStarter,admitted,busy;
|
||||
private String lastChecked="",queuedCheck;
|
||||
private AccessPayloads.Admit queuedAdmit;
|
||||
private long lastCheckAt;
|
||||
HelloTask(ServerConfigurationPacketListenerImpl handler,MinecraftServer server) { this.handler=handler;this.server=server; }
|
||||
public Type type() { return TYPE; }
|
||||
public void start(Consumer<Packet<?>> sender) {
|
||||
@@ -255,6 +274,8 @@ public final class ProgressionService {
|
||||
void offer(String error) {
|
||||
var profile=((ProgressionConfigurationAccess)handler).sanctuary$profile();
|
||||
var session=session(server);var inhabitant=session.inhabitants.get(profile.id());var starters=session.starters.get(profile.id());
|
||||
if (session.access != null && ServerConfigurationNetworking.canSend(handler,AccessPayloads.Gate.TYPE))
|
||||
ServerConfigurationNetworking.send(handler,new AccessPayloads.Gate(true,session.access.known(profile.id())!=null));
|
||||
if(starters!=null){
|
||||
var identity=new ProgressionPayloads.Hello(profile.name(),inhabitant==null?session.inhabitants.available():java.util.List.of(inhabitant.color()),error);
|
||||
ServerConfigurationNetworking.send(handler,new StarterPayloads.Welcome(identity,inhabitant==null?"":inhabitant.bio(),inhabitant!=null,starters.choices()));
|
||||
@@ -263,6 +284,7 @@ public final class ProgressionService {
|
||||
void create(ProgressionPayloads.Create payload) {
|
||||
if (awaitingIntro || awaitingStarter) return;
|
||||
if (!handler.isAcceptingMessages()) { PENDING.remove(handler); return; }
|
||||
if (AccessConfig.enabled() && !admitted) { offer("code_required"); return; }
|
||||
Session session=session(server);
|
||||
if (session == null || !session.healthy) { handler.disconnect(label("storage_invalid")); return; }
|
||||
try {
|
||||
@@ -288,6 +310,7 @@ public final class ProgressionService {
|
||||
}
|
||||
void createWithStarter(StarterPayloads.Create payload,String name) {
|
||||
if(awaitingIntro||!handler.isAcceptingMessages())return;
|
||||
if (AccessConfig.enabled() && !admitted) { offer("code_required"); return; }
|
||||
var session=session(server);if(session==null||!session.healthy){handler.disconnect(label("storage_invalid"));return;}
|
||||
try {
|
||||
var profile=((ProgressionConfigurationAccess)handler).sanctuary$profile();var uuid=profile.id();
|
||||
@@ -303,6 +326,94 @@ public final class ProgressionService {
|
||||
awaitingStarter=false;beginIntro(uuid);
|
||||
}catch(Exception error){fail(error);}
|
||||
}
|
||||
void check(String code) {
|
||||
if (awaitingIntro || !handler.isAcceptingMessages()) return;
|
||||
var session=session(server);
|
||||
if (session == null || session.access == null) return;
|
||||
var profile=((ProgressionConfigurationAccess)handler).sanctuary$profile();
|
||||
String normalized=AccessCodes.normalize(code);
|
||||
if (session.access.known(profile.id()) != null) { status(normalized,"code_recognized"); return; }
|
||||
if (!AccessCodes.validFormat(normalized)) { status(normalized,"code_malformed"); return; }
|
||||
if (!AccessCodes.validUsername(profile.name())) { status(normalized,"code_username"); return; }
|
||||
if (busy) { queuedCheck=normalized; queuedAdmit=null; return; }
|
||||
if (normalized.equals(lastChecked) && System.nanoTime()-lastCheckAt < 1_000_000_000L) return;
|
||||
verify(profile.name(),normalized);
|
||||
}
|
||||
void verify(String username,String code) {
|
||||
busy=true; lastChecked=code; lastCheckAt=System.nanoTime(); queuedCheck=null;
|
||||
var directory=AccessConfig.directory();
|
||||
java.util.concurrent.CompletableFuture.supplyAsync(() -> directory.verify(username,code)).whenComplete((result,error) -> server.execute(() -> {
|
||||
busy=false;
|
||||
if (PENDING.get(handler) != this || !handler.isAcceptingMessages()) return;
|
||||
status(code, error != null || result == null ? "code_unreachable" : AccessCodes.verifyKey(result,username));
|
||||
if (queuedAdmit != null) { var next=queuedAdmit; queuedAdmit=null; admit(next); return; }
|
||||
if (queuedCheck != null && !queuedCheck.equals(code) && AccessCodes.validFormat(queuedCheck)) {
|
||||
String next=queuedCheck; queuedCheck=null; verify(username,next);
|
||||
}
|
||||
}));
|
||||
}
|
||||
void status(String code,String reason) {
|
||||
if (handler.isAcceptingMessages() && ServerConfigurationNetworking.canSend(handler,AccessPayloads.Status.TYPE))
|
||||
ServerConfigurationNetworking.send(handler,new AccessPayloads.Status(code,reason));
|
||||
}
|
||||
void admit(AccessPayloads.Admit payload) {
|
||||
if (awaitingIntro || !handler.isAcceptingMessages()) return;
|
||||
String normalized=AccessCodes.normalize(payload.code());
|
||||
if (busy) { queuedAdmit=payload; return; }
|
||||
var session=session(server);
|
||||
if (session == null || !session.healthy) { handler.disconnect(label("storage_invalid")); return; }
|
||||
if (session.access == null) { offer("code_unconfigured"); return; }
|
||||
var profile=((ProgressionConfigurationAccess)handler).sanctuary$profile();
|
||||
var uuid=profile.id();
|
||||
if (!ready(session,uuid,payload)) return;
|
||||
if (session.access.known(uuid) != null) { finishAdmit(session,uuid,payload); drain(); return; }
|
||||
if (!AccessCodes.validFormat(normalized)) { offer("code_malformed"); return; }
|
||||
if (!AccessCodes.validUsername(profile.name())) { offer("code_username"); return; }
|
||||
busy=true;
|
||||
String username=profile.name();
|
||||
var directory=AccessConfig.directory();
|
||||
java.util.concurrent.CompletableFuture.supplyAsync(() -> directory.redeem(username,normalized)).whenComplete((result,error) -> server.execute(() -> {
|
||||
busy=false;
|
||||
if (PENDING.get(handler) != this || !handler.isAcceptingMessages()) return;
|
||||
if (error != null || result == null) { offer("code_unreachable"); drain(); return; }
|
||||
var decision=AccessCodes.redeem(result,username);
|
||||
if (!decision.accepted()) { offer(decision.reason()); drain(); return; }
|
||||
try {
|
||||
session.access.remember(uuid,decision.minecraftUsername(),decision.discordId());
|
||||
finishAdmit(session,uuid,payload);
|
||||
} catch (Exception failure) { fail(failure); }
|
||||
drain();
|
||||
}));
|
||||
}
|
||||
boolean ready(Session session,UUID uuid,AccessPayloads.Admit payload) {
|
||||
var starters=session.starters.get(uuid);
|
||||
if (starters != null) {
|
||||
if (starters.choices().stream().noneMatch(choice -> choice.mob().equals(payload.mob()))) { offer("starter_required"); return false; }
|
||||
if (!StarterCompanions.validName(payload.name())) { offer("starter_name_invalid"); return false; }
|
||||
} else if (!payload.mob().isEmpty()) { offer("starter_required"); return false; }
|
||||
if (session.inhabitants.get(uuid) == null) {
|
||||
if (!InhabitantStore.validBio(payload.bio())) { offer("bio_invalid"); return false; }
|
||||
if (!session.inhabitants.available().contains(payload.color())) { offer("color_taken"); return false; }
|
||||
}
|
||||
return true;
|
||||
}
|
||||
void drain() {
|
||||
if (!handler.isAcceptingMessages() || PENDING.get(handler) != this) return;
|
||||
if (queuedAdmit != null) { var next=queuedAdmit; queuedAdmit=null; admit(next); }
|
||||
else if (queuedCheck != null && AccessCodes.validFormat(queuedCheck)) {
|
||||
String next=queuedCheck; queuedCheck=null;
|
||||
verify(((ProgressionConfigurationAccess)handler).sanctuary$profile().name(),next);
|
||||
}
|
||||
}
|
||||
void finishAdmit(Session session,UUID uuid,AccessPayloads.Admit payload) {
|
||||
admitted=true;
|
||||
try {
|
||||
if (payload.mob().isEmpty()) create(new ProgressionPayloads.Create(payload.bio(),payload.color()));
|
||||
else createWithStarter(new StarterPayloads.Create(payload.bio(),payload.color(),payload.mob()),payload.name());
|
||||
} finally { admitted=false; }
|
||||
try { if (session.inhabitants.get(uuid) != null) session.access.commit(uuid); }
|
||||
catch (Exception error) { fail(error); }
|
||||
}
|
||||
void fail(Exception error){
|
||||
session(server).healthy=false;LOG.error("Could not persist Hello World starter",error);handler.disconnect(label("storage_invalid"));
|
||||
}
|
||||
@@ -325,6 +436,7 @@ public final class ProgressionService {
|
||||
if (!restore(player)) return;
|
||||
sendNames(player,session);
|
||||
try {
|
||||
if (session.access != null) session.access.commit(player.getUUID());
|
||||
StarterCompanions.deliver(player,session.starters);
|
||||
if (session.inhabitants.arrive(player.getUUID())) player.level().getServer().getPlayerList()
|
||||
.broadcastSystemMessage(Component.literal("helloworld ").append(player.getDisplayName()),false);
|
||||
|
||||
@@ -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",
|
||||
@@ -266,6 +267,21 @@
|
||||
"sanctuary.progression.choose_color": "Choose your colour",
|
||||
"sanctuary.progression.cancel": "Cancel",
|
||||
"sanctuary.progression.enter": "Enter Sanctuary",
|
||||
"sanctuary.progression.code": "Access code",
|
||||
"sanctuary.progression.code_hint": "SANC-XXXX-XXXX",
|
||||
"sanctuary.progression.code_recognized": "Code recognised.",
|
||||
"sanctuary.progression.code_checking": "Checking the code…",
|
||||
"sanctuary.progression.code_malformed": "The code should look like SANC-XXXX-XXXX.",
|
||||
"sanctuary.progression.code_invalid": "This code does not match this name.",
|
||||
"sanctuary.progression.code_used": "This code has already been used.",
|
||||
"sanctuary.progression.code_replay": "This code is already used and Sanctuary cannot recover the account. Try again later.",
|
||||
"sanctuary.progression.code_unreachable": "Sanctuary is unreachable, try again.",
|
||||
"sanctuary.progression.code_unauthorized": "The server cannot reach Sanctuary.",
|
||||
"sanctuary.progression.code_unconfigured": "The server has no Sanctuary token.",
|
||||
"sanctuary.progression.code_required": "Enter the code sent in a private message.",
|
||||
"sanctuary.progression.code_username": "This name cannot receive a code.",
|
||||
"sanctuary.progression.code_mismatch": "The code does not match this session.",
|
||||
"sanctuary.progression.access_version_required": "This server requires the Sanctuary access code. Update the client.",
|
||||
"sanctuary.progression.joining": "Character saved, joining…",
|
||||
"sanctuary.progression.bio_invalid": "Use plain text for your bio (256 characters).",
|
||||
"sanctuary.progression.color_taken": "Colour just taken. Choose another one.",
|
||||
@@ -2295,7 +2311,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",
|
||||
@@ -266,6 +267,21 @@
|
||||
"sanctuary.progression.choose_color": "Choisis ta couleur",
|
||||
"sanctuary.progression.cancel": "Annuler",
|
||||
"sanctuary.progression.enter": "Entrer dans Sanctuary",
|
||||
"sanctuary.progression.code": "Code d’accès",
|
||||
"sanctuary.progression.code_hint": "SANC-XXXX-XXXX",
|
||||
"sanctuary.progression.code_recognized": "Code reconnu.",
|
||||
"sanctuary.progression.code_checking": "Vérification du code…",
|
||||
"sanctuary.progression.code_malformed": "Le code doit ressembler à SANC-XXXX-XXXX.",
|
||||
"sanctuary.progression.code_invalid": "Ce code ne correspond pas à ce pseudo.",
|
||||
"sanctuary.progression.code_used": "Ce code a déjà été utilisé.",
|
||||
"sanctuary.progression.code_replay": "Ce code est déjà consommé et Sanctuary ne peut pas retrouver le compte. Réessaie plus tard.",
|
||||
"sanctuary.progression.code_unreachable": "Sanctuary est injoignable, réessaie.",
|
||||
"sanctuary.progression.code_unauthorized": "Le serveur ne peut pas joindre Sanctuary.",
|
||||
"sanctuary.progression.code_unconfigured": "Le serveur n’a pas de jeton Sanctuary.",
|
||||
"sanctuary.progression.code_required": "Entre le code reçu en message privé.",
|
||||
"sanctuary.progression.code_username": "Ce pseudo ne peut pas recevoir de code.",
|
||||
"sanctuary.progression.code_mismatch": "Le code ne correspond pas à cette session.",
|
||||
"sanctuary.progression.access_version_required": "Ce serveur exige le code d’accès Sanctuary. Mets le client à jour.",
|
||||
"sanctuary.progression.joining": "Création enregistrée, arrivée en cours…",
|
||||
"sanctuary.progression.bio_invalid": "La bio doit contenir du texte simple (256 caractères).",
|
||||
"sanctuary.progression.color_taken": "Couleur déjà prise. Choisis-en une autre.",
|
||||
@@ -2295,7 +2311,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
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
package fr.koka.sanctuary.progression;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.util.UUID;
|
||||
|
||||
public final class AccessCodeSmoke {
|
||||
public static void main(String[] args) throws Exception {
|
||||
require(AccessCodes.normalize(" sanc-ab3d-k7pq ").equals("SANC-AB3D-K7PQ"), "Codes are trimmed and uppercased");
|
||||
require(AccessCodes.validFormat("SANC-AB3D-K7PQ"), "Documented code shape is accepted");
|
||||
require(!AccessCodes.validFormat("SANC-AB3D-K7P") && !AccessCodes.validFormat("SANC-AB3D-K7PQX"), "Partial codes are refused");
|
||||
require(AccessCodes.validUsername("Steve") && !AccessCodes.validUsername("A") && !AccessCodes.validUsername("bad name"), "Session names stay Minecraft names");
|
||||
var unused = AccessCodes.parse(200, "{\"valid\":true,\"reason\":\"unused\",\"minecraft_username\":\"Steve\",\"discord_id\":\"123456789012345678\"}");
|
||||
require(AccessCodes.verifyKey(unused, "steve").equals("code_recognized"), "Verify matches the session name without consuming the code");
|
||||
require(AccessCodes.verifyKey(AccessCodes.parse(200, "{\"valid\":false,\"reason\":\"invalid\"}"), "Steve").equals("code_invalid"), "Unknown code stays on the screen");
|
||||
require(AccessCodes.verifyKey(AccessCodes.parse(200, "{\"valid\":false,\"reason\":\"already_used\"}"), "Steve").equals("code_used"), "Used code is not a fresh verification");
|
||||
require(AccessCodes.parse(401, "").reason().equals("unauthorized"), "A bad token is not a character");
|
||||
require(AccessCodes.parse(503, "nope").reason().equals("unreachable"), "An outage does not invent a character");
|
||||
var redeemed = AccessCodes.parse(200, "{\"valid\":true,\"reason\":\"redeemed\",\"minecraft_username\":\"Steve\",\"discord_id\":\"42\"}");
|
||||
var decision = AccessCodes.redeem(redeemed, "Steve");
|
||||
require(decision.accepted() && decision.discordId().equals("42"), "Redeem is the commitment");
|
||||
require(!AccessCodes.redeem(redeemed, "Alex").accepted(), "Another username cannot use the code");
|
||||
var replay = AccessCodes.parse(200, "{\"valid\":false,\"reason\":\"already_used\",\"minecraft_username\":\"Steve\",\"discord_id\":\"42\"}");
|
||||
require(AccessCodes.redeem(replay, "Steve").accepted(), "A consumed code can finish an interrupted creation when the account comes back");
|
||||
var stuck = AccessCodes.parse(200, "{\"valid\":false,\"reason\":\"already_used\"}");
|
||||
require(AccessCodes.redeem(stuck, "Steve").reason().equals("code_replay"), "A consumed code without an account does not create anyone");
|
||||
|
||||
var path = Files.createTempDirectory("sanctuary-access-").resolve("access.json");
|
||||
UUID id = UUID.fromString("01234567-89ab-cdef-fedc-ba9876543210");
|
||||
var ledger = new AccessLedger(path, 42L);
|
||||
ledger.remember(id, "Steve", "42");
|
||||
var reopened = new AccessLedger(path, 42L);
|
||||
require(reopened.known(id).discordId().equals("42"), "A crash after redeem keeps the Discord account");
|
||||
reopened.commit(id);
|
||||
var committed = new AccessLedger(path, 42L);
|
||||
require(committed.known(id).name().equals("Steve"), "The binding survives the character write");
|
||||
committed.commit(id);
|
||||
committed.remember(id, "Steve", "42");
|
||||
String saved = Files.readString(path);
|
||||
require(saved.indexOf(id.toString()) == saved.lastIndexOf(id.toString()), "An existing character is not returned to pending");
|
||||
byte[] broken = "{".getBytes();
|
||||
Files.write(path, broken);
|
||||
boolean refused = false;
|
||||
try { new AccessLedger(path, 42L); } catch (IllegalArgumentException error) { refused = true; }
|
||||
require(refused && Files.readAllBytes(path).length == 1, "A corrupt ledger is preserved and refused");
|
||||
System.out.println("ACCESS-01: code contract, replay and ledger passed");
|
||||
}
|
||||
private static void require(boolean value, String message) { if (!value) throw new AssertionError(message); }
|
||||
}
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
name = "Sanctuary"
|
||||
author = "KOKA99CAB"
|
||||
version = "beta.141"
|
||||
version = "beta.144"
|
||||
pack-format = "packwiz:1.1.0"
|
||||
|
||||
[index]
|
||||
|
||||
Reference in New Issue
Block a user