Compare commits

..
Author SHA1 Message Date
koka 01a82c3c93 merge: align Alpha Indev release with alpha.219
Verification 26.2 / check (pull_request) Canceled after 0s
2026-08-30 04:17:04 +02:00
koka f23f741a9d Merge pull request 'release: alpha.217 audio et alpha.218 terminal de stockage' (#94) from release/26.2.0-alpha.218 into main
Verification 26.2 / check (push) Canceled after 0s
Reviewed-on: #94
2026-08-30 02:08:32 +00:00
koka 4714c69255 chore(release): prepare Alpha Indev integration
Verification 26.2 / check (pull_request) Canceled after 0s
2026-08-30 03:59:19 +02:00
koka c03e5d8555 feat(ambiance): scope Golden Days to Alpha (#46) 2026-08-30 03:59:10 +02:00
koka 9abf6bbea7 feat(iliketomoveit): activate Alpha portal (#45) 2026-08-30 03:59:05 +02:00
koka 9c09664778 feat(sanctuary): port finite Indev island (#44) 2026-08-30 03:59:00 +02:00
koka 87f11f6127 release: sequence audio alpha.217 and terminal alpha.218
Verification 26.2 / check (pull_request) Canceled after 0s
2026-08-30 03:55:28 +02:00
koka c04cd78ae9 test(ambiance): verify white disc playback cadence 2026-08-30 03:48:15 +02:00
koka 7ae99a12a8 Merge pull request 'feat(redstoner): ajouter le terminal de stockage' (#91) from feature/storage-terminal into main
Verification 26.2 / check (push) Canceled after 0s
Reviewed-on: #91
2026-08-30 01:27:15 +00:00
koka 1b1f3a791d feat(redstoner): add storage terminal
Verification 26.2 / check (pull_request) Canceled after 0s
2026-08-30 03:13:48 +02:00
koka 10b1cd9c5e Merge pull request 'Fix white disc audio cadence and release alpha.216' (#89) from fix/white-disc-audio-quality into main
Verification 26.2 / check (push) Canceled after 0s
Reviewed-on: #89
2026-08-29 23:26:10 +00:00
koka ef18e0ae32 Fix white disc audio cadence and release alpha.216
Verification 26.2 / check (pull_request) Canceled after 0s
2026-08-30 01:19:02 +02:00
koka 1c2e000da3 Merge pull request 'release: Sanctuary 26.2.0-alpha.215 — disque blanc stabilisé' (#88) from release/26.2.0-alpha.215 into main
Verification 26.2 / check (push) Canceled after 0s
Reviewed-on: #88
2026-08-29 22:54:40 +00:00
koka 9e35c244ec release: prepare 26.2.0-alpha.215
Verification 26.2 / check (pull_request) Canceled after 0s
2026-08-30 00:52:59 +02:00
koka e82cb33407 Merge pull request 'fix(ambiance): stabiliser la lecture du disque blanc' (#87) from fix/white-disc-playback-crash into main
Verification 26.2 / check (push) Canceled after 0s
Reviewed-on: #87
2026-08-29 22:46:44 +00:00
koka e126ec9eea fix(ambiance): stabilize white disc playback
Verification 26.2 / check (pull_request) Canceled after 0s
2026-08-29 21:36:04 +02:00
koka 13a74ddbd2 Merge pull request 'release: Sanctuary 26.2.0-alpha.214 — disque blanc solo' (#85) from release/26.2.0-alpha.214 into main
Verification 26.2 / check (push) Canceled after 0s
Reviewed-on: #85
2026-08-29 18:44:13 +00:00
koka b221ca1b52 release: prepare 26.2.0-alpha.214
Verification 26.2 / check (pull_request) Canceled after 0s
2026-08-29 20:40:54 +02:00
koka 05505279f8 Merge pull request 'fix(ambiance): attendre la synchronisation du disque blanc' (#84) from fix/white-disc-solo-sync into main
Verification 26.2 / check (push) Canceled after 0s
Reviewed-on: #84
2026-08-29 18:35:33 +00:00
koka 61e66d3a11 fix(ambiance): attendre la synchronisation du disque blanc
Verification 26.2 / check (pull_request) Canceled after 0s
2026-08-29 20:30:41 +02:00
koka 3aa36ed129 Merge pull request 'release: Sanctuary 26.2.0-alpha.213 — correctif Ambiance' (#83) from release/26.2.0-alpha.213 into main
Verification 26.2 / check (push) Canceled after 0s
Reviewed-on: #83
2026-08-29 18:03:43 +00:00
koka 90b9eee27e release: prepare 26.2.0-alpha.213
Verification 26.2 / check (pull_request) Canceled after 0s
2026-08-29 20:02:22 +02:00
koka 7d800d8932 Merge pull request 'fix(ambiance): corriger le crash des événements de niveau' (#82) from fix/ambiance-level-event-crash into main
Verification 26.2 / check (push) Canceled after 0s
Reviewed-on: #82
2026-08-29 17:57:58 +00:00
koka 913a754f20 fix(ambiance): corriger le crash des événements de niveau
Verification 26.2 / check (pull_request) Canceled after 0s
2026-08-29 19:54:46 +02:00
koka 73d32999e5 Merge pull request 'release: Sanctuary 26.2.0-alpha.212 — correctif de démarrage' (#81) from release/26.2.0-alpha.212 into main
Verification 26.2 / check (push) Canceled after 0s
Reviewed-on: #81
2026-08-29 17:43:40 +00:00
koka 86034ca71f release: prepare 26.2.0-alpha.212
Verification 26.2 / check (pull_request) Canceled after 0s
2026-08-29 19:41:04 +02:00
koka db91698c09 Merge pull request 'fix(sanctuary): corriger le crash du clic molette' (#80) from fix/black-market-middle-click-crash into main
Verification 26.2 / check (push) Canceled after 0s
Reviewed-on: #80
2026-08-29 17:35:20 +00:00
koka 7f74114d31 fix(sanctuary): corriger le crash du clic molette
Verification 26.2 / check (pull_request) Canceled after 0s
2026-08-29 19:32:54 +02:00
koka 53db634ec2 Merge pull request 'release: Sanctuary 26.2.0-alpha.211 — Abjection et correctifs' (#79) from release/26.2.0-alpha.211 into main
Verification 26.2 / check (push) Canceled after 0s
Reviewed-on: #79
2026-08-29 17:04:04 +00:00
koka 400f1f6289 release: prepare 26.2.0-alpha.211
Verification 26.2 / check (pull_request) Canceled after 0s
2026-08-29 18:57:52 +02:00
koka 596432f3f7 Merge pull request 'feat(onlyfun): ajouter la touche Abjection' (#78) from feature/abjection-keybind into main
Verification 26.2 / check (push) Canceled after 0s
Reviewed-on: #78
2026-08-29 16:51:17 +00:00
koka 14980c993f Merge branch 'main' into feature/abjection-keybind
Verification 26.2 / check (pull_request) Canceled after 0s
# Conflicts:
#	CHANGELOG.md
#	build.gradle
2026-08-29 18:49:57 +02:00
koka e17433f28f Merge pull request 'feat(ouch): restaurer le projectile Abjection' (#77) from feature/abjection-projectile into main
Verification 26.2 / check (push) Canceled after 0s
Reviewed-on: #77
2026-08-29 16:41:13 +00:00
koka 91b4def490 feat(onlyfun): ajouter la touche Abjection 2026-08-29 15:21:38 +02:00
100 changed files with 5016 additions and 237 deletions
+89 -2
View File
@@ -6,9 +6,92 @@ Ce journal suit les **versions du modpack**. Lorsqu'un seul module change, sa ve
- Depuis `26.2.0-alpha.6`, le pack et les modules ont des versions indépendantes. I Like To Move It et Only Fun deviennent les cinquième et sixième modules actifs dans l'alpha 46.
- Les versions ci-dessous ont été reconstituées à partir des JAR archivés, des packs locaux et du code source actuel. Les dates n'étant pas enregistrées de manière fiable, elles ne sont pas inventées.
## Non distribué
## `26.2.0-alpha.219`
Modules modifiés : **It's Alive `0.0.0-alpha.36`**, **Sanctuary `0.0.0-alpha.119`**, **Ambiance `0.0.0-alpha.15`**, **Only Fun `0.0.0-alpha.23`** et **Ouch `0.0.0-alpha.10`**. Le pack distribué reste en `26.2.0-alpha.210` jusqu’à la prochaine release.
Modules modifiés : **Sanctuary `0.0.0-alpha.122`**, **I Like To Move It `0.0.0-alpha.16`** et **Ambiance `0.0.0-alpha.21`**. Les sept autres modules conservent leur version issue de l'alpha.218.
- ajoute `sanctuary:alpha`, une île Minecraft Indev finie de 128 × 128 × 64 blocs, déterministe par seed, avec plages, cavernes, minerais, eau, lave, végétation et maison de départ historique ;
- borne le monde par un fond en bedrock et un anneau de barrières, sans placement au-delà de X/Z 64…63, et expose les positions stables de la résidence et de l'arrivée sûre via l'API publique de Sanctuary ;
- ajoute la dimension, son biome, son type et ses réglages aux profils Sanctuary et Overworld, tout en conservant la migration `portals:alpha` vers `sanctuary:alpha` ;
- active la route de portail v2 en cobblestone moussue, cadre 4 × 5 et intérieur 2 × 3, avec validation serveur, refus si la dimension manque, paire persistante, déliaison à la casse et arrivée distante hors de la maison ;
- compose localement les packs officiels Golden Days Base puis Alpha uniquement dans `sanctuary:alpha`, restaure exactement la pile précédente à la sortie ou déconnexion et laisse les textures vanilla fonctionner si les ZIP manquent ;
- ne distribue aucun asset Golden Days dans les JAR, Packwiz ou `.mrpack`, documente les liens et la licence officiels, et n'impose aucun fichier au serveur dédié ;
- ajoute les traductions FR/EN/RU et des tests déterministes pour le générateur, le portail et la composition des packs ;
- conserve Sanctuary data v11/protocole 22, I Like To Move It data v3/protocole 5 et Ambiance data v2/protocole 6, sans réinitialisation de sauvegarde.
## `26.2.0-alpha.218`
Modules modifiés : **Red-Stoner `0.0.0-alpha.11`**, **Only Fun `0.0.0-alpha.25`** et **Sanctuary `0.0.0-alpha.121`**. Les sept autres modules conservent leur version issue de l'alpha.217.
- ajoute `redstoner:storage_terminal`, une demi-dalle verticale de huit pixels qui se pose uniquement sur la face horizontale dun coffre, coffre piégé ou baril ;
- découvre sans charger de chunk les conteneurs adjacents par face, verticalement compris, et refuse clairement tout réseau qui dépasse 128 blocs physiques ;
- agrège uniquement les piles dont litem et tous les composants sont strictement identiques, puis les affiche sur des pages défilantes de 54 entrées ;
- valide côté serveur la distance, le terminal, la topologie, la page et lidentité de chaque pile avant un retrait par pile, un retrait unitaire ou un dépôt par shift-clic ;
- fabrique le terminal avec quatre plaques dargent, trois verres teintés, un Controller et un lingot de titane, sans entrée Shop, Black Market ni récompense de lootbox ;
- ajoute les traductions FR/EN/RU, les ressources bloc/item/loot/recette et des tests déterministes de topologie, pagination, agrégation et transferts ;
- conserve les données Red-Stoner v3, Only Fun v2 et Sanctuary v11 ; seul le protocole réseau Red-Stoner passe de 5 à 6, sans nouveau monde ni réinitialisation des sauvegardes.
## `26.2.0-alpha.217`
Module modifié : **Ambiance `0.0.0-alpha.20`**. Les neuf autres modules conservent leur version issue de l'alpha.216.
- corrige la hauteur trop grave et la vitesse trop lente du disque blanc en compensant la cadence du flux AAC YouTube par un facteur OpenAL exact de `4/3` ;
- réapplique cette hauteur pendant toute la lecture afin quaucune mise à jour du canal audio ne la ramène à la valeur ralentie ;
- valide la correction sur le lien signalé en comparant vingt secondes décodées par Lavaplayer au flux AAC direct, puis conserve les tests PCM 48 kHz et de niveau sonore ;
- conserve les données Ambiance v2, le protocole réseau 6, les IDs et les sauvegardes existantes ;
- distribue Ambiance `0.0.0-alpha.20` dans Packwiz et le `.mrpack` partagé client/serveur, sans exiger de nouveau monde ni réinitialiser les sauvegardes.
## `26.2.0-alpha.216`
Module modifié : **Ambiance `0.0.0-alpha.19`**. Les neuf autres modules conservent leur version issue de l'alpha.215.
- corrige le disque blanc ralenti et dégradé en remplissant continûment les tampons PCM dune seconde demandés par Minecraft au lieu de les compléter prématurément avec du silence ;
- conserve une fréquence réelle de 48 kHz, un flux mono 16 bits little-endian et force la vitesse OpenAL normale à `1.0` ;
- borne chaque attente de trame à 250 ms après le démarrage afin de garder un flux continu sans bloquer indéfiniment le thread audio ;
- ajoute un test déterministe des tampons Minecraft et renforce le test YouTube réel sur quatre secondes complètes ;
- conserve les données Ambiance v2, le protocole réseau 6, les IDs et les sauvegardes existantes ;
- distribue Ambiance `0.0.0-alpha.19` dans Packwiz et le `.mrpack` partagé client/serveur, sans exiger de nouveau monde ni réinitialiser les sauvegardes.
## `26.2.0-alpha.215`
Module modifié : **Ambiance `0.0.0-alpha.18`**. Les neuf autres modules conservent leur version issue de l'alpha.214.
- corrige le crash natif déclenché par linsertion ou larrêt du disque blanc en laissant Minecraft libérer le canal OpenAL sur son thread audio ;
- rétablit le décodage audible du lien YouTube signalé grâce au profil iOS corrigé et à une révision épinglée de `youtube-source` ;
- borne uniquement lattente initiale du flux puis poursuit la lecture sans blocage, avec silence de compensation lorsque les données arrivent en retard ;
- transforme les erreurs et blocages de lecture en arrêt propre accompagné dun message au joueur ;
- conserve les données Ambiance v2, le protocole réseau 6, les IDs et les sauvegardes existantes ;
- distribue Ambiance `0.0.0-alpha.18` dans Packwiz et le `.mrpack` partagé client/serveur, sans exiger de nouveau monde ni réinitialiser les sauvegardes.
## `26.2.0-alpha.214`
Module modifié : **Ambiance `0.0.0-alpha.17`**. Les neuf autres modules conservent leur version issue de l'alpha.213.
- attend de façon bornée la synchronisation client du jukebox avant de charger un disque blanc, ce qui rétablit sa lecture en solo ;
- affiche immédiatement le chargement puis un échec explicite si le jukebox n'est toujours pas synchronisé après cinq secondes ;
- accepte toujours les liens de partage YouTube avec paramètres, notamment `https://youtu.be/Ue5ZBe-GzSM?si=…` ;
- conserve les données Ambiance v2 et le protocole réseau 6, sans migration de sauvegarde ;
- distribue Ambiance `0.0.0-alpha.17` dans Packwiz et le `.mrpack` partagé client/serveur, sans exiger de nouveau monde ni réinitialiser les sauvegardes.
## `26.2.0-alpha.213`
Module modifié : **Ambiance `0.0.0-alpha.16`**. Les neuf autres modules conservent leur version issue de l'alpha.212.
- corrige le crash client au démarrage d'Ambiance sous Minecraft 26.2 en ciblant le nouveau gestionnaire vanilla `LevelEventHandler` pour l'arrêt du disque blanc ;
- conserve les données Ambiance v2 et le protocole réseau 6, sans migration de sauvegarde ;
- remplace le JAR Ambiance cassé dans Packwiz et le `.mrpack` partagé client/serveur, sans exiger de nouveau monde ni réinitialiser les sauvegardes.
## `26.2.0-alpha.212`
Module modifié : **Sanctuary `0.0.0-alpha.120`**. Les neuf autres modules conservent leur version issue de l'alpha.211.
- corrige le crash client au démarrage provoqué par le mixin du clic molette du Black Market sous Minecraft 26.2 ;
- cible désormais la méthode vanilla `pickBlockOrEntity`, sans modifier les données Sanctuary v11 ni le protocole réseau 22.
- remplace le JAR Sanctuary cassé dans Packwiz et le `.mrpack` partagé client/serveur, sans exiger de nouveau monde ni réinitialiser les sauvegardes.
## `26.2.0-alpha.211`
Modules modifiés : **Ambiance `0.0.0-alpha.15`**, **It's Alive `0.0.0-alpha.36`**, **Only Fun `0.0.0-alpha.24`**, **Ouch `0.0.0-alpha.10`** et **Sanctuary `0.0.0-alpha.119`**. Les cinq autres modules conservent leur version issue de lalpha.210.
- remplace les pages noires du Livre interdit par laffichage clair du livre vanilla ;
- conserve les seize entrées, les découvertes personnelles, le partage sur pupitre, les IDs, les données v3 et le protocole réseau 4.
@@ -23,6 +106,10 @@ Modules modifiés : **It's Alive `0.0.0-alpha.36`**, **Sanctuary `0.0.0-alpha.11
- rend `abjection:shit_ball` lançable à la main et par dispenser grâce à une fabrique publique installée par Ouch, sans réenregistrer lobjet possédé par Only Fun ;
- restaure le projectile historique de 0,25 bloc, sa consommation unique hors créatif, son rendu dobjet, son impact boueux et ses sept secondes de nausée ajoutées à la durée existante ;
- déclare la dépendance sans cycle `ouch -> onlyfun` ; les données et protocoles réseau dOuch et dOnly Fun restent inchangés.
- ajoute la touche configurable « Chier » dans les contrôles Only Fun, assignée à `J` par défaut et sans répétition lorsque la touche reste maintenue ;
- transmet une demande C2S vide, déduplique le spam dun même tick et revalide côté serveur la connexion, la vie et le mode spectateur avant de réutiliser exactement laction `/shit` ;
- conserve les données Only Fun v2 et porte son protocole réseau de 2 à 3, sans nouveau cooldown, besoin ni donnée persistante.
- distribue les cinq JAR mis à jour dans Packwiz et le `.mrpack` partagé client/serveur, sans exiger de nouveau monde ni réinitialiser les sauvegardes.
## `26.2.0-alpha.210`
+9 -3
View File
@@ -12,7 +12,7 @@ Licence : **GNU GPL 3.0 or Later** (`GPL-3.0-or-later`)
| `anotherworld` | Another World | choses matérielles | matière, photographie, instruments météo et 108 biomes natifs |
| `itsalive` | It's Alive ! | vivant | Creepers, créatures, agriculture et gameplay serveur complet de Canaplia |
| `ambiance` | Ambiance | perception | atmosphère, météo, lumières dynamiques, audio positionnel, calendrier et composition visuelle accessible |
| `redstoner` | Red-Stoner | calcul et signaux | ordinateurs, identité serveur, Drawer et Particuleur |
| `redstoner` | Red-Stoner | calcul et signaux | ordinateurs, identité serveur, terminal de stockage, Drawer et Particuleur |
| `iliketomoveit` | I Like To Move It | déplacement | portails, véhicules, grappin, poulies et waystones locaux |
| `ouch` | Ouch | combat et destruction | arsenal actif (6 armes) et TNT météo |
| `onlyfun` | Only Fun | règles ludiques absurdes | lootboxes, Lucky Block, raretés et règles absurdes |
@@ -22,11 +22,17 @@ Licence : **GNU GPL 3.0 or Later** (`GPL-3.0-or-later`)
Les descriptions FR/EN/RU sont déclarées dans chaque `fabric.mod.json` et dans les fichiers de langue. Les images fournies servent d'icônes Fabric de leur module.
Version courante du pack distribué : **`26.2.0-alpha.210`**. Cette release distribue le Livre interdit Canaplia avec It's Alive **`0.0.0-alpha.35`**, ainsi que Sanctuary **`0.0.0-alpha.118`**, Only Fun **`0.0.0-alpha.21`** et I Like To Move It **`0.0.0-alpha.15`**.
Version courante du pack distribué : **`26.2.0-alpha.219`**. Cette release distribue Sanctuary **`0.0.0-alpha.122`**, I Like To Move It **`0.0.0-alpha.16`** et Ambiance **`0.0.0-alpha.21`** ; les sept autres modules conservent leur version issue de l'alpha.218.
La migration [CANA-01 à CANA-09](CANAPLIA.md) porte Canaplia vers Minecraft 26.2. It's Alive **`0.0.0-alpha.35`** enregistre les seize familles, les découvertes, hybridations, résines, effets serveur et le Livre interdit personnel, partageable temporairement sur un pupitre. Ambiance **`0.0.0-alpha.14`** compose les profils visuels bornés avec ses réglages d'accessibilité, et Only Fun **`0.0.0-alpha.21`** exclut explicitement Canaplia et les raccourcis de progression du Shop.
Lalpha.210 ajoute le Livre interdit, accélère le Happy Ghast piloté, regroupe les corrections du Shop, des requêtes, des capes et des noms de Zombies, puis actualise JEI en **`30.28.0.193`**. Elle met à jour les packs Modrinth, Prism et Packwiz sans exiger de nouveau monde ni réinitialiser les sauvegardes.
Lalpha.219 porte l'île finie Minecraft Indev dans `sanctuary:alpha` : 128 × 128 × 64 blocs déterministes, plages, cavernes, minerais, fluides, végétation et maison historique, le tout borné sans chunks parasites. Le cadre 4 × 5 en cobblestone moussue est maintenant actif et crée une paire persistante à une arrivée sûre hors de la maison. Ambiance peut enfin composer localement Golden Days Base puis Alpha uniquement dans cette dimension, sans redistribuer leurs textures et en restaurant exactement la pile précédente. Voir le [contrat Alpha](sanctuary/ALPHA.md), le [contrat des portails](iliketomoveit/PORTALS.md) et le [guide Golden Days](ambiance/GOLDEN_DAYS.md).
Lalpha.218 ajoute le terminal de stockage Red-Stoner : cette demi-dalle verticale se fixe sur un coffre, un coffre piégé ou un baril et donne accès, par pages défilantes de 54 types, à un réseau contigu borné à 128 conteneurs chargés. Les retraits et dépôts sont validés par le serveur, les composants ditems restent strictement séparés et le terminal endgame est exclu du Shop, du Black Market et des récompenses Only Fun.
Lalpha.217 corrige définitivement la hauteur trop grave du disque blanc : la cadence AAC mesurée sur la vidéo signalée est compensée par un facteur OpenAL exact de 4/3, maintenu pendant toute la lecture.
Lalpha.216 corrige la lecture lente et dégradée du disque blanc : les quatre tampons PCM initiaux de Minecraft sont maintenant remplis continûment à 48 kHz, avec une vitesse OpenAL normale, au lieu d’être majoritairement complétés par du silence. Les packs Modrinth, Prism et Packwiz sont mis à jour sans exiger de nouveau monde ni réinitialiser les sauvegardes.
Lalpha.207 rend le stockage du coffre-fort pratiquement illimité : rubis, saphirs et émeraudes utilisent chacun un solde 64 bits. Les 27 anciennes piles sont additionnées automatiquement lors du chargement, le Shop continue de consommer directement ces soldes et les 20 % pillés sont crédités dans le coffre-fort de lattaquant sans créer des millions dentités ou de piles dobjets.
+36
View File
@@ -0,0 +1,36 @@
# Textures historiques Golden Days dans Alpha
Ambiance peut charger les packs officiels Golden Days uniquement pendant que
le client se trouve dans `sanctuary:alpha`. Aucun fichier, texture ou dérivé de
Golden Days n'est distribué dans les JAR ou dans le `.mrpack` Sanctuary.
## Installation cliente facultative
1. Télécharger [Golden Days Base](https://www.curseforge.com/minecraft/texture-packs/golden-days)
depuis sa page officielle.
2. Télécharger [Golden Days Alpha](https://www.curseforge.com/minecraft/texture-packs/golden-days-alpha)
depuis sa page officielle.
3. Placer les deux ZIP sans les extraire dans le dossier `resourcepacks/` du
client. Les noms doivent conserver les marqueurs `golden-days-base` et
`golden-days-alpha` ; les espaces et `_` sont aussi reconnus.
4. Laisser les deux packs désactivés dans l'écran vanilla. Ambiance les compose
temporairement dans l'ordre **Base**, puis **Alpha**.
À l'entrée dans Alpha, Ambiance capture la pile sélectionnée exacte, ajoute les
deux packs en mémoire et recharge les ressources. À la sortie ou à la
déconnexion, la pile capturée est restaurée exactement. Le fichier d'options
vanilla n'est jamais modifié : après une interruption ou un crash pendant un
rechargement, le prochain démarrage retrouve donc la pile enregistrée.
Si un des deux packs manque ou si un rechargement échoue, le client reste
jouable avec les textures vanilla et affiche un message lisible. Un serveur
dédié n'a besoin d'aucun ZIP.
## Licence et redistribution
Golden Days est publié sous **All Rights Reserved**. Sa
[licence officielle](https://poeticrainbow.github.io/golden-days/license/)
interdit notamment de redistribuer ou de reconditionner les ressources sans
autorisation. Sanctuary ne fournit que l'intégration locale ci-dessus et les
liens officiels. Les archives de développement restent hors du dépôt et de
tous les artefacts distribués.
+13
View File
@@ -0,0 +1,13 @@
# Third-party notices
## Golden Days Base and Golden Days Alpha
Optional client resource packs by PoeticRainbow. No Golden Days asset is
included in Ambiance or Sanctuary.
- Base project: https://www.curseforge.com/minecraft/texture-packs/golden-days
- Alpha project: https://www.curseforge.com/minecraft/texture-packs/golden-days-alpha
- License: https://poeticrainbow.github.io/golden-days/license/
Golden Days is All Rights Reserved. Players obtain the two archives separately
from the official project pages for local use.
+7 -5
View File
@@ -24,8 +24,10 @@ Ambiance conserve ses données v2 ; son protocole passe de 5 à 6.
## Dépendances clientes embarquées
Le décodage repose sur Lavaplayer `2.2.7` (Apache-2.0) et youtube-source `1.18.2` (MIT), ainsi que
leurs dépendances transitives placées en JAR imbriqués par Loom. Leurs avis de licence restent dans
les archives dorigine. Laccès YouTube est effectué uniquement par le client et peut cesser de
fonctionner si YouTube modifie ses mécanismes de lecture ; aucune clé ou URL arbitraire nest
acceptée par le serveur.
Le décodage repose sur Lavaplayer `2.2.7` (Apache-2.0) et sur le commit youtube-source
`f45bbb7aebfcbc1c553769e04af6cd43afa8b7c3` (MIT), épinglé pour intégrer les correctifs de lecture
YouTube publiés après `1.18.2`. Le profil iOS corrigé est utilisé sans compte, OAuth, jeton privé ou
service de chiffrement tiers. Les dépendances sont placées en JAR imbriqués par Loom et leurs avis
de licence restent dans les archives dorigine. Laccès YouTube est effectué uniquement par le
client et peut cesser de fonctionner si YouTube modifie ses mécanismes de lecture ; aucune clé ou
URL arbitraire nest acceptée par le serveur.
+112 -7
View File
@@ -1,5 +1,7 @@
import groovy.json.JsonSlurper
def youtubeSourceVersion = "f45bbb7aebfcbc1c553769e04af6cd43afa8b7c3-SNAPSHOT"
configurations {
whiteDiscLibraries
implementation.extendsFrom whiteDiscLibraries
@@ -13,14 +15,14 @@ dependencies {
exclude group: "org.slf4j", module: "slf4j-api"
exclude group: "org.jetbrains", module: "annotations"
}
whiteDiscLibraries("dev.lavalink.youtube:v2:1.18.2") {
whiteDiscLibraries("dev.lavalink.youtube:v2:${youtubeSourceVersion}") {
exclude group: "org.slf4j", module: "slf4j-api"
exclude group: "org.jetbrains", module: "annotations"
}
[
"dev.arbjerg:lavaplayer-natives:2.2.7",
"dev.arbjerg:lava-common:2.2.7",
"dev.lavalink.youtube:common:1.18.2",
"dev.lavalink.youtube:common:${youtubeSourceVersion}",
"org.mozilla:rhino-engine:1.7.15",
"org.mozilla:rhino:1.7.15",
"commons-io:commons-io:2.13.0",
@@ -40,6 +42,12 @@ dependencies {
}
}
tasks.named("jar") {
from("THIRD_PARTY_NOTICES.md") {
rename { "THIRD_PARTY_NOTICES_ambiance.md" }
}
}
tasks.register("ambianceVisualApiSmoke", JavaExec) {
group = "verification"
description = "Checks bounded visual-effect requests exposed to gameplay modules."
@@ -64,15 +72,41 @@ tasks.register("whiteDiscUrlSmoke", JavaExec) {
mainClass = "fr.koka99cab.sanctuary26.ambiance.white_disc.WhiteDiscUrlSmoke"
}
tasks.register("whiteDiscPlaybackSmoke", JavaExec) {
group = "verification"
description = "Manually checks live YouTube loading and PCM decoding for the white disc."
dependsOn tasks.named("testClasses")
classpath = sourceSets.test.runtimeClasspath
mainClass = "fr.koka99cab.sanctuary26.ambiance.client.audio.WhiteDiscPlaybackSmoke"
}
tasks.register("whiteDiscStreamSmoke", JavaExec) {
group = "verification"
description = "Checks continuous 48 kHz PCM buffering for Minecraft's white-disc channel."
dependsOn tasks.named("testClasses")
classpath = sourceSets.test.runtimeClasspath
mainClass = "fr.koka99cab.sanctuary26.ambiance.client.audio.LavaplayerAudioStreamSmoke"
}
tasks.register("alphaTexturePackManagerSmoke", JavaExec) {
group = "verification"
description = "Checks local Golden Days detection, ordering and exact stack preservation."
dependsOn tasks.named("testClasses")
classpath = sourceSets.test.runtimeClasspath
mainClass = "fr.koka99cab.sanctuary26.ambiance.client.AlphaTexturePackManagerSmoke"
}
tasks.named("test") {
failOnNoDiscoveredTests = false
}
tasks.register("verifyAmbiance") {
group = "verification"
description = "Checks Ambiance alpha.15, including the bounded YouTube white-disc playback contract."
description = "Checks Ambiance alpha.21, including corrected white-disc pitch and Alpha textures."
inputs.files(fileTree("src/main/java"))
inputs.files(fileTree("src/main/resources"))
inputs.file(file("src/test/java/fr/koka99cab/sanctuary26/ambiance/client/audio/WhiteDiscPlaybackSmoke.java"))
inputs.file(file("src/test/java/fr/koka99cab/sanctuary26/ambiance/client/audio/LavaplayerAudioStreamSmoke.java"))
inputs.file(rootProject.file("pack/migrations/26.2.0-alpha.125-realtime-calendar-to-alpha.126.json"))
inputs.file(rootProject.file("pack/migrations/26.2.0-alpha.127-daily-ambiance-sodium-iris-to-alpha.128.json"))
inputs.file(rootProject.file("pack/migrations/26.2.0-alpha.128-ambiance-visuals-canon-iris-to-alpha.129.json"))
@@ -120,8 +154,14 @@ tasks.register("verifyAmbiance") {
file("src/main/resources/data/ambiance/recipe/white_disc.json"))
def whiteDiscSong = new JsonSlurper().parse(
file("src/main/resources/data/ambiance/jukebox_song/white_disc.json"))
def levelEventMixin = file(
"src/main/java/fr/koka99cab/sanctuary26/ambiance/mixin/client/LevelEventHandlerMixin.java").text
def whiteDiscAudioPlayer = file(
"src/main/java/fr/koka99cab/sanctuary26/ambiance/client/audio/WhiteDiscAudioPlayer.java").text
def whiteDiscPlaybackSmoke = file(
"src/test/java/fr/koka99cab/sanctuary26/ambiance/client/audio/WhiteDiscPlaybackSmoke.java").text
if (project.version.toString() != "0.0.0-alpha.15"
if (project.version.toString() != "0.0.0-alpha.21"
|| migration.source?.pack_version != "26.2.0-alpha.125"
|| migration.target?.pack_version != "26.2.0-alpha.126"
|| migration.target?.modules?.ambiance != "0.0.0-alpha.1"
@@ -319,10 +359,26 @@ tasks.register("verifyAmbiance") {
|| !javaText.contains('PlayerLookup.tracking(level, pos)')
|| !javaText.contains('ServerPlayNetworking.canSend(receiver, WhiteDiscPlaybackPayload.ID)')
|| !javaText.contains('MAXIMUM_SIMULTANEOUS_DISCS = 4')
|| !javaText.contains('REQUESTS.size() + PLAYBACKS.size() >= MAXIMUM_SIMULTANEOUS_DISCS')
|| !javaText.contains('PENDING_STARTS.size() + REQUESTS.size() + PLAYBACKS.size()')
|| !javaText.contains('MAXIMUM_START_WAIT_TICKS = 100')
|| !javaText.contains('tickPendingStarts(client)')
|| !javaText.contains('client.level.dimension().equals(playback.dimension())')
|| !javaText.contains('MAXIMUM_TRACK_MILLIS = 30L * 60L * 1_000L')
|| !javaText.contains('new Pcm16AudioDataFormat(1, LavaplayerAudioStream.SAMPLE_RATE')
|| !javaText.contains('new YoutubeAudioSourceManager(false, new Ios())')
|| !javaText.contains('player.addListener(new AudioEventAdapter()')
|| !javaText.contains('failPlayback(client, key, failedPlayer)')
|| !javaText.contains('initialWaitAttempted')
|| !javaText.contains('CONTINUATION_FRAME_WAIT_MILLIS = 250L')
|| !javaText.contains('player.provide(waitMillis, TimeUnit.MILLISECONDS)')
|| !whiteDiscAudioPlayer.contains('PLAYBACK_PITCH = 4.0F / 3.0F')
|| whiteDiscAudioPlayer.count('channel.setPitch(PLAYBACK_PITCH)') != 2
|| !whiteDiscAudioPlayer.contains('handle.execute(channel -> channel.stop())')
|| whiteDiscAudioPlayer.contains('.release()')
|| !whiteDiscPlaybackSmoke.contains('https://www.youtube.com/watch?v=Ue5ZBe-GzSM')
|| !whiteDiscPlaybackSmoke.contains('PCM cadence: 100%% speed')
|| !whiteDiscPlaybackSmoke.contains('stream.decodedTimelineMillis() != 4_000L')
|| !whiteDiscPlaybackSmoke.contains('stream.paddedSilenceBytes() != 0L')
|| !javaText.contains('SoundSource.RECORDS')
|| !javaText.contains('channel.linearAttenuation(ATTENUATION_DISTANCE)')
|| !javaText.contains('LONG_HOSTS = Set.of(')
@@ -334,6 +390,9 @@ tasks.register("verifyAmbiance") {
|| whiteDiscRecipe.key?.W != "minecraft:white_dye"
|| whiteDiscSong.sound_event != "minecraft:intentionally_empty"
|| whiteDiscSong.length_in_seconds != 1800
|| !levelEventMixin.contains('@Mixin(LevelEventHandler.class)')
|| !levelEventMixin.contains('@Inject(method = "levelEvent"')
|| !levelEventMixin.contains('ambiance$stopWhiteDisc(int type, BlockPos pos, int data,')
|| ambianceLanguages.any { locale, translations ->
!["item.ambiance.white_disc", "tooltip.ambiance.white_disc",
"message.ambiance.white_disc.invalid_url", "message.ambiance.white_disc.failed"].every {
@@ -341,8 +400,10 @@ tasks.register("verifyAmbiance") {
}
}
|| !file("build.gradle").text.contains('dev.arbjerg:lavaplayer:2.2.7')
|| !file("build.gradle").text.contains('dev.lavalink.youtube:v2:1.18.2')) {
throw new GradleException("Ambiance alpha.15 white-disc playback contract is incomplete")
|| !file("build.gradle").text.contains(
'def youtubeSourceVersion = "f45bbb7aebfcbc1c553769e04af6cd43afa8b7c3-SNAPSHOT"')
|| !file("build.gradle").text.contains('dev.lavalink.youtube:v2:${youtubeSourceVersion}')) {
throw new GradleException("Ambiance white-disc playback contract is incomplete")
}
[
@@ -391,9 +452,53 @@ tasks.register("verifyAmbiance") {
}
}
tasks.register("verifyAlphaTextures") {
group = "verification"
description = "Checks the alpha.21 local-only Golden Days overlay and exact restore contract."
inputs.file(file("GOLDEN_DAYS.md"))
inputs.file(file("THIRD_PARTY_NOTICES.md"))
inputs.file(file("src/main/java/fr/koka99cab/sanctuary26/ambiance/client/AlphaTexturePackManager.java"))
inputs.files(fileTree("src/main/resources/assets/ambiance/lang") { include "*.json" })
inputs.file(rootProject.file("pack/migrations/26.2.0-alpha.218-alpha-indev-to-alpha.219.json"))
doLast {
def migration = new JsonSlurper().parse(rootProject.file(
"pack/migrations/26.2.0-alpha.218-alpha-indev-to-alpha.219.json"))
def manager = file(
"src/main/java/fr/koka99cab/sanctuary26/ambiance/client/AlphaTexturePackManager.java").text
def locales = ["fr_fr", "en_us", "ru_ru"].collect {
new JsonSlurper().parse(file("src/main/resources/assets/ambiance/lang/${it}.json"))
}
def forbiddenArchives = rootProject.fileTree(rootProject.projectDir) {
include "**/*golden*days*.zip", "**/*golden*days*.jar"
exclude "**/build/**", "**/.gradle/**", "**/.git/**"
}.files
if (project.version.toString() != "0.0.0-alpha.21"
|| migration.alpha_textures?.destination != "sanctuary:alpha"
|| migration.alpha_textures?.client_only != true
|| migration.alpha_textures?.bundled_assets != false
|| migration.alpha_textures?.overlay_order != ["base", "alpha"]
|| migration.alpha_textures?.previous_selection_restored_exactly != true
|| migration.alpha_textures?.saved_options_modified != false
|| migration.alpha_textures?.dedicated_server_files_required != false
|| !manager.contains("SanctuaryDimensions.ALPHA")
|| !manager.contains("previousSelection = List.copyOf")
|| !manager.contains("selectionWithOverlay(previousSelection")
|| manager.contains("updateResourcePacks")
|| locales.any { !it.containsKey("message.ambiance.alpha_textures_missing")
|| !it.containsKey("message.ambiance.alpha_textures_restore_failed") }
|| !forbiddenArchives.isEmpty()
|| !file("THIRD_PARTY_NOTICES.md").text.contains("No Golden Days asset")) {
throw new GradleException("Ambiance alpha.21 Golden Days overlay contract is incomplete")
}
}
}
tasks.named("check") {
dependsOn tasks.named("verifyAmbiance")
dependsOn tasks.named("ambianceScheduleSmoke")
dependsOn tasks.named("ambianceVisualApiSmoke")
dependsOn tasks.named("whiteDiscUrlSmoke")
dependsOn tasks.named("whiteDiscStreamSmoke")
dependsOn tasks.named("alphaTexturePackManagerSmoke")
dependsOn tasks.named("verifyAlphaTextures")
}
@@ -0,0 +1,192 @@
package fr.koka99cab.sanctuary26.ambiance.client;
import fr.koka99cab.sanctuary26.ambiance.AmbianceMod;
import fr.koka99cab.sanctuary26.sanctuary.world.SanctuaryDimensions;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
import java.util.List;
import java.util.Locale;
import java.util.Optional;
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
import net.fabricmc.fabric.api.client.networking.v1.ClientPlayConnectionEvents;
import net.minecraft.client.Minecraft;
import net.minecraft.network.chat.Component;
import net.minecraft.server.packs.repository.PackRepository;
/**
* Applies locally installed Golden Days packs only while the player is in
* {@code sanctuary:alpha}. The selected stack is changed in memory only; the
* player's saved resource-pack options remain untouched.
*/
public final class AlphaTexturePackManager {
static final String BASE_MARKER = "golden-days-base";
static final String ALPHA_MARKER = "golden-days-alpha";
private static final int MAX_RESTORE_RETRIES = 1;
private static boolean requestKnown;
private static boolean alphaRequested;
private static boolean reloadInProgress;
private static boolean failedForCurrentRequest;
private static boolean missingMessageShown;
private static boolean reconcilePending = true;
private static int restoreFailures;
private static List<String> previousSelection = List.of();
private static boolean previousSelectionCaptured;
private AlphaTexturePackManager() {
}
public static void register() {
ClientTickEvents.END_CLIENT_TICK.register(AlphaTexturePackManager::tick);
ClientPlayConnectionEvents.DISCONNECT.register((handler, client) -> request(client, false));
}
private static void tick(Minecraft client) {
boolean shouldUseAlphaTextures = client.level != null
&& client.level.dimension().equals(SanctuaryDimensions.ALPHA);
request(client, shouldUseAlphaTextures);
if (!reloadInProgress && reconcilePending) {
reconcilePending = false;
reconcile(client);
}
}
private static void request(Minecraft client, boolean requested) {
if (requestKnown && requested == alphaRequested) return;
alphaRequested = requested;
requestKnown = true;
failedForCurrentRequest = false;
missingMessageShown = false;
restoreFailures = 0;
if (requested && !previousSelectionCaptured) {
PackRepository repository = client.getResourcePackRepository();
repository.reload();
previousSelection = List.copyOf(repository.getSelectedIds());
previousSelectionCaptured = true;
}
reconcilePending = true;
}
private static void reconcile(Minecraft client) {
PackRepository repository = client.getResourcePackRepository();
repository.reload();
List<String> selected = List.copyOf(repository.getSelectedIds());
if (alphaRequested && !failedForCurrentRequest) {
Optional<PackPair> packs = findPacks(repository.getAvailableIds());
if (packs.isEmpty()) {
failedForCurrentRequest = true;
showMissingPackMessage(client);
applyIfDifferent(client, selected, previousSelection, false);
return;
}
List<String> desired = selectionWithOverlay(previousSelection, packs.orElseThrow());
applyIfDifferent(client, selected, desired, true);
return;
}
if (!previousSelectionCaptured) return;
applyIfDifferent(client, selected, previousSelection, false);
}
private static void applyIfDifferent(Minecraft client, List<String> selected,
List<String> desired, boolean enablingAlpha) {
if (selected.equals(desired)) {
if (!enablingAlpha && !alphaRequested) clearSnapshot();
return;
}
client.getResourcePackRepository().setSelected(desired);
reloadInProgress = true;
AmbianceMod.LOGGER.info("[{}] Reloading client resources for Alpha texture mode: {}.",
AmbianceMod.DISPLAY_NAME, enablingAlpha ? "enabled" : "disabled");
client.reloadResourcePacks().whenComplete((ignored, error) -> client.execute(() -> {
reloadInProgress = false;
if (error != null) {
AmbianceMod.LOGGER.error("[{}] Could not {} the Golden Days Alpha overlay.",
AmbianceMod.DISPLAY_NAME, enablingAlpha ? "enable" : "restore", error);
if (enablingAlpha) {
failedForCurrentRequest = true;
reconcilePending = true;
} else if (++restoreFailures <= MAX_RESTORE_RETRIES) {
reconcilePending = true;
} else {
client.getResourcePackRepository().setSelected(previousSelection);
showRestoreFailureMessage(client);
}
return;
}
if (!enablingAlpha && !alphaRequested) clearSnapshot();
if (enablingAlpha != alphaRequested) reconcilePending = true;
}));
}
static Optional<PackPair> findPacks(Collection<String> availableIds) {
Optional<String> base = findPack(availableIds, BASE_MARKER);
Optional<String> alpha = findPack(availableIds, ALPHA_MARKER);
return base.isPresent() && alpha.isPresent()
? Optional.of(new PackPair(base.orElseThrow(), alpha.orElseThrow()))
: Optional.empty();
}
static List<String> selectionWithOverlay(Collection<String> previous, PackPair packs) {
List<String> desired = selectedWithoutGoldenDays(previous);
// Pack order is bottom-to-top: Base first, then its Alpha overlay.
desired.add(packs.base());
desired.add(packs.alpha());
return List.copyOf(desired);
}
private static List<String> selectedWithoutGoldenDays(Collection<String> selectedIds) {
List<String> result = new ArrayList<>(selectedIds.size());
for (String id : selectedIds) {
if (!isGoldenDaysPack(id)) result.add(id);
}
return result;
}
private static Optional<String> findPack(Collection<String> availableIds, String marker) {
return availableIds.stream()
.filter(id -> normalized(id).contains(marker))
.sorted(Comparator.naturalOrder())
.findFirst();
}
static boolean isGoldenDaysPack(String id) {
String normalized = normalized(id);
return normalized.contains(BASE_MARKER) || normalized.contains(ALPHA_MARKER);
}
private static String normalized(String id) {
return id.toLowerCase(Locale.ROOT).replace('_', '-').replace(' ', '-');
}
private static void clearSnapshot() {
previousSelection = List.of();
previousSelectionCaptured = false;
restoreFailures = 0;
}
private static void showMissingPackMessage(Minecraft client) {
if (!missingMessageShown && client.player != null) {
client.gui.hud.getChat().addClientSystemMessage(
Component.translatable("message.ambiance.alpha_textures_missing"));
missingMessageShown = true;
}
}
private static void showRestoreFailureMessage(Minecraft client) {
if (client.player != null) {
client.gui.hud.getChat().addClientSystemMessage(
Component.translatable("message.ambiance.alpha_textures_restore_failed"));
}
}
record PackPair(String base, String alpha) {
PackPair {
if (base == null || alpha == null || base.equals(alpha)) {
throw new IllegalArgumentException("Golden Days Base and Alpha packs must be distinct");
}
}
}
}
@@ -33,6 +33,7 @@ public final class AmbianceClient implements ClientModInitializer {
@Override
public void onInitializeClient() {
RealtimeClient.initialize();
AlphaTexturePackManager.register();
CelestialSkyRenderer.register();
ClientTickEvents.END_CLIENT_TICK.register(DynamicTorchLights::tick);
ClientPlayNetworking.registerGlobalReceiver(AmbianceEnvironmentPayload.TYPE, (payload, context) ->
@@ -12,13 +12,19 @@ import net.minecraft.client.sounds.AudioStream;
/** Adapts bounded mono PCM frames from Lavaplayer to Minecraft's streaming OpenAL channel. */
final class LavaplayerAudioStream implements AudioStream {
static final int SAMPLE_RATE = 48_000;
private static final int FRAME_SAMPLES = 960;
private static final int BYTES_PER_SAMPLE = 2;
private static final long INITIAL_FRAME_WAIT_MILLIS = 2_000L;
private static final long CONTINUATION_FRAME_WAIT_MILLIS = 250L;
private static final AudioFormat FORMAT =
new AudioFormat(SAMPLE_RATE, 16, 1, true, false);
private final AudioPlayer player;
private byte[] pending = new byte[0];
private int pendingOffset;
private long decodedBytes;
private long paddedSilenceBytes;
private long firstFrameTimecode = -1L;
private long lastFrameTimecode = -1L;
private int decodedFrames;
private boolean initialWaitAttempted;
private volatile boolean closed;
private volatile boolean ended;
@@ -40,19 +46,25 @@ final class LavaplayerAudioStream implements AudioStream {
int length = Math.min(output.remaining(), pending.length - pendingOffset);
output.put(pending, pendingOffset, length);
pendingOffset += length;
decodedBytes += length;
continue;
}
AudioFrame frame = nextFrame();
if (frame == null) {
if (ended) break;
int silence = Math.min(output.remaining(), FRAME_SAMPLES * BYTES_PER_SAMPLE);
for (int index = 0; index < silence; index++) output.put((byte) 0);
continue;
while (output.hasRemaining()) {
output.put((byte) 0);
paddedSilenceBytes++;
}
break;
}
if (frame.isTerminator()) {
ended = true;
break;
}
if (firstFrameTimecode < 0L) firstFrameTimecode = frame.getTimecode();
lastFrameTimecode = frame.getTimecode();
decodedFrames++;
pending = frame.getData();
pendingOffset = 0;
}
@@ -63,7 +75,11 @@ final class LavaplayerAudioStream implements AudioStream {
private AudioFrame nextFrame() throws IOException {
try {
AudioFrame frame = player.provide(2, TimeUnit.SECONDS);
long waitMillis = initialWaitAttempted
? CONTINUATION_FRAME_WAIT_MILLIS
: INITIAL_FRAME_WAIT_MILLIS;
initialWaitAttempted = true;
AudioFrame frame = player.provide(waitMillis, TimeUnit.MILLISECONDS);
if (frame == null && player.getPlayingTrack() == null) ended = true;
return frame;
} catch (TimeoutException ignored) {
@@ -78,6 +94,23 @@ final class LavaplayerAudioStream implements AudioStream {
return ended;
}
boolean uses(AudioPlayer expectedPlayer) {
return player == expectedPlayer;
}
long decodedBytes() {
return decodedBytes;
}
long paddedSilenceBytes() {
return paddedSilenceBytes;
}
long decodedTimelineMillis() {
if (decodedFrames == 0) return 0L;
return lastFrameTimecode - firstFrameTimecode + 20L;
}
@Override
public void close() {
if (closed) return;
@@ -5,10 +5,12 @@ import com.sedmelluq.discord.lavaplayer.format.Pcm16AudioDataFormat;
import com.sedmelluq.discord.lavaplayer.player.AudioLoadResultHandler;
import com.sedmelluq.discord.lavaplayer.player.AudioPlayer;
import com.sedmelluq.discord.lavaplayer.player.DefaultAudioPlayerManager;
import com.sedmelluq.discord.lavaplayer.player.event.AudioEventAdapter;
import com.sedmelluq.discord.lavaplayer.tools.FriendlyException;
import com.sedmelluq.discord.lavaplayer.track.AudioPlaylist;
import com.sedmelluq.discord.lavaplayer.track.AudioTrack;
import dev.lavalink.youtube.YoutubeAudioSourceManager;
import dev.lavalink.youtube.clients.Ios;
import fr.koka99cab.sanctuary26.ambiance.AmbianceMod;
import fr.koka99cab.sanctuary26.ambiance.mixin.client.SoundEngineAccessor;
import fr.koka99cab.sanctuary26.ambiance.mixin.client.SoundManagerAccessor;
@@ -30,10 +32,14 @@ import net.minecraft.world.phys.Vec3;
/** Client-only, positional and resource-bounded YouTube playback for white discs. */
public final class WhiteDiscAudioPlayer {
private static final int MAXIMUM_SIMULTANEOUS_DISCS = 4;
private static final int MAXIMUM_START_WAIT_TICKS = 100;
private static final long MAXIMUM_TRACK_MILLIS = 30L * 60L * 1_000L;
private static final long FINISHED_GRACE_NANOS = 5_000_000_000L;
private static final float ATTENUATION_DISTANCE = 64.0F;
/** Compensates Lavaplayer's low AAC cadence before PCM reaches OpenAL. */
static final float PLAYBACK_PITCH = 4.0F / 3.0F;
private static final DefaultAudioPlayerManager MANAGER = createManager();
private static final Map<Long, PendingStart> PENDING_STARTS = new HashMap<>();
private static final Map<Long, Long> REQUESTS = new HashMap<>();
private static final LinkedHashMap<Long, Playback> PLAYBACKS = new LinkedHashMap<>();
private static long requestSequence;
@@ -43,16 +49,27 @@ public final class WhiteDiscAudioPlayer {
}
public static void play(Minecraft client, BlockPos pos, String videoId) {
if (shutdown || !WhiteDiscUrl.validVideoId(videoId) || !validTarget(client, pos)) return;
if (shutdown || !WhiteDiscUrl.validVideoId(videoId) || !validJukeboxTarget(client, pos)) return;
long key = pos.asLong();
if (REQUESTS.containsKey(key)) return;
if (PENDING_STARTS.containsKey(key) || REQUESTS.containsKey(key)) return;
boolean replacingPlayback = PLAYBACKS.containsKey(key);
if (!replacingPlayback
&& REQUESTS.size() + PLAYBACKS.size() >= MAXIMUM_SIMULTANEOUS_DISCS) return;
&& PENDING_STARTS.size() + REQUESTS.size() + PLAYBACKS.size()
>= MAXIMUM_SIMULTANEOUS_DISCS) return;
stop(key);
client.player.sendOverlayMessage(Component.translatable("message.ambiance.white_disc.loading"));
if (!validTarget(client, pos)) {
PENDING_STARTS.put(key, new PendingStart(pos.immutable(), client.level.dimension(), videoId,
MAXIMUM_START_WAIT_TICKS));
return;
}
startLoading(client, pos, videoId);
}
private static void startLoading(Minecraft client, BlockPos pos, String videoId) {
long key = pos.asLong();
long request = ++requestSequence;
REQUESTS.put(key, request);
client.player.sendOverlayMessage(Component.translatable("message.ambiance.white_disc.loading"));
MANAGER.loadItemOrdered(key, WhiteDiscUrl.canonicalUrl(videoId), new AudioLoadResultHandler() {
@Override
public void trackLoaded(AudioTrack track) {
@@ -101,24 +118,50 @@ public final class WhiteDiscAudioPlayer {
ChannelAccess channels = ((SoundEngineAccessor) ((SoundManagerAccessor) client.getSoundManager())
.ambiance$getSoundEngine()).ambiance$getChannelAccess();
channels.createHandle(Library.Pool.STREAMING).thenAccept(handle -> client.execute(() -> {
if (handle == null || REQUESTS.getOrDefault(key, -1L) != request) {
if (handle != null) handle.release();
if (handle == null || REQUESTS.getOrDefault(key, -1L) != request
|| !validTarget(client, pos)) {
if (handle != null) handle.execute(channel -> channel.stop());
if (REQUESTS.getOrDefault(key, -1L) == request) REQUESTS.remove(key);
return;
}
AudioPlayer player = MANAGER.createPlayer();
player.playTrack(track);
LavaplayerAudioStream stream = new LavaplayerAudioStream(player);
Playback playback = new Playback(pos.immutable(), client.level.dimension(), handle, stream);
PLAYBACKS.put(key, playback);
REQUESTS.remove(key);
player.addListener(new AudioEventAdapter() {
@Override
public void onTrackException(AudioPlayer failedPlayer, AudioTrack failedTrack,
FriendlyException exception) {
AmbianceMod.LOGGER.warn("White-disc playback failed for YouTube video {}: {}",
failedTrack.getIdentifier(), exception.getMessage());
client.execute(() -> failPlayback(client, key, failedPlayer));
}
@Override
public void onTrackStuck(AudioPlayer stuckPlayer, AudioTrack stuckTrack,
long thresholdMs) {
AmbianceMod.LOGGER.warn("White-disc playback stalled for YouTube video {} after {} ms",
stuckTrack.getIdentifier(), thresholdMs);
client.execute(() -> failPlayback(client, key, stuckPlayer));
}
});
player.playTrack(track);
float volume = client.options.getFinalSoundSourceVolume(SoundSource.RECORDS);
handle.execute(channel -> {
channel.setSelfPosition(Vec3.atCenterOf(pos));
channel.setRelative(false);
channel.linearAttenuation(ATTENUATION_DISTANCE);
channel.setVolume(volume);
channel.attachBufferStream(stream);
channel.play();
try {
channel.setSelfPosition(Vec3.atCenterOf(pos));
channel.setRelative(false);
channel.setPitch(PLAYBACK_PITCH);
channel.linearAttenuation(ATTENUATION_DISTANCE);
channel.setVolume(volume);
channel.attachBufferStream(stream);
channel.play();
} catch (RuntimeException | LinkageError exception) {
AmbianceMod.LOGGER.error("Unable to attach white-disc audio to Minecraft's sound engine",
exception);
client.execute(() -> failPlayback(client, key, player));
}
});
if (client.player != null) client.player.sendOverlayMessage(
Component.translatable("message.ambiance.white_disc.playing", track.getInfo().title));
@@ -132,17 +175,25 @@ public final class WhiteDiscAudioPlayer {
Component.translatable("message.ambiance.white_disc.failed"));
}
private static void failPlayback(Minecraft client, long key, AudioPlayer player) {
Playback playback = PLAYBACKS.get(key);
if (playback == null || !playback.stream().uses(player)) return;
stop(key);
if (client.player != null) client.player.sendOverlayMessage(
Component.translatable("message.ambiance.white_disc.failed"));
}
public static void stop(BlockPos pos) {
stop(pos.asLong());
}
private static void stop(long key) {
PENDING_STARTS.remove(key);
REQUESTS.remove(key);
Playback playback = PLAYBACKS.remove(key);
if (playback == null) return;
playback.stream().close();
playback.handle().execute(channel -> channel.stop());
playback.handle().release();
}
public static void tick(Minecraft client) {
@@ -150,6 +201,7 @@ public final class WhiteDiscAudioPlayer {
clear();
return;
}
tickPendingStarts(client);
long now = System.nanoTime();
for (Map.Entry<Long, Playback> entry : Map.copyOf(PLAYBACKS).entrySet()) {
Playback playback = entry.getValue();
@@ -162,11 +214,15 @@ public final class WhiteDiscAudioPlayer {
continue;
}
float volume = client.options.getFinalSoundSourceVolume(SoundSource.RECORDS);
playback.handle().execute(channel -> channel.setVolume(volume));
playback.handle().execute(channel -> {
channel.setPitch(PLAYBACK_PITCH);
channel.setVolume(volume);
});
}
}
public static void clear() {
PENDING_STARTS.clear();
REQUESTS.clear();
for (long key : PLAYBACKS.keySet().stream().mapToLong(Long::longValue).toArray()) stop(key);
}
@@ -184,16 +240,51 @@ public final class WhiteDiscAudioPlayer {
new Pcm16AudioDataFormat(1, LavaplayerAudioStream.SAMPLE_RATE, 960, false));
manager.setFrameBufferDuration(5_000);
manager.setItemLoaderThreadPoolSize(2);
manager.registerSourceManager(new YoutubeAudioSourceManager(false));
manager.registerSourceManager(new YoutubeAudioSourceManager(false, new Ios()));
return manager;
}
private static boolean validTarget(Minecraft client, BlockPos pos) {
return validJukeboxTarget(client, pos)
&& client.level.getBlockState(pos).getValue(JukeboxBlock.HAS_RECORD);
}
private static boolean validJukeboxTarget(Minecraft client, BlockPos pos) {
return client.player != null && client.level != null
&& client.player.distanceToSqr(Vec3.atCenterOf(pos))
<= ATTENUATION_DISTANCE * ATTENUATION_DISTANCE * 4.0D
&& client.level.getBlockState(pos).is(Blocks.JUKEBOX)
&& client.level.getBlockState(pos).getValue(JukeboxBlock.HAS_RECORD);
&& client.level.getBlockState(pos).is(Blocks.JUKEBOX);
}
private static void tickPendingStarts(Minecraft client) {
for (Map.Entry<Long, PendingStart> entry : Map.copyOf(PENDING_STARTS).entrySet()) {
long key = entry.getKey();
PendingStart pending = entry.getValue();
if (!client.level.dimension().equals(pending.dimension())
|| !validJukeboxTarget(client, pending.pos())) {
PENDING_STARTS.remove(key);
continue;
}
if (validTarget(client, pending.pos())) {
PENDING_STARTS.remove(key);
startLoading(client, pending.pos(), pending.videoId());
continue;
}
if (pending.ticksRemaining() <= 1) {
PENDING_STARTS.remove(key);
if (client.player != null) client.player.sendOverlayMessage(
Component.translatable("message.ambiance.white_disc.failed"));
continue;
}
PENDING_STARTS.put(key, pending.waitOneTick());
}
}
private record PendingStart(BlockPos pos, ResourceKey<Level> dimension, String videoId,
int ticksRemaining) {
private PendingStart waitOneTick() {
return new PendingStart(pos, dimension, videoId, ticksRemaining - 1);
}
}
private record Playback(BlockPos pos, ResourceKey<Level> dimension,
@@ -1,19 +1,17 @@
package fr.koka99cab.sanctuary26.ambiance.mixin.client;
import fr.koka99cab.sanctuary26.ambiance.client.audio.WhiteDiscAudioPlayer;
import net.minecraft.client.renderer.LevelRenderer;
import net.minecraft.client.renderer.LevelEventHandler;
import net.minecraft.core.BlockPos;
import net.minecraft.world.entity.Entity;
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.CallbackInfo;
@Mixin(LevelRenderer.class)
public abstract class LevelRendererMixin {
@Mixin(LevelEventHandler.class)
public abstract class LevelEventHandlerMixin {
@Inject(method = "levelEvent", at = @At("HEAD"))
private void ambiance$stopWhiteDisc(Entity source, int type, BlockPos pos, int data,
CallbackInfo callback) {
private void ambiance$stopWhiteDisc(int type, BlockPos pos, int data, CallbackInfo callback) {
if (type == 1011) WhiteDiscAudioPlayer.stop(pos);
}
}
@@ -11,7 +11,7 @@
"GameRendererMixin",
"LightCoordsUtilMixin",
"LightmapRenderStateExtractorMixin",
"LevelRendererMixin",
"LevelEventHandlerMixin",
"SoundEngineAccessor",
"SoundManagerAccessor",
"SkyRendererMixin"
@@ -32,5 +32,7 @@
"message.ambiance.white_disc.loading": "Loading YouTube video…",
"message.ambiance.white_disc.playing": "Now playing: %s",
"message.ambiance.white_disc.failed": "This YouTube video could not be played.",
"message.ambiance.white_disc.too_long": "This video is live or longer than 30 minutes."
"message.ambiance.white_disc.too_long": "This video is live or longer than 30 minutes.",
"message.ambiance.alpha_textures_missing": "Alpha textures are unavailable: install Golden Days Base then Golden Days Alpha in resourcepacks and leave them disabled.",
"message.ambiance.alpha_textures_restore_failed": "The previous resource-pack stack could not be reloaded. It will be restored on the next start."
}
@@ -32,5 +32,7 @@
"message.ambiance.white_disc.loading": "Chargement de la vidéo YouTube…",
"message.ambiance.white_disc.playing": "Lecture : %s",
"message.ambiance.white_disc.failed": "Impossible de lire cette vidéo YouTube.",
"message.ambiance.white_disc.too_long": "Cette vidéo est en direct ou dépasse 30 minutes."
"message.ambiance.white_disc.too_long": "Cette vidéo est en direct ou dépasse 30 minutes.",
"message.ambiance.alpha_textures_missing": "Textures Alpha indisponibles : installez Golden Days Base puis Golden Days Alpha dans resourcepacks en les laissant désactivés.",
"message.ambiance.alpha_textures_restore_failed": "La pile de packs précédente na pas pu être rechargée. Elle sera restaurée au prochain démarrage."
}
@@ -32,5 +32,7 @@
"message.ambiance.white_disc.loading": "Загрузка видео YouTube…",
"message.ambiance.white_disc.playing": "Сейчас играет: %s",
"message.ambiance.white_disc.failed": "Не удалось воспроизвести это видео YouTube.",
"message.ambiance.white_disc.too_long": "Это прямая трансляция или видео длиннее 30 минут."
"message.ambiance.white_disc.too_long": "Это прямая трансляция или видео длиннее 30 минут.",
"message.ambiance.alpha_textures_missing": "Текстуры Alpha недоступны: установите Golden Days Base, затем Golden Days Alpha в resourcepacks и оставьте их выключенными.",
"message.ambiance.alpha_textures_restore_failed": "Не удалось перезагрузить прежний набор ресурсов. Он будет восстановлен при следующем запуске."
}
@@ -0,0 +1,36 @@
package fr.koka99cab.sanctuary26.ambiance.client;
import java.util.List;
/** Executable tests for local Golden Days discovery and exact stack composition. */
public final class AlphaTexturePackManagerSmoke {
private AlphaTexturePackManagerSmoke() {
}
public static void main(String[] args) {
var packs = AlphaTexturePackManager.findPacks(List.of(
"vanilla",
"file/Golden Days Base 16.2 [1.20 to 26.2].zip",
"file/golden_days_alpha_16.2.zip"
)).orElseThrow(() -> new AssertionError("Golden Days packs were not detected"));
List<String> previous = List.of(
"vanilla", "file/custom.zip", "file/golden-days-alpha-old.zip");
List<String> desired = AlphaTexturePackManager.selectionWithOverlay(previous, packs);
require(desired.equals(List.of("vanilla", "file/custom.zip", packs.base(), packs.alpha())),
"Base and Alpha must be appended in bottom-to-top order without disturbing custom packs");
require(previous.equals(List.of(
"vanilla", "file/custom.zip", "file/golden-days-alpha-old.zip")),
"Composing the overlay must not mutate the captured stack");
require(AlphaTexturePackManager.findPacks(List.of("file/golden-days-base.zip")).isEmpty(),
"A partial installation must keep the vanilla stack");
require(AlphaTexturePackManager.isGoldenDaysPack("FILE/GOLDEN_DAYS_ALPHA.ZIP"),
"Pack matching must be case- and separator-insensitive");
require(!AlphaTexturePackManager.isGoldenDaysPack("file/custom-alpha.zip"),
"Unrelated packs must never be removed from the captured stack");
}
private static void require(boolean condition, String message) {
if (!condition) throw new AssertionError(message);
}
}
@@ -0,0 +1,83 @@
package fr.koka99cab.sanctuary26.ambiance.client.audio;
import com.sedmelluq.discord.lavaplayer.format.Pcm16AudioDataFormat;
import com.sedmelluq.discord.lavaplayer.player.AudioPlayer;
import com.sedmelluq.discord.lavaplayer.track.playback.AudioFrame;
import com.sedmelluq.discord.lavaplayer.track.playback.ImmutableAudioFrame;
import java.lang.reflect.Proxy;
import java.nio.ByteBuffer;
import java.util.ArrayDeque;
import java.util.Arrays;
import java.util.Queue;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
/** Deterministic check for Minecraft's four one-second streaming buffers. */
public final class LavaplayerAudioStreamSmoke {
private static final int FRAME_SAMPLES = 960;
private static final int FRAME_BYTES = FRAME_SAMPLES * 2;
private static final int FRAMES_PER_SECOND = LavaplayerAudioStream.SAMPLE_RATE / FRAME_SAMPLES;
private LavaplayerAudioStreamSmoke() {
}
public static void main(String[] args) throws Exception {
if (Math.abs(WhiteDiscAudioPlayer.PLAYBACK_PITCH - 4.0F / 3.0F) > 0.0001F) {
throw new IllegalStateException("The white-disc AAC pitch compensation changed");
}
Queue<AudioFrame> frames = new ArrayDeque<>();
Pcm16AudioDataFormat format = new Pcm16AudioDataFormat(
1, LavaplayerAudioStream.SAMPLE_RATE, FRAME_SAMPLES, false);
for (int index = 0; index < FRAMES_PER_SECOND; index++) {
byte[] data = new byte[FRAME_BYTES];
Arrays.fill(data, (byte) (index + 1));
frames.add(new ImmutableAudioFrame(index * 20L, data, 100, format));
}
AtomicInteger timedProvides = new AtomicInteger();
AtomicInteger nonBlockingProvides = new AtomicInteger();
AtomicBoolean destroyed = new AtomicBoolean();
AudioPlayer player = (AudioPlayer) Proxy.newProxyInstance(
AudioPlayer.class.getClassLoader(), new Class<?>[] {AudioPlayer.class},
(proxy, method, arguments) -> switch (method.getName()) {
case "provide" -> {
if (method.getParameterCount() == 2) {
timedProvides.incrementAndGet();
yield frames.poll();
}
nonBlockingProvides.incrementAndGet();
yield null;
}
case "destroy" -> {
destroyed.set(true);
yield null;
}
case "getPlayingTrack" -> null;
case "getVolume" -> 100;
case "isPaused", "startTrack" -> false;
default -> null;
});
LavaplayerAudioStream stream = new LavaplayerAudioStream(player);
ByteBuffer pcm = stream.read(LavaplayerAudioStream.SAMPLE_RATE * 2);
if (pcm == null || pcm.remaining() != LavaplayerAudioStream.SAMPLE_RATE * 2) {
throw new IllegalStateException("The stream did not fill Minecraft's one-second buffer");
}
while (pcm.hasRemaining()) {
if (pcm.get() == 0) {
throw new IllegalStateException("The stream padded available PCM with silence");
}
}
if (timedProvides.get() != FRAMES_PER_SECOND || nonBlockingProvides.get() != 0) {
throw new IllegalStateException("PCM frames were not read with bounded waits");
}
if (stream.getFormat().getSampleRate() != LavaplayerAudioStream.SAMPLE_RATE
|| stream.getFormat().isBigEndian()) {
throw new IllegalStateException("The OpenAL PCM format is not 48 kHz little-endian");
}
stream.close();
if (!destroyed.get()) {
throw new IllegalStateException("Closing the stream did not destroy its player");
}
}
}
@@ -0,0 +1,109 @@
package fr.koka99cab.sanctuary26.ambiance.client.audio;
import com.sedmelluq.discord.lavaplayer.format.Pcm16AudioDataFormat;
import com.sedmelluq.discord.lavaplayer.player.AudioLoadResultHandler;
import com.sedmelluq.discord.lavaplayer.player.AudioPlayer;
import com.sedmelluq.discord.lavaplayer.player.DefaultAudioPlayerManager;
import com.sedmelluq.discord.lavaplayer.tools.FriendlyException;
import com.sedmelluq.discord.lavaplayer.track.AudioPlaylist;
import com.sedmelluq.discord.lavaplayer.track.AudioTrack;
import dev.lavalink.youtube.YoutubeAudioSourceManager;
import dev.lavalink.youtube.clients.Ios;
import java.nio.ByteBuffer;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
/** Manual network smoke test for the exact YouTube playback path used by white discs. */
public final class WhiteDiscPlaybackSmoke {
private static final String VIDEO_URL = "https://www.youtube.com/watch?v=Ue5ZBe-GzSM";
private WhiteDiscPlaybackSmoke() {
}
public static void main(String[] args) throws Exception {
DefaultAudioPlayerManager manager = new DefaultAudioPlayerManager();
manager.getConfiguration().setOutputFormat(
new Pcm16AudioDataFormat(1, LavaplayerAudioStream.SAMPLE_RATE, 960, false));
manager.registerSourceManager(new YoutubeAudioSourceManager(false, new Ios()));
try {
AudioTrack track = load(manager).get(30, TimeUnit.SECONDS);
AudioPlayer player = manager.createPlayer();
player.playTrack(track);
LavaplayerAudioStream stream = new LavaplayerAudioStream(player);
int totalBytes = 0;
long squaredSampleSum = 0L;
int sampleCount = 0;
int peakSample = 0;
for (int index = 0; index < 4; index++) {
ByteBuffer pcm = stream.read(LavaplayerAudioStream.SAMPLE_RATE * 2);
if (pcm == null) break;
totalBytes += pcm.remaining();
while (pcm.remaining() >= 2) {
int low = Byte.toUnsignedInt(pcm.get());
int high = pcm.get();
short sample = (short) ((high << 8) | low);
int magnitude = Math.abs((int) sample);
peakSample = Math.max(peakSample, magnitude);
squaredSampleSum += (long) sample * sample;
sampleCount++;
}
}
int expectedBytes = LavaplayerAudioStream.SAMPLE_RATE * 2 * 4;
if (totalBytes != expectedBytes) {
throw new IllegalStateException("YouTube playback did not fill four PCM seconds");
}
if (stream.decodedBytes() != expectedBytes || stream.paddedSilenceBytes() != 0L) {
throw new IllegalStateException("YouTube playback was not 100% decoded PCM: "
+ stream.decodedBytes() + " decoded bytes and "
+ stream.paddedSilenceBytes() + " padded bytes");
}
if (stream.decodedTimelineMillis() != 4_000L) {
throw new IllegalStateException("PCM cadence mismatch: four seconds of samples cover "
+ stream.decodedTimelineMillis() + " ms of the YouTube timeline");
}
double rmsSample = Math.sqrt((double) squaredSampleSum / sampleCount);
if (rmsSample < 256.0 || peakSample < 2_048) {
throw new IllegalStateException("YouTube PCM signal is too quiet: RMS "
+ Math.round(rmsSample) + ", peak " + peakSample);
}
System.out.printf(
"PCM cadence: 100%% speed, 4000 ms decoded into 4000 ms at %.0f Hz; RMS %.0f, peak %d from %s%n",
stream.getFormat().getSampleRate(), rmsSample, peakSample, track.getInfo().title);
stream.close();
} finally {
manager.shutdown();
}
}
private static CompletableFuture<AudioTrack> load(DefaultAudioPlayerManager manager) {
CompletableFuture<AudioTrack> result = new CompletableFuture<>();
manager.loadItem(VIDEO_URL, new AudioLoadResultHandler() {
@Override
public void trackLoaded(AudioTrack track) {
result.complete(track);
}
@Override
public void playlistLoaded(AudioPlaylist playlist) {
AudioTrack selected = playlist.getSelectedTrack();
if (selected == null && !playlist.getTracks().isEmpty()) {
selected = playlist.getTracks().getFirst();
}
if (selected == null) result.completeExceptionally(
new IllegalStateException("YouTube playlist contained no track"));
else result.complete(selected);
}
@Override
public void noMatches() {
result.completeExceptionally(new IllegalStateException("YouTube returned no match"));
}
@Override
public void loadFailed(FriendlyException exception) {
result.completeExceptionally(exception);
}
});
return result;
}
}
@@ -10,6 +10,8 @@ public final class WhiteDiscUrlSmoke {
public static void main(String[] args) {
require(VIDEO_ID.equals(id("https://www.youtube.com/watch?v=" + VIDEO_ID)), "watch URL rejected");
require(VIDEO_ID.equals(id("https://youtu.be/" + VIDEO_ID + "?si=abc")), "short URL rejected");
require("Ue5ZBe-GzSM".equals(id("https://youtu.be/Ue5ZBe-GzSM?si=78LSbtBQzUYzhA2Y")),
"reported white-disc URL rejected");
require(VIDEO_ID.equals(id("https://music.youtube.com/watch?v=" + VIDEO_ID)), "music URL rejected");
require(VIDEO_ID.equals(id("https://www.youtube.com/shorts/" + VIDEO_ID)), "shorts URL rejected");
require(VIDEO_ID.equals(id("https://www.youtube.com/embed/" + VIDEO_ID)), "embed URL rejected");
+970 -112
View File
File diff suppressed because it is too large Load Diff
+6 -6
View File
@@ -12,9 +12,9 @@ fabric_api_version=0.158.0+26.2
# Standby modules stay at zero until their migration really begins.
mod_version=0.0.0-alpha.0
module_lifecycle=standby
ambiance_version=0.0.0-alpha.15
ambiance_version=0.0.0-alpha.21
ambiance_lifecycle=active
redstoner_version=0.0.0-alpha.10
redstoner_version=0.0.0-alpha.11
redstoner_lifecycle=active
anotherworld_version=0.0.0-alpha.55
anotherworld_lifecycle=active
@@ -24,11 +24,11 @@ ouch_version=0.0.0-alpha.10
ouch_lifecycle=active
masterkey_version=0.0.0-alpha.8
masterkey_lifecycle=active
iliketomoveit_version=0.0.0-alpha.15
iliketomoveit_version=0.0.0-alpha.16
iliketomoveit_lifecycle=active
onlyfun_version=0.0.0-alpha.23
onlyfun_version=0.0.0-alpha.25
onlyfun_lifecycle=active
sanctuary_version=0.0.0-alpha.119
sanctuary_version=0.0.0-alpha.122
sanctuary_lifecycle=active
pack_version=26.2.0-alpha.210
pack_version=26.2.0-alpha.219
maven_group=fr.koka99cab.sanctuary26
+10 -2
View File
@@ -29,7 +29,7 @@ Une route est un JSON sous `data/<namespace>/sanctuary_portal/<id>.json` :
- `arrival.type` vaut `surface`, `matched_y` ou `fixed_y`; ce dernier exige `arrival.y`.
- `priority` départage deux routes acceptant le même cadre.
Les neuf routes historiques restent déclarées pour la compatibilité. Pour l'alpha.150, seules deux routes sont actives : terre vers la paire Sanctuary/Overworld et pierre vers `sanctuary:caverns`. Alpha, Sheep Land, Skeleton Waste, Lost City, Painted Canyons, Breaker Sea et Laputa restent réservées mais ne sont pas chargeables.
Les neuf routes historiques restent déclarées pour la compatibilité. Depuis l'alpha.219, trois routes sont actives : terre vers la paire Sanctuary/Overworld, pierre vers `sanctuary:caverns` et cobblestone moussue vers `sanctuary:alpha`. Sheep Land, Skeleton Waste, Lost City, Painted Canyons, Breaker Sea et Laputa restent réservées mais ne sont pas chargeables.
La route en terre est la seule route v2 dynamique. Dans le profil Sanctuary, elle relie `minecraft:overworld` à `sanctuary:overworld`. Dans le profil Overworld, elle relie `minecraft:overworld` à `sanctuary:main`. Le choix appartient à la sauvegarde, jamais à une préférence cliente.
@@ -43,8 +43,16 @@ La route en terre est la seule route v2 dynamique. Dans le profil Sanctuary, ell
4. Reconstruire le même cadre réactive son endpoint; la traversée suivante régénère un lien.
5. Une destination absente refuse la traversée proprement. Elle ne crée aucune dimension et n'endommage pas la sauvegarde.
La route Alpha exige un cadre extérieur de 4 × 5 avec un intérieur vide de
2 × 3, entièrement en cobblestone moussue, puis un briquet. Le serveur valide
le cadre, l'activateur et la présence de `sanctuary:alpha` avant de l'allumer.
Sa première moitié distante utilise l'arrivée sûre calculée par l'API publique
de Sanctuary, hors de la maison Indev ; le client ne choisit ni la destination
ni les coordonnées. L'Atlas découvre le bloc de portail actif comme les autres
points d'intérêt locaux.
Le bloc intérieur actif est `iliketomoveit:portal`. Les neuf anciens blocs `portals:*_portal` restent enregistrés comme compatibilité de lecture; une première utilisation les rattache au nouvel index.
## État des générateurs
Le générateur principal Sanctuary et sa paire Overworld sont actifs depuis l'alpha.71. Caverns, Alpha, Sheep Land, Skeleton Waste, Lost City, Painted Canyons, Breaker Sea et Laputa restent réservés mais non générés. Leurs cadres peuvent être allumés et persistés ; une traversée vers une destination encore absente est refusée proprement.
Le générateur principal Sanctuary et sa paire Overworld sont actifs depuis l'alpha.71. Caverns et Alpha sont également générés ; Alpha est le monde Indev fini 128 × 128 × 64 possédé par Sanctuary. Sheep Land, Skeleton Waste, Lost City, Painted Canyons, Breaker Sea et Laputa restent réservés mais non générés. Une traversée vers une destination absente est refusée proprement dès l'allumage.
+54 -6
View File
@@ -24,6 +24,14 @@ tasks.register("happyGhastCruiseSpeedSmoke", JavaExec) {
dependsOn tasks.named("testClasses")
}
tasks.register("alphaPortalContractSmoke", JavaExec) {
group = "verification"
description = "Checks the active Alpha route and its finite-world safe-arrival contract."
classpath = sourceSets.test.runtimeClasspath
mainClass = "fr.koka99cab.sanctuary26.iliketomoveit.portal.AlphaPortalContractSmoke"
dependsOn tasks.named("testClasses")
}
tasks.named("test") {
// Runtime contracts are executable smoke tasks, not JUnit engine tests.
failOnNoDiscoveredTests = false
@@ -268,7 +276,7 @@ tasks.register("verifyPortals") {
def routeFile = file("src/main/resources/data/iliketomoveit/sanctuary_portal/${route}.json")
if (!routeFile.isFile()) throw new GradleException("Missing portal route ${route}")
def json = new JsonSlurper().parse(routeFile)
def expectedSchema = route == 'overworld' ? 2 : 1
def expectedSchema = route in ['overworld', 'alpha'] ? 2 : 1
if (json.schema_version != expectedSchema || json.destination_dimension != destination
|| json.source_dimension != "minecraft:overworld"
|| (route == 'overworld' && json.destination_resolver != "overworld_pair")
@@ -305,7 +313,7 @@ tasks.register("verifyPortals") {
}
def atlasCavernsMigration = new JsonSlurper().parse(rootProject.file(
"pack/migrations/26.2.0-alpha.149-atlas-caverns-to-alpha.150.json"))
if (project.version.toString() != "0.0.0-alpha.15"
if (project.version.toString() != "0.0.0-alpha.16"
|| atlasCavernsMigration.target?.pack_version != "26.2.0-alpha.150"
|| atlasCavernsMigration.target?.modules?.iliketomoveit != "0.0.0-alpha.9"
|| atlasCavernsMigration.portals?.active_routes != [
@@ -313,7 +321,7 @@ tasks.register("verifyPortals") {
|| atlasCavernsMigration.portals?.inactive_routes?.size() != 7
|| atlasCavernsMigration.portals?.reserved_ids_retained != true
|| atlasCavernsMigration.portals?.inactive_routes_loadable != false
|| !javaText.contains("ACTIVE_ROUTES = List.of(OVERWORLD, CAVERNS)")
|| !javaText.contains("ACTIVE_ROUTES = List.of(OVERWORLD, CAVERNS, ALPHA)")
|| !javaText.contains("REQUIRED_ROUTES = ACTIVE_ROUTES")
|| atlasCavernsMigration.compatibility?.iliketomoveit_data_version != 3
|| atlasCavernsMigration.compatibility?.iliketomoveit_network_protocol != 3) {
@@ -341,7 +349,7 @@ tasks.register("verifySilverAircraft") {
def manifest = new JsonSlurper().parse(file("src/main/resources/fabric.mod.json"))
def migration = new JsonSlurper().parse(rootProject.file(
"pack/migrations/26.2.0-alpha.184-silver-engineering-to-alpha.185.json"))
if (project.version.toString() != "0.0.0-alpha.15"
if (project.version.toString() != "0.0.0-alpha.16"
|| recipe.pattern != ["PGP", "GCG", "PBP"]
|| recipe.key != [B: "anotherworld:silver_block", C: "anotherworld:aircraft_cockpit",
G: "anotherworld:silver_gear", P: "anotherworld:silver_plate"]
@@ -370,7 +378,7 @@ tasks.register("verifyGrapplePrediction") {
def payload = file("src/main/java/fr/koka99cab/sanctuary26/iliketomoveit/network/GrappleStatePayload.java").text
def controller = file("src/main/java/fr/koka99cab/sanctuary26/iliketomoveit/grapple/GrappleController.java").text
def prediction = file("src/main/java/fr/koka99cab/sanctuary26/iliketomoveit/client/grapple/ClientGrapplePrediction.java").text
if (project.version.toString() != "0.0.0-alpha.15"
if (project.version.toString() != "0.0.0-alpha.16"
|| migration.target?.modules?.iliketomoveit != "0.0.0-alpha.13"
|| migration.grappling?.owner_prediction_payload != "iliketomoveit:s2c/grapple_state"
|| migration.compatibility?.iliketomoveit_source_network_protocol != 4
@@ -402,7 +410,7 @@ tasks.register("verifyWaystoneDescendingRelease") {
"src/main/java/fr/koka99cab/sanctuary26/iliketomoveit/waystone/WaystoneElevatorRules.java").text
def teleportation = file(
"src/main/java/fr/koka99cab/sanctuary26/iliketomoveit/network/WaystoneTeleportation.java").text
if (project.version.toString() != "0.0.0-alpha.15"
if (project.version.toString() != "0.0.0-alpha.16"
|| migration.target?.modules?.iliketomoveit != "0.0.0-alpha.14"
|| migration.waystone_elevator?.undiscovered_downward_destination_allowed != true
|| migration.waystone_elevator?.destination_discovered_after_arrival != true
@@ -418,6 +426,44 @@ tasks.register("verifyWaystoneDescendingRelease") {
}
}
tasks.register("verifyAlphaPortal") {
group = "verification"
description = "Checks the alpha.16 mossy Alpha portal lifecycle and server authority."
inputs.file(file("PORTALS.md"))
inputs.file(file("src/main/resources/data/iliketomoveit/sanctuary_portal/alpha.json"))
inputs.files(fileTree("src/main/resources/assets/iliketomoveit/lang") { include "*.json" })
inputs.files(fileTree("src/main/java/fr/koka99cab/sanctuary26/iliketomoveit/portal") { include "*.java" })
inputs.file(rootProject.file("pack/migrations/26.2.0-alpha.218-alpha-indev-to-alpha.219.json"))
doLast {
def route = new JsonSlurper().parse(file(
"src/main/resources/data/iliketomoveit/sanctuary_portal/alpha.json"))
def migration = new JsonSlurper().parse(rootProject.file(
"pack/migrations/26.2.0-alpha.218-alpha-indev-to-alpha.219.json"))
def javaText = fileTree("src/main/java/fr/koka99cab/sanctuary26/iliketomoveit/portal")
.matching { include "*.java" }.files*.text.join("\n")
def locales = ["fr_fr", "en_us", "ru_ru"].collect {
new JsonSlurper().parse(file("src/main/resources/assets/iliketomoveit/lang/${it}.json"))
}
if (project.version.toString() != "0.0.0-alpha.16"
|| route.schema_version != 2 || route.source_dimension != "minecraft:overworld"
|| route.destination_dimension != "sanctuary:alpha"
|| route.destination_resolver != "fixed"
|| route.frame_blocks != ["minecraft:mossy_cobblestone"]
|| route.activation_items != ["minecraft:flint_and_steel"]
|| migration.alpha_portal?.outer_width != 4 || migration.alpha_portal?.outer_height != 5
|| migration.alpha_portal?.inner_width != 2 || migration.alpha_portal?.inner_height != 3
|| migration.alpha_portal?.inactive_when_destination_absent != true
|| migration.alpha_portal?.persistent_pair_reused != true
|| migration.alpha_portal?.break_unlinks_pair != true
|| !javaText.contains("ACTIVE_ROUTES = List.of(OVERWORLD, CAVERNS, ALPHA)")
|| !javaText.contains("SanctuaryAlphaWorld.portalArrival(targetLevel.getSeed())")
|| !javaText.contains("getServer().getLevel(")
|| locales.any { !it.containsKey("message.iliketomoveit.portal.destination_unavailable") }) {
throw new GradleException("I Like To Move It alpha.16 Alpha portal contract is incomplete")
}
}
}
tasks.named("check") {
dependsOn tasks.named("grapplePhysicsSmoke")
dependsOn tasks.named("waystoneElevatorRulesSmoke")
@@ -428,4 +474,6 @@ tasks.named("check") {
dependsOn tasks.named("verifyWaystoneDescendingRelease")
dependsOn tasks.named("verifyPortals")
dependsOn tasks.named("verifySilverAircraft")
dependsOn tasks.named("alphaPortalContractSmoke")
dependsOn tasks.named("verifyAlphaPortal")
}
@@ -26,7 +26,7 @@ public final class PortalDefinitions {
public static final Identifier LAPUTA = ILikeToMoveItMod.id("laputa");
/** Routes enabled for the current gameplay milestone. Other stable IDs stay reserved. */
public static final List<Identifier> ACTIVE_ROUTES = List.of(OVERWORLD, CAVERNS);
public static final List<Identifier> ACTIVE_ROUTES = List.of(OVERWORLD, CAVERNS, ALPHA);
public static final List<Identifier> REQUIRED_ROUTES = ACTIVE_ROUTES;
private static volatile Map<Identifier, PortalDefinition> definitions = Map.of();
@@ -4,6 +4,7 @@ import fr.koka99cab.sanctuary26.iliketomoveit.registry.ILikeToMoveItBlocks;
import java.util.Optional;
import net.fabricmc.fabric.api.event.player.UseBlockCallback;
import net.minecraft.core.BlockPos;
import net.minecraft.network.chat.Component;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.sounds.SoundEvents;
import net.minecraft.sounds.SoundSource;
@@ -37,6 +38,12 @@ public final class PortalIgnitionHandler {
}
ServerLevel serverLevel = (ServerLevel) level;
if (serverLevel.getServer().getLevel(
selected.targetDimension(serverLevel.getServer(), serverLevel.dimension())) == null) {
player.sendOverlayMessage(Component.translatable(
"message.iliketomoveit.portal.destination_unavailable"));
return InteractionResult.FAIL;
}
PortalEndpoint endpoint = PortalSavedData.get(serverLevel.getServer())
.register(serverLevel, selectedFrame, selected.id());
selectedFrame.fill(serverLevel, selected, endpoint, ILikeToMoveItBlocks.PORTAL);
@@ -3,6 +3,8 @@ package fr.koka99cab.sanctuary26.iliketomoveit.portal;
import fr.koka99cab.sanctuary26.iliketomoveit.ILikeToMoveItMod;
import fr.koka99cab.sanctuary26.iliketomoveit.block.PortalBlock;
import fr.koka99cab.sanctuary26.iliketomoveit.registry.ILikeToMoveItBlocks;
import fr.koka99cab.sanctuary26.sanctuary.api.world.SanctuaryAlphaWorld;
import fr.koka99cab.sanctuary26.sanctuary.world.SanctuaryDimensions;
import java.util.Optional;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
@@ -92,6 +94,9 @@ public final class PortalTeleporter {
private static BlockPos targetCenter(ServerLevel sourceLevel, ServerLevel targetLevel,
PortalFrame sourceFrame, PortalDefinition definition) {
if (targetLevel.dimension().equals(SanctuaryDimensions.ALPHA)) {
return SanctuaryAlphaWorld.portalArrival(targetLevel.getSeed());
}
double scale = definition.scaleFrom(sourceLevel.dimension());
int x = Mth.floor(sourceFrame.center().getX() * scale);
int z = Mth.floor(sourceFrame.center().getZ() * scale);
@@ -1,7 +1,8 @@
{
"schema_version": 1,
"schema_version": 2,
"source_dimension": "minecraft:overworld",
"destination_dimension": "sanctuary:alpha",
"destination_resolver": "fixed",
"frame_blocks": ["minecraft:mossy_cobblestone"],
"preferred_frame_block": "minecraft:mossy_cobblestone",
"activation_items": ["minecraft:flint_and_steel"],
@@ -0,0 +1,37 @@
package fr.koka99cab.sanctuary26.iliketomoveit.portal;
import fr.koka99cab.sanctuary26.sanctuary.api.world.SanctuaryAlphaWorld;
import java.util.Set;
import net.minecraft.SharedConstants;
import net.minecraft.core.BlockPos;
import net.minecraft.server.Bootstrap;
/** Executable cross-module contract for the active mossy Alpha portal. */
public final class AlphaPortalContractSmoke {
private AlphaPortalContractSmoke() {
}
public static void main(String[] args) {
SharedConstants.tryDetectVersion();
Bootstrap.bootStrap();
require(PortalDefinitions.ACTIVE_ROUTES.equals(PortalDefinitions.REQUIRED_ROUTES),
"Every active portal definition must be required at reload");
require(Set.copyOf(PortalDefinitions.ACTIVE_ROUTES).equals(Set.of(
PortalDefinitions.OVERWORLD, PortalDefinitions.CAVERNS, PortalDefinitions.ALPHA)),
"Exactly the Overworld, Caverns and Alpha routes must be active");
for (long seed : new long[] { 0L, 1L, 46L }) {
BlockPos arrival = SanctuaryAlphaWorld.portalArrival(seed);
BlockPos residence = SanctuaryAlphaWorld.residence(seed);
require(SanctuaryAlphaWorld.containsHorizontal(arrival),
"Server-selected Alpha arrival must stay inside the finite island");
require(Math.max(Math.abs(arrival.getX() - residence.getX()),
Math.abs(arrival.getZ() - residence.getZ())) >= 10,
"The remote portal must not damage the Indev residence");
}
}
private static void require(boolean condition, String message) {
if (!condition) throw new AssertionError(message);
}
}
+9
View File
@@ -28,3 +28,12 @@ explicitement dOnly Fun sans cycle inverse, enregistre le type dentité so
`abjection:shit_ball` et installe la fabrique commune au lancer manuel et au dispenser. Le
projectile mesure 0,25 bloc, consomme une unité hors créatif, joue le son de boue à limpact et
ajoute sept secondes à la nausée déjà présente sans dupliquer dobjet.
## ABJ-03 — raccourci configurable
La catégorie de contrôles Only Fun expose « Chier », assigné à `J` par défaut. Le client utilise
`consumeClick`, puis envoie un payload vide `onlyfun:c2s/shit_action`. Le serveur accepte au plus
une demande par joueur et par tick, revalide que le joueur est toujours connecté, vivant et non
spectateur, puis appelle le même `AbjectionActions.start(SHIT)` que `/shit`. Maintenir la touche
ne produit donc pas de répétition par tick et la commande reste disponible. Only Fun conserve ses
données v2 et passe son protocole réseau de 2 à 3.
@@ -0,0 +1,43 @@
package fr.koka99cab.sanctuary26.onlyfun.abjection;
import fr.koka99cab.sanctuary26.onlyfun.api.abjection.AbjectionActions;
import fr.koka99cab.sanctuary26.onlyfun.network.ShitActionRequestPayload;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import net.fabricmc.fabric.api.networking.v1.ServerPlayConnectionEvents;
import net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerPlayer;
/** Validates connection/player state and deduplicates malicious same-tick key requests. */
public final class ShitActionRequestHandler {
private static final Map<UUID, Integer> LAST_REQUEST_TICK = new HashMap<>();
private ShitActionRequestHandler() {
}
public static void register() {
ServerPlayNetworking.registerGlobalReceiver(ShitActionRequestPayload.ID,
(payload, context) -> handle(context.player()));
ServerPlayConnectionEvents.DISCONNECT.register((handler, server) ->
clear(handler.player.getUUID()));
}
private static void handle(ServerPlayer player) {
MinecraftServer server = player.level().getServer();
if (server == null || server.getPlayerList().getPlayer(player.getUUID()) != player
|| !player.isAlive() || player.isSpectator()
|| !acceptTick(player.getUUID(), server.getTickCount())) return;
AbjectionActions.start(player, AbjectionActions.Action.SHIT);
}
static boolean acceptTick(UUID playerId, int tick) {
Integer previous = LAST_REQUEST_TICK.put(playerId, tick);
return previous == null || previous != tick;
}
static void clear(UUID playerId) {
LAST_REQUEST_TICK.remove(playerId);
}
}
@@ -3,17 +3,34 @@ package fr.koka99cab.sanctuary26.onlyfun.client;
import fr.koka99cab.sanctuary26.iliketomoveit.physics.VehicleFlightPhysics;
import fr.koka99cab.sanctuary26.onlyfun.chickenboat.ChickenBoatControlPayload;
import fr.koka99cab.sanctuary26.onlyfun.chickenboat.ChickenBoatUtil;
import fr.koka99cab.sanctuary26.onlyfun.OnlyFunMod;
import fr.koka99cab.sanctuary26.onlyfun.network.ShitActionRequestPayload;
import com.mojang.blaze3d.platform.InputConstants;
import net.fabricmc.api.ClientModInitializer;
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
import net.fabricmc.fabric.api.client.keymapping.v1.KeyMappingHelper;
import net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking;
import net.minecraft.client.KeyMapping;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.vehicle.boat.AbstractBoat;
import org.lwjgl.glfw.GLFW;
public final class OnlyFunClient implements ClientModInitializer {
private static final KeyMapping.Category ONLY_FUN_CATEGORY =
KeyMapping.Category.register(OnlyFunMod.id("onlyfun"));
private static KeyMapping shitKey;
@Override
public void onInitializeClient() {
shitKey = KeyMappingHelper.registerKeyMapping(new KeyMapping(
"key.onlyfun.shit", InputConstants.Type.KEYSYM, GLFW.GLFW_KEY_J, ONLY_FUN_CATEGORY));
ClientTickEvents.END_CLIENT_TICK.register(client -> {
if (client.player == null || client.level == null) return;
while (shitKey.consumeClick()) {
if (ClientPlayNetworking.canSend(ShitActionRequestPayload.ID)) {
ClientPlayNetworking.send(new ShitActionRequestPayload());
}
}
Entity vehicle = client.player.getVehicle();
if (!(vehicle instanceof AbstractBoat boat) || !ChickenBoatUtil.hasChickenPassenger(boat)) return;
if (!ClientPlayNetworking.canSend(ChickenBoatControlPayload.ID)) return;
@@ -9,7 +9,7 @@ import net.fabricmc.fabric.api.networking.v1.PayloadTypeRegistry;
* Networking contract for Only Fun. Payload IDs must use the module namespace.
*/
public final class OnlyFunNetworking {
public static final int PROTOCOL_VERSION = 2;
public static final int PROTOCOL_VERSION = 3;
private static boolean initialized;
private OnlyFunNetworking() {
@@ -21,7 +21,9 @@ public final class OnlyFunNetworking {
}
initialized = true;
PayloadTypeRegistry.serverboundPlay().register(ChickenBoatControlPayload.ID, ChickenBoatControlPayload.CODEC);
PayloadTypeRegistry.serverboundPlay().register(ShitActionRequestPayload.ID, ShitActionRequestPayload.CODEC);
ChickenBoatServerController.register();
fr.koka99cab.sanctuary26.onlyfun.abjection.ShitActionRequestHandler.register();
OnlyFunMod.LOGGER.debug("[{}] Networking contract v{} ready; chicken boat controls registered.",
OnlyFunMod.DISPLAY_NAME, PROTOCOL_VERSION);
}
@@ -0,0 +1,19 @@
package fr.koka99cab.sanctuary26.onlyfun.network;
import fr.koka99cab.sanctuary26.onlyfun.OnlyFunMod;
import net.minecraft.network.RegistryFriendlyByteBuf;
import net.minecraft.network.codec.StreamCodec;
import net.minecraft.network.protocol.common.custom.CustomPacketPayload;
/** Empty and therefore strictly bounded request to start the existing server-owned action. */
public record ShitActionRequestPayload() implements CustomPacketPayload {
public static final Type<ShitActionRequestPayload> ID =
new Type<>(OnlyFunMod.id("c2s/shit_action"));
public static final StreamCodec<RegistryFriendlyByteBuf, ShitActionRequestPayload> CODEC =
CustomPacketPayload.codec((payload, buffer) -> {}, buffer -> new ShitActionRequestPayload());
@Override
public Type<? extends CustomPacketPayload> type() {
return ID;
}
}
@@ -44,6 +44,7 @@ public final class OnlyFunItemPolicy {
if (namespace.equals("canaplia") || namespace.equals("itsalive")) return true;
if (namespace.equals("minecraft") && VANILLA_SHOP_GAMEPLAY_BYPASSES.contains(path)) return true;
if (namespace.equals("anotherworld") && isProtectedAnotherWorldItem(path)) return true;
if (isStorageTerminal(namespace, path)) return true;
if (TECHNICAL_OR_REWARD_BLOCKS.contains(path) || path.startsWith("infested_")
|| path.contains("command_block")) return true;
if (path.contains("lootbox") || path.contains("loot_box")) return true;
@@ -61,6 +62,7 @@ public final class OnlyFunItemPolicy {
String namespace = id.getNamespace().toLowerCase(Locale.ROOT);
String path = normalizedPath(id);
return namespace.equals("canaplia") || path.contains("lootbox") || path.contains("loot_box")
|| isStorageTerminal(namespace, path)
|| (namespace.equals("onlyfun") && path.equals("lucky_block"))
|| (namespace.equals("sanctuary") && path.equals("delivery_box"))
|| TECHNICAL_OR_REWARD_BLOCKS.contains(path) || path.startsWith("infested_")
@@ -77,6 +79,10 @@ public final class OnlyFunItemPolicy {
|| path.contains("titanium") || path.contains("rainbow_block");
}
private static boolean isStorageTerminal(String namespace, String path) {
return namespace.equals("redstoner") && path.equals("storage_terminal");
}
private static String normalizedPath(Identifier id) {
return id.getPath().toLowerCase(Locale.ROOT);
}
@@ -41,5 +41,7 @@
"command.onlyfun.history": "Rolls: %s | dry streak: %s | anomalies: %s | last: %s",
"command.onlyfun.give": "Granted Only Fun source: %s",
"command.onlyfun.roll": "Outcome: %s (%s)",
"command.onlyfun.simulate": "Simulation %s × %s: %s"
"command.onlyfun.simulate": "Simulation %s × %s: %s",
"key.categories.onlyfun.onlyfun": "Only Fun",
"key.onlyfun.shit": "Shit"
}
@@ -41,5 +41,7 @@
"command.onlyfun.history": "Tirages : %s | série sèche : %s | anomalies : %s | dernier : %s",
"command.onlyfun.give": "Source Only Fun donnée : %s",
"command.onlyfun.roll": "Résultat : %s (%s)",
"command.onlyfun.simulate": "Simulation %s × %s : %s"
"command.onlyfun.simulate": "Simulation %s × %s : %s",
"key.categories.onlyfun.onlyfun": "Only Fun",
"key.onlyfun.shit": "Chier"
}
@@ -41,5 +41,7 @@
"command.onlyfun.history": "Розыгрыши: %s | серия: %s | аномалии: %s | последнее: %s",
"command.onlyfun.give": "Выдан источник Only Fun: %s",
"command.onlyfun.roll": "Результат: %s (%s)",
"command.onlyfun.simulate": "Симуляция %s × %s: %s"
"command.onlyfun.simulate": "Симуляция %s × %s: %s",
"key.categories.onlyfun.onlyfun": "Only Fun",
"key.onlyfun.shit": "Испражниться"
}
+1 -1
View File
@@ -34,7 +34,7 @@
"system_type": "gameplay-system",
"namespace": "onlyfun",
"data_version": 2,
"network_protocol": 2,
"network_protocol": 3,
"lifecycle": "${lifecycle}",
"pack_version": "${pack_version}",
"descriptions": {
@@ -0,0 +1,23 @@
package fr.koka99cab.sanctuary26.onlyfun.abjection;
import java.util.UUID;
/** Executable check for one physical press and same-tick spam deduplication. */
public final class ShitActionRequestSmoke {
private ShitActionRequestSmoke() {
}
public static void main(String[] args) {
UUID playerId = UUID.fromString("00000000-0000-0000-0000-000000000059");
require(ShitActionRequestHandler.acceptTick(playerId, 100), "first press was rejected");
require(!ShitActionRequestHandler.acceptTick(playerId, 100), "same-tick spam was accepted");
require(ShitActionRequestHandler.acceptTick(playerId, 101), "next physical press was rejected");
ShitActionRequestHandler.clear(playerId);
require(ShitActionRequestHandler.acceptTick(playerId, 101), "disconnect cleanup retained input state");
ShitActionRequestHandler.clear(playerId);
}
private static void require(boolean condition, String message) {
if (!condition) throw new AssertionError(message);
}
}
@@ -81,6 +81,13 @@ public final class OnlyFunShopCatalogSmoke {
require(!OnlyFunItemPolicy.excludedFromShop(Identifier.parse(rawFoodId)),
"Raw ingredient was removed with finished food: " + rawFoodId);
}
Identifier storageTerminal = Identifier.parse("redstoner:storage_terminal");
require(OnlyFunItemPolicy.excludedFromShop(storageTerminal),
"Storage Terminal reached the Shop policy");
require(OnlyFunItemPolicy.excludedFromRewards(storageTerminal),
"Storage Terminal reached lootbox rewards");
require(find(all, storageTerminal.toString()) == null,
"Storage Terminal reached the generated Shop catalogue");
require(OnlyFunBossShopGate.requiresBoss("iliketomoveit:magic_carpet"),
"Magic Carpet must remain boss-gated");
require(OnlyFunBossShopGate.requiresBoss("iliketomoveit:magic_cloud"),
@@ -0,0 +1,64 @@
{
"schema_version": 1,
"source": {
"pack_version": "26.2.0-alpha.210",
"modules": {
"ambiance": "0.0.0-alpha.14",
"itsalive": "0.0.0-alpha.35",
"onlyfun": "0.0.0-alpha.21",
"ouch": "0.0.0-alpha.9",
"sanctuary": "0.0.0-alpha.118"
}
},
"target": {
"pack_version": "26.2.0-alpha.211",
"modules": {
"ambiance": "0.0.0-alpha.15",
"itsalive": "0.0.0-alpha.36",
"onlyfun": "0.0.0-alpha.24",
"ouch": "0.0.0-alpha.10",
"sanctuary": "0.0.0-alpha.119"
}
},
"features": {
"canaplia_codex_uses_vanilla_book_pages": true,
"black_market_middle_click_restored": true,
"white_disc_youtube_audio": true,
"white_disc_max_concurrent_streams": 4,
"abjection_server_actions": ["piss", "vomit", "shit", "smoke"],
"abjection_shit_ball_projectile": true,
"abjection_shit_cube": true,
"abjection_default_key": "J",
"abjection_projectile_nausea_seconds": 7
},
"compatibility": {
"new_world_required": false,
"save_reset_required": false,
"existing_saves_supported": true,
"registry_ids_added": [
"ambiance:white_disc",
"abjection:shit_ball",
"abjection:shit_cube"
],
"data_versions": {
"ambiance": 2,
"itsalive": 3,
"onlyfun": 2,
"ouch": 1,
"sanctuary": 11
},
"network_protocols": {
"ambiance": 6,
"itsalive": 4,
"onlyfun": 3,
"ouch": 1,
"sanctuary": 22
}
},
"distribution": {
"pack_release_updated": true,
"packwiz_index_refreshed": true,
"prism_modrinth_index_updated": true,
"shared_client_server_mrpack": true
}
}
@@ -0,0 +1,34 @@
{
"schema_version": 1,
"source": {
"pack_version": "26.2.0-alpha.211",
"modules": {
"sanctuary": "0.0.0-alpha.119"
}
},
"target": {
"pack_version": "26.2.0-alpha.212",
"modules": {
"sanctuary": "0.0.0-alpha.120"
}
},
"features": {
"black_market_middle_click_preserved": true,
"minecraft_mixin_target": "pickBlockOrEntity",
"client_startup_crash_fixed": true
},
"compatibility": {
"new_world_required": false,
"save_reset_required": false,
"existing_saves_supported": true,
"registry_ids_changed": false,
"sanctuary_data_version": 11,
"sanctuary_network_protocol": 22
},
"distribution": {
"pack_release_updated": true,
"packwiz_index_refreshed": true,
"prism_modrinth_index_updated": true,
"shared_client_server_mrpack": true
}
}
@@ -0,0 +1,35 @@
{
"schema_version": 1,
"source": {
"pack_version": "26.2.0-alpha.212",
"modules": {
"ambiance": "0.0.0-alpha.15"
}
},
"target": {
"pack_version": "26.2.0-alpha.213",
"modules": {
"ambiance": "0.0.0-alpha.16"
}
},
"features": {
"white_disc_stop_event_preserved": true,
"minecraft_mixin_target_class": "LevelEventHandler",
"minecraft_mixin_target_method": "levelEvent",
"client_startup_crash_fixed": true
},
"compatibility": {
"new_world_required": false,
"save_reset_required": false,
"existing_saves_supported": true,
"registry_ids_changed": false,
"ambiance_data_version": 2,
"ambiance_network_protocol": 6
},
"distribution": {
"pack_release_updated": true,
"packwiz_index_refreshed": true,
"prism_modrinth_index_updated": true,
"shared_client_server_mrpack": true
}
}
@@ -0,0 +1,36 @@
{
"schema_version": 1,
"source": {
"pack_version": "26.2.0-alpha.213",
"modules": {
"ambiance": "0.0.0-alpha.16"
}
},
"target": {
"pack_version": "26.2.0-alpha.214",
"modules": {
"ambiance": "0.0.0-alpha.17"
}
},
"features": {
"white_disc_solo_playback_restored": true,
"client_jukebox_sync_wait_ticks": 100,
"loading_feedback_immediate": true,
"sync_timeout_feedback": true,
"youtube_share_parameters_supported": true
},
"compatibility": {
"new_world_required": false,
"save_reset_required": false,
"existing_saves_supported": true,
"registry_ids_changed": false,
"ambiance_data_version": 2,
"ambiance_network_protocol": 6
},
"distribution": {
"pack_release_updated": true,
"packwiz_index_refreshed": true,
"prism_modrinth_index_updated": true,
"shared_client_server_mrpack": true
}
}
@@ -0,0 +1,39 @@
{
"schema_version": 1,
"source": {
"pack_version": "26.2.0-alpha.214",
"modules": {
"ambiance": "0.0.0-alpha.17"
}
},
"target": {
"pack_version": "26.2.0-alpha.215",
"modules": {
"ambiance": "0.0.0-alpha.18"
}
},
"features": {
"white_disc_native_crash_fixed": true,
"openal_channel_release_owned_by_audio_thread": true,
"youtube_client_profile": "Ios",
"youtube_source_commit": "f45bbb7aebfcbc1c553769e04af6cd43afa8b7c3",
"initial_stream_wait_bounded": true,
"subsequent_stream_polls_non_blocking": true,
"playback_errors_stop_safely": true,
"live_pcm_smoke_video_id": "Ue5ZBe-GzSM"
},
"compatibility": {
"new_world_required": false,
"save_reset_required": false,
"existing_saves_supported": true,
"registry_ids_changed": false,
"ambiance_data_version": 2,
"ambiance_network_protocol": 6
},
"distribution": {
"pack_release_updated": true,
"packwiz_index_refreshed": true,
"prism_modrinth_index_updated": true,
"shared_client_server_mrpack": true
}
}
@@ -0,0 +1,43 @@
{
"schema_version": 1,
"source": {
"pack_version": "26.2.0-alpha.215",
"modules": {
"ambiance": "0.0.0-alpha.18"
}
},
"target": {
"pack_version": "26.2.0-alpha.216",
"modules": {
"ambiance": "0.0.0-alpha.19"
}
},
"features": {
"white_disc_continuous_pcm_buffering": true,
"sample_rate_hz": 48000,
"channel_count": 1,
"sample_size_bits": 16,
"minecraft_stream_buffer_seconds": 1,
"minecraft_initial_buffer_count": 4,
"initial_frame_wait_millis": 2000,
"continuation_frame_wait_millis": 250,
"openal_pitch": 1.0,
"live_pcm_smoke_video_id": "Ue5ZBe-GzSM",
"minimum_live_audible_byte_ratio": 0.75,
"deterministic_stream_smoke": true
},
"compatibility": {
"new_world_required": false,
"save_reset_required": false,
"existing_saves_supported": true,
"registry_ids_changed": false,
"ambiance_data_version": 2,
"ambiance_network_protocol": 6
},
"distribution": {
"pack_release_updated": true,
"packwiz_index_refreshed": true,
"prism_modrinth_index_updated": true,
"shared_client_server_mrpack": true
}
}
@@ -0,0 +1,41 @@
{
"schema_version": 1,
"source": {
"pack_version": "26.2.0-alpha.216",
"modules": {
"ambiance": "0.0.0-alpha.19"
}
},
"target": {
"pack_version": "26.2.0-alpha.217",
"modules": {
"ambiance": "0.0.0-alpha.20"
}
},
"features": {
"white_disc_pitch_corrected": true,
"youtube_source_format": "MP4_AAC_LC",
"openal_pitch_numerator": 4,
"openal_pitch_denominator": 3,
"openal_pitch_reapplied_during_playback": true,
"reference_video_id": "Ue5ZBe-GzSM",
"direct_source_comparison": true,
"decoded_sample_rate_hz": 48000,
"decoded_channel_count": 1,
"decoded_sample_size_bits": 16
},
"compatibility": {
"new_world_required": false,
"save_reset_required": false,
"existing_saves_supported": true,
"registry_ids_changed": false,
"ambiance_data_version": 2,
"ambiance_network_protocol": 6
},
"distribution": {
"pack_release_updated": true,
"packwiz_index_refreshed": true,
"prism_modrinth_index_updated": true,
"shared_client_server_mrpack": true
}
}
@@ -0,0 +1,70 @@
{
"schema_version": 1,
"source": {
"pack_version": "26.2.0-alpha.217",
"modules": {
"redstoner": "0.0.0-alpha.10",
"onlyfun": "0.0.0-alpha.24",
"sanctuary": "0.0.0-alpha.120"
}
},
"target": {
"pack_version": "26.2.0-alpha.218",
"modules": {
"redstoner": "0.0.0-alpha.11",
"onlyfun": "0.0.0-alpha.25",
"sanctuary": "0.0.0-alpha.121"
}
},
"storage_terminal": {
"owner": "redstoner",
"id": "redstoner:storage_terminal",
"shape_depth_pixels": 8,
"horizontal_wall_mount_only": true,
"supported_containers": [
"minecraft:chest",
"minecraft:trapped_chest",
"minecraft:barrel"
],
"ender_chest_supported": false,
"face_adjacent_network_includes_vertical": true,
"loaded_chunks_only": true,
"maximum_physical_containers": 128,
"oversized_network_rejected": true,
"page_size": 54,
"strict_item_component_aggregation": true,
"server_authoritative_transfers": true,
"left_click_extracts_stack": true,
"right_click_extracts_one": true,
"shift_click_deposits_player_stack": true,
"crafting_pattern": ["PGP", "GCG", "PTP"],
"crafting_ingredients": {
"P": "anotherworld:silver_plate",
"G": "minecraft:tinted_glass",
"C": "redstoner:controller",
"T": "anotherworld:titanium_ingot"
},
"available_in_shop": false,
"available_in_black_market": false,
"available_in_lootbox_rewards": false
},
"compatibility": {
"new_world_required": false,
"save_reset_required": false,
"existing_saves_supported": true,
"registry_ids_added": ["redstoner:storage_terminal"],
"redstoner_data_version": 3,
"redstoner_source_network_protocol": 5,
"redstoner_target_network_protocol": 6,
"onlyfun_data_version": 2,
"onlyfun_network_protocol": 3,
"sanctuary_data_version": 11,
"sanctuary_network_protocol": 22
},
"distribution": {
"pack_release_updated": true,
"packwiz_index_refreshed": true,
"prism_modrinth_index_updated": true,
"shared_client_server_mrpack": true
}
}
@@ -0,0 +1,90 @@
{
"schema_version": 1,
"source": {
"pack_version": "26.2.0-alpha.218",
"modules": {
"sanctuary": "0.0.0-alpha.121",
"iliketomoveit": "0.0.0-alpha.15",
"ambiance": "0.0.0-alpha.20"
}
},
"target": {
"pack_version": "26.2.0-alpha.219",
"modules": {
"sanctuary": "0.0.0-alpha.122",
"iliketomoveit": "0.0.0-alpha.16",
"ambiance": "0.0.0-alpha.21"
}
},
"alpha_world": {
"owner": "sanctuary",
"dimension": "sanctuary:alpha",
"legacy_dimension": "portals:alpha",
"legacy_identifier_migrated": true,
"width": 128,
"depth": 128,
"height": 64,
"minimum_xz": -64,
"maximum_xz": 63,
"deterministic_per_seed": true,
"finite_barrier_boundary": true,
"bedrock_floor": true,
"historical_house": true,
"historical_residence_exposed_by_api": true,
"safe_portal_arrival_exposed_by_api": true,
"registered_profiles": ["sanctuary", "overworld"],
"dedicated_server_supported": true
},
"alpha_portal": {
"owner": "iliketomoveit",
"route": "iliketomoveit:alpha",
"schema_version": 2,
"frame_block": "minecraft:mossy_cobblestone",
"outer_width": 4,
"outer_height": 5,
"inner_width": 2,
"inner_height": 3,
"activation_item": "minecraft:flint_and_steel",
"inactive_when_destination_absent": true,
"server_authoritative_destination": true,
"server_authoritative_coordinates": true,
"persistent_pair_reused": true,
"break_unlinks_pair": true,
"remote_half_avoids_historical_house": true,
"atlas_discovery_supported": true
},
"alpha_textures": {
"owner": "ambiance",
"client_only": true,
"destination": "sanctuary:alpha",
"bundled_assets": false,
"bundled_archives": false,
"external_packs": ["Golden Days Base", "Golden Days Alpha"],
"overlay_order": ["base", "alpha"],
"previous_selection_restored_exactly": true,
"saved_options_modified": false,
"missing_packs_fallback": "vanilla",
"dedicated_server_files_required": false,
"official_base_url": "https://www.curseforge.com/minecraft/texture-packs/golden-days",
"official_alpha_url": "https://www.curseforge.com/minecraft/texture-packs/golden-days-alpha",
"official_license_url": "https://poeticrainbow.github.io/golden-days/license/"
},
"compatibility": {
"new_world_required": false,
"save_reset_required": false,
"existing_saves_supported": true,
"sanctuary_data_version": 11,
"sanctuary_network_protocol": 22,
"iliketomoveit_data_version": 3,
"iliketomoveit_network_protocol": 5,
"ambiance_data_version": 2,
"ambiance_network_protocol": 6
},
"distribution": {
"pack_release_updated": true,
"packwiz_index_refreshed": true,
"prism_modrinth_index_updated": true,
"golden_days_assets_in_mrpack": false,
"shared_client_server_mrpack": true
}
}
+17 -17
View File
@@ -13,16 +13,16 @@ file = "instance-icon.png"
hash = "41f4f4c88c71d0a23446d5014f7407d55ad477441847099cad24492b032335a0"
[[files]]
file = "mods/ambiance-0.0.0-alpha.14.jar"
hash = "a4b2a36e046a3095ada58e3dbf5fd30ab7c8ba42eaf2abcf513b38b8cc2f3e4a"
file = "mods/ambiance-0.0.0-alpha.21.jar"
hash = "c9dfd6ed13d1fdd2277e62d9bd2faa18c008d621bbcf8ef605736eefc3378663"
[[files]]
file = "mods/anotherworld-0.0.0-alpha.55.jar"
hash = "da7163a34ca71e1c2385a84221cddccc09f0f83d9a00bce0ed6263b19ec519cb"
hash = "0137f9a8109438c659c7a126ecde2e28982e063ee95f1991603a9ea81bbe7501"
[[files]]
file = "mods/events-0.0.0-alpha.0.jar"
hash = "7400d63c905d1c87a5290adaa2fe91305fa1f067827ca8f0610a6ae4e17624da"
hash = "3a666f7b7bdb9cbf7181eff3e49455768ac76905a482577990fdc1eb018e78d7"
[[files]]
file = "mods/fabric-api.pw.toml"
@@ -30,8 +30,8 @@ hash = "df032a6b051d0262cf24945c43e0e6b915e8cc1d62af62fe933932e7f35960c4"
metafile = true
[[files]]
file = "mods/iliketomoveit-0.0.0-alpha.15.jar"
hash = "a084cf25a4afa50086ce220f40b6b3946a06389de105f23c0246df1577deb3c8"
file = "mods/iliketomoveit-0.0.0-alpha.16.jar"
hash = "f42ed4fe37cbf3422e3b2493db562e82da3f2ba04568cea6ae304862e64e54df"
[[files]]
file = "mods/iris.pw.toml"
@@ -39,8 +39,8 @@ hash = "84a3cef87679c0d21dceb4641167dc0368b9a3593698f6a3632485e9b3a8d5da"
metafile = true
[[files]]
file = "mods/itsalive-0.0.0-alpha.35.jar"
hash = "4a6433abdfc03a0872f7c810fb17016a0f20acda5e223e0fef479dd786cc73ac"
file = "mods/itsalive-0.0.0-alpha.36.jar"
hash = "71c9d893fd0862610f203b854d1af19e0e6e4b2bc9e06f6da38d057e23a54ebd"
[[files]]
file = "mods/jei.pw.toml"
@@ -49,23 +49,23 @@ metafile = true
[[files]]
file = "mods/masterkey-0.0.0-alpha.8.jar"
hash = "eaf70f6c5a78ee7ab2d3be498d4f7d67fcba433ed61b65b305e958f03b387d94"
hash = "75ae764c2b86d2f4479f576d9a1cd8272c1979d754069277052ef11064fabbda"
[[files]]
file = "mods/onlyfun-0.0.0-alpha.21.jar"
hash = "7e319a0cc6149d5819ef2359fe3ad4035066a9cddd5ae0a16c12c9db64bc3f6a"
file = "mods/onlyfun-0.0.0-alpha.25.jar"
hash = "69cca044dd3cd3fbbd6d9d45706a502ec17689c040521524eac3aa20ab93bf5f"
[[files]]
file = "mods/ouch-0.0.0-alpha.9.jar"
hash = "f3cf2d8c83afa32b1c70e8239dd514be3c74834397f9d09efe177bc8c5ef9ac4"
file = "mods/ouch-0.0.0-alpha.10.jar"
hash = "c414a5b308796a797c6dce9b0337edfd06c536a18d1c746307f58e99e3a6be2c"
[[files]]
file = "mods/redstoner-0.0.0-alpha.10.jar"
hash = "a9049b9d3a323015545194c92ce9da7a5dca4ff9a28ea26b02bead96748328ab"
file = "mods/redstoner-0.0.0-alpha.11.jar"
hash = "753a7b4f8a26a836904099ed034dc36dcb371b87f5b67044940345fdadeb6b33"
[[files]]
file = "mods/sanctuary-0.0.0-alpha.118.jar"
hash = "1a1319e5f09de941ad8a7561aff9d2fb8c7460a8cd8d2374817d8f39c5682030"
file = "mods/sanctuary-0.0.0-alpha.122.jar"
hash = "b4ab04b167c50d412af791ccadf838ebcb0443303e05864c9940e02ec3cc0d40"
[[files]]
file = "mods/simple-voice-chat.pw.toml"
+2 -2
View File
@@ -1,12 +1,12 @@
name = "Sanctuary 26.2"
author = "KOKA99CAB"
version = "26.2.0-alpha.210"
version = "26.2.0-alpha.219"
pack-format = "packwiz:1.1.0"
[index]
file = "index.toml"
hash-format = "sha256"
hash = "e20c051f8895ce3164ec72b7764b8904f411e36f63bf1e046b1bcc9108d00a31"
hash = "a694c1327286dca4369aa64dfea7decbc36fedbba77188e7d8d310c08365d415"
[versions]
fabric = "0.19.3"
+1 -1
View File
@@ -3,7 +3,7 @@
"game": "minecraft",
"name": "Sanctuary 26.2",
"summary": "Sanctuary 26.2 — progression, exploration, worldgen and multiplayer systems by KOKA99CAB.",
"versionId": "26.2.0-alpha.210",
"versionId": "26.2.0-alpha.219",
"files": [
{
"path": "mods/fabric-api-0.158.0+26.2.jar",
+2 -2
View File
@@ -1,6 +1,6 @@
{
"schema_version": 1,
"pack_version": "26.2.0-alpha.210",
"pack_version": "26.2.0-alpha.219",
"release_channel": "alpha",
"minecraft_version": "26.2",
"fabric_loader_version": "0.19.3",
@@ -9,7 +9,7 @@
"project_id": "FgOqyAXd",
"project_slug": "sanctuary-koka99",
"project_url": "https://modrinth.com/modpack/sanctuary-koka99",
"primary_file": "sanctuary-26.2.0-alpha.210.mrpack",
"primary_file": "sanctuary-26.2.0-alpha.219.mrpack",
"shared_client_server_pack": true,
"update_owner": "packwiz-installer",
"check_on_launch": true,
@@ -27,7 +27,7 @@ public final class RedStonerMod implements ModInitializer {
RedStonerApi.registerServerIdentityProvider(SanctuaryServerIdentity::get);
RedStonerRegistries.initialize();
RedStonerNetworking.initialize();
LOGGER.info("[{}] Computers, Drawer and Particuleur ready (data v{}, network v{}).",
LOGGER.info("[{}] Computers, Drawer, Particuleur and Storage Terminal ready (data v{}, network v{}).",
DISPLAY_NAME, CURRENT_DATA_VERSION, RedStonerNetworking.PROTOCOL_VERSION);
}
}
@@ -0,0 +1,114 @@
package fr.koka99cab.sanctuary26.redstoner.block;
import com.mojang.serialization.MapCodec;
import fr.koka99cab.sanctuary26.redstoner.network.RedStonerNetworking;
import fr.koka99cab.sanctuary26.redstoner.storage.StorageTerminalNetwork;
import java.util.Map;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.util.RandomSource;
import net.minecraft.world.InteractionHand;
import net.minecraft.world.InteractionResult;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.context.BlockPlaceContext;
import net.minecraft.world.level.BlockGetter;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.LevelReader;
import net.minecraft.world.level.ScheduledTickAccess;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.Mirror;
import net.minecraft.world.level.block.Rotation;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.block.state.StateDefinition;
import net.minecraft.world.level.block.state.properties.BlockStateProperties;
import net.minecraft.world.level.block.state.properties.EnumProperty;
import net.minecraft.world.phys.BlockHitResult;
import net.minecraft.world.phys.shapes.CollisionContext;
import net.minecraft.world.phys.shapes.VoxelShape;
public final class StorageTerminalBlock extends Block {
public static final MapCodec<StorageTerminalBlock> CODEC = simpleCodec(StorageTerminalBlock::new);
public static final EnumProperty<Direction> FACING = BlockStateProperties.HORIZONTAL_FACING;
private static final Map<Direction, VoxelShape> SHAPES = Map.of(
Direction.NORTH, Block.box(0.0D, 0.0D, 8.0D, 16.0D, 16.0D, 16.0D),
Direction.SOUTH, Block.box(0.0D, 0.0D, 0.0D, 16.0D, 16.0D, 8.0D),
Direction.WEST, Block.box(8.0D, 0.0D, 0.0D, 16.0D, 16.0D, 16.0D),
Direction.EAST, Block.box(0.0D, 0.0D, 0.0D, 8.0D, 16.0D, 16.0D)
);
public StorageTerminalBlock(Properties properties) {
super(properties);
registerDefaultState(stateDefinition.any().setValue(FACING, Direction.NORTH));
}
@Override
public MapCodec<StorageTerminalBlock> codec() {
return CODEC;
}
@Override
public BlockState getStateForPlacement(BlockPlaceContext context) {
Direction facing = context.getClickedFace();
if (!facing.getAxis().isHorizontal()) return null;
BlockState state = defaultBlockState().setValue(FACING, facing);
return state.canSurvive(context.getLevel(), context.getClickedPos()) ? state : null;
}
@Override
protected boolean canSurvive(BlockState state, LevelReader level, BlockPos pos) {
BlockPos support = pos.relative(state.getValue(FACING).getOpposite());
return StorageTerminalNetwork.isCompatibleStorage(level.getBlockState(support));
}
@Override
protected BlockState updateShape(BlockState state, LevelReader level, ScheduledTickAccess ticks,
BlockPos pos, Direction direction, BlockPos neighborPos, BlockState neighborState, RandomSource random) {
return direction == state.getValue(FACING).getOpposite() && !state.canSurvive(level, pos)
? Blocks.AIR.defaultBlockState()
: super.updateShape(state, level, ticks, pos, direction, neighborPos, neighborState, random);
}
@Override
protected VoxelShape getShape(BlockState state, BlockGetter level, BlockPos pos, CollisionContext context) {
return SHAPES.get(state.getValue(FACING));
}
@Override
protected InteractionResult useItemOn(ItemStack stack, BlockState state, Level level, BlockPos pos,
Player player, InteractionHand hand, BlockHitResult hit) {
return open(level, pos, player);
}
@Override
protected InteractionResult useWithoutItem(BlockState state, Level level, BlockPos pos,
Player player, BlockHitResult hit) {
return open(level, pos, player);
}
@Override
protected void createBlockStateDefinition(StateDefinition.Builder<Block, BlockState> builder) {
builder.add(FACING);
}
@Override
protected BlockState rotate(BlockState state, Rotation rotation) {
return state.setValue(FACING, rotation.rotate(state.getValue(FACING)));
}
@Override
protected BlockState mirror(BlockState state, Mirror mirror) {
return state.rotate(mirror.getRotation(state.getValue(FACING)));
}
private static InteractionResult open(Level level, BlockPos pos, Player player) {
if (level.isClientSide()) return InteractionResult.SUCCESS;
if (player instanceof ServerPlayer serverPlayer) {
RedStonerNetworking.openStorageTerminal(serverPlayer, pos);
return InteractionResult.SUCCESS_SERVER;
}
return InteractionResult.CONSUME;
}
}
@@ -3,9 +3,11 @@ package fr.koka99cab.sanctuary26.redstoner.client;
import fr.koka99cab.sanctuary26.redstoner.RedStonerMod;
import fr.koka99cab.sanctuary26.redstoner.client.gui.ComputerScreen;
import fr.koka99cab.sanctuary26.redstoner.client.gui.PersonalComputerScreen;
import fr.koka99cab.sanctuary26.redstoner.client.gui.StorageTerminalScreen;
import fr.koka99cab.sanctuary26.redstoner.client.renderer.DrawerBlockEntityRenderer;
import fr.koka99cab.sanctuary26.redstoner.network.OpenComputerPayload;
import fr.koka99cab.sanctuary26.redstoner.network.OpenPersonalComputerPayload;
import fr.koka99cab.sanctuary26.redstoner.network.StorageTerminalSnapshotPayload;
import fr.koka99cab.sanctuary26.redstoner.registry.RedStonerRegistries;
import net.fabricmc.api.ClientModInitializer;
import net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking;
@@ -26,6 +28,15 @@ public final class RedStonerClient implements ClientModInitializer {
context.client().gui.setScreen(new PersonalComputerScreen(payload));
}
}));
ClientPlayNetworking.registerGlobalReceiver(StorageTerminalSnapshotPayload.TYPE, (payload, context) ->
context.client().execute(() -> {
if (context.client().gui.screen() instanceof StorageTerminalScreen screen
&& screen.represents(payload)) {
screen.acceptSnapshot(payload);
} else {
context.client().gui.setScreen(new StorageTerminalScreen(payload));
}
}));
RedStonerMod.LOGGER.info("[{}] Client renderers and laboratory screens ready.", RedStonerMod.DISPLAY_NAME);
}
@@ -0,0 +1,244 @@
package fr.koka99cab.sanctuary26.redstoner.client.gui;
import fr.koka99cab.sanctuary26.redstoner.network.StorageTerminalActionPayload;
import fr.koka99cab.sanctuary26.redstoner.network.StorageTerminalSnapshotPayload;
import fr.koka99cab.sanctuary26.redstoner.storage.StorageTerminalRules;
import fr.koka99cab.sanctuary26.redstoner.storage.StorageTerminalStatus;
import java.util.Locale;
import net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking;
import net.minecraft.client.gui.GuiGraphicsExtractor;
import net.minecraft.client.gui.components.Button;
import net.minecraft.client.gui.screens.Screen;
import net.minecraft.client.input.MouseButtonEvent;
import net.minecraft.network.chat.Component;
import net.minecraft.world.entity.player.Inventory;
import net.minecraft.world.item.ItemStack;
public final class StorageTerminalScreen extends Screen {
private static final int PANEL_WIDTH = 204;
private static final int PANEL_HEIGHT = 236;
private static final int SLOT_SIZE = 18;
private static final int GRID_X = 8;
private static final int GRID_Y = 20;
private static final int PLAYER_GRID_Y = 152;
private static final int HOTBAR_Y = 210;
private static final int SCROLL_X = 176;
private static final int SCROLL_WIDTH = 8;
private static final int REFRESH_TICKS = 20;
private StorageTerminalSnapshotPayload snapshot;
private int requestedPage;
private int refreshCooldown = REFRESH_TICKS;
public StorageTerminalScreen(StorageTerminalSnapshotPayload snapshot) {
super(Component.translatable("screen.redstoner.storage_terminal.title"));
this.snapshot = snapshot;
this.requestedPage = snapshot.page();
}
public boolean represents(StorageTerminalSnapshotPayload other) {
return snapshot.pos().equals(other.pos());
}
public void acceptSnapshot(StorageTerminalSnapshotPayload refreshed) {
if (!represents(refreshed)) return;
snapshot = refreshed;
requestedPage = refreshed.page();
refreshCooldown = REFRESH_TICKS;
}
@Override
protected void init() {
addRenderableWidget(Button.builder(Component.literal("×"), button -> onClose())
.bounds(panelX() + PANEL_WIDTH - 22, panelY() + 4, 18, 14).build());
}
@Override
public void tick() {
if (--refreshCooldown <= 0) requestPage(requestedPage);
}
@Override
public void extractRenderState(GuiGraphicsExtractor graphics, int mouseX, int mouseY, float tickDelta) {
graphics.fill(0, 0, width, height, 0xCC101010);
int x = panelX();
int y = panelY();
graphics.fill(x + 3, y + 3, x + PANEL_WIDTH + 3, y + PANEL_HEIGHT + 3, 0xA0000000);
graphics.fill(x, y, x + PANEL_WIDTH, y + PANEL_HEIGHT, 0xFFC6C6C6);
graphics.outline(x, y, PANEL_WIDTH, PANEL_HEIGHT, 0xFF373737);
graphics.text(font, title, x + 8, y + 7, 0xFF202020, false);
if (snapshot.status() == StorageTerminalStatus.READY) {
renderTerminalGrid(graphics, x, y, mouseX, mouseY);
} else {
graphics.centeredText(font, Component.translatable("screen.redstoner.storage_terminal.error."
+ snapshot.status().name().toLowerCase(Locale.ROOT)), x + 90, y + 70, 0xFFAA3333);
}
renderPlayerInventory(graphics, x, y, mouseX, mouseY);
super.extractRenderState(graphics, mouseX, mouseY, tickDelta);
}
@Override
public boolean mouseScrolled(double mouseX, double mouseY, double scrollX, double scrollY) {
if (snapshot.status() == StorageTerminalStatus.READY && scrollY != 0.0D && insidePanel(mouseX, mouseY)) {
requestPage(requestedPage + (scrollY < 0.0D ? 1 : -1));
return true;
}
return super.mouseScrolled(mouseX, mouseY, scrollX, scrollY);
}
@Override
public boolean mouseClicked(MouseButtonEvent event, boolean doubleClick) {
if (super.mouseClicked(event, doubleClick)) return true;
if (snapshot.status() == StorageTerminalStatus.READY) {
int entryIndex = terminalEntryAt(event.x(), event.y());
if (entryIndex >= 0 && entryIndex < snapshot.entries().size()
&& (event.button() == 0 || event.button() == 1)) {
StorageTerminalSnapshotPayload.Entry entry = snapshot.entries().get(entryIndex);
send(event.button() == 1 ? StorageTerminalActionPayload.Action.TAKE_ONE
: StorageTerminalActionPayload.Action.TAKE_STACK, snapshot.page(), entryIndex, -1, entry.stack());
return true;
}
if (insideScrollBar(event.x(), event.y()) && event.button() == 0) {
double ratio = Math.max(0.0D, Math.min(0.999D,
(event.y() - panelY() - GRID_Y) / (StorageTerminalRules.PAGE_SIZE / 9.0D * SLOT_SIZE)));
requestPage((int) Math.floor(ratio * snapshot.pageCount()));
return true;
}
int playerSlot = playerSlotAt(event.x(), event.y());
if (playerSlot >= 0 && event.button() == 0 && event.hasShiftDown() && minecraft.player != null) {
ItemStack stack = minecraft.player.getInventory().getItem(playerSlot);
if (!stack.isEmpty()) {
send(StorageTerminalActionPayload.Action.DEPOSIT_SLOT, snapshot.page(), -1, playerSlot, stack);
return true;
}
}
}
return false;
}
@Override
public boolean isPauseScreen() {
return false;
}
private void renderTerminalGrid(GuiGraphicsExtractor graphics, int x, int y, int mouseX, int mouseY) {
for (int index = 0; index < StorageTerminalRules.PAGE_SIZE; index++) {
int slotX = x + GRID_X + index % 9 * SLOT_SIZE;
int slotY = y + GRID_Y + index / 9 * SLOT_SIZE;
drawSlot(graphics, slotX, slotY);
if (index >= snapshot.entries().size()) continue;
StorageTerminalSnapshotPayload.Entry entry = snapshot.entries().get(index);
graphics.item(entry.stack(), slotX + 1, slotY + 1);
graphics.itemDecorations(font, entry.stack(), slotX + 1, slotY + 1, compactCount(entry.total()));
if (inside(mouseX, mouseY, slotX, slotY, SLOT_SIZE, SLOT_SIZE)) {
graphics.setTooltipForNextFrame(font, entry.stack(), mouseX, mouseY);
}
}
int trackX = x + SCROLL_X;
int trackY = y + GRID_Y;
int trackHeight = 6 * SLOT_SIZE;
graphics.fill(trackX, trackY, trackX + SCROLL_WIDTH, trackY + trackHeight, 0xFF575757);
int thumbHeight = snapshot.pageCount() <= 1 ? trackHeight : Math.max(10, trackHeight / snapshot.pageCount());
int travel = trackHeight - thumbHeight;
int thumbY = trackY + (snapshot.pageCount() <= 1 ? 0
: Math.round(travel * snapshot.page() / (float) (snapshot.pageCount() - 1)));
graphics.fill(trackX + 1, thumbY + 1, trackX + SCROLL_WIDTH - 1, thumbY + thumbHeight - 1, 0xFFD8D8D8);
graphics.text(font, Component.translatable("screen.redstoner.storage_terminal.summary",
snapshot.page() + 1, snapshot.pageCount(), snapshot.containerCount(), snapshot.totalEntries()),
x + 8, y + 131, 0xFF555555, false);
}
private void renderPlayerInventory(GuiGraphicsExtractor graphics, int x, int y, int mouseX, int mouseY) {
graphics.text(font, Component.translatable("container.inventory"), x + 8, y + 141, 0xFF202020, false);
if (minecraft.player == null) return;
Inventory inventory = minecraft.player.getInventory();
for (int row = 0; row < 3; row++) {
for (int column = 0; column < 9; column++) {
int inventorySlot = 9 + row * 9 + column;
drawPlayerSlot(graphics, inventory, inventorySlot, x + GRID_X + column * SLOT_SIZE,
y + PLAYER_GRID_Y + row * SLOT_SIZE, mouseX, mouseY);
}
}
for (int column = 0; column < 9; column++) {
drawPlayerSlot(graphics, inventory, column, x + GRID_X + column * SLOT_SIZE,
y + HOTBAR_Y, mouseX, mouseY);
}
}
private void drawPlayerSlot(GuiGraphicsExtractor graphics, Inventory inventory, int inventorySlot,
int x, int y, int mouseX, int mouseY) {
drawSlot(graphics, x, y);
ItemStack stack = inventory.getItem(inventorySlot);
if (stack.isEmpty()) return;
graphics.item(stack, x + 1, y + 1);
graphics.itemDecorations(font, stack, x + 1, y + 1);
if (inside(mouseX, mouseY, x, y, SLOT_SIZE, SLOT_SIZE)) {
graphics.setTooltipForNextFrame(font, stack, mouseX, mouseY);
}
}
private static void drawSlot(GuiGraphicsExtractor graphics, int x, int y) {
graphics.fill(x, y, x + SLOT_SIZE, y + SLOT_SIZE, 0xFF373737);
graphics.fill(x + 1, y + 1, x + SLOT_SIZE - 1, y + SLOT_SIZE - 1, 0xFF8B8B8B);
}
private void requestPage(int page) {
requestedPage = Math.max(0, Math.min(snapshot.pageCount() - 1, page));
refreshCooldown = REFRESH_TICKS;
send(StorageTerminalActionPayload.Action.REQUEST_PAGE, requestedPage, -1, -1, ItemStack.EMPTY);
}
private void send(StorageTerminalActionPayload.Action action, int page, int entryIndex,
int playerSlot, ItemStack expected) {
if (ClientPlayNetworking.canSend(StorageTerminalActionPayload.TYPE)) {
ClientPlayNetworking.send(new StorageTerminalActionPayload(
snapshot.pos(), action, page, entryIndex, playerSlot, expected));
}
}
private int terminalEntryAt(double mouseX, double mouseY) {
int relativeX = (int) mouseX - panelX() - GRID_X;
int relativeY = (int) mouseY - panelY() - GRID_Y;
if (relativeX < 0 || relativeY < 0 || relativeX >= 9 * SLOT_SIZE || relativeY >= 6 * SLOT_SIZE) return -1;
return relativeY / SLOT_SIZE * 9 + relativeX / SLOT_SIZE;
}
private int playerSlotAt(double mouseX, double mouseY) {
int relativeX = (int) mouseX - panelX() - GRID_X;
if (relativeX < 0 || relativeX >= 9 * SLOT_SIZE) return -1;
int column = relativeX / SLOT_SIZE;
int mainY = (int) mouseY - panelY() - PLAYER_GRID_Y;
if (mainY >= 0 && mainY < 3 * SLOT_SIZE) return 9 + mainY / SLOT_SIZE * 9 + column;
int hotbarY = (int) mouseY - panelY() - HOTBAR_Y;
return hotbarY >= 0 && hotbarY < SLOT_SIZE ? column : -1;
}
private boolean insideScrollBar(double mouseX, double mouseY) {
return inside(mouseX, mouseY, panelX() + SCROLL_X, panelY() + GRID_Y,
SCROLL_WIDTH, 6 * SLOT_SIZE);
}
private boolean insidePanel(double mouseX, double mouseY) {
return inside(mouseX, mouseY, panelX(), panelY(), PANEL_WIDTH, PANEL_HEIGHT);
}
private static boolean inside(double mouseX, double mouseY, int x, int y, int width, int height) {
return mouseX >= x && mouseX < x + width && mouseY >= y && mouseY < y + height;
}
private int panelX() {
return (width - PANEL_WIDTH) / 2;
}
private int panelY() {
return (height - PANEL_HEIGHT) / 2;
}
private static String compactCount(int count) {
if (count >= 1_000_000) return (count / 1_000_000) + "m";
if (count >= 10_000) return (count / 1_000) + "k";
return Integer.toString(count);
}
}
@@ -4,7 +4,11 @@ import fr.koka99cab.sanctuary26.redstoner.RedStonerMod;
import fr.koka99cab.sanctuary26.redstoner.block.entity.ComputerBlockEntity;
import fr.koka99cab.sanctuary26.redstoner.block.entity.PersonalComputerBlockEntity;
import fr.koka99cab.sanctuary26.redstoner.registry.RedStonerRegistries;
import fr.koka99cab.sanctuary26.redstoner.storage.StorageTerminalNetwork;
import fr.koka99cab.sanctuary26.redstoner.storage.StorageTerminalService;
import fr.koka99cab.sanctuary26.sanctuary.api.atlas.SanctuaryAtlasLandmarks;
import java.util.List;
import net.minecraft.core.BlockPos;
import net.fabricmc.fabric.api.networking.v1.PayloadTypeRegistry;
import net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking;
import net.minecraft.server.level.ServerPlayer;
@@ -12,7 +16,7 @@ import net.minecraft.world.entity.player.Player;
import net.minecraft.world.phys.Vec3;
public final class RedStonerNetworking {
public static final int PROTOCOL_VERSION = 5;
public static final int PROTOCOL_VERSION = 6;
private static boolean initialized;
private RedStonerNetworking() {
@@ -25,6 +29,8 @@ public final class RedStonerNetworking {
initialized = true;
PayloadTypeRegistry.clientboundPlay().register(OpenComputerPayload.TYPE, OpenComputerPayload.STREAM_CODEC);
PayloadTypeRegistry.clientboundPlay().register(OpenPersonalComputerPayload.TYPE, OpenPersonalComputerPayload.STREAM_CODEC);
PayloadTypeRegistry.clientboundPlay().register(StorageTerminalSnapshotPayload.TYPE,
StorageTerminalSnapshotPayload.STREAM_CODEC);
PayloadTypeRegistry.serverboundPlay().register(SaveComputerProgramPayload.TYPE, SaveComputerProgramPayload.STREAM_CODEC);
PayloadTypeRegistry.serverboundPlay().register(PlaceAtlasLandmarkPayload.TYPE, PlaceAtlasLandmarkPayload.STREAM_CODEC);
PayloadTypeRegistry.serverboundPlay().register(RequestPersonalComputerSnapshotPayload.TYPE,
@@ -33,6 +39,8 @@ public final class RedStonerNetworking {
AcknowledgePersonalComputerIntroPayload.STREAM_CODEC);
PayloadTypeRegistry.serverboundPlay().register(ShutdownPersonalComputerPayload.TYPE,
ShutdownPersonalComputerPayload.STREAM_CODEC);
PayloadTypeRegistry.serverboundPlay().register(StorageTerminalActionPayload.TYPE,
StorageTerminalActionPayload.STREAM_CODEC);
ServerPlayNetworking.registerGlobalReceiver(SaveComputerProgramPayload.TYPE, (payload, context) ->
context.server().execute(() -> saveProgram(context.player(), payload)));
ServerPlayNetworking.registerGlobalReceiver(PlaceAtlasLandmarkPayload.TYPE, (payload, context) ->
@@ -43,6 +51,8 @@ public final class RedStonerNetworking {
context.server().execute(() -> acknowledgePersonalComputerIntro(context.player(), payload)));
ServerPlayNetworking.registerGlobalReceiver(ShutdownPersonalComputerPayload.TYPE, (payload, context) ->
context.server().execute(() -> shutdownPersonalComputer(context.player(), payload)));
ServerPlayNetworking.registerGlobalReceiver(StorageTerminalActionPayload.TYPE, (payload, context) ->
context.server().execute(() -> storageTerminalAction(context.player(), payload)));
RedStonerMod.LOGGER.debug("[{}] Networking contract v{} registered.", RedStonerMod.DISPLAY_NAME, PROTOCOL_VERSION);
}
@@ -77,6 +87,23 @@ public final class RedStonerNetworking {
}
}
public static void openStorageTerminal(ServerPlayer player, BlockPos pos) {
sendStorageTerminalSnapshot(player, pos, StorageTerminalService.snapshot(player, pos, 0));
}
private static void storageTerminalAction(ServerPlayer player, StorageTerminalActionPayload payload) {
sendStorageTerminalSnapshot(player, payload.pos(), StorageTerminalService.handle(player, payload));
}
private static void sendStorageTerminalSnapshot(ServerPlayer player, BlockPos pos,
StorageTerminalNetwork.Snapshot snapshot) {
if (!ServerPlayNetworking.canSend(player, StorageTerminalSnapshotPayload.TYPE)) return;
List<StorageTerminalSnapshotPayload.Entry> entries = snapshot.entries().stream()
.map(entry -> new StorageTerminalSnapshotPayload.Entry(entry.stack(), entry.total())).toList();
ServerPlayNetworking.send(player, new StorageTerminalSnapshotPayload(pos, snapshot.status(), snapshot.page(),
snapshot.pageCount(), snapshot.containerCount(), snapshot.totalEntries(), entries));
}
private static void requestPersonalComputerSnapshot(ServerPlayer player,
RequestPersonalComputerSnapshotPayload payload) {
PersonalComputerBlockEntity computer = validatedPersonalComputer(
@@ -0,0 +1,56 @@
package fr.koka99cab.sanctuary26.redstoner.network;
import fr.koka99cab.sanctuary26.redstoner.RedStonerMod;
import fr.koka99cab.sanctuary26.redstoner.storage.StorageTerminalRules;
import net.minecraft.core.BlockPos;
import net.minecraft.network.RegistryFriendlyByteBuf;
import net.minecraft.network.codec.StreamCodec;
import net.minecraft.network.protocol.common.custom.CustomPacketPayload;
import net.minecraft.world.item.ItemStack;
public record StorageTerminalActionPayload(BlockPos pos, Action action, int page, int entryIndex,
int playerSlot, ItemStack expected) implements CustomPacketPayload {
public static final Type<StorageTerminalActionPayload> TYPE =
new Type<>(RedStonerMod.id("c2s/storage_terminal_action"));
public static final StreamCodec<RegistryFriendlyByteBuf, StorageTerminalActionPayload> STREAM_CODEC =
StreamCodec.of(StorageTerminalActionPayload::write, StorageTerminalActionPayload::read);
public StorageTerminalActionPayload {
pos = pos == null ? BlockPos.ZERO : pos.immutable();
action = action == null ? Action.REQUEST_PAGE : action;
page = Math.max(0, Math.min(StorageTerminalRules.MAX_PAGE_COUNT - 1, page));
entryIndex = entryIndex >= 0 && entryIndex < StorageTerminalRules.PAGE_SIZE ? entryIndex : -1;
playerSlot = playerSlot >= 0 && playerSlot < StorageTerminalRules.PLAYER_STORAGE_SLOTS ? playerSlot : -1;
expected = expected == null || expected.isEmpty() ? ItemStack.EMPTY : expected.copyWithCount(1);
}
private static StorageTerminalActionPayload read(RegistryFriendlyByteBuf buffer) {
BlockPos pos = buffer.readBlockPos();
int ordinal = buffer.readVarInt();
Action[] values = Action.values();
Action action = ordinal >= 0 && ordinal < values.length ? values[ordinal] : Action.REQUEST_PAGE;
return new StorageTerminalActionPayload(pos, action, buffer.readVarInt(), buffer.readVarInt(),
buffer.readVarInt(), ItemStack.OPTIONAL_UNTRUSTED_STREAM_CODEC.decode(buffer));
}
private static void write(RegistryFriendlyByteBuf buffer, StorageTerminalActionPayload payload) {
buffer.writeBlockPos(payload.pos);
buffer.writeVarInt(payload.action.ordinal());
buffer.writeVarInt(payload.page);
buffer.writeVarInt(payload.entryIndex);
buffer.writeVarInt(payload.playerSlot);
ItemStack.OPTIONAL_UNTRUSTED_STREAM_CODEC.encode(buffer, payload.expected);
}
@Override
public Type<? extends CustomPacketPayload> type() {
return TYPE;
}
public enum Action {
REQUEST_PAGE,
TAKE_STACK,
TAKE_ONE,
DEPOSIT_SLOT
}
}
@@ -0,0 +1,80 @@
package fr.koka99cab.sanctuary26.redstoner.network;
import fr.koka99cab.sanctuary26.redstoner.RedStonerMod;
import fr.koka99cab.sanctuary26.redstoner.storage.StorageTerminalRules;
import fr.koka99cab.sanctuary26.redstoner.storage.StorageTerminalStatus;
import java.util.ArrayList;
import java.util.List;
import net.minecraft.core.BlockPos;
import net.minecraft.network.RegistryFriendlyByteBuf;
import net.minecraft.network.codec.StreamCodec;
import net.minecraft.network.protocol.common.custom.CustomPacketPayload;
import net.minecraft.world.item.ItemStack;
public record StorageTerminalSnapshotPayload(BlockPos pos, StorageTerminalStatus status, int page,
int pageCount, int containerCount, int totalEntries, List<Entry> entries) implements CustomPacketPayload {
public static final Type<StorageTerminalSnapshotPayload> TYPE =
new Type<>(RedStonerMod.id("s2c/storage_terminal_snapshot"));
public static final StreamCodec<RegistryFriendlyByteBuf, StorageTerminalSnapshotPayload> STREAM_CODEC =
StreamCodec.of(StorageTerminalSnapshotPayload::write, StorageTerminalSnapshotPayload::read);
public StorageTerminalSnapshotPayload {
pos = pos == null ? BlockPos.ZERO : pos.immutable();
status = status == null ? StorageTerminalStatus.INVALID_TERMINAL : status;
pageCount = Math.max(1, Math.min(StorageTerminalRules.MAX_PAGE_COUNT, pageCount));
page = Math.max(0, Math.min(pageCount - 1, page));
containerCount = Math.max(0, Math.min(StorageTerminalRules.MAX_CONTAINERS, containerCount));
totalEntries = Math.max(0, Math.min(StorageTerminalRules.MAX_ENTRIES, totalEntries));
entries = entries == null ? List.of() : List.copyOf(entries.stream()
.limit(StorageTerminalRules.PAGE_SIZE).toList());
}
private static StorageTerminalSnapshotPayload read(RegistryFriendlyByteBuf buffer) {
BlockPos pos = buffer.readBlockPos();
StorageTerminalStatus status = status(buffer.readVarInt());
int page = buffer.readVarInt();
int pageCount = buffer.readVarInt();
int containerCount = buffer.readVarInt();
int totalEntries = buffer.readVarInt();
int size = buffer.readVarInt();
if (size < 0 || size > StorageTerminalRules.PAGE_SIZE) {
throw new IllegalArgumentException("Storage terminal page exceeds " + StorageTerminalRules.PAGE_SIZE);
}
List<Entry> entries = new ArrayList<>(size);
for (int index = 0; index < size; index++) {
entries.add(new Entry(ItemStack.OPTIONAL_UNTRUSTED_STREAM_CODEC.decode(buffer), buffer.readVarInt()));
}
return new StorageTerminalSnapshotPayload(pos, status, page, pageCount, containerCount, totalEntries, entries);
}
private static void write(RegistryFriendlyByteBuf buffer, StorageTerminalSnapshotPayload payload) {
buffer.writeBlockPos(payload.pos);
buffer.writeVarInt(payload.status.ordinal());
buffer.writeVarInt(payload.page);
buffer.writeVarInt(payload.pageCount);
buffer.writeVarInt(payload.containerCount);
buffer.writeVarInt(payload.totalEntries);
buffer.writeVarInt(payload.entries.size());
for (Entry entry : payload.entries) {
ItemStack.OPTIONAL_UNTRUSTED_STREAM_CODEC.encode(buffer, entry.stack);
buffer.writeVarInt(entry.total);
}
}
private static StorageTerminalStatus status(int ordinal) {
StorageTerminalStatus[] values = StorageTerminalStatus.values();
return values[Math.max(0, Math.min(values.length - 1, ordinal))];
}
@Override
public Type<? extends CustomPacketPayload> type() {
return TYPE;
}
public record Entry(ItemStack stack, int total) {
public Entry {
stack = stack == null || stack.isEmpty() ? ItemStack.EMPTY : stack.copyWithCount(1);
total = stack.isEmpty() ? 0 : Math.max(1, total);
}
}
}
@@ -5,6 +5,7 @@ import fr.koka99cab.sanctuary26.redstoner.block.ComputerBlock;
import fr.koka99cab.sanctuary26.redstoner.block.DrawerBlock;
import fr.koka99cab.sanctuary26.redstoner.block.PersonalComputerBlock;
import fr.koka99cab.sanctuary26.redstoner.block.ParticuleurBlock;
import fr.koka99cab.sanctuary26.redstoner.block.StorageTerminalBlock;
import fr.koka99cab.sanctuary26.redstoner.block.entity.ComputerBlockEntity;
import fr.koka99cab.sanctuary26.redstoner.block.entity.DrawerBlockEntity;
import fr.koka99cab.sanctuary26.redstoner.block.entity.PersonalComputerBlockEntity;
@@ -64,6 +65,12 @@ public final class RedStonerRegistries {
ParticuleurBlock::new,
particuleurProperties(RedStonerMod.id("particuleur"))
);
public static final StorageTerminalBlock STORAGE_TERMINAL = registerBlock(
RedStonerMod.id("storage_terminal"),
StorageTerminalBlock::new,
BlockBehaviour.Properties.of().mapColor(MapColor.COLOR_GRAY).strength(4.0F, 6.0F).sound(SoundType.METAL)
.requiresCorrectToolForDrops().noOcclusion().setId(blockKey(RedStonerMod.id("storage_terminal")))
);
public static final ComputerBlock LEGACY_EIGHT_BIT_COMPUTER = registerBlock(
LEGACY_COMPUTER_ID, ComputerBlock::new, computerProperties(LEGACY_COMPUTER_ID, MapColor.COLOR_RED));
@@ -83,6 +90,8 @@ public final class RedStonerRegistries {
public static final Item PERSONAL_COMPUTER_ITEM = registerBlockItem(RedStonerMod.id("personal_computer"), PERSONAL_COMPUTER);
public static final Item DRAWER_ITEM = registerBlockItem(RedStonerMod.id("drawer"), DRAWER);
public static final Item PARTICULEUR_ITEM = registerBlockItem(RedStonerMod.id("particuleur"), PARTICULEUR);
public static final Item STORAGE_TERMINAL_ITEM = registerBlockItem(
RedStonerMod.id("storage_terminal"), STORAGE_TERMINAL);
public static final Item LEGACY_EIGHT_BIT_COMPUTER_ITEM = registerBlockItem(LEGACY_COMPUTER_ID, LEGACY_EIGHT_BIT_COMPUTER);
public static final Item LEGACY_CONTROLLER_ITEM = registerBlockItem(LEGACY_CONTROLLER_ID, LEGACY_CONTROLLER);
@@ -117,6 +126,7 @@ public final class RedStonerRegistries {
output.accept(EIGHT_BIT_COMPUTER_ITEM);
output.accept(CONTROLLER_ITEM);
output.accept(PARTICULEUR_ITEM);
output.accept(STORAGE_TERMINAL_ITEM);
})
.build()
);
@@ -132,7 +142,7 @@ public final class RedStonerRegistries {
}
initialized = true;
registerDrawerAttack();
RedStonerMod.LOGGER.info("[{}] Registered five active blocks and five legacy block aliases.", RedStonerMod.DISPLAY_NAME);
RedStonerMod.LOGGER.info("[{}] Registered six active blocks and five legacy block aliases.", RedStonerMod.DISPLAY_NAME);
}
public static boolean isController(BlockState state) {
@@ -0,0 +1,118 @@
package fr.koka99cab.sanctuary26.redstoner.storage;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import net.minecraft.world.Container;
import net.minecraft.world.item.ItemStack;
public final class StorageTerminalInventory {
private StorageTerminalInventory() {
}
public static List<Entry> aggregate(List<? extends Container> containers) {
Map<StackKey, MutableEntry> grouped = new LinkedHashMap<>();
for (Container container : containers) {
for (int slot = 0; slot < container.getContainerSize(); slot++) {
ItemStack stack = container.getItem(slot);
if (stack.isEmpty()) continue;
StackKey key = new StackKey(stack);
MutableEntry entry = grouped.computeIfAbsent(key, ignored -> new MutableEntry(key.stack));
entry.total = saturatedAdd(entry.total, stack.getCount());
}
}
List<Entry> result = new ArrayList<>(grouped.size());
for (MutableEntry entry : grouped.values()) result.add(new Entry(entry.stack, entry.total));
return List.copyOf(result);
}
/** Inserts into compatible existing stacks first, then into empty slots. Mutates {@code source}. */
public static int insert(List<? extends Container> containers, ItemStack source) {
if (source == null || source.isEmpty()) return 0;
int before = source.getCount();
for (Container container : containers) {
for (int slot = 0; slot < container.getContainerSize() && !source.isEmpty(); slot++) {
ItemStack target = container.getItem(slot);
if (target.isEmpty() || !ItemStack.isSameItemSameComponents(target, source)
|| !container.canPlaceItem(slot, source)) continue;
int limit = Math.min(container.getMaxStackSize(source), source.getMaxStackSize());
int moved = Math.min(source.getCount(), Math.max(0, limit - target.getCount()));
if (moved <= 0) continue;
target.grow(moved);
source.shrink(moved);
container.setChanged();
}
}
for (Container container : containers) {
for (int slot = 0; slot < container.getContainerSize() && !source.isEmpty(); slot++) {
if (!container.getItem(slot).isEmpty() || !container.canPlaceItem(slot, source)) continue;
int limit = Math.min(container.getMaxStackSize(source), source.getMaxStackSize());
int moved = Math.min(source.getCount(), Math.max(0, limit));
if (moved <= 0) continue;
container.setItem(slot, source.copyWithCount(moved));
source.shrink(moved);
container.setChanged();
}
}
return before - source.getCount();
}
public static ItemStack take(List<? extends Container> containers, ItemStack expected, int requested) {
if (expected == null || expected.isEmpty() || requested <= 0) return ItemStack.EMPTY;
int remaining = requested;
int total = 0;
for (Container container : containers) {
for (int slot = 0; slot < container.getContainerSize() && remaining > 0; slot++) {
ItemStack stored = container.getItem(slot);
if (stored.isEmpty() || !ItemStack.isSameItemSameComponents(stored, expected)) continue;
ItemStack removed = container.removeItem(slot, Math.min(remaining, stored.getCount()));
if (removed.isEmpty()) continue;
total += removed.getCount();
remaining -= removed.getCount();
container.setChanged();
}
}
return total <= 0 ? ItemStack.EMPTY : expected.copyWithCount(total);
}
private static int saturatedAdd(int left, int right) {
return (int) Math.min(Integer.MAX_VALUE, (long) left + Math.max(0, right));
}
public record Entry(ItemStack stack, int total) {
public Entry {
stack = stack == null || stack.isEmpty() ? ItemStack.EMPTY : stack.copyWithCount(1);
total = stack.isEmpty() ? 0 : Math.max(1, total);
}
}
private static final class MutableEntry {
private final ItemStack stack;
private int total;
private MutableEntry(ItemStack stack) {
this.stack = stack.copyWithCount(1);
}
}
private static final class StackKey {
private final ItemStack stack;
private final int hash;
private StackKey(ItemStack stack) {
this.stack = stack.copyWithCount(1);
this.hash = ItemStack.hashItemAndComponents(this.stack);
}
@Override
public int hashCode() {
return hash;
}
@Override
public boolean equals(Object other) {
return other instanceof StackKey key && ItemStack.isSameItemSameComponents(stack, key.stack);
}
}
}
@@ -0,0 +1,92 @@
package fr.koka99cab.sanctuary26.redstoner.storage;
import fr.koka99cab.sanctuary26.redstoner.block.StorageTerminalBlock;
import fr.koka99cab.sanctuary26.redstoner.registry.RedStonerRegistries;
import java.util.ArrayList;
import java.util.List;
import net.minecraft.core.BlockPos;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.Container;
import net.minecraft.world.RandomizableContainer;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.entity.BarrelBlockEntity;
import net.minecraft.world.level.block.entity.BaseContainerBlockEntity;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.block.entity.ChestBlockEntity;
import net.minecraft.world.level.block.state.BlockState;
public final class StorageTerminalNetwork {
private StorageTerminalNetwork() {
}
public static boolean isCompatibleStorage(BlockState state) {
return state.is(Blocks.CHEST) || state.is(Blocks.TRAPPED_CHEST) || state.is(Blocks.BARREL);
}
public static Discovery discover(ServerPlayer player, BlockPos terminalPos) {
if (!(player.level() instanceof ServerLevel level)) return Discovery.error(StorageTerminalStatus.INVALID_TERMINAL);
BlockState terminalState = level.getBlockState(terminalPos);
if (!terminalState.is(RedStonerRegistries.STORAGE_TERMINAL)
|| !terminalState.hasProperty(StorageTerminalBlock.FACING)) {
return Discovery.error(StorageTerminalStatus.INVALID_TERMINAL);
}
BlockPos start = terminalPos.relative(terminalState.getValue(StorageTerminalBlock.FACING).getOpposite());
StorageTerminalTopology.Result topology = StorageTerminalTopology.discover(start, pos ->
level.hasChunkAt(pos) && isCompatibleStorage(level.getBlockState(pos)) && storageEntity(level, pos) != null);
if (topology.tooLarge()) return Discovery.error(StorageTerminalStatus.TOO_LARGE);
if (topology.positions().isEmpty()) return Discovery.error(StorageTerminalStatus.DISCONNECTED);
List<BaseContainerBlockEntity> containers = new ArrayList<>(topology.positions().size());
for (BlockPos pos : topology.positions()) {
BaseContainerBlockEntity container = storageEntity(level, pos);
if (container == null) return Discovery.error(StorageTerminalStatus.DISCONNECTED);
if (container.isLocked()) return Discovery.error(StorageTerminalStatus.LOCKED);
if (container instanceof RandomizableContainer randomizable) randomizable.unpackLootTable(player);
containers.add(container);
}
return new Discovery(StorageTerminalStatus.READY, containers);
}
public static Snapshot snapshot(Discovery discovery, int requestedPage) {
if (discovery.status() != StorageTerminalStatus.READY) {
return new Snapshot(discovery.status(), 0, 1, 0, 0, List.of());
}
List<StorageTerminalInventory.Entry> all = StorageTerminalInventory.aggregate(discovery.containers());
int page = StorageTerminalRules.clampPage(requestedPage, all.size());
int start = StorageTerminalRules.pageStart(page, all.size());
int end = Math.min(all.size(), start + StorageTerminalRules.PAGE_SIZE);
return new Snapshot(StorageTerminalStatus.READY, page, StorageTerminalRules.pageCount(all.size()),
discovery.containers().size(), all.size(), all.subList(start, end));
}
private static BaseContainerBlockEntity storageEntity(ServerLevel level, BlockPos pos) {
BlockEntity blockEntity = level.getBlockEntity(pos);
if (blockEntity instanceof ChestBlockEntity chest) return chest;
if (blockEntity instanceof BarrelBlockEntity barrel) return barrel;
return null;
}
public record Discovery(StorageTerminalStatus status, List<BaseContainerBlockEntity> containers) {
public Discovery {
status = status == null ? StorageTerminalStatus.INVALID_TERMINAL : status;
containers = containers == null ? List.of() : List.copyOf(containers);
}
public static Discovery error(StorageTerminalStatus status) {
return new Discovery(status, List.of());
}
public List<? extends Container> inventoryContainers() {
return containers;
}
}
public record Snapshot(StorageTerminalStatus status, int page, int pageCount, int containerCount,
int totalEntries, List<StorageTerminalInventory.Entry> entries) {
public Snapshot {
status = status == null ? StorageTerminalStatus.INVALID_TERMINAL : status;
entries = entries == null ? List.of() : List.copyOf(entries);
}
}
}
@@ -0,0 +1,26 @@
package fr.koka99cab.sanctuary26.redstoner.storage;
public final class StorageTerminalRules {
public static final int PAGE_SIZE = 54;
public static final int MAX_CONTAINERS = 128;
public static final int SLOTS_PER_CONTAINER = 27;
public static final int MAX_ENTRIES = MAX_CONTAINERS * SLOTS_PER_CONTAINER;
public static final int MAX_PAGE_COUNT = MAX_ENTRIES / PAGE_SIZE;
public static final int PLAYER_STORAGE_SLOTS = 36;
private StorageTerminalRules() {
}
public static int pageCount(int entryCount) {
int bounded = Math.max(0, Math.min(MAX_ENTRIES, entryCount));
return Math.max(1, (bounded + PAGE_SIZE - 1) / PAGE_SIZE);
}
public static int clampPage(int requestedPage, int entryCount) {
return Math.max(0, Math.min(pageCount(entryCount) - 1, requestedPage));
}
public static int pageStart(int requestedPage, int entryCount) {
return clampPage(requestedPage, entryCount) * PAGE_SIZE;
}
}
@@ -0,0 +1,109 @@
package fr.koka99cab.sanctuary26.redstoner.storage;
import fr.koka99cab.sanctuary26.redstoner.network.StorageTerminalActionPayload;
import net.minecraft.core.BlockPos;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.entity.player.Inventory;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.phys.Vec3;
public final class StorageTerminalService {
private static final double MAX_DISTANCE_SQUARED = 64.0D;
private StorageTerminalService() {
}
public static StorageTerminalNetwork.Snapshot snapshot(ServerPlayer player, BlockPos pos, int page) {
if (player.distanceToSqr(Vec3.atCenterOf(pos)) > MAX_DISTANCE_SQUARED) {
return StorageTerminalNetwork.snapshot(
StorageTerminalNetwork.Discovery.error(StorageTerminalStatus.INVALID_TERMINAL), 0);
}
return StorageTerminalNetwork.snapshot(StorageTerminalNetwork.discover(player, pos), page);
}
public static StorageTerminalNetwork.Snapshot handle(ServerPlayer player, StorageTerminalActionPayload payload) {
if (player.distanceToSqr(Vec3.atCenterOf(payload.pos())) > MAX_DISTANCE_SQUARED) {
return StorageTerminalNetwork.snapshot(
StorageTerminalNetwork.Discovery.error(StorageTerminalStatus.INVALID_TERMINAL), 0);
}
StorageTerminalNetwork.Discovery discovery = StorageTerminalNetwork.discover(player, payload.pos());
StorageTerminalNetwork.Snapshot before = StorageTerminalNetwork.snapshot(discovery, payload.page());
if (before.status() != StorageTerminalStatus.READY
|| payload.action() == StorageTerminalActionPayload.Action.REQUEST_PAGE) return before;
boolean changed = switch (payload.action()) {
case TAKE_STACK -> take(player, discovery, before, payload, false);
case TAKE_ONE -> take(player, discovery, before, payload, true);
case DEPOSIT_SLOT -> deposit(player, discovery, payload);
case REQUEST_PAGE -> false;
};
if (changed) {
player.getInventory().setChanged();
player.containerMenu.broadcastChanges();
player.inventoryMenu.broadcastChanges();
}
return StorageTerminalNetwork.snapshot(discovery, before.page());
}
private static boolean take(ServerPlayer player, StorageTerminalNetwork.Discovery discovery,
StorageTerminalNetwork.Snapshot snapshot, StorageTerminalActionPayload payload, boolean single) {
if (payload.entryIndex() < 0 || payload.entryIndex() >= snapshot.entries().size()) return false;
StorageTerminalInventory.Entry entry = snapshot.entries().get(payload.entryIndex());
if (payload.expected().isEmpty()
|| !ItemStack.isSameItemSameComponents(entry.stack(), payload.expected())) return false;
int capacity = playerCapacity(player.getInventory(), entry.stack());
int requested = single ? 1 : entry.stack().getMaxStackSize();
int amount = Math.min(entry.total(), Math.min(capacity, requested));
if (amount <= 0) return false;
ItemStack extracted = StorageTerminalInventory.take(discovery.inventoryContainers(), entry.stack(), amount);
if (extracted.isEmpty()) return false;
int inserted = insertPlayer(player.getInventory(), extracted);
if (!extracted.isEmpty()) StorageTerminalInventory.insert(discovery.inventoryContainers(), extracted);
return inserted > 0;
}
private static boolean deposit(ServerPlayer player, StorageTerminalNetwork.Discovery discovery,
StorageTerminalActionPayload payload) {
int slot = payload.playerSlot();
if (slot < 0 || slot >= StorageTerminalRules.PLAYER_STORAGE_SLOTS) return false;
Inventory inventory = player.getInventory();
ItemStack source = inventory.getItem(slot);
if (source.isEmpty() || payload.expected().isEmpty()
|| !ItemStack.isSameItemSameComponents(source, payload.expected())) return false;
int inserted = StorageTerminalInventory.insert(discovery.inventoryContainers(), source);
if (source.isEmpty()) inventory.setItem(slot, ItemStack.EMPTY);
return inserted > 0;
}
private static int playerCapacity(Inventory inventory, ItemStack expected) {
long capacity = 0L;
for (int slot = 0; slot < StorageTerminalRules.PLAYER_STORAGE_SLOTS; slot++) {
ItemStack stack = inventory.getItem(slot);
if (stack.isEmpty()) capacity += expected.getMaxStackSize();
else if (ItemStack.isSameItemSameComponents(stack, expected)) {
capacity += Math.max(0, stack.getMaxStackSize() - stack.getCount());
}
}
return (int) Math.min(Integer.MAX_VALUE, capacity);
}
/** Inserts into the 36 survival storage slots and mutates {@code source}. */
private static int insertPlayer(Inventory inventory, ItemStack source) {
int before = source.getCount();
for (int slot = 0; slot < StorageTerminalRules.PLAYER_STORAGE_SLOTS && !source.isEmpty(); slot++) {
ItemStack target = inventory.getItem(slot);
if (target.isEmpty() || !ItemStack.isSameItemSameComponents(target, source)) continue;
int moved = Math.min(source.getCount(), Math.max(0, target.getMaxStackSize() - target.getCount()));
if (moved <= 0) continue;
target.grow(moved);
source.shrink(moved);
}
for (int slot = 0; slot < StorageTerminalRules.PLAYER_STORAGE_SLOTS && !source.isEmpty(); slot++) {
if (!inventory.getItem(slot).isEmpty()) continue;
int moved = Math.min(source.getCount(), source.getMaxStackSize());
inventory.setItem(slot, source.copyWithCount(moved));
source.shrink(moved);
}
return before - source.getCount();
}
}
@@ -0,0 +1,9 @@
package fr.koka99cab.sanctuary26.redstoner.storage;
public enum StorageTerminalStatus {
READY,
INVALID_TERMINAL,
DISCONNECTED,
TOO_LARGE,
LOCKED
}
@@ -0,0 +1,44 @@
package fr.koka99cab.sanctuary26.redstoner.storage;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.function.Predicate;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
public final class StorageTerminalTopology {
private StorageTerminalTopology() {
}
public static Result discover(BlockPos start, Predicate<BlockPos> storageAt) {
if (start == null || storageAt == null) return new Result(List.of(), false);
ArrayDeque<BlockPos> pending = new ArrayDeque<>();
Set<BlockPos> visited = new HashSet<>();
List<BlockPos> storage = new ArrayList<>();
pending.add(start.immutable());
while (!pending.isEmpty()) {
BlockPos pos = pending.removeFirst();
if (!visited.add(pos) || !storageAt.test(pos)) continue;
if (storage.size() >= StorageTerminalRules.MAX_CONTAINERS) {
return new Result(storage, true);
}
storage.add(pos);
for (Direction direction : Direction.values()) {
BlockPos neighbor = pos.relative(direction);
if (!visited.contains(neighbor)) pending.addLast(neighbor.immutable());
}
}
return new Result(storage, false);
}
public record Result(List<BlockPos> positions, boolean tooLarge) {
public Result {
positions = positions == null ? List.of() : List.copyOf(positions);
}
}
}
@@ -0,0 +1,8 @@
{
"variants": {
"facing=north": { "model": "redstoner:block/storage_terminal" },
"facing=east": { "model": "redstoner:block/storage_terminal", "y": 90 },
"facing=south": { "model": "redstoner:block/storage_terminal", "y": 180 },
"facing=west": { "model": "redstoner:block/storage_terminal", "y": 270 }
}
}
@@ -0,0 +1,6 @@
{
"model": {
"type": "minecraft:model",
"model": "redstoner:block/storage_terminal"
}
}
@@ -49,5 +49,12 @@
"message.redstoner.computer.invalid": "Program rejected: %s",
"block.redstoner.particuleur": "Particle Emitter",
"block.particuleur.particuleur": "Legacy Particle Emitter",
"tooltip.redstoner.particuleur": "Insert a catalyst, then power the block with redstone."
"tooltip.redstoner.particuleur": "Insert a catalyst, then power the block with redstone.",
"block.redstoner.storage_terminal": "Storage Terminal",
"screen.redstoner.storage_terminal.title": "Storage Terminal",
"screen.redstoner.storage_terminal.summary": "%s/%s · %s blocks · %s types",
"screen.redstoner.storage_terminal.error.invalid_terminal": "The terminal is no longer accessible.",
"screen.redstoner.storage_terminal.error.disconnected": "No connected chest or barrel.",
"screen.redstoner.storage_terminal.error.too_large": "Network too large (maximum 128 storage blocks).",
"screen.redstoner.storage_terminal.error.locked": "The network contains a locked storage block."
}
@@ -49,5 +49,12 @@
"message.redstoner.computer.invalid": "Programme refusé : %s",
"block.redstoner.particuleur": "Particuleur",
"block.particuleur.particuleur": "Particuleur historique",
"tooltip.redstoner.particuleur": "Insérez un catalyseur puis alimentez le bloc en redstone."
"tooltip.redstoner.particuleur": "Insérez un catalyseur puis alimentez le bloc en redstone.",
"block.redstoner.storage_terminal": "Terminal de stockage",
"screen.redstoner.storage_terminal.title": "Terminal de stockage",
"screen.redstoner.storage_terminal.summary": "%s/%s · %s blocs · %s types",
"screen.redstoner.storage_terminal.error.invalid_terminal": "Le terminal n'est plus accessible.",
"screen.redstoner.storage_terminal.error.disconnected": "Aucun coffre ou baril connecté.",
"screen.redstoner.storage_terminal.error.too_large": "Réseau trop grand (128 stockages maximum).",
"screen.redstoner.storage_terminal.error.locked": "Le réseau contient un stockage verrouillé."
}
@@ -49,5 +49,12 @@
"message.redstoner.computer.invalid": "Программа отклонена: %s",
"block.redstoner.particuleur": "Генератор частиц",
"block.particuleur.particuleur": "Старый генератор частиц",
"tooltip.redstoner.particuleur": "Вставьте катализатор и подайте сигнал редстоуна."
"tooltip.redstoner.particuleur": "Вставьте катализатор и подайте сигнал редстоуна.",
"block.redstoner.storage_terminal": "Терминал хранилища",
"screen.redstoner.storage_terminal.title": "Терминал хранилища",
"screen.redstoner.storage_terminal.summary": "%s/%s · блоков: %s · типов: %s",
"screen.redstoner.storage_terminal.error.invalid_terminal": "Терминал больше недоступен.",
"screen.redstoner.storage_terminal.error.disconnected": "Нет подключённых сундуков или бочек.",
"screen.redstoner.storage_terminal.error.too_large": "Сеть слишком велика (не более 128 хранилищ).",
"screen.redstoner.storage_terminal.error.locked": "В сети есть запертое хранилище."
}
@@ -0,0 +1,25 @@
{
"parent": "minecraft:block/block",
"textures": {
"particle": "redstoner:block/redstone_controller_front",
"front": "redstoner:block/redstone_controller_front",
"back": "redstoner:block/redstone_controller_back",
"side": "redstoner:block/redstone_controller_right",
"top": "redstoner:block/redstone_controller_top",
"bottom": "redstoner:block/redstone_controller_bottom"
},
"elements": [
{
"from": [0, 0, 8],
"to": [16, 16, 16],
"faces": {
"north": { "uv": [0, 0, 16, 16], "texture": "#front" },
"south": { "uv": [0, 0, 16, 16], "texture": "#back" },
"west": { "uv": [8, 0, 16, 16], "texture": "#side" },
"east": { "uv": [0, 0, 8, 16], "texture": "#side" },
"up": { "uv": [0, 8, 16, 16], "texture": "#top" },
"down": { "uv": [0, 0, 16, 8], "texture": "#bottom" }
}
}
]
}
@@ -1 +1 @@
{"replace":false,"values":["redstoner:eight_bit_computer","redstoner:controller","redstoner:personal_computer","redstoner:particuleur","redstone_computer:redstone_computer","redstone_computer:redstone_controller","redstone_computer:personal_computer","particuleur:particuleur"]}
{"replace":false,"values":["redstoner:eight_bit_computer","redstoner:controller","redstoner:personal_computer","redstoner:particuleur","redstoner:storage_terminal","redstone_computer:redstone_computer","redstone_computer:redstone_controller","redstone_computer:personal_computer","particuleur:particuleur"]}
@@ -1 +1 @@
{"replace":false,"values":["redstoner:eight_bit_computer","redstoner:controller","redstoner:personal_computer","redstoner:particuleur","redstone_computer:redstone_computer","redstone_computer:redstone_controller","redstone_computer:personal_computer","particuleur:particuleur"]}
{"replace":false,"values":["redstoner:eight_bit_computer","redstoner:controller","redstoner:personal_computer","redstoner:particuleur","redstoner:storage_terminal","redstone_computer:redstone_computer","redstone_computer:redstone_controller","redstone_computer:personal_computer","particuleur:particuleur"]}
@@ -0,0 +1,20 @@
{
"type": "minecraft:block",
"pools": [
{
"rolls": 1,
"entries": [
{
"type": "minecraft:item",
"name": "redstoner:storage_terminal"
}
],
"conditions": [
{
"condition": "minecraft:survives_explosion"
}
]
}
],
"random_sequence": "redstoner:blocks/storage_terminal"
}
@@ -0,0 +1,19 @@
{
"type": "minecraft:crafting_shaped",
"category": "redstone",
"pattern": [
"PGP",
"GCG",
"PTP"
],
"key": {
"P": "anotherworld:silver_plate",
"G": "minecraft:tinted_glass",
"C": "redstoner:controller",
"T": "anotherworld:titanium_ingot"
},
"result": {
"id": "redstoner:storage_terminal",
"count": 1
}
}
+1 -1
View File
@@ -31,7 +31,7 @@
"system_type": "gameplay-system",
"namespace": "redstoner",
"data_version": 3,
"network_protocol": 5,
"network_protocol": 6,
"lifecycle": "${lifecycle}",
"pack_version": "${pack_version}",
"descriptions": {
@@ -0,0 +1,103 @@
package fr.koka99cab.sanctuary26.redstoner.storage;
import fr.koka99cab.sanctuary26.redstoner.network.StorageTerminalActionPayload;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import net.minecraft.SharedConstants;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Holder;
import net.minecraft.core.component.DataComponentMap;
import net.minecraft.core.component.DataComponents;
import net.minecraft.network.chat.Component;
import net.minecraft.server.Bootstrap;
import net.minecraft.world.SimpleContainer;
import net.minecraft.world.item.Item;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.Items;
public final class StorageTerminalSmoke {
private StorageTerminalSmoke() {
}
public static void main(String[] args) {
SharedConstants.tryDetectVersion();
Bootstrap.bootStrap();
verifyTopology();
verifyPagination();
verifyActionBounds();
verifyAggregationAndTransfers();
}
private static void verifyTopology() {
Set<BlockPos> line = new HashSet<>();
for (int x = 0; x < StorageTerminalRules.MAX_CONTAINERS; x++) line.add(new BlockPos(x, 0, 0));
StorageTerminalTopology.Result exact = StorageTerminalTopology.discover(BlockPos.ZERO, line::contains);
require(!exact.tooLarge() && exact.positions().size() == StorageTerminalRules.MAX_CONTAINERS,
"An exact 128-container network was rejected");
line.add(new BlockPos(StorageTerminalRules.MAX_CONTAINERS, 0, 0));
StorageTerminalTopology.Result oversized = StorageTerminalTopology.discover(BlockPos.ZERO, line::contains);
require(oversized.tooLarge() && oversized.positions().size() == StorageTerminalRules.MAX_CONTAINERS,
"A 129-container network was not bounded");
Set<BlockPos> loaded = Set.of(BlockPos.ZERO, new BlockPos(0, 1, 0), new BlockPos(0, 2, 0),
new BlockPos(20, 0, 0));
StorageTerminalTopology.Result bounded = StorageTerminalTopology.discover(BlockPos.ZERO, loaded::contains);
require(!bounded.tooLarge() && bounded.positions().size() == 3
&& !bounded.positions().contains(new BlockPos(20, 0, 0)),
"Topology crossed a disconnected or unloaded gap");
}
private static void verifyPagination() {
require(StorageTerminalRules.pageCount(0) == 1, "An empty terminal must keep one page");
require(StorageTerminalRules.pageCount(54) == 1, "A full visible page overflowed");
require(StorageTerminalRules.pageCount(55) == 2, "The second page did not open");
require(StorageTerminalRules.clampPage(999, 55) == 1, "A hostile page index was not clamped");
}
private static void verifyActionBounds() {
StorageTerminalActionPayload hostile = new StorageTerminalActionPayload(BlockPos.ZERO,
StorageTerminalActionPayload.Action.DEPOSIT_SLOT, -5, 999, 999, ItemStack.EMPTY);
require(hostile.page() == 0 && hostile.entryIndex() == -1 && hostile.playerSlot() == -1,
"Hostile terminal action indexes were mapped to valid slots");
StorageTerminalActionPayload missingAction = new StorageTerminalActionPayload(
BlockPos.ZERO, null, 0, -1, -1, ItemStack.EMPTY);
require(missingAction.action() == StorageTerminalActionPayload.Action.REQUEST_PAGE,
"A missing terminal action did not fall back to a read-only request");
}
private static void verifyAggregationAndTransfers() {
SimpleContainer first = new SimpleContainer(27);
SimpleContainer second = new SimpleContainer(27);
Holder<Item> cobblestone = new Holder.Direct<>(Items.COBBLESTONE, DataComponentMap.builder()
.set(DataComponents.MAX_STACK_SIZE, 64).build());
first.setItem(0, new ItemStack(cobblestone, 60));
ItemStack named = new ItemStack(cobblestone, 5);
named.set(DataComponents.CUSTOM_NAME, Component.literal("Distinct"));
second.setItem(3, named);
ItemStack deposit = new ItemStack(cobblestone, 10);
int inserted = StorageTerminalInventory.insert(List.of(first, second), deposit);
require(inserted == 10 && deposit.isEmpty(), "Terminal deposit lost or retained items");
require(first.getItem(0).getCount() == 64 && first.getItem(1).getCount() == 6,
"Terminal deposit did not fill matching stacks before empty slots");
List<StorageTerminalInventory.Entry> entries = StorageTerminalInventory.aggregate(List.of(first, second));
require(entries.size() == 2, "Items with distinct components were merged");
StorageTerminalInventory.Entry plain = entries.stream()
.filter(entry -> !entry.stack().has(DataComponents.CUSTOM_NAME)).findFirst().orElseThrow();
require(plain.total() == 70, "Aggregated terminal quantity is incorrect");
ItemStack taken = StorageTerminalInventory.take(List.of(first, second), plain.stack(), 64);
require(taken.getCount() == 64 && ItemStack.isSameItemSameComponents(taken, plain.stack()),
"Terminal extraction changed item identity or quantity");
List<StorageTerminalInventory.Entry> remaining = StorageTerminalInventory.aggregate(List.of(first, second));
require(remaining.stream().filter(entry -> !entry.stack().has(DataComponents.CUSTOM_NAME))
.findFirst().orElseThrow().total() == 6, "Terminal extraction did not update physical slots");
}
private static void require(boolean condition, String message) {
if (!condition) throw new IllegalStateException(message);
}
}
+38
View File
@@ -0,0 +1,38 @@
# Dimension Alpha / Indev
`sanctuary:alpha` est la dimension historique finie de Sanctuary. Sanctuary
possède son identifiant, son type de dimension et son générateur ; I Like To
Move It possède exclusivement son portail et sa persistance.
## Contrat de génération
- volume historique exact : **128 × 128 × 64**, de X/Z `-64` à `63` et de
Y `0` à `63` ;
- terrain déterministe par seed : relief insulaire, plages, cavernes, minerais,
eau, lave, végétation et maison de départ Indev ;
- anneau extérieur en barrières, fond en bedrock et aucun placement au-delà de
l'enveloppe finie ;
- résidence et arrivée de portail calculées par l'API publique en lecture seule
`SanctuaryAlphaWorld` ; l'arrivée est sèche, dégagée et séparée de la maison ;
- la dimension est déclarée dans les profils Sanctuary et Overworld et reste
disponible en solo comme sur serveur dédié.
Une même seed reconstruit le même volume, la même maison et les mêmes positions.
L'ancien identifiant `portals:alpha` est migré vers `sanctuary:alpha` ; aucun
nouvel identifiant persistant de Portals n'est repris.
## Accès
En jeu normal, construire un cadre extérieur de 4 × 5 en cobblestone moussue,
laisser un intérieur vide de 2 × 3 et l'allumer au briquet. La première
traversée crée une moitié distante à l'arrivée sûre ; les traversées suivantes
réutilisent la paire persistée.
Au laboratoire, un opérateur peut vérifier le chargement de la dimension avec
`/masterkey teleport @s sanctuary:alpha 0 48 0`. Cette commande téléporte aux
coordonnées demandées sans recherche de surface : utiliser le portail pour
l'arrivée sûre du gameplay normal.
Les textures historiques facultatives sont décrites dans
[`ambiance/GOLDEN_DAYS.md`](../ambiance/GOLDEN_DAYS.md). Elles ne font pas
partie de Sanctuary.
+36
View File
@@ -0,0 +1,36 @@
# Third-party notices
## minecraft-python — Indev 20100223 level generator
The `AlphaIndevChunkFeature` terrain algorithm is adapted from the
`minecraft-python` Indev 20100223 implementation by pythonengineer.
Source: https://github.com/pythonengineer/minecraft-python/tree/indev-20100223
BSD 3-Clause License
Copyright (c) 2023 pythonengineer
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its contributors
may be used to endorse or promote products derived from this software
without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+78 -13
View File
@@ -6,6 +6,12 @@ dependencies {
implementation "maven.modrinth:u6dRKJwZ:VI3quY4l"
}
tasks.named("jar") {
from("THIRD_PARTY_NOTICES.md") {
rename { "THIRD_PARTY_NOTICES_sanctuary.md" }
}
}
tasks.register("progressionModelSmoke", JavaExec) {
group = "verification"
description = "Round-trips the Sanctuary v3 statistic and unlock codec."
@@ -86,6 +92,14 @@ tasks.register("capeJoinSyncSmoke", JavaExec) {
mainClass = "fr.koka99cab.sanctuary26.sanctuary.gameplay.CapeJoinSyncQueueSmoke"
}
tasks.register("alphaIndevGeneratorSmoke", JavaExec) {
group = "verification"
description = "Checks deterministic finite Indev islands, residences and safe portal arrivals."
dependsOn tasks.named("testClasses")
classpath = sourceSets.test.runtimeClasspath
mainClass = "fr.koka99cab.sanctuary26.sanctuary.worldgen.feature.AlphaIndevGeneratorSmoke"
}
tasks.register("verifyBlackMarket") {
group = "verification"
description = "Checks the physical sign market, flash Shop, escrow ledger and mailbox delivery."
@@ -121,7 +135,7 @@ tasks.register("verifyBlackMarket") {
def questData = file("src/main/java/fr/koka99cab/sanctuary26/sanctuary/shop/QuestBoardData.java").text
def quests = file("src/main/java/fr/koka99cab/sanctuary26/sanctuary/shop/QuestBoardService.java").text
def inventoryView = file("src/main/java/fr/koka99cab/sanctuary26/sanctuary/api/inventory/SanctuaryPlayerInventoryView.java").text
if (project.version.toString() != "0.0.0-alpha.119"
if (project.version.toString() != "0.0.0-alpha.122"
|| !data.contains('SAVE_KEY = "black_market"') || !data.contains("DATA_VERSION = 2")
|| !data.contains("ItemStack.CODEC.fieldOf(\"item\")") || !data.contains("replaceListing")
|| !service.contains("LISTING_FEE_RUBIES = 1") || !service.contains("COMMISSION_PERCENT = 5")
@@ -132,7 +146,7 @@ tasks.register("verifyBlackMarket") {
|| !interactionRules.contains("middleClick || secondaryUse")
|| !client.contains("handleBlackMarketMiddleClick")
|| !client.contains("ClientPlayNetworking.send(new BlackMarketInteractPayload())")
|| !minecraftMixin.contains('method = "pickBlock"')
|| !minecraftMixin.contains('method = "pickBlockOrEntity"')
|| !middleClickPayload.contains('SanctuaryMod.id("c2s/black_market_interact")')
|| !middleClickPayload.contains("No client position, listing ID, item or price is trusted")
|| !network.contains("PROTOCOL_VERSION = 22")
@@ -1602,8 +1616,8 @@ tasks.register("verifySanctuary") {
def worldgenRoot = file("src/main/resources/data/sanctuary/worldgen")
def worldgenCounts = [
density_function: 12, biome: 5, configured_feature: 12, configured_carver: 1,
placed_feature: 18, noise_settings: 2, world_preset: 2
density_function: 12, biome: 6, configured_feature: 13, configured_carver: 1,
placed_feature: 19, noise_settings: 3, world_preset: 2
]
worldgenCounts.each { type, expected ->
def actual = fileTree(new File(worldgenRoot, type.toString())).matching { include "**/*.json" }.files.size()
@@ -2818,7 +2832,7 @@ tasks.register("verifyShopCurrencyRowsRelease") {
"src/main/java/fr/koka99cab/sanctuary26/sanctuary/shop/BlackMarketService.java").text
def shop = file(
"src/main/java/fr/koka99cab/sanctuary26/sanctuary/shop/ShopService.java").text
if (project.version.toString() != "0.0.0-alpha.119"
if (project.version.toString() != "0.0.0-alpha.122"
|| migration.target?.modules?.sanctuary != "0.0.0-alpha.117"
|| migration.shop_currency_inventory?.all_unlocked_rows_counted != true
|| migration.shop_currency_inventory?.overflow_rows_counted != true
@@ -2984,7 +2998,7 @@ tasks.register("verifyProgressivePauseMenu") {
"pack/migrations/26.2.0-alpha.190-progressive-pause-menu-to-alpha.191.json"))
def pauseMenu = file("src/main/java/fr/koka99cab/sanctuary26/sanctuary/mixin/client/PauseScreenMixin.java").text
def costs = file("src/main/java/fr/koka99cab/sanctuary26/sanctuary/progression/SanctuaryProgressionCosts.java").text
if (project.version.toString() != "0.0.0-alpha.119"
if (project.version.toString() != "0.0.0-alpha.122"
|| migration.target?.pack_version != "26.2.0-alpha.191"
|| migration.target?.modules?.sanctuary != "0.0.0-alpha.109"
|| migration.pause_menu?.progression_full_width != true
@@ -3022,7 +3036,7 @@ tasks.register("verifyShopPricingLayout") {
"pack/migrations/26.2.0-alpha.192-shop-pricing-layout-to-alpha.193.json"))
def screen = file("src/main/java/fr/koka99cab/sanctuary26/sanctuary/client/SanctuaryShopScreen.java").text
def progression = file("src/main/java/fr/koka99cab/sanctuary26/sanctuary/shop/ShopProgression.java").text
if (project.version.toString() != "0.0.0-alpha.119"
if (project.version.toString() != "0.0.0-alpha.122"
|| migration.target?.modules?.sanctuary != "0.0.0-alpha.110"
|| migration.shop_layout?.locked_offer_buttons != 6
|| migration.shop_layout?.button_slots != [4, 5, 6, 7, 8, 9]
@@ -3053,7 +3067,7 @@ tasks.register("verifyShopCsvBossGates") {
"pack/migrations/26.2.0-alpha.194-shop-csv-boss-gates-to-alpha.195.json"))
def provider = file("src/main/java/fr/koka99cab/sanctuary26/sanctuary/shop/ShopCatalogProvider.java").text
def catalog = file("src/main/java/fr/koka99cab/sanctuary26/sanctuary/shop/ShopOfferCatalog.java").text
if (project.version.toString() != "0.0.0-alpha.119"
if (project.version.toString() != "0.0.0-alpha.122"
|| migration.target?.pack_version != "26.2.0-alpha.195"
|| migration.target?.modules?.sanctuary != "0.0.0-alpha.111"
|| migration.compatibility?.sanctuary_data_version != 11
@@ -3084,7 +3098,7 @@ tasks.register("verifyDeliveryBoxOrientation") {
def blockstate = new JsonSlurper().parse(
file("src/main/resources/assets/sanctuary/blockstates/delivery_box.json"))
def expectedVariants = ["facing=north", "facing=east", "facing=south", "facing=west"].toSet()
if (project.version.toString() != "0.0.0-alpha.119"
if (project.version.toString() != "0.0.0-alpha.122"
|| migration.target?.modules?.sanctuary != "0.0.0-alpha.112"
|| migration.mailbox?.id != "sanctuary:delivery_box"
|| migration.mailbox?.horizontal_facing != true
@@ -3112,7 +3126,7 @@ tasks.register("verifyInventoryShiftClick") {
def mixin = file("src/main/java/fr/koka99cab/sanctuary26/sanctuary/mixin/AbstractContainerMenuMixin.java").text
def screen = file("src/main/java/fr/koka99cab/sanctuary26/sanctuary/mixin/client/AbstractContainerScreenMixin.java").text
def gui = file("INVENTORY_GUI.md").text
if (project.version.toString() != "0.0.0-alpha.119"
if (project.version.toString() != "0.0.0-alpha.122"
|| !transfer.contains("shouldAppendOverflow")
|| !transfer.contains("isVanillaStorageRange")
|| !transfer.contains("hotbarStart")
@@ -3158,7 +3172,7 @@ tasks.register("verifyQuestRequests") {
new JsonSlurper().parse(file("src/main/resources/data/sanctuary/loot_table/blocks/${path}.json"))
.pools[0].entries[0].name
}
if (project.version.toString() != "0.0.0-alpha.119"
if (project.version.toString() != "0.0.0-alpha.122"
|| migration.target?.modules?.sanctuary != "0.0.0-alpha.113"
|| migration.quest_requests?.save_key != "quest_requests"
|| migration.quest_requests?.data_version != 1
@@ -3216,7 +3230,7 @@ tasks.register("verifyQuestRequestContracts") {
def locales = ["fr_fr", "en_us", "ru_ru"].collectEntries { locale ->
[(locale): new JsonSlurper().parse(file("src/main/resources/assets/sanctuary/lang/${locale}.json"))]
}
if (project.version.toString() != "0.0.0-alpha.119"
if (project.version.toString() != "0.0.0-alpha.122"
|| migration.source?.pack_version != "26.2.0-alpha.198"
|| migration.source?.modules?.sanctuary != "0.0.0-alpha.114"
|| migration.target?.pack_version != "26.2.0-alpha.199"
@@ -3285,7 +3299,7 @@ tasks.register("verifyQuestRequestEscrow") {
def locales = ["fr_fr", "en_us", "ru_ru"].collectEntries { locale ->
[(locale): new JsonSlurper().parse(file("src/main/resources/assets/sanctuary/lang/${locale}.json"))]
}
if (project.version.toString() != "0.0.0-alpha.119"
if (project.version.toString() != "0.0.0-alpha.122"
|| migration.source?.pack_version != "26.2.0-alpha.200"
|| migration.source?.modules?.sanctuary != "0.0.0-alpha.115"
|| migration.target?.pack_version != "26.2.0-alpha.201"
@@ -3330,6 +3344,55 @@ tasks.named("verifySanctuary") {
dependsOn tasks.named("verifyInventoryShiftClick")
}
tasks.register("verifyAlphaIndev") {
group = "verification"
description = "Checks the alpha.122 finite Indev world, migration and attribution contract."
inputs.file(file("ALPHA.md"))
inputs.file(file("THIRD_PARTY_NOTICES.md"))
inputs.file(file("src/main/java/fr/koka99cab/sanctuary26/sanctuary/api/world/SanctuaryAlphaWorld.java"))
inputs.file(file("src/main/java/fr/koka99cab/sanctuary26/sanctuary/worldgen/feature/AlphaIndevChunkFeature.java"))
inputs.files(fileTree("src/main/resources/data/sanctuary") { include "**/alpha*.json" })
inputs.files(fileTree("src/main/resources/assets/sanctuary/lang") { include "*.json" })
inputs.file(rootProject.file("pack/migrations/26.2.0-alpha.218-alpha-indev-to-alpha.219.json"))
doLast {
def migration = new JsonSlurper().parse(rootProject.file(
"pack/migrations/26.2.0-alpha.218-alpha-indev-to-alpha.219.json"))
def presetFiles = ["sanctuary", "overworld"].collect {
new JsonSlurper().parse(file("src/main/resources/data/sanctuary/worldgen/world_preset/${it}.json"))
}
def dimensionType = new JsonSlurper().parse(file(
"src/main/resources/data/sanctuary/dimension_type/alpha.json"))
def configured = new JsonSlurper().parse(file(
"src/main/resources/data/sanctuary/worldgen/configured_feature/alpha_indev_chunk.json"))
def javaText = [
file("src/main/java/fr/koka99cab/sanctuary26/sanctuary/api/world/SanctuaryAlphaWorld.java"),
file("src/main/java/fr/koka99cab/sanctuary26/sanctuary/worldgen/feature/AlphaIndevChunkFeature.java")
]*.text.join("\n")
def locales = ["fr_fr", "en_us", "ru_ru"].collect {
new JsonSlurper().parse(file("src/main/resources/assets/sanctuary/lang/${it}.json"))
}
if (project.version.toString() != "0.0.0-alpha.122"
|| migration.target?.modules?.sanctuary != "0.0.0-alpha.122"
|| migration.alpha_world?.dimension != "sanctuary:alpha"
|| migration.alpha_world?.legacy_dimension != "portals:alpha"
|| migration.alpha_world?.width != 128 || migration.alpha_world?.depth != 128
|| migration.alpha_world?.height != 64 || migration.alpha_world?.deterministic_per_seed != true
|| migration.alpha_world?.finite_barrier_boundary != true
|| presetFiles.any { it.dimensions?.get("sanctuary:alpha") == null }
|| dimensionType.min_y != 0 || dimensionType.height != 64 || dimensionType.logical_height != 64
|| configured.type != "sanctuary:alpha_indev_chunk"
|| !javaText.contains("public static final int WIDTH = 128")
|| !javaText.contains("public static final int HEIGHT = 64")
|| !javaText.contains("Blocks.BARRIER.defaultBlockState()")
|| !javaText.contains("Blocks.BEDROCK.defaultBlockState()")
|| !javaText.contains("public static BlockPos portalArrival(long seed)")
|| locales.any { !it.containsKey("dimension.sanctuary.alpha") }
|| !file("THIRD_PARTY_NOTICES.md").text.contains("BSD 3-Clause License")) {
throw new GradleException("Sanctuary alpha.122 finite Indev contract is incomplete")
}
}
}
tasks.named("check") {
dependsOn tasks.named("verifySanctuary")
dependsOn tasks.named("verifyDeliveryBoxOrientation")
@@ -3339,4 +3402,6 @@ tasks.named("check") {
dependsOn tasks.named("verifyShopCurrencyRowsRelease")
dependsOn tasks.named("celestialGraphSmoke")
dependsOn tasks.named("questRequestModelSmoke")
dependsOn tasks.named("alphaIndevGeneratorSmoke")
dependsOn tasks.named("verifyAlphaIndev")
}
@@ -0,0 +1,29 @@
package fr.koka99cab.sanctuary26.sanctuary.api.world;
import fr.koka99cab.sanctuary26.sanctuary.worldgen.feature.AlphaIndevChunkFeature;
import net.minecraft.core.BlockPos;
/** Public, read-only geometry contract for the finite {@code sanctuary:alpha} world. */
public final class SanctuaryAlphaWorld {
public static final int WIDTH = AlphaIndevChunkFeature.WIDTH;
public static final int DEPTH = AlphaIndevChunkFeature.DEPTH;
public static final int HEIGHT = AlphaIndevChunkFeature.HEIGHT;
private SanctuaryAlphaWorld() {
}
/** Historical residence position inside the deterministic Indev starting house. */
public static BlockPos residence(long seed) {
return AlphaIndevChunkFeature.notchHome(seed);
}
/** Safe portal feet position outside the house for this world's seed. */
public static BlockPos portalArrival(long seed) {
return AlphaIndevChunkFeature.portalArrival(seed);
}
public static boolean containsHorizontal(BlockPos pos) {
return pos.getX() >= AlphaIndevChunkFeature.MIN_X && pos.getX() <= AlphaIndevChunkFeature.MAX_X
&& pos.getZ() >= AlphaIndevChunkFeature.MIN_Z && pos.getZ() <= AlphaIndevChunkFeature.MAX_Z;
}
}
@@ -11,7 +11,7 @@ import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
/** Gives the game window the Sanctuary identity while preserving its connection suffix. */
@Mixin(Minecraft.class)
public abstract class MinecraftMixin {
@Inject(method = "pickBlock", at = @At("HEAD"), cancellable = true)
@Inject(method = "pickBlockOrEntity", at = @At("HEAD"), cancellable = true)
private void sanctuary$blackMarketMiddleClick(CallbackInfo callback) {
if (SanctuaryClient.handleBlackMarketMiddleClick((Minecraft) (Object) this)) callback.cancel();
}
@@ -29,6 +29,15 @@ public final class BlackMarketItemPolicy {
if (stack == null || stack.isEmpty() || stack.has(DataComponents.CREATIVE_SLOT_LOCK)) return true;
if (depth > 8 || stack.has(DataComponents.CONTAINER_LOOT)) return true;
Identifier id = BuiltInRegistries.ITEM.getKey(stack.getItem());
if (forbidden(id)
|| stack.getItem() instanceof BlockItem blockItem && blockItem.getBlock() instanceof GameMasterBlock) return true;
var container = stack.get(DataComponents.CONTAINER);
if (container != null && container.nonEmptyItemCopyStream().anyMatch(item -> forbidden(item, depth + 1))) return true;
var bundle = stack.get(DataComponents.BUNDLE_CONTENTS);
return bundle != null && bundle.itemCopyStream().anyMatch(item -> forbidden(item, depth + 1));
}
public static boolean forbidden(Identifier id) {
if (id == null) return true;
String namespace = id.getNamespace().toLowerCase(Locale.ROOT);
String path = id.getPath().toLowerCase(Locale.ROOT);
@@ -37,13 +46,10 @@ public final class BlackMarketItemPolicy {
|| path.contains("rainbow_block")) return true;
if (namespace.equals("onlyfun") && path.equals("lucky_block")) return true;
if (namespace.equals("sanctuary") && path.equals("delivery_box")) return true;
if (namespace.equals("redstoner") && path.equals("storage_terminal")) return true;
if (namespace.equals("anotherworld") && (path.equals("ruby") || path.equals("sapphire")
|| path.equals("saphire") || path.startsWith("ruby_") || path.startsWith("sapphire_")
|| path.startsWith("saphire_"))) return true;
if (stack.getItem() instanceof BlockItem blockItem && blockItem.getBlock() instanceof GameMasterBlock) return true;
var container = stack.get(DataComponents.CONTAINER);
if (container != null && container.nonEmptyItemCopyStream().anyMatch(item -> forbidden(item, depth + 1))) return true;
var bundle = stack.get(DataComponents.BUNDLE_CONTENTS);
return bundle != null && bundle.itemCopyStream().anyMatch(item -> forbidden(item, depth + 1));
return false;
}
}
@@ -2,6 +2,7 @@ package fr.koka99cab.sanctuary26.sanctuary.worldgen;
import fr.koka99cab.sanctuary26.sanctuary.SanctuaryMod;
import fr.koka99cab.sanctuary26.sanctuary.worldgen.feature.MatterDepthMaterializationFeature;
import fr.koka99cab.sanctuary26.sanctuary.worldgen.feature.AlphaIndevChunkFeature;
import fr.koka99cab.sanctuary26.sanctuary.worldgen.feature.SanctuaryWaterLakeFeature;
import fr.koka99cab.sanctuary26.sanctuary.worldgen.feature.SulfurCaveGeologyFeature;
import net.fabricmc.fabric.api.biome.v1.BiomeModifications;
@@ -21,6 +22,9 @@ import net.minecraft.world.level.levelgen.placement.PlacedFeature;
/** Registers the two generator-specific features migrated from Sanctuary 26.1.2. */
public final class SanctuaryFeatures {
public static final Identifier ALPHA_INDEV_CHUNK_ID = SanctuaryMod.id("alpha_indev_chunk");
public static final Feature<NoneFeatureConfiguration> ALPHA_INDEV_CHUNK =
new AlphaIndevChunkFeature(NoneFeatureConfiguration.CODEC);
public static final Identifier MATTER_DEPTH_MATERIALIZATION_ID =
SanctuaryMod.id("matter_depth_materialization");
public static final ResourceKey<PlacedFeature> MATTER_DEPTH_MATERIALIZATION_PLACED_FEATURE =
@@ -52,6 +56,7 @@ public final class SanctuaryFeatures {
}
public static void register() {
Registry.register(BuiltInRegistries.FEATURE, ALPHA_INDEV_CHUNK_ID, ALPHA_INDEV_CHUNK);
Registry.register(BuiltInRegistries.FEATURE,
MATTER_DEPTH_MATERIALIZATION_ID, MATTER_DEPTH_MATERIALIZATION);
Registry.register(BuiltInRegistries.FEATURE, WATER_LAKE_ID, WATER_LAKE);
@@ -0,0 +1,857 @@
/*
* SPDX-FileCopyrightText: 2026 KOKA99CAB
* SPDX-License-Identifier: GPL-3.0-or-later
*
* The terrain algorithm is a Java adaptation of the BSD-3-Clause
* minecraft-python Indev 20100223 LevelGenerator by pythonengineer.
* See Sanctuary's bundled THIRD_PARTY_NOTICES.md.
*/
package fr.koka99cab.sanctuary26.sanctuary.worldgen.feature;
import com.mojang.serialization.Codec;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Random;
import net.minecraft.core.BlockPos;
import net.minecraft.world.level.WorldGenLevel;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.LeavesBlock;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.levelgen.feature.Feature;
import net.minecraft.world.level.levelgen.feature.FeaturePlaceContext;
import net.minecraft.world.level.levelgen.feature.configurations.NoneFeatureConfiguration;
/** Generates the finite 128x128x64 normal Island preset from Minecraft Indev. */
public final class AlphaIndevChunkFeature extends Feature<NoneFeatureConfiguration> {
public static final int WIDTH = 128;
public static final int DEPTH = 128;
public static final int HEIGHT = 64;
public static final int MIN_X = -WIDTH / 2;
public static final int MIN_Z = -DEPTH / 2;
public static final int MAX_X = MIN_X + WIDTH - 1;
public static final int MAX_Z = MIN_Z + DEPTH - 1;
public static final int WATER_LEVEL = HEIGHT - 32;
private static final int CHUNK_SIZE = 16;
private static final int MAX_CACHED_ISLANDS = 4;
private static final Map<Long, IndevIsland> CACHE = Collections.synchronizedMap(
new LinkedHashMap<>(MAX_CACHED_ISLANDS + 1, 0.75F, true) {
@Override
protected boolean removeEldestEntry(Map.Entry<Long, IndevIsland> eldest) {
return size() > MAX_CACHED_ISLANDS;
}
}
);
private static final byte AIR = 0;
private static final byte STONE = 1;
private static final byte GRASS = 2;
private static final byte DIRT = 3;
private static final byte WATER = 4;
private static final byte LAVA = 5;
private static final byte SAND = 6;
private static final byte GRAVEL = 7;
private static final byte COAL_ORE = 8;
private static final byte IRON_ORE = 9;
private static final byte GOLD_ORE = 10;
private static final byte DIAMOND_ORE = 11;
private static final byte OAK_LOG = 12;
private static final byte OAK_LEAVES = 13;
private static final byte DANDELION = 14;
private static final byte POPPY = 15;
private static final byte BROWN_MUSHROOM = 16;
private static final byte RED_MUSHROOM = 17;
private static final byte OBSIDIAN = 18;
private static final byte OAK_PLANKS = 19;
private static final byte TORCH = 20;
private static final byte MARKER = (byte) 0xFF;
private static final BlockState PERSISTENT_OAK_LEAVES = Blocks.OAK_LEAVES.defaultBlockState()
.setValue(LeavesBlock.PERSISTENT, true);
public AlphaIndevChunkFeature(Codec<NoneFeatureConfiguration> codec) {
super(codec);
}
/** Returns the block where the resident stands inside the deterministic Indev house. */
public static BlockPos notchHome(long seed) {
return island(seed).notchHome();
}
/** Returns a safe surface position outside the deterministic Indev house. */
public static BlockPos portalArrival(long seed) {
return island(seed).portalArrival();
}
/** Stable test fingerprint covering terrain, decorations, house and portal arrival. */
static long fingerprint(long seed) {
return island(seed).fingerprint();
}
@Override
public boolean place(FeaturePlaceContext<NoneFeatureConfiguration> context) {
WorldGenLevel level = context.level();
BlockPos origin = context.origin();
int chunkX = Math.floorDiv(origin.getX(), CHUNK_SIZE) * CHUNK_SIZE;
int chunkZ = Math.floorDiv(origin.getZ(), CHUNK_SIZE) * CHUNK_SIZE;
int startX = Math.max(chunkX, MIN_X);
int endX = Math.min(chunkX + CHUNK_SIZE - 1, MAX_X);
int startZ = Math.max(chunkZ, MIN_Z);
int endZ = Math.min(chunkZ + CHUNK_SIZE - 1, MAX_Z);
if (startX > endX || startZ > endZ) {
return false;
}
IndevIsland island = island(level.getSeed());
BlockPos.MutableBlockPos cursor = new BlockPos.MutableBlockPos();
boolean placed = false;
for (int worldX = startX; worldX <= endX; worldX++) {
int localX = worldX - MIN_X;
for (int worldZ = startZ; worldZ <= endZ; worldZ++) {
int localZ = worldZ - MIN_Z;
boolean boundary = localX == 0 || localX == WIDTH - 1 || localZ == 0 || localZ == DEPTH - 1;
for (int y = 0; y < HEIGHT; y++) {
byte block = island.get(localX, y, localZ);
BlockState state;
if (boundary) {
state = Blocks.BARRIER.defaultBlockState();
} else if (y == 0) {
state = Blocks.BEDROCK.defaultBlockState();
} else if (block == AIR) {
continue;
} else {
state = stateFor(block);
}
cursor.set(worldX, y, worldZ);
placed |= level.setBlock(cursor, state, 2);
}
}
}
return placed;
}
private static IndevIsland island(long seed) {
synchronized (CACHE) {
return CACHE.computeIfAbsent(seed, IndevIsland::new);
}
}
private static BlockState stateFor(byte block) {
return switch (block) {
case STONE -> Blocks.STONE.defaultBlockState();
case GRASS -> Blocks.GRASS_BLOCK.defaultBlockState();
case DIRT -> Blocks.DIRT.defaultBlockState();
case WATER -> Blocks.WATER.defaultBlockState();
case LAVA -> Blocks.LAVA.defaultBlockState();
case SAND -> Blocks.SAND.defaultBlockState();
case GRAVEL -> Blocks.GRAVEL.defaultBlockState();
case COAL_ORE -> Blocks.COAL_ORE.defaultBlockState();
case IRON_ORE -> Blocks.IRON_ORE.defaultBlockState();
case GOLD_ORE -> Blocks.GOLD_ORE.defaultBlockState();
case DIAMOND_ORE -> Blocks.DIAMOND_ORE.defaultBlockState();
case OAK_LOG -> Blocks.OAK_LOG.defaultBlockState();
case OAK_LEAVES -> PERSISTENT_OAK_LEAVES;
case DANDELION -> Blocks.DANDELION.defaultBlockState();
case POPPY -> Blocks.POPPY.defaultBlockState();
case BROWN_MUSHROOM -> Blocks.BROWN_MUSHROOM.defaultBlockState();
case RED_MUSHROOM -> Blocks.RED_MUSHROOM.defaultBlockState();
case OBSIDIAN -> Blocks.OBSIDIAN.defaultBlockState();
case OAK_PLANKS -> Blocks.OAK_PLANKS.defaultBlockState();
case TORCH -> Blocks.TORCH.defaultBlockState();
default -> Blocks.AIR.defaultBlockState();
};
}
private static final class IndevIsland {
private final byte[] blocks = new byte[WIDTH * DEPTH * HEIGHT];
private final int[] floodQueue = new int[blocks.length];
private final int[] spawn;
private final BlockPos portalArrival;
private IndevIsland(long seed) {
Random random = new Random(seed);
int[] heightMap = raiseTerrain(random);
erodeTerrain(random, heightMap);
soilTerrain(random, heightMap);
growBeaches(random, heightMap);
carveCaves(random);
populateOre(random, COAL_ORE, 1000, 10, (HEIGHT << 2) / 5);
populateOre(random, IRON_ORE, 800, 8, HEIGHT * 3 / 5);
populateOre(random, GOLD_ORE, 500, 6, (HEIGHT << 1) / 5);
populateOre(random, DIAMOND_ORE, 800, 4, HEIGHT / 5);
meltLava(random);
waterIsland(random);
this.spawn = findSpawn(seed);
generateSpawnHouse(this.spawn[0], this.spawn[1], this.spawn[2]);
growGrass();
growTrees(random);
populatePlants(random, DANDELION, 100);
populatePlants(random, POPPY, 100);
populatePlants(random, BROWN_MUSHROOM, 50);
populatePlants(random, RED_MUSHROOM, 50);
this.portalArrival = findPortalArrival();
}
private byte get(int x, int y, int z) {
if (!inside(x, y, z)) {
return AIR;
}
return blocks[index(x, y, z)];
}
private BlockPos notchHome() {
return new BlockPos(this.spawn[0] + MIN_X, this.spawn[1] - 1, this.spawn[2] + MIN_Z);
}
private BlockPos portalArrival() {
return portalArrival;
}
private long fingerprint() {
long hash = 0xcbf29ce484222325L;
for (byte block : blocks) {
hash ^= Byte.toUnsignedLong(block);
hash *= 0x100000001b3L;
}
hash ^= notchHome().asLong();
hash *= 0x100000001b3L;
hash ^= portalArrival.asLong();
return hash;
}
private void set(int x, int y, int z, byte block) {
if (inside(x, y, z)) {
blocks[index(x, y, z)] = block;
}
}
private int[] raiseTerrain(Random random) {
int[] heightMap = new int[WIDTH * DEPTH];
Distort raiseOne = new Distort(new Octaves(random, 8), new Octaves(random, 8));
Distort raiseTwo = new Distort(new Octaves(random, 8), new Octaves(random, 8));
Octaves selector = new Octaves(random, 6);
Octaves islandNoise = new Octaves(random, 2);
for (int x = 0; x < WIDTH; x++) {
double normalizedX = Math.abs((x / (WIDTH - 1.0D) - 0.5D) * 2.0D);
for (int z = 0; z < DEPTH; z++) {
double normalizedZ = Math.abs((z / (DEPTH - 1.0D) - 0.5D) * 2.0D);
double first = raiseOne.noise(x * 1.3D, z * 1.3D) / 6.0D - 4.0D;
double second = raiseTwo.noise(x * 1.3D, z * 1.3D) / 5.0D + 6.0D;
if (selector.noise(x, z) / 8.0D > 0.0D) {
second = first;
}
double highest = Math.max(first, second) / 2.0D;
double islandDistance = Math.sqrt(normalizedX * normalizedX + normalizedZ * normalizedZ) * 1.2D;
double islandShape = islandNoise.noise(x * 0.05D, z * 0.05D) / 4.0D + 1.0D;
islandDistance = Math.min(islandDistance, islandShape);
islandDistance = Math.max(islandDistance, Math.max(normalizedX, normalizedZ));
islandDistance = Math.max(0.0D, Math.min(1.0D, islandDistance));
islandDistance *= islandDistance;
highest = highest * (1.0D - islandDistance) - islandDistance * 10.0D + 5.0D;
if (highest < 0.0D) {
highest -= highest * highest * 0.2D;
}
heightMap[x + z * WIDTH] = (int) highest;
}
}
return heightMap;
}
private void erodeTerrain(Random random, int[] heightMap) {
Distort erosion = new Distort(new Octaves(random, 8), new Octaves(random, 8));
Distort erosionSelector = new Distort(new Octaves(random, 8), new Octaves(random, 8));
for (int x = 0; x < WIDTH; x++) {
for (int z = 0; z < DEPTH; z++) {
double noise = erosion.noise(x << 1, z << 1) / 8.0D;
int offset = erosionSelector.noise(x << 1, z << 1) > 0.0D ? 1 : 0;
if (noise > 2.0D) {
int height = heightMap[x + z * WIDTH];
heightMap[x + z * WIDTH] = ((height - offset) / 2 << 1) + offset;
}
}
}
}
private void soilTerrain(Random random, int[] heightMap) {
Octaves soilNoise = new Octaves(random, 8);
Octaves floatingNoise = new Octaves(random, 8);
for (int x = 0; x < WIDTH; x++) {
double normalizedX = Math.abs((x / (WIDTH - 1.0D) - 0.5D) * 2.0D);
for (int z = 0; z < DEPTH; z++) {
double normalizedZ = Math.abs((z / (DEPTH - 1.0D) - 0.5D) * 2.0D);
double floatingDistance = Math.max(normalizedX, normalizedZ);
floatingDistance *= floatingDistance * floatingDistance;
int dirtLevel = heightMap[x + z * WIDTH] + WATER_LEVEL;
int stoneLevel = dirtLevel + (int) (soilNoise.noise(x, z) / 24.0D) - 4;
heightMap[x + z * WIDTH] = Math.max(dirtLevel, stoneLevel);
heightMap[x + z * WIDTH] = Math.max(1, Math.min(HEIGHT - 2, heightMap[x + z * WIDTH]));
// Consumed by the original generator even for a non-floating map.
double floatingValue = floatingNoise.noise(x * 2.3D, z * 2.3D) / 24.0D;
double unusedFloatingHeight = Math.copySign(Math.sqrt(Math.abs(floatingValue)), floatingValue) * 20.0D + WATER_LEVEL;
if (unusedFloatingHeight * (1.0D - floatingDistance) + floatingDistance * HEIGHT > WATER_LEVEL) {
// Deliberately empty: this preserves the historical noise calls and arithmetic.
}
for (int y = 0; y < HEIGHT; y++) {
byte block = AIR;
if (y <= dirtLevel) {
block = DIRT;
}
if (y <= stoneLevel) {
block = STONE;
}
set(x, y, z, block);
}
}
}
}
private void growBeaches(Random random, int[] heightMap) {
Octaves sandNoise = new Octaves(random, 8);
Octaves gravelNoise = new Octaves(random, 8);
int beachLevel = WATER_LEVEL - 1;
for (int x = 0; x < WIDTH; x++) {
for (int z = 0; z < DEPTH; z++) {
boolean beach = sandNoise.noise(x, z) > -8.0D;
boolean gravel = gravelNoise.noise(x, z) > 12.0D;
int top = heightMap[x + z * WIDTH];
byte above = get(x, top + 1, z);
if ((above == WATER || above == AIR) && top <= WATER_LEVEL - 1 && gravel) {
set(x, top, z, GRAVEL);
}
if (above == AIR && top <= beachLevel && beach && get(x, top, z) != AIR) {
set(x, top, z, SAND);
}
}
}
}
private void carveCaves(Random random) {
int caveCount = WIDTH * HEIGHT * DEPTH / 256 / 64 << 1;
for (int cave = 0; cave < caveCount; cave++) {
float x = random.nextFloat() * WIDTH;
float y = random.nextFloat() * HEIGHT;
float z = random.nextFloat() * DEPTH;
int length = (int) ((random.nextFloat() + random.nextFloat()) * 200.0F);
float horizontal = random.nextFloat() * (float) Math.PI * 2.0F;
float horizontalVelocity = 0.0F;
float vertical = random.nextFloat() * (float) Math.PI * 2.0F;
float verticalVelocity = 0.0F;
float thickness = random.nextFloat() * random.nextFloat();
for (int step = 0; step < length; step++) {
x += Math.sin(horizontal) * Math.cos(vertical);
z += Math.cos(horizontal) * Math.cos(vertical);
y += Math.sin(vertical);
horizontal += horizontalVelocity * 0.2F;
horizontalVelocity *= 0.9F;
horizontalVelocity += random.nextFloat() - random.nextFloat();
vertical += verticalVelocity * 0.5F;
vertical *= 0.5F;
verticalVelocity *= 12.0F / 16.0F;
verticalVelocity += random.nextFloat() - random.nextFloat();
if (random.nextFloat() < 0.25F || length == 0) {
continue;
}
float disturbedX = x + (random.nextFloat() * 4.0F - 2.0F) * 0.2F;
float disturbedY = y + (random.nextFloat() * 4.0F - 2.0F) * 0.2F;
float disturbedZ = z + (random.nextFloat() * 4.0F - 2.0F) * 0.2F;
float depthScale = (HEIGHT - disturbedY) / HEIGHT;
float radius = 1.2F + (depthScale * 3.5F + 1.0F) * thickness;
radius = (float) Math.sin(step * Math.PI / length) * radius;
for (int blockX = (int) (disturbedX - radius); blockX <= (int) (disturbedX + radius); blockX++) {
for (int blockY = (int) (disturbedY - radius); blockY <= (int) (disturbedY + radius); blockY++) {
for (int blockZ = (int) (disturbedZ - radius); blockZ <= (int) (disturbedZ + radius); blockZ++) {
double dx = blockX - disturbedX;
double dy = blockY - disturbedY;
double dz = blockZ - disturbedZ;
if (dx * dx + dy * dy * 2.0D + dz * dz < radius * radius
&& blockX > 0 && blockY > 0 && blockZ > 0
&& blockX < WIDTH - 1 && blockY < HEIGHT - 1 && blockZ < DEPTH - 1
&& get(blockX, blockY, blockZ) == STONE) {
set(blockX, blockY, blockZ, AIR);
}
}
}
}
}
}
}
private void populateOre(Random random, byte ore, int frequency, int volume, int maxHeight) {
int veinCount = WIDTH * DEPTH * HEIGHT / 256 / 64 * frequency / 100;
for (int vein = 0; vein < veinCount; vein++) {
float x = random.nextFloat() * WIDTH;
float y = random.nextFloat() * HEIGHT;
float z = random.nextFloat() * DEPTH;
if (y > maxHeight) {
continue;
}
int steps = (int) ((random.nextFloat() + random.nextFloat()) * 75.0F * volume / 100.0F);
float horizontal = random.nextFloat() * (float) Math.PI * 2.0F;
float horizontalVelocity = 0.0F;
float vertical = random.nextFloat() * (float) Math.PI * 2.0F;
float verticalVelocity = 0.0F;
for (int step = 0; step < steps; step++) {
x += Math.sin(horizontal) * Math.cos(vertical);
z += Math.cos(horizontal) * Math.cos(vertical);
y += Math.sin(vertical);
horizontal += horizontalVelocity * 0.2F;
horizontalVelocity *= 0.9F;
horizontalVelocity = random.nextFloat() - random.nextFloat();
vertical = (vertical + verticalVelocity * 0.5F) * 0.5F;
verticalVelocity = verticalVelocity * 0.9F + random.nextFloat() - random.nextFloat();
float radius = (float) Math.sin(step * Math.PI / steps) * volume / 100.0F + 1.0F;
for (int blockX = (int) (x - radius); blockX <= (int) (x + radius); blockX++) {
for (int blockY = (int) (y - radius); blockY <= (int) (y + radius); blockY++) {
for (int blockZ = (int) (z - radius); blockZ <= (int) (z + radius); blockZ++) {
double dx = blockX - x;
double dy = blockY - y;
double dz = blockZ - z;
if (dx * dx + dy * dy * 2.0D + dz * dz < radius * radius
&& blockX >= 1 && blockY >= 1 && blockZ >= 1
&& blockX < WIDTH - 1 && blockY < HEIGHT - 1 && blockZ < DEPTH - 1
&& get(blockX, blockY, blockZ) == STONE) {
set(blockX, blockY, blockZ, ore);
}
}
}
}
}
}
}
private void meltLava(Random random) {
int attempts = WIDTH * DEPTH * HEIGHT / 2000;
int groundLevel = WATER_LEVEL - 2;
for (int attempt = 0; attempt < attempts; attempt++) {
int x = random.nextInt(WIDTH);
int y = Math.min(Math.min(random.nextInt(groundLevel), random.nextInt(groundLevel)),
Math.min(random.nextInt(groundLevel), random.nextInt(groundLevel)));
int z = random.nextInt(DEPTH);
if (get(x, y, z) != AIR) {
continue;
}
int filled = floodFill(x, y, z, AIR, MARKER);
if (filled > 0 && filled < 640) {
floodFill(x, y, z, MARKER, LAVA);
} else {
floodFill(x, y, z, MARKER, AIR);
}
}
}
private void waterIsland(Random random) {
for (int x = 0; x < WIDTH; x++) {
floodFill(x, WATER_LEVEL - 1, 0, AIR, WATER);
floodFill(x, WATER_LEVEL - 1, DEPTH - 1, AIR, WATER);
}
for (int z = 0; z < DEPTH; z++) {
floodFill(WIDTH - 1, WATER_LEVEL - 1, z, AIR, WATER);
floodFill(0, WATER_LEVEL - 1, z, AIR, WATER);
}
int attempts = WIDTH * DEPTH * HEIGHT / 1000;
for (int attempt = 0; attempt < attempts; attempt++) {
int x = random.nextInt(WIDTH);
int y = random.nextInt(HEIGHT);
int z = random.nextInt(DEPTH);
if (get(x, y, z) != AIR) {
continue;
}
int filled = floodFill(x, y, z, AIR, MARKER);
if (filled > 0 && filled < 640) {
floodFill(x, y, z, MARKER, WATER);
} else {
floodFill(x, y, z, MARKER, AIR);
}
}
}
private int floodFill(int startX, int startY, int startZ, byte source, byte target) {
if (!inside(startX, startY, startZ) || get(startX, startY, startZ) != source || source == target) {
return 0;
}
int read = 0;
int write = 0;
int start = index(startX, startY, startZ);
floodQueue[write++] = start;
blocks[start] = target;
boolean touchedBoundary = false;
while (read < write) {
int current = floodQueue[read++];
int x = current % WIDTH;
int yz = current / WIDTH;
int z = yz % DEPTH;
int y = yz / DEPTH;
touchedBoundary |= x == 0 || x == WIDTH - 1 || z == 0 || z == DEPTH - 1 || y == 0 || y == HEIGHT - 1;
write = enqueueIfSource(write, x - 1, y, z, source, target);
write = enqueueIfSource(write, x + 1, y, z, source, target);
write = enqueueIfSource(write, x, y, z - 1, source, target);
write = enqueueIfSource(write, x, y, z + 1, source, target);
write = enqueueIfSource(write, x, y - 1, z, source, target);
}
return touchedBoundary ? -1 : write;
}
private int enqueueIfSource(int write, int x, int y, int z, byte source, byte target) {
if (!inside(x, y, z)) {
return write;
}
int position = index(x, y, z);
if (blocks[position] == source) {
blocks[position] = target;
floodQueue[write++] = position;
}
return write;
}
private void growGrass() {
for (int x = 0; x < WIDTH; x++) {
for (int z = 0; z < DEPTH; z++) {
for (int y = 0; y < HEIGHT - 1; y++) {
if (get(x, y, z) == DIRT && !isOpaque(get(x, y + 1, z))) {
set(x, y, z, GRASS);
}
}
}
}
}
private void growTrees(Random random) {
int groups = WIDTH * DEPTH * HEIGHT / 80000;
for (int group = 0; group < groups; group++) {
int originX = random.nextInt(WIDTH);
int originY = random.nextInt(HEIGHT);
int originZ = random.nextInt(DEPTH);
for (int patch = 0; patch < 25; patch++) {
int x = originX;
int y = originY;
int z = originZ;
for (int attempt = 0; attempt < 20; attempt++) {
x += random.nextInt(12) - random.nextInt(12);
y += random.nextInt(3) - random.nextInt(6);
z += random.nextInt(12) - random.nextInt(12);
if (inside(x, y, z)) {
growTree(random, x, y, z);
}
}
}
}
}
private boolean growTree(Random random, int x, int y, int z) {
int logs = random.nextInt(3) + 4;
if (y <= 0 || y + logs + 1 > HEIGHT) {
return false;
}
for (int blockY = y; blockY < y + 2 + logs; blockY++) {
int radius = blockY == y ? 0 : blockY >= y + logs - 1 ? 2 : 1;
for (int blockX = x - radius; blockX <= x + radius; blockX++) {
for (int blockZ = z - radius; blockZ <= z + radius; blockZ++) {
if (!inside(blockX, blockY, blockZ) || get(blockX, blockY, blockZ) != AIR) {
return false;
}
}
}
}
byte ground = get(x, y - 1, z);
if ((ground != GRASS && ground != DIRT) || y >= HEIGHT - logs - 1) {
return false;
}
set(x, y - 1, z, DIRT);
for (int blockY = y - 3 + logs; blockY <= y + logs; blockY++) {
int distanceFromTop = blockY - (y + logs);
int radius = 1 - distanceFromTop / 2;
for (int blockX = x - radius; blockX <= x + radius; blockX++) {
for (int blockZ = z - radius; blockZ <= z + radius; blockZ++) {
boolean corner = Math.abs(blockX - x) == radius && Math.abs(blockZ - z) == radius;
if ((!corner || random.nextInt(2) == 0 || distanceFromTop == 0)
&& !isOpaque(get(blockX, blockY, blockZ))) {
set(blockX, blockY, blockZ, OAK_LEAVES);
}
}
}
}
for (int log = 0; log < logs; log++) {
if (!isOpaque(get(x, y + log, z))) {
set(x, y + log, z, OAK_LOG);
}
}
return true;
}
private void populatePlants(Random random, byte plant, int frequency) {
int groups = WIDTH * DEPTH * HEIGHT * frequency / 1_600_000;
for (int group = 0; group < groups; group++) {
int originX = random.nextInt(WIDTH);
int originY = random.nextInt(HEIGHT);
int originZ = random.nextInt(DEPTH);
for (int patch = 0; patch < 10; patch++) {
int x = originX;
int y = originY;
int z = originZ;
for (int attempt = 0; attempt < 10; attempt++) {
x += random.nextInt(4) - random.nextInt(4);
y += random.nextInt(2) - random.nextInt(2);
z += random.nextInt(4) - random.nextInt(4);
if (canPlant(plant, x, y, z)) {
set(x, y, z, plant);
}
}
}
}
}
private boolean canPlant(byte plant, int x, int y, int z) {
if (!inside(x, y, z) || y <= 0 || get(x, y, z) != AIR) {
return false;
}
byte below = get(x, y - 1, z);
if (plant == DANDELION || plant == POPPY) {
return below == GRASS || below == DIRT;
}
return below == GRASS || below == DIRT || below == STONE;
}
private int[] findSpawn(long seed) {
Random spawnRandom = new Random(seed ^ 0x49A4F1D3C2B7E659L);
for (int attempt = 0; attempt < 10_000; attempt++) {
int x = spawnRandom.nextInt(WIDTH / 2) + WIDTH / 4;
int z = spawnRandom.nextInt(DEPTH / 2) + DEPTH / 4;
int y = firstUncoveredBlock(x, z) + 1;
if (y < 4 || y <= WATER_LEVEL || !clearSpawnVolume(x, y, z)) {
continue;
}
return new int[] { x, y, z };
}
return new int[] { WIDTH / 2, HEIGHT - 6, DEPTH / 2 };
}
private int firstUncoveredBlock(int x, int z) {
int y = HEIGHT;
while (y > 0 && get(x, y - 1, z) == AIR) {
y--;
}
return y;
}
private boolean clearSpawnVolume(int x, int y, int z) {
for (int blockX = x - 3; blockX <= x + 3; blockX++) {
for (int blockZ = z - 5; blockZ <= z + 3; blockZ++) {
if (!isOpaque(get(blockX, y - 2, blockZ))) {
return false;
}
for (int blockY = y - 1; blockY <= y + 2; blockY++) {
if (get(blockX, blockY, blockZ) != AIR) {
return false;
}
}
}
}
return true;
}
private void generateSpawnHouse(int x, int y, int z) {
for (int blockX = x - 3; blockX <= x + 3; blockX++) {
for (int blockY = y - 2; blockY <= y + 2; blockY++) {
for (int blockZ = z - 3; blockZ <= z + 3; blockZ++) {
byte block = blockY < y - 1 ? OBSIDIAN : AIR;
boolean shell = blockX == x - 3 || blockX == x + 3
|| blockZ == z - 3 || blockZ == z + 3
|| blockY == y - 2 || blockY == y + 2;
if (shell) {
block = blockY >= y - 1 ? OAK_PLANKS : STONE;
}
if (blockZ == z - 3 && blockX == x && blockY >= y - 1 && blockY <= y) {
block = AIR;
}
set(blockX, blockY, blockZ, block);
}
}
}
// Standing torches one block lower remain valid in the modern block engine.
set(x - 2, y - 1, z, TORCH);
set(x + 2, y - 1, z, TORCH);
}
private BlockPos findPortalArrival() {
int centerX = spawn[0];
int centerZ = spawn[2];
for (int radius = 10; radius < WIDTH / 2; radius++) {
for (int dx = -radius; dx <= radius; dx++) {
for (int dz = -radius; dz <= radius; dz++) {
if (Math.abs(dx) != radius && Math.abs(dz) != radius) continue;
int x = centerX + dx;
int z = centerZ + dz;
int y = firstUncoveredBlock(x, z);
if (safePortalColumn(x, y, z)) {
return new BlockPos(x + MIN_X, y, z + MIN_Z);
}
}
}
}
throw new IllegalStateException("Indev island has no safe portal arrival outside its house");
}
private boolean safePortalColumn(int x, int y, int z) {
if (x < 2 || x >= WIDTH - 2 || z < 2 || z >= DEPTH - 2 || y <= WATER_LEVEL || y >= HEIGHT - 5) {
return false;
}
byte floor = get(x, y - 1, z);
if (!isOpaque(floor) || floor == OBSIDIAN || floor == OAK_PLANKS) return false;
for (int offset = 0; offset <= 4; offset++) {
byte block = get(x, y + offset, z);
if (block != AIR && block != DANDELION && block != POPPY
&& block != BROWN_MUSHROOM && block != RED_MUSHROOM) return false;
}
return true;
}
private static boolean inside(int x, int y, int z) {
return x >= 0 && x < WIDTH && y >= 0 && y < HEIGHT && z >= 0 && z < DEPTH;
}
private static int index(int x, int y, int z) {
return (y * DEPTH + z) * WIDTH + x;
}
private static boolean isOpaque(byte block) {
return block != AIR && block != WATER && block != LAVA
&& block != OAK_LEAVES && block != DANDELION && block != POPPY
&& block != BROWN_MUSHROOM && block != RED_MUSHROOM && block != TORCH;
}
}
private static final class Distort {
private final Octaves source;
private final Octaves distortion;
private Distort(Octaves source, Octaves distortion) {
this.source = source;
this.distortion = distortion;
}
private double noise(double x, double z) {
return source.noise(x + distortion.noise(x, z), z);
}
}
private static final class Octaves {
private final Perlin[] octaves;
private Octaves(Random random, int count) {
octaves = new Perlin[count];
for (int index = 0; index < count; index++) {
octaves[index] = new Perlin(random);
}
}
private double noise(double x, double z) {
double value = 0.0D;
double power = 1.0D;
for (Perlin octave : octaves) {
value += octave.noise(x / power, z / power) * power;
power *= 2.0D;
}
return value;
}
}
private static final class Perlin {
private final int[] permutations = new int[512];
private Perlin(Random random) {
for (int index = 0; index < 256; index++) {
permutations[index] = index;
}
for (int index = 0; index < 256; index++) {
int swap = random.nextInt(256 - index) + index;
int temporary = permutations[index];
permutations[index] = permutations[swap];
permutations[swap] = temporary;
permutations[index + 256] = permutations[index];
}
}
private double noise(double x, double z) {
int floorX = (int) Math.floor(x);
int floorZ = (int) Math.floor(z);
int gridX = floorX & 0xFF;
int gridZ = floorZ & 0xFF;
x -= floorX;
z -= floorZ;
double fadedX = fade(x);
double fadedZ = fade(z);
int a = permutations[gridX] + gridZ;
int b = permutations[gridX + 1] + gridZ;
double near = lerp(fadedX,
grad(permutations[permutations[a]], x, z, 0.0D),
grad(permutations[permutations[b]], x - 1.0D, z, 0.0D));
double far = lerp(fadedX,
grad(permutations[permutations[a + 1]], x, z - 1.0D, 0.0D),
grad(permutations[permutations[b + 1]], x - 1.0D, z - 1.0D, 0.0D));
return lerp(fadedZ, near, far);
}
private static double fade(double value) {
return value * value * value * (value * (value * 6.0D - 15.0D) + 10.0D);
}
private static double lerp(double amount, double start, double end) {
return start + amount * (end - start);
}
private static double grad(int hash, double x, double z, double y) {
hash &= 0xF;
double first = hash < 8 ? x : z;
double second = hash < 4 ? z : hash != 12 && hash != 14 ? y : x;
return ((hash & 1) == 0 ? first : -first) + ((hash & 2) == 0 ? second : -second);
}
}
}
@@ -496,5 +496,6 @@
"item.sanctuary.minecon_pickaxe_cape": "Minecon Pickaxe Cape",
"item.sanctuary.minecon_2011_cape": "Minecon 2011 Cape",
"item.sanctuary.mojang_classic_cape": "Mojang Classic Cape",
"item.sanctuary.blue_scroll_cape": "Blue Scroll Cape"
"item.sanctuary.blue_scroll_cape": "Blue Scroll Cape",
"dimension.sanctuary.alpha": "Alpha / Indev Island"
}
@@ -496,5 +496,6 @@
"item.sanctuary.minecon_pickaxe_cape": "Cape pioche Minecon",
"item.sanctuary.minecon_2011_cape": "Cape Minecon 2011",
"item.sanctuary.mojang_classic_cape": "Cape Mojang classique",
"item.sanctuary.blue_scroll_cape": "Cape parchemin bleu"
"item.sanctuary.blue_scroll_cape": "Cape parchemin bleu",
"dimension.sanctuary.alpha": "Île Alpha / Indev"
}
@@ -452,5 +452,6 @@
"item.sanctuary.minecon_pickaxe_cape": "Плащ кирки Minecon",
"item.sanctuary.minecon_2011_cape": "Плащ Minecon 2011",
"item.sanctuary.mojang_classic_cape": "Классический плащ Mojang",
"item.sanctuary.blue_scroll_cape": "Плащ синего свитка"
"item.sanctuary.blue_scroll_cape": "Плащ синего свитка",
"dimension.sanctuary.alpha": "Остров Альфа / Indev"
}
@@ -0,0 +1,11 @@
{
"type": "sanctuary:alpha",
"generator": {
"type": "minecraft:noise",
"biome_source": {
"type": "minecraft:fixed",
"biome": "sanctuary:alpha"
},
"settings": "sanctuary:alpha"
}
}
@@ -0,0 +1,43 @@
{
"ambient_light": 0.0,
"attributes": {
"minecraft:audio/ambient_sounds": {
"mood": {
"block_search_extent": 8,
"offset": 2.0,
"sound": "minecraft:ambient.cave",
"tick_delay": 9000
}
},
"minecraft:gameplay/bed_rule": {
"can_set_spawn": "never",
"can_sleep": "never"
},
"minecraft:gameplay/can_start_raid": false,
"minecraft:gameplay/respawn_anchor_works": false,
"minecraft:gameplay/sky_light_level": 15.0,
"minecraft:gameplay/water_evaporates": false,
"minecraft:visual/ambient_light_color": "#ffffff",
"minecraft:visual/cloud_color": "#ffffffff",
"minecraft:visual/fog_color": "#ffffff",
"minecraft:visual/fog_end_distance": 128.0,
"minecraft:visual/fog_start_distance": 16.0,
"minecraft:visual/sky_color": "#99ccff",
"minecraft:visual/sky_light_color": "#ffffff",
"minecraft:visual/sky_light_factor": 1.0
},
"cardinal_light": "default",
"coordinate_scale": 1.0,
"has_ceiling": false,
"has_ender_dragon_fight": false,
"has_fixed_time": false,
"has_skylight": true,
"height": 64,
"infiniburn": "#minecraft:infiniburn_overworld",
"logical_height": 64,
"min_y": 0,
"monster_spawn_block_light_limit": 0,
"monster_spawn_light_level": 7,
"skybox": "overworld",
"timelines": "#minecraft:in_overworld"
}
@@ -0,0 +1,58 @@
{
"temperature": 0.8,
"downfall": 0.4,
"has_precipitation": true,
"attributes": {
"minecraft:audio/background_music": {
"default": {
"max_delay": 24000,
"min_delay": 12000,
"sound": "minecraft:music.overworld.forest"
}
}
},
"effects": {
"water_color": "#3f76e4",
"water_fog_color": "#050533",
"fog_color": "#ffffff",
"sky_color": "#99ccff",
"grass_color": "#7cbd6b",
"foliage_color": "#48b518"
},
"creature_spawn_probability": 0.2,
"spawners": {
"ambient": [],
"axolotls": [],
"creature": [
{ "type": "minecraft:pig", "maxCount": 4, "minCount": 2, "weight": 10 },
{ "type": "minecraft:sheep", "maxCount": 4, "minCount": 2, "weight": 10 },
{ "type": "minecraft:cow", "maxCount": 4, "minCount": 2, "weight": 8 },
{ "type": "minecraft:chicken", "maxCount": 4, "minCount": 2, "weight": 8 }
],
"misc": [],
"monster": [
{ "type": "minecraft:zombie", "maxCount": 4, "minCount": 1, "weight": 10 },
{ "type": "minecraft:skeleton", "maxCount": 4, "minCount": 1, "weight": 10 },
{ "type": "minecraft:spider", "maxCount": 4, "minCount": 1, "weight": 10 },
{ "type": "minecraft:creeper", "maxCount": 4, "minCount": 1, "weight": 10 }
],
"underground_water_creature": [],
"water_ambient": [],
"water_creature": []
},
"spawn_costs": {},
"carvers": [],
"features": [
[],
[],
[],
[],
["sanctuary:alpha_indev_chunk"],
[],
[],
[],
[],
[],
[]
]
}
@@ -0,0 +1,4 @@
{
"type": "sanctuary:alpha_indev_chunk",
"config": {}
}
@@ -0,0 +1,43 @@
{
"aquifers_enabled": false,
"default_block": {
"Name": "minecraft:air"
},
"default_fluid": {
"Name": "minecraft:air"
},
"disable_mob_generation": false,
"legacy_random_source": false,
"noise": {
"height": 64,
"min_y": 0,
"size_horizontal": 1,
"size_vertical": 2
},
"noise_router": {
"barrier": 0.0,
"continents": 0.0,
"depth": 0.0,
"erosion": 0.0,
"final_density": -1.0,
"fluid_level_floodedness": 0.0,
"fluid_level_spread": 0.0,
"lava": 0.0,
"preliminary_surface_level": 0.0,
"ridges": 0.0,
"temperature": 0.0,
"vegetation": 0.0,
"vein_gap": 0.0,
"vein_ridged": 0.0,
"vein_toggle": 0.0
},
"ore_veins_enabled": false,
"sea_level": 32,
"spawn_target": [],
"surface_rule": {
"type": "minecraft:block",
"result_state": {
"Name": "minecraft:air"
}
}
}
@@ -0,0 +1,12 @@
{
"feature": "sanctuary:alpha_indev_chunk",
"placement": [
{
"type": "minecraft:count",
"count": 1
},
{
"type": "minecraft:biome"
}
]
}
@@ -47,6 +47,17 @@
"settings": "sanctuary:caverns"
}
},
"sanctuary:alpha": {
"type": "sanctuary:alpha",
"generator": {
"type": "minecraft:noise",
"biome_source": {
"type": "minecraft:fixed",
"biome": "sanctuary:alpha"
},
"settings": "sanctuary:alpha"
}
},
"minecraft:the_end": {
"type": "minecraft:the_end",
"generator": {
@@ -47,6 +47,17 @@
"settings": "sanctuary:caverns"
}
},
"sanctuary:alpha": {
"type": "sanctuary:alpha",
"generator": {
"type": "minecraft:noise",
"biome_source": {
"type": "minecraft:fixed",
"biome": "sanctuary:alpha"
},
"settings": "sanctuary:alpha"
}
},
"minecraft:the_end": {
"type": "minecraft:the_end",
"generator": {
@@ -1,6 +1,7 @@
package fr.koka99cab.sanctuary26.sanctuary.shop;
import java.util.UUID;
import net.minecraft.resources.Identifier;
import net.minecraft.world.item.ItemStack;
/** Pure ledger/policy smoke; live sign and mailbox behavior is covered by the server smoke. */
@@ -40,6 +41,8 @@ public final class BlackMarketModelSmoke {
require(data.parcels().isEmpty() && data.casinoVaultSize() == 1,
"An expired parcel did not transfer exactly once to the casino vault");
require(BlackMarketItemPolicy.forbidden(ItemStack.EMPTY), "An empty escrow item was accepted");
require(BlackMarketItemPolicy.forbidden(Identifier.parse("redstoner:storage_terminal")),
"Storage Terminal reached the Black Market");
}
private static void require(boolean value, String message) {
@@ -0,0 +1,54 @@
package fr.koka99cab.sanctuary26.sanctuary.worldgen.feature;
import fr.koka99cab.sanctuary26.sanctuary.api.world.SanctuaryAlphaWorld;
import net.minecraft.SharedConstants;
import net.minecraft.core.BlockPos;
import net.minecraft.server.Bootstrap;
/** Executable invariants for the deterministic, finite Indev island generator. */
public final class AlphaIndevGeneratorSmoke {
private static final long[] SEEDS = {
0L, 1L, -1L, 26_200_044L, Long.MIN_VALUE, Long.MAX_VALUE
};
private AlphaIndevGeneratorSmoke() {
}
public static void main(String[] args) {
SharedConstants.tryDetectVersion();
Bootstrap.bootStrap();
require(SanctuaryAlphaWorld.WIDTH == 128, "Alpha width must remain 128 blocks");
require(SanctuaryAlphaWorld.DEPTH == 128, "Alpha depth must remain 128 blocks");
require(SanctuaryAlphaWorld.HEIGHT == 64, "Alpha height must remain 64 blocks");
long originalFingerprint = AlphaIndevChunkFeature.fingerprint(SEEDS[0]);
for (long seed : SEEDS) {
long fingerprint = AlphaIndevChunkFeature.fingerprint(seed);
BlockPos residence = SanctuaryAlphaWorld.residence(seed);
BlockPos arrival = SanctuaryAlphaWorld.portalArrival(seed);
require(fingerprint != 0L, "Generated island fingerprint must be non-zero");
require(SanctuaryAlphaWorld.containsHorizontal(residence), "Residence left finite bounds");
require(SanctuaryAlphaWorld.containsHorizontal(arrival), "Portal arrival left finite bounds");
require(arrival.getY() > AlphaIndevChunkFeature.WATER_LEVEL,
"Portal arrival must be above the historical water level");
require(arrival.getY() < AlphaIndevChunkFeature.HEIGHT - 4,
"Portal arrival must leave a four-block clear column");
int distance = Math.max(Math.abs(arrival.getX() - residence.getX()),
Math.abs(arrival.getZ() - residence.getZ()));
require(distance >= 10, "Portal arrival must not overlap the historical house");
}
// Six distinct seeds evict the first entry from the bounded cache. Rebuilding it
// must still produce the exact same terrain, house and arrival contract.
require(AlphaIndevChunkFeature.fingerprint(SEEDS[0]) == originalFingerprint,
"Same seed changed after cache eviction");
require(AlphaIndevChunkFeature.fingerprint(SEEDS[0])
!= AlphaIndevChunkFeature.fingerprint(SEEDS[1]),
"Different seeds unexpectedly generated the same island");
}
private static void require(boolean condition, String message) {
if (!condition) throw new AssertionError(message);
}
}