Compare commits
8
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8de569bbf1 | ||
|
|
580b2414da | ||
|
|
97ff2b54cb | ||
|
|
59346d7f0a | ||
|
|
ea151dd5df | ||
|
|
7ce01c47f2 | ||
|
|
93ed5574a4 | ||
|
|
c9f5c56e68 |
@@ -1,5 +1,12 @@
|
|||||||
# Changelog
|
# 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
|
## 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.
|
- 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,12 +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
|
L’économie, les claims et les dimensions décrits dans la vision restent
|
||||||
à implémenter.
|
à implémenter.
|
||||||
|
|
||||||
La version **beta.140** relie les lumières colorées aux objets en main et sur
|
La version **beta.143** garde le sampler natif de précipitation pour la neige
|
||||||
la tête, renforce leur teinte et ajoute une distance réglable de 8 à 64 blocs
|
saisonnière, mais ralentit son rythme : l'accumulation est espacée par un
|
||||||
avec apparition progressive. Activation par défaut à 30 % ; torches natives,
|
facteur 12 et la fonte par le facteur temps réel 72. Les protections existantes
|
||||||
option « Torches chaudes » désactivée, cuivre vert et redstone rouge saturé.
|
restent inchangées : neige construite, biomes froids, hiver, profils neigeux
|
||||||
Les frontières de saturation des lumières bleues s’atténuent progressivement.
|
et météo Vanilla. Le [ticket](docs/realtime-snow-melt-beta143.md) documente le
|
||||||
Le [ticket](docs/colored-dynamic-beta140.md) décrit le rendu et les vérifications.
|
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à
|
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**.
|
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.
|
Les [synchronisations antérieures](docs/git-sync-beta092.md) restent documentées.
|
||||||
@@ -36,6 +38,33 @@ La [carte native beta.005](docs/atlas-beta005.md) prend le relais après
|
|||||||
l’étude de Xaero et des alternatives libres.
|
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
|
||||||
|
réduite pour les sources opposées, objets portés et fondus. Activer PBR et
|
||||||
|
Lumières colorées ; portée PBR 32 blocs.
|
||||||
|
[Contrat et vérifications](docs/directional-lights-beta141.md) ·
|
||||||
|
[Pack normal](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.141/Sanctuary-beta.141.mrpack) ·
|
||||||
|
[Pack de test](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.141/Sanctuary-Test-beta.141.mrpack).
|
||||||
|
|
||||||
## beta.140 — Lumières portées et fondus
|
## beta.140 — Lumières portées et fondus
|
||||||
|
|
||||||
Mains et tête, distances 8/16/32/64, fondus, cuivre vert, redstone rouge et
|
Mains et tête, distances 8/16/32/64, fondus, cuivre vert, redstone rouge et
|
||||||
|
|||||||
@@ -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.
|
||||||
@@ -99,3 +99,14 @@ SHA-256 du JAR : `853284c1a61c7fce12edd2f7763717321eef56977290eb0e7c5db5e30bff44
|
|||||||
|
|
||||||
- Sanctuary-beta.140.mrpack : `1b366746e4d6bebfbe0115c849524bdeb81c14eee89a2510f9dbe18808974d6a`.
|
- Sanctuary-beta.140.mrpack : `1b366746e4d6bebfbe0115c849524bdeb81c14eee89a2510f9dbe18808974d6a`.
|
||||||
- Sanctuary-Test-beta.140.mrpack : `50c3cdf759117bb5286d41b4535fde15d9d84deaac833b7f8f61c4a0d29b72e7`.
|
- Sanctuary-Test-beta.140.mrpack : `50c3cdf759117bb5286d41b4535fde15d9d84deaac833b7f8f61c4a0d29b72e7`.
|
||||||
|
|
||||||
|
## Livraison
|
||||||
|
|
||||||
|
[Release beta.140](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.140)
|
||||||
|
publiée sur le commit source `6e1b3b71088437fe57cf2b9b979b6d62711628d3` ; canal stable
|
||||||
|
`2c360a0759d4a164f82c89ac2bef04ff69d50875` 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.140/`. Aucun monde personnel
|
||||||
|
ouvert. Reçus : `build/shader140-isolated.json` et `build/shader140-prism.json`.
|
||||||
|
Archives normale/Test/template et comparaison copiées dans `sanctuary-beta/build/`.
|
||||||
|
|||||||
@@ -0,0 +1,127 @@
|
|||||||
|
# LIGHT-141 — Direction locale pour les normales PBR
|
||||||
|
|
||||||
|
Socle beta.140 `c9f5c56`, branche `codex/directional-lights-beta141`.
|
||||||
|
Minecraft 26.3 / Java 25. Livraison beta.141.
|
||||||
|
|
||||||
|
## Contrat
|
||||||
|
|
||||||
|
Estimer une direction d'arrivée depuis les six voisins du champ lumineux,
|
||||||
|
avec intensité et confiance. Conserver cette estimation dans une texture
|
||||||
|
compacte reconstruite avec le champ, uniquement quand le PBR est actif.
|
||||||
|
Les sources portées utilisent leurs positions déjà disponibles. Brancher ces
|
||||||
|
directions sur les normales et reflets PBR existants, sans nouveau bouton.
|
||||||
|
Les sources opposées doivent réduire la directionnalité, sans bascule brutale.
|
||||||
|
Les champs précédent/courant gardent leur fondu et les obstacles restent pris
|
||||||
|
en compte pour les blocs posés. Aucun changement de lumière de gameplay,
|
||||||
|
de sauvegarde ou de génération ; aucune promesse de lancer de rayons exact.
|
||||||
|
|
||||||
|
## Programme de vérification
|
||||||
|
|
||||||
|
Contrôles natifs OpenGL/Vulkan : direction, sources opposées, murs, rendu de
|
||||||
|
relief en intérieur, déplacement des sources, transitions et PBR OFF.
|
||||||
|
Régression des lumières beta.139/140 puis check/build et assemblage. GameTest
|
||||||
|
serveur dédié exclu selon le refus EULA antérieur. Aucun monde personnel ouvert.
|
||||||
|
|
||||||
|
## Réalisation
|
||||||
|
|
||||||
|
Le champ RGB conserve une texture directionnelle RGBA8 supplémentaire :
|
||||||
|
les trois composantes codent la direction pondérée, l'alpha sa réponse lumineuse.
|
||||||
|
Les voisins plus lumineux et accessibles contribuent à l'estimation ; les
|
||||||
|
arrivées opposées s'annulent dans le vecteur, sans supprimer l'éclairage natif.
|
||||||
|
Le shader interpole les moments pondérés avant normalisation, y compris entre
|
||||||
|
les champs précédent et courant. La confiance réduit le relief quand aucune
|
||||||
|
direction ne domine.
|
||||||
|
|
||||||
|
Cette texture est préparée dans le budget de reconstruction existant, avec
|
||||||
|
un maximum de six voisins par cellule éclairée et une table d'énergie réutilisée.
|
||||||
|
Aucune recherche de sources ni rayons par pixel pour les blocs posés. La
|
||||||
|
lecture GPU interpole huit cellules par champ ; les deux champs sont utilisés
|
||||||
|
pendant le fondu. Les sources mobiles réutilisent les positions, intensités et
|
||||||
|
transitions déjà collectées, dans la limite existante de 128 sources.
|
||||||
|
|
||||||
|
Une texture directionnelle occupe environ 1,5 Mo à 16 blocs de distance et
|
||||||
|
19 Mo à 64 blocs, par copie CPU/GPU ; deux champs coexistent lors des transitions.
|
||||||
|
Le champ directionnel n'est construit que si PBR est actif avec une intensité
|
||||||
|
non nulle. Sa désactivation libère les directions après la transition.
|
||||||
|
Le coût est borné par ces réglages, sans garantie de FPS.
|
||||||
|
|
||||||
|
La passe PBR applique la différence entre normale de base et normale perturbée,
|
||||||
|
puis un reflet GGX local. Elle garde sa portée de 32 blocs et les réglages PBR
|
||||||
|
existants ; Lumières colorées doit être actif. Le soleil et la lune continuent
|
||||||
|
à fonctionner lorsque les lumières colorées sont coupées. Les aides FR/EN
|
||||||
|
expliquent cette dépendance.
|
||||||
|
|
||||||
|
C'est une direction dominante approximative : deux lumières colorées opposées
|
||||||
|
ne produisent pas deux reflets indépendants. Les sources mobiles gardent la
|
||||||
|
propagation radiale et les limites d'occultation existantes. Les normales PBR
|
||||||
|
concernent les surfaces de blocs prises en charge, pas tous les modèles de mobs.
|
||||||
|
Les exclusions existantes des panoramas et de l'immersion restent inchangées.
|
||||||
|
|
||||||
|
## Premier essai ciblé
|
||||||
|
|
||||||
|
L'essai natif OpenGL ciblé réussit en **1 min 12 s** : direction gauche/droite,
|
||||||
|
annulation des sources opposées avec énergie conservée, occultation par un mur,
|
||||||
|
relief sur la pierre en intérieur et avec une torche en main, stabilité au repos,
|
||||||
|
libération des directions avec PBR OFF et maintien du chemin solaire avec
|
||||||
|
Lumières colorées OFF. La somme d'écarts PBR OFF/ON sur la pierre vaut 878 114
|
||||||
|
au réglage PBR 100 % ; c'est une comparaison de pixels, pas une mesure de FPS.
|
||||||
|
Log : `build/directional141-focused.log`.
|
||||||
|
|
||||||
|
Les essais de préparation ont révélé un conflit de nom de matrice GLSL, corrigé,
|
||||||
|
puis des erreurs de fixture (angle 180° normalisé par Minecraft, suppression
|
||||||
|
d'entités alors que la sélection était vide, commande d'heure déjà à minuit),
|
||||||
|
corrigées sans changer le rendu.
|
||||||
|
|
||||||
|
## Validation complète OpenGL
|
||||||
|
|
||||||
|
La chaîne complète réussit en **3 min 1 s** sur OpenGL / Apple M1 :
|
||||||
|
`COLORED139_PASS`, `COLORED140_PASS` et `DIRECTIONAL141_PASS`.
|
||||||
|
Elle réunit les contrôles de couleurs, objets portés, caméra, occultation,
|
||||||
|
rechargement des ressources, préférences, distances/fondus et directions PBR.
|
||||||
|
Le test ciblé est reproductible avec `-PsanctuaryDirectional141Only=true` ;
|
||||||
|
la chaîne complète utilise `-PsanctuaryDirectional141ClientTests=true` avec
|
||||||
|
les deux suites de lumières existantes.
|
||||||
|
Log : `build/directional141-opengl.log`. Captures natives conservées dans
|
||||||
|
`build/directional141-opengl-screenshots/`. Comparaison autonome sans retraitement :
|
||||||
|
`build/Shader-beta.141-comparaison.html`.
|
||||||
|
|
||||||
|
## Validation complète Vulkan
|
||||||
|
|
||||||
|
La même chaîne réussit en **2 min 56 s** sur Vulkan / Apple M1, MoltenVK 1.4.2,
|
||||||
|
profondeur 0–1 et transparence améliorée. Les trois marqueurs de réussite sont
|
||||||
|
présents dans `build/directional141-vulkan.log`. L'écart PBR local OFF/ON vaut
|
||||||
|
878 107, contre 878 114 sous OpenGL dans cette scène. Les captures natives sont
|
||||||
|
conservées dans `build/directional141-vulkan-screenshots/`.
|
||||||
|
Les deux moteurs vérifient la compilation, les chemins actifs et désactivés,
|
||||||
|
le cache, les murs, les sources opposées et portées, ainsi que les régressions
|
||||||
|
beta.139/140. Pas de validation des pilotes Windows de l'utilisateur revendiquée.
|
||||||
|
|
||||||
|
## Construction et archives
|
||||||
|
|
||||||
|
`./gradlew check build assemblePack assembleTestPack -x :sanctuary:runGameTest`
|
||||||
|
réussit en **2 min 35 s**, 126 tâches. Le GameTest serveur dédié reste exclu
|
||||||
|
selon le refus EULA antérieur. Les suites clientes ci-dessus ont été exécutées
|
||||||
|
séparément. Log : `build/shader141-build.log`.
|
||||||
|
|
||||||
|
Comparaison à beta.140 : **8 entrées de production modifiées**, limitées au
|
||||||
|
champ lumineux, à sa préparation avant le PBR, au shader PBR et aux aides FR/EN.
|
||||||
|
Les archives normale/Test contiennent le même JAR Sanctuary, sans classes
|
||||||
|
de test. Version, dépendances exactes, sources des JAR, textures de gemmes et
|
||||||
|
de la clé de Steve, et template complet sont vérifiés.
|
||||||
|
Reçu : `build/shader141-artifact.json`.
|
||||||
|
|
||||||
|
SHA-256 du JAR : `660f29c3fa77fee4de6a8db5673c61619985804494f70e961998da36f396d0f1`.
|
||||||
|
|
||||||
|
- 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,55 @@ 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
|
dépendance téléchargée n'entre dans l'historique Git. Le canal Beta est distinct
|
||||||
de l'ancien pack 26.2.
|
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
|
||||||
|
préservant la teinte. Torches natives par défaut, option chaude, cuivre vert
|
||||||
|
et redstone rouge. [Contrat et vérifications](colored-dynamic-beta140.md).
|
||||||
|
[Release beta.140](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.140)
|
||||||
|
publiée ; canal `2c360a0759d4a164f82c89ac2bef04ff69d50875`. 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.140/`. Archives vérifiées dans `sanctuary-beta/build/`.
|
||||||
|
|
||||||
## beta.138 — Densité des rayons selon la météo
|
## beta.138 — Densité des rayons selon la météo
|
||||||
|
|
||||||
Brouillard plus dense et diffus, précipitations et atténuation de l'orage.
|
Brouillard plus dense et diffus, précipitations et atténuation de l'orage.
|
||||||
|
|||||||
@@ -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
|
fabric_api_version=0.160.5+26.3
|
||||||
|
|
||||||
# Release counter: beta.001, .002, .003, ... (see docs/versioning.md).
|
# Release counter: beta.001, .002, .003, ... (see docs/versioning.md).
|
||||||
mod_version=beta.140
|
mod_version=beta.144
|
||||||
pack_version=beta.140
|
pack_version=beta.144
|
||||||
resource_pack_version=beta.121
|
resource_pack_version=beta.121
|
||||||
maven_group=fr.koka.sanctuary
|
maven_group=fr.koka.sanctuary
|
||||||
jei_version=30.32.0-sanctuary.3
|
jei_version=30.32.0-sanctuary.3
|
||||||
|
|||||||
@@ -34,6 +34,9 @@ if (clientRenderTests) {
|
|||||||
if (improvedTransparency.isPresent()) {
|
if (improvedTransparency.isPresent()) {
|
||||||
systemProperty('sanctuary.test.improvedTransparency', improvedTransparency.get())
|
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'))
|
systemProperty('sanctuary.test.colored140', providers.gradleProperty('sanctuaryColored140ClientTests').getOrElse('false'))
|
||||||
systemProperty('sanctuary.test.noVsync', providers.gradleProperty('sanctuaryClientNoVsync').getOrElse('false'))
|
systemProperty('sanctuary.test.noVsync', providers.gradleProperty('sanctuaryClientNoVsync').getOrElse('false'))
|
||||||
systemProperty('sanctuary.test.quick', providers.gradleProperty('sanctuaryQuickTests').getOrElse('false'))
|
systemProperty('sanctuary.test.quick', providers.gradleProperty('sanctuaryQuickTests').getOrElse('false'))
|
||||||
@@ -1134,6 +1137,14 @@ tasks.register('intro031Smoke', JavaExec) {
|
|||||||
}
|
}
|
||||||
tasks.named('check') { dependsOn('intro031Smoke') }
|
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)) {
|
if (providers.gradleProperty('sanctuaryQuickTests').map { it.toBoolean() }.getOrElse(false)) {
|
||||||
dependencies { gametestRuntimeOnly project(':sanctuary-test') }
|
dependencies { gametestRuntimeOnly project(':sanctuary-test') }
|
||||||
}
|
}
|
||||||
|
|||||||
+19
-11
@@ -12,6 +12,7 @@ import java.time.Instant;
|
|||||||
import java.time.ZoneOffset;
|
import java.time.ZoneOffset;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
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.event.lifecycle.v1.ClientTickEvents;
|
||||||
import net.fabricmc.fabric.api.client.gametest.v1.FabricClientGameTest;
|
import net.fabricmc.fabric.api.client.gametest.v1.FabricClientGameTest;
|
||||||
import net.fabricmc.fabric.api.client.gametest.v1.context.ClientGameTestContext;
|
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());
|
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);
|
clock(l,"2026-07-10T12:00:00Z",SeasonalWeather.Season.SUMMER);
|
||||||
check(l.getGameRules().get(SeasonalSnowMelt.ENABLED),"Melt enabled by default");
|
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");
|
deposit(l,AT,16);check(l.getBlockState(AT.above()).is(Blocks.SNOW_BLOCK),"Default two-block accumulation retained");
|
||||||
clear(l);step(l,AT,1);
|
clear(l);
|
||||||
check(layers(l,AT.above())==7&&layers(l,AT)==8,"Summer melts only one top layer");
|
until(l,AT,5000,()->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");
|
until(l,AT,20000,()->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");
|
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 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 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 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);step(l,atop,16);check(layers(l,atop)==8&&l.getBlockState(atop.above()).isAir(),"Weather on a snow construction melts; construction stays");
|
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 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 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 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");
|
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-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");
|
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);step(l,roof,128);check(l.getBlockState(roof).isAir(),"Autumn 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);
|
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");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");
|
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);
|
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(!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");
|
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(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");
|
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");
|
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);}
|
}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 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 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 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 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 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 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 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 KeyEvent enter(){return new KeyEvent(InputConstants.KEY_RETURN,0,0);}
|
||||||
private static void check(boolean ok,String message){if(!ok)throw new AssertionError(message);}
|
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 fr.koka.sanctuary.weather.SnowAccumulation;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
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.event.lifecycle.v1.ClientTickEvents;
|
||||||
import net.fabricmc.fabric.api.client.gametest.v1.FabricClientGameTest;
|
import net.fabricmc.fabric.api.client.gametest.v1.FabricClientGameTest;
|
||||||
import net.fabricmc.fabric.api.client.gametest.v1.context.ClientGameTestContext;
|
import net.fabricmc.fabric.api.client.gametest.v1.context.ClientGameTestContext;
|
||||||
@@ -45,42 +46,45 @@ public final class Snow108ClientChecks implements FabricClientGameTest {
|
|||||||
if(enabled) {
|
if(enabled) {
|
||||||
command(l,"sanctuary weather snow");l.setRainLevel(1);
|
command(l,"sanctuary weather snow");l.setRainLevel(1);
|
||||||
check(l.getGameRules().get(SnowAccumulation.MAX_HEIGHT)==2,"Default two blocks");
|
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);
|
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(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();
|
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");
|
until(l,AT,1000,()->layers(l,AT.above())==1,"New layer begins above solid snow");
|
||||||
for(int i=0;i<30;i++)l.tickPrecipitation(AT);
|
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");
|
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");
|
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");
|
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");
|
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");
|
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");
|
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);l.tickPrecipitation(fresh);check(l.getBlockState(fresh).isAir(),"Native zero remains authoritative");l.getGameRules().set(GameRules.MAX_SNOW_ACCUMULATION_HEIGHT,1,server);
|
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);l.tickPrecipitation(fresh);check(l.getBlockState(fresh).isAir(),"No accumulation between showers");
|
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);l.tickPrecipitation(fresh);check(layers(l,fresh)==1,"Snow showers accumulate while falling");
|
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());l.tickPrecipitation(flower);check(l.getBlockState(flower).is(Blocks.DANDELION),"Plant preserved");
|
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());l.tickPrecipitation(water);check(l.getBlockState(water).is(Blocks.WATER)&&l.getBlockState(water.above()).isAir(),"Open water not filled with snow");
|
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());l.tickPrecipitation(roof);check(layers(l,roof.above())==1&&l.getBlockState(roof.below()).isAir(),"Snow on roof, not indoors");
|
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");
|
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 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);
|
var light=new BlockPos(11,80,11);l.setBlockAndUpdate(light.below(),Blocks.GLOWSTONE.defaultBlockState());command(l,"sanctuary weather snow");l.setRainLevel(1);
|
||||||
} else {
|
} 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");
|
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");
|
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);}
|
}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 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 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 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 KeyEvent enter(){return new KeyEvent(InputConstants.KEY_RETURN,0,0);}
|
||||||
private static void check(boolean ok,String message){if(!ok)throw new AssertionError(message);}
|
private static void check(boolean ok,String message){if(!ok)throw new AssertionError(message);}
|
||||||
|
|||||||
+51
@@ -0,0 +1,51 @@
|
|||||||
|
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;
|
||||||
|
|
||||||
|
/** Real enclosed stone surfaces: local normal response without sunlight. */
|
||||||
|
final class DirectionalLights141ClientChecks {
|
||||||
|
private static final BlockPos LEFT=new BlockPos(96,101,102),RIGHT=new BlockPos(104,101,102),PROBE=new BlockPos(100,101,102);
|
||||||
|
static void run(ClientGameTestContext c,TestServerContext server){
|
||||||
|
c.runOnClient(m->{VanillaLight.setEnabled(true);VanillaLight.setSaturation(100);VanillaLight.setSsgi(false);VanillaLight.setBloom(false);VanillaLight.setOreEmission(false);VanillaLight.setLensFlare(false);VanillaLight.setEdgeHighlights(false);VanillaLight.setPixelShadows(false);VanillaLight.setSunshafts(false);m.options.particles().set(net.minecraft.server.level.ParticleStatus.MINIMAL);VanillaLight.setColoredLights(true);VanillaLight.setColoredLightIntensity(30);VanillaLight.setColoredLightDistance(16);VanillaLight.setWarmTorches(false);VanillaLight.setPbr(false);VanillaLight.setPbrIntensity(100);});
|
||||||
|
camera(c,server,100.5,101,98.5,70);look(c,server,0,35);c.waitTicks(30);
|
||||||
|
server.runOnServer(s->{if(s.clockManager().getInstance(s.overworld().dimensionType().defaultClock().orElseThrow()).totalTicks()!=18000)command(s,"time set 18000");
|
||||||
|
for(int x=88;x<=112;x++)for(int z=92;z<=116;z++)for(int y=100;y<=109;y++)
|
||||||
|
s.overworld().setBlockAndUpdate(new BlockPos(x,y,z),(y==100)?Blocks.STONE.defaultBlockState():(y==109||x==88||x==112||z==92||z==116)?Blocks.CONCRETE.white().defaultBlockState():Blocks.AIR.defaultBlockState());
|
||||||
|
s.overworld().setBlockAndUpdate(LEFT,Blocks.TORCH.defaultBlockState());
|
||||||
|
});c.waitTicks(40);settle(c);var flat=capture(c,"direction-left-pbr-off");
|
||||||
|
c.runOnClient(m->VanillaLight.setPbr(true));c.waitFor(m->ColoredLights.directionSample(PROBE)!=0&&ProceduralPbr.ready(),2000);settle(c);
|
||||||
|
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);
|
||||||
|
int opposed=c.computeOnClient(m->ColoredLights.directionSample(PROBE));check(Math.abs(dx(opposed))<.02&&Math.abs(dy(opposed))<.02&&Math.abs(dz(opposed))<.02&&(opposed>>>24)>0,"Equal opposing lights cancel direction, retain illumination");capture(c,"direction-opposed");
|
||||||
|
// A sealed barrier must prevent both intensity and direction reaching the probe.
|
||||||
|
server.runOnServer(s->{s.overworld().setBlockAndUpdate(RIGHT,Blocks.AIR.defaultBlockState());for(int y=101;y<109;y++)for(int z=93;z<116;z++)s.overworld().setBlockAndUpdate(new BlockPos(99,y,z),Blocks.STONE.defaultBlockState());});c.waitTicks(40);settle(c);
|
||||||
|
check(c.computeOnClient(m->ColoredLights.directionSample(PROBE))==0,"Closed wall blocks local direction and energy");capture(c,"direction-blocked");
|
||||||
|
server.runOnServer(s->{s.overworld().setBlockAndUpdate(LEFT,Blocks.AIR.defaultBlockState());
|
||||||
|
for(int y=101;y<109;y++)for(int z=93;z<116;z++)s.overworld().setBlockAndUpdate(new BlockPos(99,y,z),Blocks.AIR.defaultBlockState());
|
||||||
|
command(s,"gamemode creative @a");s.getPlayerList().getPlayers().getFirst().setItemSlot(net.minecraft.world.entity.EquipmentSlot.MAINHAND,new net.minecraft.world.item.ItemStack(net.minecraft.world.item.Items.TORCH));});
|
||||||
|
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.;}
|
||||||
|
private static double dy(int c){return ((c>>8&255)-128)/127.;}
|
||||||
|
private static double dz(int c){return ((c&255)-128)/127.;}
|
||||||
|
private static void settle(ClientGameTestContext c){c.waitFor(m->ColoredLights.applied()&&ColoredLights.settled(),2400);c.waitTicks(14);}
|
||||||
|
private static long difference(Frame a,Frame b){long n=0;for(int i=0;i<a.pixels().length;i++)n+=channelDifference(a.pixels()[i],b.pixels()[i]);return n;}
|
||||||
|
}
|
||||||
+2
-2
@@ -13,7 +13,7 @@ import java.util.Arrays;
|
|||||||
public final class Pbr134ClientChecks implements FabricClientGameTest {
|
public final class Pbr134ClientChecks implements FabricClientGameTest {
|
||||||
public void runTest(ClientGameTestContext c){new PixelShadows122ClientChecks(false,false,true).runTest(c);}
|
public void runTest(ClientGameTestContext c){new PixelShadows122ClientChecks(false,false,true).runTest(c);}
|
||||||
static void run(ClientGameTestContext c,TestServerContext server){
|
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++){
|
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());
|
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());
|
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{
|
private static void preferences(){try{
|
||||||
var p=FabricLoader.getInstance().getConfigDir().resolve("sanctuary-shaders.json");Files.writeString(p,"{\"ssgi\":true,\"ssgiIntensity\":35,\"bloom\":false}");forgetPreferences();
|
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");}
|
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);
|
VanillaLight.setPbrIntensity(50);
|
||||||
}catch(Exception e){throw new AssertionError(e);}}
|
}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);}
|
||||||
|
}
|
||||||
+8
-5
@@ -54,7 +54,9 @@ public final class PixelShadows122ClientChecks implements FabricClientGameTest {
|
|||||||
public void runTest(ClientGameTestContext c) {
|
public void runTest(ClientGameTestContext c) {
|
||||||
c.runOnClient(m -> {
|
c.runOnClient(m -> {
|
||||||
var info = com.mojang.blaze3d.systems.RenderSystem.getDevice().getDeviceInfo();
|
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),
|
check(expected.isEmpty() || info.backendName().equalsIgnoreCase(expected),
|
||||||
"Requested graphics backend must not silently fall back: " + expected + " / " + info.backendName());
|
"Requested graphics backend must not silently fall back: " + expected + " / " + info.backendName());
|
||||||
System.out.println("SHADER139_BACKEND " + info.backendName() + " / " + info.name()
|
System.out.println("SHADER139_BACKEND " + info.backendName() + " / " + info.name()
|
||||||
@@ -149,7 +151,7 @@ public final class PixelShadows122ClientChecks implements FabricClientGameTest {
|
|||||||
c.runOnClient(m -> check(VanillaLight.active(), "Native Sanctuary shader is active without an external engine"));
|
c.runOnClient(m -> check(VanillaLight.active(), "Native Sanctuary shader is active without an external engine"));
|
||||||
float morning = c.computeOnClient(m -> m.gameRenderer.gameRenderState().levelRenderState.skyRenderState.sunAngle);
|
float morning = c.computeOnClient(m -> m.gameRenderer.gameRenderState().levelRenderState.skyRenderState.sunAngle);
|
||||||
|
|
||||||
if(coloredOnly){ColoredLights139ClientChecks.run(c,server);if(Boolean.getBoolean("sanctuary.test.colored140"))ColoredLights140ClientChecks.run(c,server);return;}
|
if(coloredOnly){if(Boolean.getBoolean("sanctuary.test.directional141Only")){DirectionalLights141ClientChecks.run(c,server);return;}ColoredLights139ClientChecks.run(c,server);if(Boolean.getBoolean("sanctuary.test.colored140"))ColoredLights140ClientChecks.run(c,server);if(Boolean.getBoolean("sanctuary.test.directional141"))DirectionalLights141ClientChecks.run(c,server);return;}
|
||||||
if(sunshaftOnly){Sunshafts137ClientChecks.run(c,server);return;}
|
if(sunshaftOnly){Sunshafts137ClientChecks.run(c,server);return;}
|
||||||
if(closeupOnly){SsgiCloseup135ClientChecks.run(c,server);Pbr134ClientChecks.run(c,server);return;}
|
if(closeupOnly){SsgiCloseup135ClientChecks.run(c,server);Pbr134ClientChecks.run(c,server);return;}
|
||||||
if(pbrOnly){Pbr134ClientChecks.run(c,server);return;}
|
if(pbrOnly){Pbr134ClientChecks.run(c,server);return;}
|
||||||
@@ -281,7 +283,7 @@ public final class PixelShadows122ClientChecks implements FabricClientGameTest {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
c.waitTicks(3);
|
c.waitTicks(3);
|
||||||
c.takeScreenshot("sanctuary-beta140-shader-options-" + locale);
|
c.takeScreenshot("sanctuary-beta142-shader-options-" + locale);
|
||||||
}
|
}
|
||||||
c.runOnClient(m -> m.gui.setScreen(null));
|
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");
|
var path=FabricLoader.getInstance().getConfigDir().resolve("sanctuary-shaders.json");
|
||||||
Files.deleteIfExists(path);forgetPreferences();
|
Files.deleteIfExists(path);forgetPreferences();
|
||||||
check(VanillaLight.bloom()&&VanillaLight.bloomIntensity()==20&&VanillaLight.pbr()&&VanillaLight.ssgi(),"Fresh defaults: bloom20, PBR and SSGI ON");
|
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();
|
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");
|
check(!VanillaLight.pbr()&&!VanillaLight.ssgi()&&VanillaLight.bloomIntensity()==73,"Explicit personal toggles and intensity survive changed defaults");
|
||||||
Files.writeString(path,"{\"bloom\":false}");forgetPreferences();
|
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");
|
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");
|
System.out.println("SHADER135_DEFAULTS_PASS fresh, partial and personal preferences");
|
||||||
} catch(IOException e){throw new AssertionError(e);}
|
} catch(IOException e){throw new AssertionError(e);}
|
||||||
}
|
}
|
||||||
@@ -565,7 +568,7 @@ public final class PixelShadows122ClientChecks implements FabricClientGameTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static Frame capture(ClientGameTestContext c, String name) {
|
static Frame capture(ClientGameTestContext c, String name) {
|
||||||
if (name != null) c.takeScreenshot("sanctuary-beta140-shader-" + name);
|
if (name != null) c.takeScreenshot("sanctuary-beta142-shader-" + name);
|
||||||
var result = new CompletableFuture<Frame>();
|
var result = new CompletableFuture<Frame>();
|
||||||
c.runOnClient(m -> Screenshot.takeScreenshot(m.gameRenderer.mainRenderTarget(), image -> {
|
c.runOnClient(m -> Screenshot.takeScreenshot(m.gameRenderer.mainRenderTarget(), image -> {
|
||||||
try (image) {
|
try (image) {
|
||||||
|
|||||||
+1
-1
@@ -13,7 +13,7 @@ import java.nio.file.Files;
|
|||||||
public final class Ssgi132ClientChecks implements FabricClientGameTest {
|
public final class Ssgi132ClientChecks implements FabricClientGameTest {
|
||||||
public void runTest(ClientGameTestContext c){new PixelShadows122ClientChecks(false,true).runTest(c);}
|
public void runTest(ClientGameTestContext c){new PixelShadows122ClientChecks(false,true).runTest(c);}
|
||||||
static void run(ClientGameTestContext c,TestServerContext server){
|
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++){
|
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());
|
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());
|
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 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);}
|
public void runTest(ClientGameTestContext c){new PixelShadows122ClientChecks(false,false,false,true).runTest(c);}
|
||||||
static void run(ClientGameTestContext c,TestServerContext server){
|
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->{
|
server.runOnServer(s->{
|
||||||
// Minecraft 26.3 rejects setting a clock to its existing value.
|
// Minecraft 26.3 rejects setting a clock to its existing value.
|
||||||
command(s,"time add 1");command(s,"time set 6000");
|
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.client.renderer.RenderPipelines;
|
||||||
import net.minecraft.resources.Identifier;
|
import net.minecraft.resources.Identifier;
|
||||||
import net.minecraft.network.chat.Component;
|
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;
|
import net.minecraft.ChatFormatting;
|
||||||
|
|
||||||
/** Configuration-stage identity and starter slots. Closing cancels connection, never the saved offer. */
|
/** Configuration-stage identity and starter slots. Closing cancels connection, never the saved offer. */
|
||||||
public final class HelloWorldScreen extends Screen {
|
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 ProgressionPayloads.Hello offer;private final Runnable disconnect;
|
||||||
private EditBox bio,familiarName;private int color=-1;private Button submit;private boolean waiting,identityLocked;
|
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 final List<Button> colors=new ArrayList<>(),starters=new ArrayList<>();
|
||||||
private int panelX,panelY,panelWidth,bioLabelY,colorLabelY,starterLabelY,nameLabelY;private boolean compact;
|
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;}
|
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(identityLocked){draft=welcome.bio();color=offer.colors().getFirst();}
|
||||||
if(starterChoices.stream().noneMatch(c->c.mob().equals(starter)))starter="";
|
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 prepareAccess(boolean pending){accessRequired=true;accessPending=pending;if(pending)codeStatus="code_recognized";}
|
||||||
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 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(){
|
@Override protected void init(){
|
||||||
boolean naming=!starterChoices.isEmpty();compact=naming&&height<278;
|
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))/2);colors.clear();starters.clear();familiarName=null;
|
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);colorLabelY=panelY+(compact?71:81);starterLabelY=panelY+(compact?145:177);nameLabelY=starterLabelY+36;
|
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);
|
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;
|
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;
|
int cell=w/4;
|
||||||
for(int i=0;i<offer.colors().size();i++){
|
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);
|
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))));
|
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.setMaxLength(StarterCompanions.NAME_LIMIT);familiarName.setHint(Component.translatable("sanctuary.starter.name_hint"));familiarName.setValue(nameDraft);
|
||||||
familiarName.setResponder(value->{nameDraft=value;updateSelection();});
|
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());
|
addRenderableWidget(Button.builder(ProgressionService.label("cancel"),b->onClose()).bounds(panelX,actionsY,90,20).build());
|
||||||
submit=addRenderableWidget(Button.builder(ProgressionService.label("enter"),b->{
|
submit=addRenderableWidget(Button.builder(ProgressionService.label("enter"),b->{
|
||||||
if(waiting||color<0||(!starterChoices.isEmpty()&&(starter.isEmpty()||!StarterCompanions.validName(nameDraft))))return;
|
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)){
|
if(!starterChoices.isEmpty()&&!nameDraft.isBlank()&&!ClientConfigurationNetworking.canSend(StarterPayloads.NamedCreate.TYPE)){
|
||||||
offer=new ProgressionPayloads.Hello(offer.name(),offer.colors(),"starter_name_version_required");return;
|
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));
|
else ClientConfigurationNetworking.send(new StarterPayloads.Create(bio.getValue(),color,starter));
|
||||||
}).bounds(panelX+96,actionsY,panelWidth-96,20).build());updateSelection();
|
}).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(){
|
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(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);
|
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){
|
@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){
|
@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);
|
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);
|
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);
|
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()){
|
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,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);
|
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 {
|
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());}
|
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 final class ProgressionClient {
|
||||||
public static ProgressRecord progress;
|
public static ProgressRecord progress;
|
||||||
|
private static boolean accessGate,accessPending;
|
||||||
public static boolean operator;
|
public static boolean operator;
|
||||||
public static InhabitantStore.Identity identity;
|
public static InhabitantStore.Identity identity;
|
||||||
public static ProgressionConfig config=ProgressionConfig.DEFAULT;
|
public static ProgressionConfig config=ProgressionConfig.DEFAULT;
|
||||||
@@ -38,14 +39,21 @@ public final class ProgressionClient {
|
|||||||
CycleClient.register();
|
CycleClient.register();
|
||||||
EntityRendererRegistry.register(SanctuarySitting.SEAT,NoopRenderer::new);
|
EntityRendererRegistry.register(SanctuarySitting.SEAT,NoopRenderer::new);
|
||||||
ClientConfigurationNetworking.registerGlobalReceiver(ProgressionPayloads.InventoryLayout.TYPE,(payload,context) -> {});
|
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(()->{
|
ClientConfigurationNetworking.registerGlobalReceiver(StarterPayloads.Welcome.TYPE,(payload,context)->context.client().execute(()->{
|
||||||
if(context.client().gui.screen() instanceof HelloWorldScreen screen)screen.accept(payload);
|
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) ->
|
ClientConfigurationNetworking.registerGlobalReceiver(ProgressionPayloads.Hello.TYPE,(payload,context) ->
|
||||||
context.client().execute(() -> {
|
context.client().execute(() -> {
|
||||||
if (context.client().gui.screen() instanceof HelloWorldScreen screen) screen.accept(payload);
|
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"))));
|
() -> context.responseSender().disconnect(ProgressionService.label("cancelled"))));
|
||||||
}));
|
}));
|
||||||
ClientPlayNetworking.registerGlobalReceiver(ProgressionPayloads.Snapshot.TYPE,(payload,context) ->
|
ClientPlayNetworking.registerGlobalReceiver(ProgressionPayloads.Snapshot.TYPE,(payload,context) ->
|
||||||
@@ -91,9 +99,11 @@ public final class ProgressionClient {
|
|||||||
if (active() && ClientPlayNetworking.canSend(ProgressionPayloads.Action.TYPE))
|
if (active() && ClientPlayNetworking.canSend(ProgressionPayloads.Action.TYPE))
|
||||||
ClientPlayNetworking.send(new ProgressionPayloads.Action(target,progress.revision()));
|
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) {
|
private static void reset(Minecraft client) {
|
||||||
if (client.player != null) ProgressEffects.clear(client.player);
|
if (client.player != null) ProgressEffects.clear(client.player);
|
||||||
identity=null;progress=null;operator=false;config=ProgressionConfig.DEFAULT;names=MobNamesConfig.DEFAULT;
|
identity=null;progress=null;operator=false;config=ProgressionConfig.DEFAULT;names=MobNamesConfig.DEFAULT;
|
||||||
|
accessGate=false;accessPending=false;
|
||||||
wasSneaking=false;pendingSit=false;lastSneak=-100;
|
wasSneaking=false;pendingSit=false;lastSneak=-100;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+38
-11
@@ -22,7 +22,10 @@ final class ColoredLightVolume implements AutoCloseable {
|
|||||||
private ClientLevel level;
|
private ClientLevel level;
|
||||||
private Object models;
|
private Object models;
|
||||||
private Build build;
|
private Build build;
|
||||||
private DynamicTexture texture,previous;
|
private DynamicTexture texture,previous,direction,previousDirection;
|
||||||
|
private boolean directional;
|
||||||
|
DynamicTexture direction(){return direction;}
|
||||||
|
DynamicTexture previousDirection(){return previousDirection;}
|
||||||
private BlockPos previousOrigin;
|
private BlockPos previousOrigin;
|
||||||
private int side,previousSide;
|
private int side,previousSide;
|
||||||
private long transitionStarted;
|
private long transitionStarted;
|
||||||
@@ -40,18 +43,20 @@ final class ColoredLightVolume implements AutoCloseable {
|
|||||||
if(level!=mc.level||models!=modelSet){close();level=mc.level;models=modelSet;}
|
if(level!=mc.level||models!=modelSet){close();level=mc.level;models=modelSet;}
|
||||||
if(warm!=VanillaLight.warmTorches()){warm=VanillaLight.warmTorches();materials.clear();revision++;if(build!=null){build.close();build=null;}}
|
if(warm!=VanillaLight.warmTorches()){warm=VanillaLight.warmTorches();materials.clear();revision++;if(build!=null){build.close();build=null;}}
|
||||||
var p=mc.gameRenderer.gameRenderState().levelRenderState.cameraRenderState.pos;
|
var p=mc.gameRenderer.gameRenderState().levelRenderState.cameraRenderState.pos;
|
||||||
|
boolean wantedDirection=VanillaLight.pbr()&&VanillaLight.pbrIntensity()>0;
|
||||||
int wantedSide=sideFor(VanillaLight.coloredLightDistance());
|
int wantedSide=sideFor(VanillaLight.coloredLightDistance());
|
||||||
var wanted=new BlockPos(anchor(p.x,wantedSide),anchor(p.y,wantedSide),anchor(p.z,wantedSide));
|
var wanted=new BlockPos(anchor(p.x,wantedSide),anchor(p.y,wantedSide),anchor(p.z,wantedSide));
|
||||||
long now=System.nanoTime();
|
long now=System.nanoTime();
|
||||||
if(previous!=null&&blend()>=1){previous.close();previous=null;previousOrigin=null;}
|
if(previous!=null&&blend()>=1){previous.close();previous=null;previousOrigin=null;if(previousDirection!=null){previousDirection.close();previousDirection=null;}}
|
||||||
// Complete a snapshot even if the world keeps changing, then catch up; moving
|
// Complete a snapshot even if the world keeps changing, then catch up; moving
|
||||||
// beyond it cancels immediately. Static worlds do no further scanning or flooding.
|
// beyond it cancels immediately. Static worlds do no further scanning or flooding.
|
||||||
if(build!=null&&(build.side!=wantedSide||!inside(build.origin,BlockPos.containing(p),build.side))){build.close();build=null;}
|
if(build!=null&&(build.directional!=wantedDirection||build.side!=wantedSide||!inside(build.origin,BlockPos.containing(p),build.side))){build.close();build=null;}
|
||||||
if(build==null&&previous==null&&(texture==null||side!=wantedSide||!wanted.equals(origin)||published!=revision)&&now-lastStart>=100_000_000L){
|
if(build==null&&previous==null&&(texture==null||directional!=wantedDirection||side!=wantedSide||!wanted.equals(origin)||published!=revision)&&now-lastStart>=100_000_000L){
|
||||||
build=new Build(level,wanted,wantedSide,revision,materials);lastStart=now;
|
build=new Build(level,wanted,wantedSide,revision,materials,wantedDirection);lastStart=now;
|
||||||
}
|
}
|
||||||
if(build!=null&&build.advance(now+2_000_000L)){
|
if(build!=null&&build.advance(now+2_000_000L)){
|
||||||
previous=texture;previousOrigin=origin;previousSide=side;
|
previous=texture;previousOrigin=origin;previousSide=side;previousDirection=direction;
|
||||||
|
direction=build.directionImage==null?null:new DynamicTexture(()->"Sanctuary light direction",build.takeDirectionImage());directional=build.directional;
|
||||||
texture=new DynamicTexture(()->"Sanctuary coloured light volume",build.takeImage());
|
texture=new DynamicTexture(()->"Sanctuary coloured light volume",build.takeImage());
|
||||||
side=build.side;transitionStarted=System.nanoTime();
|
side=build.side;transitionStarted=System.nanoTime();
|
||||||
origin=build.origin;published=build.revision;build.close();build=null;completed++;
|
origin=build.origin;published=build.revision;build.close();build=null;completed++;
|
||||||
@@ -68,9 +73,10 @@ final class ColoredLightVolume implements AutoCloseable {
|
|||||||
BlockPos origin(){return origin;}
|
BlockPos origin(){return origin;}
|
||||||
boolean settled(){return texture!=null&&build==null&&revision==published&&blend()>=1;}
|
boolean settled(){return texture!=null&&build==null&&revision==published&&blend()>=1;}
|
||||||
int completed(){return completed;}
|
int completed(){return completed;}
|
||||||
|
int directionSample(BlockPos p){if(direction==null||!inside(origin,p,side))return 0;int x=p.getX()-origin.getX(),y=p.getY()-origin.getY(),z=p.getZ()-origin.getZ(),cols=columns(side);return direction.getPixels().getPixel(x+(y%cols)*side,z+(y/cols)*side);}
|
||||||
int cachedStates(){return materials.cachedStates();}
|
int cachedStates(){return materials.cachedStates();}
|
||||||
int sample(BlockPos p){if(texture==null||!inside(origin,p,side))return 0;int x=p.getX()-origin.getX(),y=p.getY()-origin.getY(),z=p.getZ()-origin.getZ(),cols=columns(side);return texture.getPixels().getPixel(x+(y%cols)*side,z+(y/cols)*side);}
|
int sample(BlockPos p){if(texture==null||!inside(origin,p,side))return 0;int x=p.getX()-origin.getX(),y=p.getY()-origin.getY(),z=p.getZ()-origin.getZ(),cols=columns(side);return texture.getPixels().getPixel(x+(y%cols)*side,z+(y/cols)*side);}
|
||||||
public void close(){if(build!=null){build.close();build=null;}if(texture!=null){texture.close();texture=null;}if(previous!=null){previous.close();previous=null;}materials.clear();level=null;models=null;origin=null;previousOrigin=null;side=previousSide=0;published=-1;revision=0;lastStart=0;}
|
public void close(){if(build!=null){build.close();build=null;}if(texture!=null){texture.close();texture=null;}if(previous!=null){previous.close();previous=null;}if(direction!=null){direction.close();direction=null;}if(previousDirection!=null){previousDirection.close();previousDirection=null;}materials.clear();level=null;models=null;origin=null;previousOrigin=null;side=previousSide=0;published=-1;revision=0;lastStart=0;}
|
||||||
|
|
||||||
private static final class Build implements AutoCloseable {
|
private static final class Build implements AutoCloseable {
|
||||||
final int side,count,cols,chunkSide;
|
final int side,count,cols,chunkSide;
|
||||||
@@ -80,11 +86,14 @@ final class ColoredLightVolume implements AutoCloseable {
|
|||||||
final boolean[] queued;final IntArrayFIFOQueue queue=new IntArrayFIFOQueue();
|
final boolean[] queued;final IntArrayFIFOQueue queue=new IntArrayFIFOQueue();
|
||||||
final LevelChunk[] chunks;final int cx,cz;
|
final LevelChunk[] chunks;final int cx,cz;
|
||||||
final BlockPos.MutableBlockPos pos=new BlockPos.MutableBlockPos();
|
final BlockPos.MutableBlockPos pos=new BlockPos.MutableBlockPos();
|
||||||
NativeImage image;int scan,packed;
|
NativeImage image,directionImage;final boolean directional;int scan,packed;
|
||||||
Build(ClientLevel level,BlockPos origin,int side,long revision,ColoredLightMaterials materials){
|
private static final double[] ENERGY=new double[256];
|
||||||
this.side=side;count=side*side*side;cols=columns(side);chunkSide=(side+15)/16+1;
|
static{for(int i=129;i<256;i++)ENERGY[i]=Math.max(0,(Math.pow(.8,31-i/8.)-Math.pow(.8,15))/(1-Math.pow(.8,15)));}
|
||||||
|
Build(ClientLevel level,BlockPos origin,int side,long revision,ColoredLightMaterials materials,boolean directional){
|
||||||
|
this.directional=directional;this.side=side;count=side*side*side;cols=columns(side);chunkSide=(side+15)/16+1;
|
||||||
states=new BlockState[count];red=new byte[count];green=new byte[count];blue=new byte[count];sky=new byte[count];queued=new boolean[count];chunks=new LevelChunk[chunkSide*chunkSide];
|
states=new BlockState[count];red=new byte[count];green=new byte[count];blue=new byte[count];sky=new byte[count];queued=new boolean[count];chunks=new LevelChunk[chunkSide*chunkSide];
|
||||||
image=new NativeImage(cols*side,((side+cols-1)/cols)*side,false);
|
image=new NativeImage(cols*side,((side+cols-1)/cols)*side,false);
|
||||||
|
if(directional)directionImage=new NativeImage(image.getWidth(),image.getHeight(),false);
|
||||||
this.level=level;this.origin=origin;this.revision=revision;this.materials=materials;
|
this.level=level;this.origin=origin;this.revision=revision;this.materials=materials;
|
||||||
cx=origin.getX()>>4;cz=origin.getZ()>>4;
|
cx=origin.getX()>>4;cz=origin.getZ()>>4;
|
||||||
for(int z=0;z<chunkSide;z++)for(int x=0;x<chunkSide;x++)chunks[x+z*chunkSide]=level.getChunkSource().getChunk(cx+x,cz+z,ChunkStatus.FULL,false);
|
for(int z=0;z<chunkSide;z++)for(int x=0;x<chunkSide;x++)chunks[x+z*chunkSide]=level.getChunkSource().getChunk(cx+x,cz+z,ChunkStatus.FULL,false);
|
||||||
@@ -99,6 +108,7 @@ final class ColoredLightVolume implements AutoCloseable {
|
|||||||
while(packed<count){
|
while(packed<count){
|
||||||
int i=packed++,x=i%side,z=i/side%side,y=i/(side*side);
|
int i=packed++,x=i%side,z=i/side%side,y=i/(side*side);
|
||||||
image.setPixel(x+(y%cols)*side,z+(y/cols)*side,(sky[i]&255)<<24|(red[i]&255)<<16|(green[i]&255)<<8|(blue[i]&255));
|
image.setPixel(x+(y%cols)*side,z+(y/cols)*side,(sky[i]&255)<<24|(red[i]&255)<<16|(green[i]&255)<<8|(blue[i]&255));
|
||||||
|
if(directional)directionImage.setPixel(x+(y%cols)*side,z+(y/cols)*side,directionAt(i,x,y,z));
|
||||||
if((packed&511)==0&&System.nanoTime()>=deadline)return false;
|
if((packed&511)==0&&System.nanoTime()>=deadline)return false;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
@@ -124,9 +134,26 @@ final class ColoredLightVolume implements AutoCloseable {
|
|||||||
if(changed)enqueue(n);
|
if(changed)enqueue(n);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
double energy(int i){return ENERGY[Math.max(red[i]&255,Math.max(green[i]&255,blue[i]&255))];}
|
||||||
|
int directionAt(int i,int x,int y,int z){
|
||||||
|
double energy=energy(i);if(energy<=0)return 0;
|
||||||
|
double vx=0,vy=0,vz=0,total=0;
|
||||||
|
for(var d:DIRECTIONS){
|
||||||
|
int nx=x+d.getStepX(),ny=y+d.getStepY(),nz=z+d.getStepZ();
|
||||||
|
if(nx<0||nx>=side||ny<0||ny>=side||nz<0||nz>=side)continue;
|
||||||
|
int n=nx+nz*side+ny*side*side;double w=Math.max(0,energy(n)-energy);
|
||||||
|
if(w<=0||net.minecraft.world.level.lighting.LightEngine.getLightDampeningInto(states[n],states[i],d.getOpposite(),Math.max(1,states[i].getLightDampening()))>=15)continue;
|
||||||
|
vx+=d.getStepX()*w;vy+=d.getStepY()*w;vz+=d.getStepZ()*w;total+=w;
|
||||||
|
}
|
||||||
|
// The vector length is confidence: opposing arrivals cancel instead of flipping.
|
||||||
|
double scale=total>1e-12?127/total:0;
|
||||||
|
int a=(int)Math.round(255*energy/(energy+.08));
|
||||||
|
return a<<24|(int)Math.round(128+vx*scale)<<16|(int)Math.round(128+vy*scale)<<8|(int)Math.round(128+vz*scale);
|
||||||
|
}
|
||||||
|
NativeImage takeDirectionImage(){var result=directionImage;directionImage=null;return result;}
|
||||||
void enqueue(int i){if(!queued[i]){queued[i]=true;queue.enqueue(i);}}
|
void enqueue(int i){if(!queued[i]){queued[i]=true;queue.enqueue(i);}}
|
||||||
NativeImage takeImage(){var result=image;image=null;return result;}
|
NativeImage takeImage(){var result=image;image=null;return result;}
|
||||||
public void close(){if(image!=null){image.close();image=null;}}
|
public void close(){if(image!=null){image.close();image=null;}if(directionImage!=null){directionImage.close();directionImage=null;}}
|
||||||
}
|
}
|
||||||
// An offset preserves dim colour channels below native level zero. The shader
|
// An offset preserves dim colour channels below native level zero. The shader
|
||||||
// applies the native cutoff to overall energy, never to each channel separately.
|
// applies the native cutoff to overall energy, never to each channel separately.
|
||||||
|
|||||||
@@ -39,15 +39,15 @@ public final class ColoredLights {
|
|||||||
private static float displayedDistance;
|
private static float displayedDistance;
|
||||||
private static int dynamicCount;
|
private static int dynamicCount;
|
||||||
private static MappableRingBuffer uniforms;
|
private static MappableRingBuffer uniforms;
|
||||||
private static boolean applied;
|
private static boolean applied,prepared;
|
||||||
private static int frames;
|
private static int frames;
|
||||||
private ColoredLights(){}
|
private ColoredLights(){}
|
||||||
public static void captureProjection(Matrix4f matrix){projection.set(matrix);}
|
public static void captureProjection(Matrix4f matrix){projection.set(matrix);}
|
||||||
public static void dirtyBlock(BlockPos pos){VOLUME.dirty(pos);}
|
public static void dirtyBlock(BlockPos pos){VOLUME.dirty(pos);}
|
||||||
public static void dirtyChunk(int x,int z){VOLUME.dirtyChunk(x,z);}
|
public static void dirtyChunk(int x,int z){VOLUME.dirtyChunk(x,z);}
|
||||||
public static void dirtySection(int x,int y,int z){if(!fr.koka.sanctuary.client.DynamicLightClient.invalidatingSections())VOLUME.dirtySection(x,y,z);}
|
public static void dirtySection(int x,int y,int z){if(!fr.koka.sanctuary.client.DynamicLightClient.invalidatingSections())VOLUME.dirtySection(x,y,z);}
|
||||||
public static void render(){
|
public static void prepare(){
|
||||||
applied=false;var mc=Minecraft.getInstance();
|
applied=false;prepared=false;var mc=Minecraft.getInstance();
|
||||||
if(!VanillaLight.active()||!VanillaLight.coloredLights()||VanillaLight.coloredLightIntensity()==0||mc.level==null){release();return;}
|
if(!VanillaLight.active()||!VanillaLight.coloredLights()||VanillaLight.coloredLightIntensity()==0||mc.level==null){release();return;}
|
||||||
if(PanoramaCapture.rendering())return;
|
if(PanoramaCapture.rendering())return;
|
||||||
var camera=mc.gameRenderer.gameRenderState().levelRenderState.cameraRenderState;
|
var camera=mc.gameRenderer.gameRenderState().levelRenderState.cameraRenderState;
|
||||||
@@ -78,6 +78,20 @@ public final class ColoredLights {
|
|||||||
for(int i=0;i<ColoredDynamicLights.LIMIT;i++){if(i<lights.size()){var l=lights.get(i);data.putVec4((float)(l.x-camera.pos.x),(float)(l.y-camera.pos.y),(float)(l.z-camera.pos.z),l.level);}else data.putVec4(0,0,0,0);}
|
for(int i=0;i<ColoredDynamicLights.LIMIT;i++){if(i<lights.size()){var l=lights.get(i);data.putVec4((float)(l.x-camera.pos.x),(float)(l.y-camera.pos.y),(float)(l.z-camera.pos.z),l.level);}else data.putVec4(0,0,0,0);}
|
||||||
for(int i=0;i<ColoredDynamicLights.LIMIT;i++){if(i<lights.size()){var l=lights.get(i);data.putVec4((float)l.r,(float)l.g,(float)l.b,(float)l.visibility);}else data.putVec4(0,0,0,0);}
|
for(int i=0;i<ColoredDynamicLights.LIMIT;i++){if(i<lights.size()){var l=lights.get(i);data.putVec4((float)l.r,(float)l.g,(float)l.b,(float)l.visibility);}else data.putVec4(0,0,0,0);}
|
||||||
}
|
}
|
||||||
|
prepared=true;
|
||||||
|
}
|
||||||
|
static void bindDirections(com.mojang.renderpearl.api.commands.RenderPass pass){
|
||||||
|
var nearest=RenderSystem.getSamplerCache().getClampToEdge(FilterMode.NEAREST);
|
||||||
|
pass.setUniform("ColoredLightScene",uniforms.currentBuffer());
|
||||||
|
pass.setUniform("LightDirection",(VOLUME.direction()==null?black:VOLUME.direction()).getTextureView(),nearest);
|
||||||
|
pass.setUniform("PreviousDirection",(VOLUME.previousDirection()==null?black:VOLUME.previousDirection()).getTextureView(),nearest);
|
||||||
|
}
|
||||||
|
static boolean directionalReady(){return prepared&&VanillaLight.pbr();}
|
||||||
|
public static void render(){
|
||||||
|
if(!prepared)return;
|
||||||
|
var main=Minecraft.getInstance().gameRenderer.mainRenderTarget();
|
||||||
|
var current=VOLUME.texture()==null?black:VOLUME.texture();
|
||||||
|
var previous=VOLUME.previous()==null?black:VOLUME.previous();
|
||||||
var encoder=RenderSystem.getDevice().createCommandEncoder();
|
var encoder=RenderSystem.getDevice().createCommandEncoder();
|
||||||
encoder.copyTextureToTexture(main.getColorTexture(),source.getColorTexture(),0,0,0,0,0,main.width,main.height);
|
encoder.copyTextureToTexture(main.getColorTexture(),source.getColorTexture(),0,0,0,0,0,main.width,main.height);
|
||||||
var nearest=RenderSystem.getSamplerCache().getClampToEdge(FilterMode.NEAREST);
|
var nearest=RenderSystem.getSamplerCache().getClampToEdge(FilterMode.NEAREST);
|
||||||
@@ -87,7 +101,7 @@ public final class ColoredLights {
|
|||||||
}
|
}
|
||||||
applied=true;frames++;
|
applied=true;frames++;
|
||||||
}
|
}
|
||||||
public static void release(){VOLUME.close();DYNAMIC.clear();activated=lastRender=0;dynamicCount=0;if(black!=null){black.close();black=null;}if(source!=null){source.destroyBuffers();source=null;}if(uniforms!=null){uniforms.close();uniforms=null;}applied=false;}
|
public static void release(){prepared=false;VOLUME.close();DYNAMIC.clear();activated=lastRender=0;dynamicCount=0;if(black!=null){black.close();black=null;}if(source!=null){source.destroyBuffers();source=null;}if(uniforms!=null){uniforms.close();uniforms=null;}applied=false;}
|
||||||
static boolean applied(){return applied;}
|
static boolean applied(){return applied;}
|
||||||
static boolean allocated(){return source!=null||VOLUME.texture()!=null;}
|
static boolean allocated(){return source!=null||VOLUME.texture()!=null;}
|
||||||
static boolean settled(){return VOLUME.settled();}
|
static boolean settled(){return VOLUME.settled();}
|
||||||
@@ -98,5 +112,6 @@ public final class ColoredLights {
|
|||||||
static int dynamicCount(){return dynamicCount;}
|
static int dynamicCount(){return dynamicCount;}
|
||||||
static float blend(){return VOLUME.blend();}
|
static float blend(){return VOLUME.blend();}
|
||||||
static int volumeSide(){return VOLUME.side();}
|
static int volumeSide(){return VOLUME.side();}
|
||||||
|
static int directionSample(BlockPos pos){return VOLUME.directionSample(pos);}
|
||||||
static int materials(){return VOLUME.cachedStates();}
|
static int materials(){return VOLUME.cachedStates();}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ final class PbrTerrain implements AutoCloseable {
|
|||||||
final PbrMaterials materials=new PbrMaterials();
|
final PbrMaterials materials=new PbrMaterials();
|
||||||
private Build building;
|
private Build building;
|
||||||
private long bytes;
|
private long bytes;
|
||||||
|
private int completed;
|
||||||
private Object models,level;
|
private Object models,level;
|
||||||
private List<Section> visible=List.of();
|
private List<Section> visible=List.of();
|
||||||
record Entry(BlockPos origin,GpuBuffer vertices,int indices){}
|
record Entry(BlockPos origin,GpuBuffer vertices,int indices){}
|
||||||
@@ -45,7 +46,11 @@ final class PbrTerrain implements AutoCloseable {
|
|||||||
if(chunk==null)continue;
|
if(chunk==null)continue;
|
||||||
var storage=chunk.getSection(mc.level.getSectionIndex(pos.getY()));
|
var storage=chunk.getSection(mc.level.getSectionIndex(pos.getY()));
|
||||||
var section=sections.get(key);
|
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);
|
section.seen=frame;active.add(section);
|
||||||
}
|
}
|
||||||
visible=active;
|
visible=active;
|
||||||
@@ -56,23 +61,30 @@ final class PbrTerrain implements AutoCloseable {
|
|||||||
if(started++>=4)break;
|
if(started++>=4)break;
|
||||||
Section next=active.stream().filter(s->s.dirty).min(Comparator.comparingDouble(s->s.origin.distToCenterSqr(camera))).orElse(null);
|
Section next=active.stream().filter(s->s.dirty).min(Comparator.comparingDouble(s->s.origin.distToCenterSqr(camera))).orElse(null);
|
||||||
if(next==null)break;
|
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);
|
building=new Build(next);
|
||||||
}
|
}
|
||||||
if(!advance(building,deadline))break;
|
if(!advance(building,deadline))break;
|
||||||
var done=building;building=null;
|
var done=building;building=null;
|
||||||
try(done){
|
try(done){
|
||||||
done.section.dirty=false;
|
done.section.dirty=done.section.revision!=done.revision;
|
||||||
|
Entry replacement=null;
|
||||||
if(done.quads>0&&!done.overflow){
|
if(done.quads>0&&!done.overflow){
|
||||||
try(var mesh=done.out.buildOrThrow()){
|
try(var mesh=done.out.buildOrThrow()){
|
||||||
long size=mesh.vertexBuffer().remaining();
|
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());
|
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);
|
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();
|
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 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 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);}
|
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 dirty(Section section){if(section!=null){section.revision++;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 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);}
|
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;}
|
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{
|
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;}
|
Section(long key,BlockPos origin,LevelChunkSection storage){this.key=key;this.origin=origin;this.storage=storage;}
|
||||||
}
|
}
|
||||||
private static final class Build implements AutoCloseable{
|
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;
|
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();}
|
public void close(){buffer.close();}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ public final class ProceduralPbr {
|
|||||||
private static final BindGroupLayout SCENE=BindGroupLayout.builder().withUniform("PbrScene",UniformType.UNIFORM_BUFFER)
|
private static final BindGroupLayout SCENE=BindGroupLayout.builder().withUniform("PbrScene",UniformType.UNIFORM_BUFFER)
|
||||||
.withUniform("PixelShadows",UniformType.UNIFORM_BUFFER).withUniform("SunDepth",UniformType.COMBINED_IMAGE_SAMPLER)
|
.withUniform("PixelShadows",UniformType.UNIFORM_BUFFER).withUniform("SunDepth",UniformType.COMBINED_IMAGE_SAMPLER)
|
||||||
.withUniform("WorldDepth",UniformType.COMBINED_IMAGE_SAMPLER).withUniform("SceneColor",UniformType.COMBINED_IMAGE_SAMPLER)
|
.withUniform("WorldDepth",UniformType.COMBINED_IMAGE_SAMPLER).withUniform("SceneColor",UniformType.COMBINED_IMAGE_SAMPLER)
|
||||||
|
.withUniform("ColoredLightScene",UniformType.UNIFORM_BUFFER).withUniform("LightDirection",UniformType.COMBINED_IMAGE_SAMPLER).withUniform("PreviousDirection",UniformType.COMBINED_IMAGE_SAMPLER)
|
||||||
.withUniform("BlockAtlas",UniformType.COMBINED_IMAGE_SAMPLER).withUniform("MaterialAtlas",UniformType.COMBINED_IMAGE_SAMPLER).build();
|
.withUniform("BlockAtlas",UniformType.COMBINED_IMAGE_SAMPLER).withUniform("MaterialAtlas",UniformType.COMBINED_IMAGE_SAMPLER).build();
|
||||||
private static final RenderPipeline PIPELINE=RenderPipelines.register(RenderPipeline.builder()
|
private static final RenderPipeline PIPELINE=RenderPipelines.register(RenderPipeline.builder()
|
||||||
.withLocation(Identifier.parse("sanctuary:pipeline/pbr")).withVertexShader(Identifier.parse("sanctuary:core/pbr"))
|
.withLocation(Identifier.parse("sanctuary:pipeline/pbr")).withVertexShader(Identifier.parse("sanctuary:core/pbr"))
|
||||||
@@ -44,7 +45,7 @@ public final class ProceduralPbr {
|
|||||||
public static void captureProjection(Matrix4f matrix){projection.set(matrix);}
|
public static void captureProjection(Matrix4f matrix){projection.set(matrix);}
|
||||||
public static void dirtyBlock(BlockPos pos){TERRAIN.dirty(pos);}
|
public static void dirtyBlock(BlockPos pos){TERRAIN.dirty(pos);}
|
||||||
public static void dirtyChunk(int x,int z){TERRAIN.dirtyChunk(x,z);}
|
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(){
|
public static void render(){
|
||||||
applied=false;var mc=Minecraft.getInstance();
|
applied=false;var mc=Minecraft.getInstance();
|
||||||
if(!VanillaLight.active()||!VanillaLight.pbr()||VanillaLight.pbrIntensity()==0||mc.level==null){release();return;}
|
if(!VanillaLight.active()||!VanillaLight.pbr()||VanillaLight.pbrIntensity()==0||mc.level==null){release();return;}
|
||||||
@@ -52,8 +53,8 @@ public final class ProceduralPbr {
|
|||||||
if(PanoramaCapture.rendering()||camera.fogType!=FogType.NONE||sky.skybox!=DimensionType.Skybox.OVERWORLD)return;
|
if(PanoramaCapture.rendering()||camera.fogType!=FogType.NONE||sky.skybox!=DimensionType.Skybox.OVERWORLD)return;
|
||||||
var main=mc.gameRenderer.mainRenderTarget();
|
var main=mc.gameRenderer.mainRenderTarget();
|
||||||
if(source==null||source.width!=main.width||source.height!=main.height){if(source!=null)source.destroyBuffers();source=new TextureTarget("Sanctuary PBR source",main.width,main.height,GpuFormat.RGBA8_UNORM,null);}
|
if(source==null||source.width!=main.width||source.height!=main.height){if(source!=null)source.destroyBuffers();source=new TextureTarget("Sanctuary PBR source",main.width,main.height,GpuFormat.RGBA8_UNORM,null);}
|
||||||
// The extra padding also supplies a valid inactive PixelShadows binding (192 bytes).
|
// Padding supplies valid inactive PixelShadows and local-light uniform bindings.
|
||||||
if(scene==null)scene=new MappableRingBuffer(()->"Sanctuary PBR scene",GpuBuffer.USAGE_UNIFORM|GpuBuffer.USAGE_MAP_WRITE,192);
|
if(scene==null)scene=new MappableRingBuffer(()->"Sanctuary PBR scene",GpuBuffer.USAGE_UNIFORM|GpuBuffer.USAGE_MAP_WRITE,176+ColoredDynamicLights.LIMIT*32);
|
||||||
var entries=TERRAIN.update();var atlas=TERRAIN.materials.atlas();
|
var entries=TERRAIN.update();var atlas=TERRAIN.materials.atlas();
|
||||||
var vp=new Matrix4f(projection).mul(camera.viewRotationMatrix);var fog=camera.fogData;
|
var vp=new Matrix4f(projection).mul(camera.viewRotationMatrix);var fog=camera.fogData;
|
||||||
var sun=PixelShadows.sunDirection(sky.sunAngle);boolean lunar=sun.y<=.08f;
|
var sun=PixelShadows.sunDirection(sky.sunAngle);boolean lunar=sun.y<=.08f;
|
||||||
@@ -65,7 +66,7 @@ public final class ProceduralPbr {
|
|||||||
Std140Builder.intoBuffer(mapped.data()).putMat4f(new Matrix4f(vp).invert())
|
Std140Builder.intoBuffer(mapped.data()).putMat4f(new Matrix4f(vp).invert())
|
||||||
.putVec4(sun.x,sun.y,sun.z,strength)
|
.putVec4(sun.x,sun.y,sun.z,strength)
|
||||||
.putVec4(VanillaLight.pbrIntensity()/100f,RenderSystem.getDevice().getDeviceInfo().isZZeroToOne()?1:0,shadow?1:0,32)
|
.putVec4(VanillaLight.pbrIntensity()/100f,RenderSystem.getDevice().getDeviceInfo().isZZeroToOne()?1:0,shadow?1:0,32)
|
||||||
.putVec4(fog.renderDistanceStart,fog.renderDistanceEnd,fog.environmentalStart,fog.environmentalEnd).putVec4(fog.color.w,0,0,0)
|
.putVec4(fog.renderDistanceStart,fog.renderDistanceEnd,fog.environmentalStart,fog.environmentalEnd).putVec4(fog.color.w,ColoredLights.directionalReady()?1:0,0,0)
|
||||||
.putVec4(0,0,0,0).putVec4(0,0,0,0).putVec4(0,0,0,0).putVec4(0,0,0,0);
|
.putVec4(0,0,0,0).putVec4(0,0,0,0).putVec4(0,0,0,0).putVec4(0,0,0,0);
|
||||||
}
|
}
|
||||||
var transforms=new ArrayList<GpuBufferSlice>(entries.size());int max=0;
|
var transforms=new ArrayList<GpuBufferSlice>(entries.size());int max=0;
|
||||||
@@ -81,7 +82,10 @@ public final class ProceduralPbr {
|
|||||||
pass.setUniform("SunDepth",shadow?PixelShadows.materialDepth():main.getDepthTextureView(),nearest);
|
pass.setUniform("SunDepth",shadow?PixelShadows.materialDepth():main.getDepthTextureView(),nearest);
|
||||||
pass.setUniform("WorldDepth",main.getDepthTextureView(),nearest);pass.setUniform("SceneColor",source.getColorTextureView(),nearest);
|
pass.setUniform("WorldDepth",main.getDepthTextureView(),nearest);pass.setUniform("SceneColor",source.getColorTextureView(),nearest);
|
||||||
pass.setUniform("BlockAtlas",mc.getTextureManager().getTexture(TextureAtlas.LOCATION_BLOCKS).getTextureView(),nearest);
|
pass.setUniform("BlockAtlas",mc.getTextureManager().getTexture(TextureAtlas.LOCATION_BLOCKS).getTextureView(),nearest);
|
||||||
pass.setUniform("MaterialAtlas",atlas.getTextureView(),nearest);pass.setIndexBuffer(indices.getBuffer(),indices.type());
|
pass.setUniform("MaterialAtlas",atlas.getTextureView(),nearest);
|
||||||
|
if(ColoredLights.directionalReady())ColoredLights.bindDirections(pass);
|
||||||
|
else{pass.setUniform("ColoredLightScene",scene.currentBuffer());pass.setUniform("LightDirection",source.getColorTextureView(),nearest);pass.setUniform("PreviousDirection",source.getColorTextureView(),nearest);}
|
||||||
|
pass.setIndexBuffer(indices.getBuffer(),indices.type());
|
||||||
for(int i=0;i<entries.size();i++){var entry=entries.get(i);pass.setUniform("DynamicTransforms",transforms.get(i));pass.setVertexBuffer(0,entry.vertices().slice());pass.drawIndexed(entry.indices(),1,0,0,0);}
|
for(int i=0;i<entries.size();i++){var entry=entries.get(i);pass.setUniform("DynamicTransforms",transforms.get(i));pass.setVertexBuffer(0,entry.vertices().slice());pass.drawIndexed(entry.indices(),1,0,0,0);}
|
||||||
}
|
}
|
||||||
applied=true;frames++;
|
applied=true;frames++;
|
||||||
@@ -89,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;}
|
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 applied(){return applied;}static boolean ready(){return TERRAIN.settled();}
|
||||||
static boolean allocated(){return source!=null;}static int renderedFrames(){return frames;}
|
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();}
|
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;
|
boolean lensFlare=true;
|
||||||
int lensFlareIntensity=30;
|
int lensFlareIntensity=30;
|
||||||
boolean pbr=true;
|
boolean pbr=true;
|
||||||
int pbrIntensity=50;
|
int pbrIntensity=80;
|
||||||
boolean ssgi=true;
|
boolean ssgi=true;
|
||||||
boolean coloredLights=true;
|
boolean coloredLights=true;
|
||||||
boolean warmTorches=false;
|
boolean warmTorches=false;
|
||||||
|
|||||||
+1
-1
@@ -31,7 +31,7 @@ abstract class PixelShadowRendererMixin {
|
|||||||
return original.call(buffer, matrix);
|
return original.call(buffer, matrix);
|
||||||
}
|
}
|
||||||
@Inject(method="renderLevel", at=@At(value="INVOKE", target="Lnet/minecraft/client/renderer/GameRenderer;render3dHud(Lnet/minecraft/client/renderer/state/level/CameraRenderState;Lnet/minecraft/client/renderer/state/level/PlayerRenderState;Lnet/minecraft/client/renderer/state/OptionsRenderState;Z)V"))
|
@Inject(method="renderLevel", at=@At(value="INVOKE", target="Lnet/minecraft/client/renderer/GameRenderer;render3dHud(Lnet/minecraft/client/renderer/state/level/CameraRenderState;Lnet/minecraft/client/renderer/state/level/PlayerRenderState;Lnet/minecraft/client/renderer/state/OptionsRenderState;Z)V"))
|
||||||
private void sanctuary$shadows(CallbackInfo ci) { PixelShadows.render(); ProceduralPbr.render(); ColoredLights.render(); ScreenSpaceGI.render(); EdgeHighlights.render(); Bloom.render(); Sunshafts.render(); }
|
private void sanctuary$shadows(CallbackInfo ci) { ColoredLights.prepare(); PixelShadows.render(); ProceduralPbr.render(); ColoredLights.render(); ScreenSpaceGI.render(); EdgeHighlights.render(); Bloom.render(); Sunshafts.render(); }
|
||||||
@Inject(method={"close", "setLevel"}, at=@At("HEAD"))
|
@Inject(method={"close", "setLevel"}, at=@At("HEAD"))
|
||||||
private void sanctuary$close(CallbackInfo ci) { PixelShadows.release(); ProceduralPbr.release(); EdgeHighlights.release(); Bloom.release(); ColoredLights.release(); ScreenSpaceGI.release(); Sunshafts.release(); }
|
private void sanctuary$close(CallbackInfo ci) { PixelShadows.release(); ProceduralPbr.release(); EdgeHighlights.release(); Bloom.release(); ColoredLights.release(); ScreenSpaceGI.release(); Sunshafts.release(); }
|
||||||
}
|
}
|
||||||
|
|||||||
+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 StarterCompanions.Store starters;
|
||||||
public CycleConfig cycleConfig=CycleConfig.DEFAULT;
|
public CycleConfig cycleConfig=CycleConfig.DEFAULT;
|
||||||
public TutorialConfig tutorialConfig=TutorialConfig.DEFAULT;
|
public TutorialConfig tutorialConfig=TutorialConfig.DEFAULT;
|
||||||
|
public AccessLedger access;
|
||||||
Session(InhabitantStore inhabitants, ProgressionConfig config, MobNamesConfig names) {
|
Session(InhabitantStore inhabitants, ProgressionConfig config, MobNamesConfig names) {
|
||||||
this.inhabitants=inhabitants; this.config=config; this.names=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.clientboundConfiguration().register(StarterPayloads.Welcome.TYPE,StarterPayloads.Welcome.CODEC);
|
||||||
PayloadTypeRegistry.serverboundConfiguration().register(StarterPayloads.Create.TYPE,StarterPayloads.Create.CODEC);
|
PayloadTypeRegistry.serverboundConfiguration().register(StarterPayloads.Create.TYPE,StarterPayloads.Create.CODEC);
|
||||||
PayloadTypeRegistry.serverboundConfiguration().register(StarterPayloads.NamedCreate.TYPE,StarterPayloads.NamedCreate.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();
|
SanctuarySitting.register();
|
||||||
fr.koka.sanctuary.inventory.HotbarService.register();
|
fr.koka.sanctuary.inventory.HotbarService.register();
|
||||||
fr.koka.sanctuary.inventory.InventorySortService.register();
|
fr.koka.sanctuary.inventory.InventorySortService.register();
|
||||||
@@ -87,6 +92,9 @@ public final class ProgressionService {
|
|||||||
});
|
});
|
||||||
ServerConfigurationConnectionEvents.CONFIGURE.register((handler,server) -> {
|
ServerConfigurationConnectionEvents.CONFIGURE.register((handler,server) -> {
|
||||||
if (session(server) == null) return;
|
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)) {
|
if (!ServerConfigurationNetworking.canSend(handler,ProgressionPayloads.Hello.TYPE)) {
|
||||||
handler.disconnect(label("client_required")); return;
|
handler.disconnect(label("client_required")); return;
|
||||||
}
|
}
|
||||||
@@ -147,6 +155,12 @@ public final class ProgressionService {
|
|||||||
ServerConfigurationNetworking.registerGlobalReceiver(StarterPayloads.NamedCreate.TYPE,(payload,context)->context.server().execute(()->{
|
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());
|
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) ->
|
ServerConfigurationNetworking.registerGlobalReceiver(ProgressionPayloads.IntroFinished.TYPE, (payload,context) ->
|
||||||
context.server().execute(() -> {
|
context.server().execute(() -> {
|
||||||
HelloTask task = PENDING.get(context.packetListener());
|
HelloTask task = PENDING.get(context.packetListener());
|
||||||
@@ -194,6 +208,7 @@ public final class ProgressionService {
|
|||||||
private static void start(MinecraftServer server) {
|
private static void start(MinecraftServer server) {
|
||||||
if (!SanctuaryGameplay.supported(server)) return;
|
if (!SanctuaryGameplay.supported(server)) return;
|
||||||
try {
|
try {
|
||||||
|
AccessConfig.load(CONFIG);
|
||||||
var config=ProgressionConfig.load(CONFIG.resolve("progression.json"));
|
var config=ProgressionConfig.load(CONFIG.resolve("progression.json"));
|
||||||
var names=MobNamesConfig.load(CONFIG.resolve("mob-names.json"));
|
var names=MobNamesConfig.load(CONFIG.resolve("mob-names.json"));
|
||||||
Path world=server.getWorldPath(LevelResource.ROOT);
|
Path world=server.getWorldPath(LevelResource.ROOT);
|
||||||
@@ -210,6 +225,7 @@ public final class ProgressionService {
|
|||||||
session.cycleConfig=CycleConfig.load(CONFIG.resolve("cycle-factions.json"));
|
session.cycleConfig=CycleConfig.load(CONFIG.resolve("cycle-factions.json"));
|
||||||
session.tutorialConfig=TutorialConfig.load(CONFIG.resolve("tutorial.json"));
|
session.tutorialConfig=TutorialConfig.load(CONFIG.resolve("tutorial.json"));
|
||||||
session.cycles=new CycleStore(world.resolve("data/sanctuary-cycle-factions.json"),server.overworld().getSeed());
|
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()) {
|
for (var event:session.cycles.events()) {
|
||||||
if (inhabitants.get(event.actor())==null || (!event.target().isEmpty() && inhabitants.get(UUID.fromString(event.target()))==null))
|
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");
|
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 static final Type TYPE=new Type("sanctuary:hello_world");
|
||||||
private final ServerConfigurationPacketListenerImpl handler;
|
private final ServerConfigurationPacketListenerImpl handler;
|
||||||
private final MinecraftServer server;
|
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; }
|
HelloTask(ServerConfigurationPacketListenerImpl handler,MinecraftServer server) { this.handler=handler;this.server=server; }
|
||||||
public Type type() { return TYPE; }
|
public Type type() { return TYPE; }
|
||||||
public void start(Consumer<Packet<?>> sender) {
|
public void start(Consumer<Packet<?>> sender) {
|
||||||
@@ -255,6 +274,8 @@ public final class ProgressionService {
|
|||||||
void offer(String error) {
|
void offer(String error) {
|
||||||
var profile=((ProgressionConfigurationAccess)handler).sanctuary$profile();
|
var profile=((ProgressionConfigurationAccess)handler).sanctuary$profile();
|
||||||
var session=session(server);var inhabitant=session.inhabitants.get(profile.id());var starters=session.starters.get(profile.id());
|
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){
|
if(starters!=null){
|
||||||
var identity=new ProgressionPayloads.Hello(profile.name(),inhabitant==null?session.inhabitants.available():java.util.List.of(inhabitant.color()),error);
|
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()));
|
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) {
|
void create(ProgressionPayloads.Create payload) {
|
||||||
if (awaitingIntro || awaitingStarter) return;
|
if (awaitingIntro || awaitingStarter) return;
|
||||||
if (!handler.isAcceptingMessages()) { PENDING.remove(handler); return; }
|
if (!handler.isAcceptingMessages()) { PENDING.remove(handler); return; }
|
||||||
|
if (AccessConfig.enabled() && !admitted) { offer("code_required"); return; }
|
||||||
Session session=session(server);
|
Session session=session(server);
|
||||||
if (session == null || !session.healthy) { handler.disconnect(label("storage_invalid")); return; }
|
if (session == null || !session.healthy) { handler.disconnect(label("storage_invalid")); return; }
|
||||||
try {
|
try {
|
||||||
@@ -288,6 +310,7 @@ public final class ProgressionService {
|
|||||||
}
|
}
|
||||||
void createWithStarter(StarterPayloads.Create payload,String name) {
|
void createWithStarter(StarterPayloads.Create payload,String name) {
|
||||||
if(awaitingIntro||!handler.isAcceptingMessages())return;
|
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;}
|
var session=session(server);if(session==null||!session.healthy){handler.disconnect(label("storage_invalid"));return;}
|
||||||
try {
|
try {
|
||||||
var profile=((ProgressionConfigurationAccess)handler).sanctuary$profile();var uuid=profile.id();
|
var profile=((ProgressionConfigurationAccess)handler).sanctuary$profile();var uuid=profile.id();
|
||||||
@@ -303,6 +326,94 @@ public final class ProgressionService {
|
|||||||
awaitingStarter=false;beginIntro(uuid);
|
awaitingStarter=false;beginIntro(uuid);
|
||||||
}catch(Exception error){fail(error);}
|
}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){
|
void fail(Exception error){
|
||||||
session(server).healthy=false;LOG.error("Could not persist Hello World starter",error);handler.disconnect(label("storage_invalid"));
|
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;
|
if (!restore(player)) return;
|
||||||
sendNames(player,session);
|
sendNames(player,session);
|
||||||
try {
|
try {
|
||||||
|
if (session.access != null) session.access.commit(player.getUUID());
|
||||||
StarterCompanions.deliver(player,session.starters);
|
StarterCompanions.deliver(player,session.starters);
|
||||||
if (session.inhabitants.arrive(player.getUUID())) player.level().getServer().getPlayerList()
|
if (session.inhabitants.arrive(player.getUUID())) player.level().getServer().getPlayerList()
|
||||||
.broadcastSystemMessage(Component.literal("helloworld ").append(player.getDisplayName()),false);
|
.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. */
|
/** Melt only known weather deposits, using the existing precipitation column sampler. */
|
||||||
public final class SeasonalSnowMelt {
|
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)
|
public static final GameRule<Boolean> ENABLED = GameRuleBuilder.forBoolean(true)
|
||||||
.category(GameRuleCategory.UPDATES).buildAndRegister(SanctuaryMod.id("seasonal_snow_melt"));
|
.category(GameRuleCategory.UPDATES).buildAndRegister(SanctuaryMod.id("seasonal_snow_melt"));
|
||||||
|
|
||||||
@@ -77,12 +80,7 @@ public final class SeasonalSnowMelt {
|
|||||||
var chunk = level.getChunkAt(column);
|
var chunk = level.getChunkAt(column);
|
||||||
var entries = chunk.getAttached(NATURAL);
|
var entries = chunk.getAttached(NATURAL);
|
||||||
if (entries == null || entries.isEmpty()) return;
|
if (entries == null || entries.isEmpty()) return;
|
||||||
int chance = switch (WeatherService.season(level.getServer())) {
|
int chance = chance(WeatherService.season(level.getServer()));
|
||||||
case WINTER -> 0;
|
|
||||||
case SPRING -> 2;
|
|
||||||
case SUMMER -> 1;
|
|
||||||
case AUTUMN -> 4;
|
|
||||||
};
|
|
||||||
if (chance == 0 || level.getRandom().nextInt(chance) != 0) return;
|
if (chance == 0 || level.getRandom().nextInt(chance) != 0) return;
|
||||||
var pos = level.getHeightmapPos(Heightmap.Types.MOTION_BLOCKING, column);
|
var pos = level.getHeightmapPos(Heightmap.Types.MOTION_BLOCKING, column);
|
||||||
// Thin layers are above the heightmap; a full snow block is just below it.
|
// 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);
|
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()); }
|
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.GameRule;
|
||||||
import net.minecraft.world.level.gamerules.GameRuleCategory;
|
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 {
|
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)
|
public static final GameRule<Integer> MAX_HEIGHT = GameRuleBuilder.forInteger(2)
|
||||||
.range(-1, 128).category(GameRuleCategory.UPDATES)
|
.range(-1, 128).category(GameRuleCategory.UPDATES)
|
||||||
.buildAndRegister(SanctuaryMod.id("snow_accumulation_height"));
|
.buildAndRegister(SanctuaryMod.id("snow_accumulation_height"));
|
||||||
@@ -38,7 +41,7 @@ public final class SnowAccumulation {
|
|||||||
/** Return whether vanilla should perform its own deposit. */
|
/** Return whether vanilla should perform its own deposit. */
|
||||||
public static boolean replaceNative(ServerLevel level, BlockPos pos, boolean canSnow) {
|
public static boolean replaceNative(ServerLevel level, BlockPos pos, boolean canSnow) {
|
||||||
if (!canSnow || !ENABLED.contains(level.getServer())) return 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;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
{
|
{
|
||||||
|
"sanctuary.graphics.default_vulkan": "Vulkan (default)",
|
||||||
"sanctuary.statuary.clay_ready": "Clay: original model colors.",
|
"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.selected": "Real Time weather: %s for %s (%s). The daily forecast resumes at midnight.",
|
||||||
"sanctuary.weather.overcast": "Overcast",
|
"sanctuary.weather.overcast": "Overcast",
|
||||||
@@ -266,6 +267,21 @@
|
|||||||
"sanctuary.progression.choose_color": "Choose your colour",
|
"sanctuary.progression.choose_color": "Choose your colour",
|
||||||
"sanctuary.progression.cancel": "Cancel",
|
"sanctuary.progression.cancel": "Cancel",
|
||||||
"sanctuary.progression.enter": "Enter Sanctuary",
|
"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.joining": "Character saved, joining…",
|
||||||
"sanctuary.progression.bio_invalid": "Use plain text for your bio (256 characters).",
|
"sanctuary.progression.bio_invalid": "Use plain text for your bio (256 characters).",
|
||||||
"sanctuary.progression.color_taken": "Colour just taken. Choose another one.",
|
"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_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.ssgi_intensity_value": "SSGI: %s%%",
|
||||||
"sanctuary.shaders.pbr": "PBR · material relief",
|
"sanctuary.shaders.pbr": "PBR · material relief",
|
||||||
"sanctuary.shaders.pbr_help": "Experimental: procedural block normals and roughness, sun and moon highlights. 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": "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_help": "Material relief and highlights. Zero stops rendering and frees the cache. Original textures remain unchanged.",
|
||||||
"sanctuary.shaders.pbr_intensity_value": "PBR: %s%%",
|
"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.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.selected": "Météo Real Time : %s pour la journée du %s (%s). Le tirage quotidien reprend à minuit.",
|
||||||
"sanctuary.weather.overcast": "Ciel gris",
|
"sanctuary.weather.overcast": "Ciel gris",
|
||||||
@@ -266,6 +267,21 @@
|
|||||||
"sanctuary.progression.choose_color": "Choisis ta couleur",
|
"sanctuary.progression.choose_color": "Choisis ta couleur",
|
||||||
"sanctuary.progression.cancel": "Annuler",
|
"sanctuary.progression.cancel": "Annuler",
|
||||||
"sanctuary.progression.enter": "Entrer dans Sanctuary",
|
"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.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.bio_invalid": "La bio doit contenir du texte simple (256 caractères).",
|
||||||
"sanctuary.progression.color_taken": "Couleur déjà prise. Choisis-en une autre.",
|
"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_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.ssgi_intensity_value": "SSGI : %s %%",
|
||||||
"sanctuary.shaders.pbr": "PBR · relief des matériaux",
|
"sanctuary.shaders.pbr": "PBR · relief des matériaux",
|
||||||
"sanctuary.shaders.pbr_help": "Expérimental : normales procédurales et rugosité des blocs, reflets du soleil et de la lune. 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": "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_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 %%",
|
"sanctuary.shaders.pbr_intensity_value": "PBR : %s %%",
|
||||||
|
|||||||
@@ -5,6 +5,20 @@ uniform sampler2D SceneColor;
|
|||||||
uniform sampler2D BlockAtlas;
|
uniform sampler2D BlockAtlas;
|
||||||
uniform sampler2D MaterialAtlas;
|
uniform sampler2D MaterialAtlas;
|
||||||
uniform sampler2D SunDepth;
|
uniform sampler2D SunDepth;
|
||||||
|
uniform sampler2D LightDirection;
|
||||||
|
uniform sampler2D PreviousDirection;
|
||||||
|
layout(std140) uniform ColoredLightScene {
|
||||||
|
mat4 LocalInverseViewProjection;
|
||||||
|
vec4 CameraStrength;
|
||||||
|
vec4 ClipSkyFog;
|
||||||
|
vec4 FogDistance;
|
||||||
|
vec4 VolumeSize;
|
||||||
|
vec4 Atlas;
|
||||||
|
vec4 PreviousCamera;
|
||||||
|
vec4 PreviousAtlas;
|
||||||
|
vec4 DynamicPosition[128];
|
||||||
|
vec4 DynamicColor[128];
|
||||||
|
};
|
||||||
layout(std140) uniform PbrScene {
|
layout(std140) uniform PbrScene {
|
||||||
mat4 PbrInverseVP;
|
mat4 PbrInverseVP;
|
||||||
vec4 PbrSun;
|
vec4 PbrSun;
|
||||||
@@ -34,7 +48,55 @@ float visibility(vec3 p,vec3 n){
|
|||||||
vec3 q=light.xyz/light.w;q.xy=q.xy*0.5+0.5;
|
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(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;
|
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.
|
||||||
|
vec4 directionField(sampler2D field,vec3 p,vec4 atlas){
|
||||||
|
if(textureSize(field,0).x<int(atlas.z))return vec4(0);
|
||||||
|
vec3 edges=min(p,vec3(atlas.x)-p);float margin=smoothstep(14.0,18.0,min(edges.x,min(edges.y,edges.z)));
|
||||||
|
if(margin<=0.0)return vec4(0);
|
||||||
|
int side=int(atlas.x),cols=int(atlas.y);vec3 at=p-.5;ivec3 base=ivec3(floor(at));vec3 f=fract(at);vec4 sum=vec4(0);
|
||||||
|
for(int y=0;y<2;y++)for(int z=0;z<2;z++)for(int x=0;x<2;x++){
|
||||||
|
ivec3 q=clamp(base+ivec3(x,y,z),ivec3(0),ivec3(side-1));
|
||||||
|
vec4 cell=texelFetch(field,ivec2(q.x+(q.y%cols)*side,q.z+(q.y/cols)*side),0);
|
||||||
|
float w=(x==0?1.0-f.x:f.x)*(y==0?1.0-f.y:f.y)*(z==0?1.0-f.z:f.z);
|
||||||
|
sum+=vec4((cell.rgb*255.0-128.0)/127.0*cell.a,cell.a)*w;
|
||||||
|
}
|
||||||
|
return sum*margin;
|
||||||
|
}
|
||||||
|
vec4 localArrival(vec3 p,vec3 normal){
|
||||||
|
if(PbrEnvironment.y<.5)return vec4(0);
|
||||||
|
vec3 offset=normal*.51;
|
||||||
|
vec4 field=directionField(LightDirection,p+CameraStrength.xyz+offset,Atlas);
|
||||||
|
if(VolumeSize.y<1.0){
|
||||||
|
vec4 before=PreviousCamera.w>.5?directionField(PreviousDirection,p+PreviousCamera.xyz+offset,PreviousAtlas):vec4(0);
|
||||||
|
field=mix(before,field,smoothstep(0.0,1.0,VolumeSize.y));
|
||||||
|
}
|
||||||
|
for(int i=0;i<int(VolumeSize.w);i++){
|
||||||
|
vec3 delta=DynamicPosition[i].xyz-p;float distance=length(delta),radius=DynamicPosition[i].w;
|
||||||
|
if(distance>=radius||distance<.001)continue;
|
||||||
|
float power=max(0.0,(pow(.8,15.0-radius+distance)-pow(.8,15.0))/(1.0-pow(.8,15.0)));
|
||||||
|
float weight=power/(power+.08)*smoothstep(0.0,1.0,DynamicColor[i].a);
|
||||||
|
field+=vec4(delta/distance*weight,weight);
|
||||||
|
}
|
||||||
|
float magnitude=length(field.xyz);if(magnitude<.00001||field.a<.00001)return vec4(0);
|
||||||
|
float confidence=smoothstep(.02,1.0,magnitude/field.a);
|
||||||
|
float fade=(1.0-smoothstep(VolumeSize.x*.75,VolumeSize.x,length(p)))*smoothstep(0.0,1.0,VolumeSize.z);
|
||||||
|
return vec4(field.xyz/magnitude,confidence*min(field.a,1.0)*fade*clamp(CameraStrength.w/0.3,0.0,1.0));
|
||||||
|
}
|
||||||
|
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=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);
|
||||||
|
// 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(){
|
void main(){
|
||||||
// Derivatives precede divergent discard; UV-space tangents preserve rotated/mirrored block textures.
|
// Derivatives precede divergent discard; UV-space tangents preserve rotated/mirrored block textures.
|
||||||
@@ -42,37 +104,40 @@ void main(){
|
|||||||
vec2 tx=dFdx(blockUv),ty=dFdy(blockUv);
|
vec2 tx=dFdx(blockUv),ty=dFdy(blockUv);
|
||||||
float det=tx.x*ty.y-tx.y*ty.x;
|
float det=tx.x*ty.y-tx.y*ty.x;
|
||||||
vec3 baseNormal=normalize(cross(dx,dy));if(dot(baseNormal,-relativePosition)<0.0)baseNormal=-baseNormal;
|
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);
|
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;
|
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;
|
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);
|
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.
|
// 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;
|
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 n=normalize(baseNormal+tangent*slope.x+bitangent*slope.y);
|
||||||
vec3 l=normalize(PbrSun.xyz),v=normalize(-relativePosition),h=normalize(l+v);
|
// Reject degenerate/interpolated geometry without replacing the native scene with NaNs.
|
||||||
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);
|
if(any(isnan(n))||any(isinf(n))||dot(relativePosition,relativePosition)<.000001)discard;
|
||||||
float roughness=max(material.b,0.25),alpha=roughness*roughness,a2=alpha*alpha;
|
vec3 l=normalize(PbrSun.xyz),v=normalize(-relativePosition);
|
||||||
float denom=nh*nh*(a2-1.0)+1.0;
|
float nl=clamp(dot(n,l),0.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);
|
|
||||||
vec3 albedo=texel.rgb*tint.rgb;albedo*=albedo;
|
vec3 albedo=texel.rgb*tint.rgb;albedo*=albedo;
|
||||||
vec3 f0=mix(vec3(0.04),albedo,material.a);
|
vec3 specular=localShine(n,v,l,material,albedo);
|
||||||
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;
|
|
||||||
float fog=max(clamp((max(length(relativePosition.xz),abs(relativePosition.y))-PbrFog.x)/max(PbrFog.y-PbrFog.x,.001),0.0,1.0),
|
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));
|
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)
|
float amount=PbrSun.w*pow(tint.a,8.0)*visibility(relativePosition,baseNormal)*(1.0-fog*PbrEnvironment.x)
|
||||||
*(1.0-smoothstep(PbrParams.w*.75,PbrParams.w,length(relativePosition)));
|
*(1.0-smoothstep(PbrParams.w*.75,PbrParams.w,length(relativePosition)));
|
||||||
if(amount<=0.0001)discard;
|
vec4 arrival=localArrival(relativePosition,baseNormal);
|
||||||
vec3 scene=texture(SceneColor,uv).rgb;vec3 linear=scene*scene;
|
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=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.
|
// Native diffuse light already exists. Apply only the perturbed-normal difference, then add GGX.
|
||||||
float relief=(nl-max(dot(baseNormal,l),0.0))*.7;
|
float relief=(nl-max(dot(baseNormal,l),0.0))*.7;
|
||||||
vec3 lit=clamp(linear*(1.0+relief*amount),0.0,1.0);
|
vec3 lit=clamp(linear*(1.0+relief*amount),0.0,1.0);
|
||||||
vec3 shine=min(specular*PbrParams.x*amount*3.0,vec3(0.5));
|
if(localAmount>.0001){
|
||||||
|
float localRelief=(max(dot(n,arrival.xyz),0.0)-max(dot(baseNormal,arrival.xyz),0.0))*.7;
|
||||||
|
lit=clamp(lit*(1.0+localRelief*localAmount),0.0,1.0);
|
||||||
|
}
|
||||||
|
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));
|
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);
|
fragColor=vec4(sqrt(clamp(lit,0.0,1.0)),1.0);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -277,7 +277,8 @@
|
|||||||
"client.SculptureParticlesMixin",
|
"client.SculptureParticlesMixin",
|
||||||
"client.PixelShadowRendererMixin",
|
"client.PixelShadowRendererMixin",
|
||||||
"client.PixelShadowLevelMixin",
|
"client.PixelShadowLevelMixin",
|
||||||
"client.PixelShadowDirtyMixin"
|
"client.PixelShadowDirtyMixin",
|
||||||
|
"client.PreferredGraphicsApiMixin"
|
||||||
],
|
],
|
||||||
"injectors": {
|
"injectors": {
|
||||||
"defaultRequire": 1
|
"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"
|
name = "Sanctuary"
|
||||||
author = "KOKA99CAB"
|
author = "KOKA99CAB"
|
||||||
version = "beta.140"
|
version = "beta.144"
|
||||||
pack-format = "packwiz:1.1.0"
|
pack-format = "packwiz:1.1.0"
|
||||||
|
|
||||||
[index]
|
[index]
|
||||||
|
|||||||
Reference in New Issue
Block a user