Compare commits
45
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
77fa9c5d21 | ||
|
|
da61a3b1ec | ||
|
|
bd7ee21e7f | ||
|
|
b4a4e25684 | ||
|
|
d95d8af8ec | ||
|
|
29db709e7e | ||
|
|
9b432f2ecc | ||
|
|
d1f9d6a30e | ||
|
|
aefb23deee | ||
|
|
7f2b53e41d | ||
|
|
60819a31ab | ||
|
|
e692a051ac | ||
|
|
22fb027398 | ||
|
|
2d0ae127a9 | ||
|
|
253c8f132e | ||
|
|
c973d535a8 | ||
|
|
a764ba9c42 | ||
|
|
1cb14b997f | ||
|
|
78457fd80b | ||
|
|
8c8f7bbbde | ||
|
|
ac4542079f | ||
|
|
5403938320 | ||
|
|
0ad9763666 | ||
|
|
0196b6efa5 | ||
|
|
3bdc5fe241 | ||
|
|
789faeb708 | ||
|
|
3491158785 | ||
|
|
c0229d4ea9 | ||
|
|
ddaf71c7cd | ||
|
|
7e1ed85307 | ||
|
|
5840621acf | ||
|
|
974cea20b5 | ||
|
|
81a98ed197 | ||
|
|
b022be5739 | ||
|
|
27b29c1340 | ||
|
|
274d4c1ffa | ||
|
|
f217bb3661 | ||
|
|
5100091b17 | ||
|
|
398d94e7d1 | ||
|
|
0743ff33a6 | ||
|
|
bfda90f4f4 | ||
|
|
a698140963 | ||
|
|
88ec91e9f4 | ||
|
|
e1d48e9017 | ||
|
|
cce58c27c8 |
+140
-1
@@ -8,7 +8,143 @@ Ce journal suit les **versions du modpack**. Lorsqu'un seul module change, sa ve
|
||||
|
||||
## Non publié
|
||||
|
||||
Modules modifiés : **Sanctuary `0.0.0-alpha.135`** et **Red-Stoner `0.0.0-alpha.17`**.
|
||||
Aucun changement non publié.
|
||||
|
||||
## `26.2.0-alpha.242`
|
||||
|
||||
Dépendance mise à jour : **MyDisplay `1.0.0` (quatrième build fourni)**.
|
||||
|
||||
- remplace le troisième JAR MyDisplay sous un nom distinct afin de forcer sa mise à jour par Packwiz ;
|
||||
- actualise les composants client de performance, de session multimédia et de lecture WaterMedia ;
|
||||
- conserve l’adaptation du manifeste pour le serveur dédié et les traductions EN/FR/RU ;
|
||||
- conserve WaterMedia `3.0.0.23`, ses binaires `3.0.0.6`, les écrans existants, les sauvegardes et les protocoles ;
|
||||
- ne modifie aucun des dix modules Sanctuary.
|
||||
|
||||
## `26.2.0-alpha.241`
|
||||
|
||||
Dépendance mise à jour : **MyDisplay `1.0.0` (troisième build fourni)**.
|
||||
|
||||
- remplace le second JAR MyDisplay sous un nom distinct pour forcer sa mise à jour par Packwiz ;
|
||||
- accélère la copie ARGB vers RGBA avec un `IntBuffer` et adapte la résolution de décodage WaterMedia à la distance ;
|
||||
- plafonne et priorise les uploads vidéo par proximité, sans masquer les écrans qui attendent leur prochaine image ;
|
||||
- suspend les uploads hors champ, coupe le rendu au-delà de 128 blocs et regroupe le rendu du mur ;
|
||||
- partage Streamlink entre les écrans identiques et échelonne le démarrage des lecteurs ;
|
||||
- évite de créer les décodeurs audio OpenAL/FFmpeg lorsque l’écran est muet et sécurise les captures F2 ;
|
||||
- conserve WaterMedia `3.0.0.23`, ses binaires `3.0.0.6`, les écrans existants, les sauvegardes et les protocoles.
|
||||
|
||||
## `26.2.0-alpha.240`
|
||||
|
||||
Dépendance mise à jour : **MyDisplay `1.0.0` (second build fourni)**.
|
||||
|
||||
- remplace le premier JAR MyDisplay par une nouvelle révision sous un nom distinct pour contourner les caches Packwiz ;
|
||||
- ajoute la sélection côté client de l’écran MyDisplay actuellement visé et met à jour la gestion multimédia associée ;
|
||||
- conserve WaterMedia `3.0.0.23` et WaterMedia Binaries `3.0.0.6` côté client ;
|
||||
- conserve l’adaptation du manifeste pour le serveur dédié et complète toujours EN/FR avec le russe ;
|
||||
- rend le résolveur des dépendances Packwiz compatible avec les téléchargements directs privés épinglés ;
|
||||
- conserve les écrans MyDisplay existants, les dix modules Sanctuary, leurs sauvegardes et leurs protocoles.
|
||||
|
||||
## `26.2.0-alpha.239`
|
||||
|
||||
Dépendances remplacées : **MyDisplay `1.0.0`**, **WaterMedia `3.0.0.23`** et **WaterMedia Binaries `3.0.0.6`**.
|
||||
|
||||
- retire Dream Displays et son second build de test audio du client comme du serveur ;
|
||||
- ajoute MyDisplay côté client et serveur pour les blocs-écrans, leur configuration et leur synchronisation ;
|
||||
- ajoute WaterMedia et ses binaires natifs côté client uniquement pour la lecture et le rendu multimédia ;
|
||||
- adapte seulement le manifeste de MyDisplay afin que ses dépendances client optionnelles ne bloquent pas le serveur dédié, et complète ses traductions EN/FR avec le russe ;
|
||||
- ne convertit pas les écrans Dream Displays déjà placés, qui doivent être remplacés par des écrans MyDisplay ;
|
||||
- conserve les dix modules Sanctuary, leurs versions, sauvegardes et protocoles sans changement ;
|
||||
- garde le module temporaire ZEVENT entièrement absent du workspace et du pack.
|
||||
|
||||
## `26.2.0-alpha.238`
|
||||
|
||||
Dépendance remplacée : **Dream Displays `1.10.0-dev` (audio-buffer-test, révision 2)**.
|
||||
|
||||
- remplace le premier JAR de test Dream Displays par la nouvelle révision fournie ;
|
||||
- utilise un nouveau nom de fichier afin de contourner les caches et de garantir son téléchargement ;
|
||||
- distribue ce build côté client et serveur avec ses nouvelles empreintes vérifiées ;
|
||||
- conserve les dix modules Sanctuary, leurs versions, sauvegardes et protocoles sans changement ;
|
||||
- garde le module temporaire ZEVENT entièrement absent du workspace et du pack.
|
||||
|
||||
## `26.2.0-alpha.237`
|
||||
|
||||
Dépendance remplacée : **Dream Displays `1.10.0-dev` (audio-buffer-test)**.
|
||||
|
||||
- remplace Dream Displays `1.9.6` par le JAR de test fourni pour corriger la gestion du tampon audio ;
|
||||
- utilise un nom de fichier distinct afin que Packwiz supprime l’ancien JAR et ne réutilise pas son cache ;
|
||||
- distribue le nouveau build côté client et serveur depuis le dépôt de téléchargement Sanctuary avec empreintes vérifiées ;
|
||||
- conserve les dix modules Sanctuary, leurs versions, sauvegardes et protocoles sans changement ;
|
||||
- garde le module temporaire ZEVENT entièrement absent du workspace et du pack.
|
||||
|
||||
## `26.2.0-alpha.236`
|
||||
|
||||
Module temporaire supprimé : **ZEVENT `0.0.0-alpha.3`**.
|
||||
|
||||
- retire entièrement le sous-projet, le JAR client/serveur et les identifiants `zevent:*` devenus redondants ;
|
||||
- conserve Dream Displays `1.9.6`, qui fournit directement les écrans et la lecture des flux vidéo ;
|
||||
- retire automatiquement `zevent-0.0.0-alpha.3.jar` des installations gérées par Packwiz ;
|
||||
- ne convertit pas les anciens écrans muraux ZEVENT : ils doivent être remplacés par des écrans Dream Displays ;
|
||||
- conserve sans changement les sauvegardes, inventaires, monnaies et protocoles des dix modules Sanctuary restants.
|
||||
|
||||
## `26.2.0-alpha.235`
|
||||
|
||||
Module modifié temporairement : **ZEVENT `0.0.0-alpha.3`**.
|
||||
|
||||
- remplace MCEF Modern, dont Chromium ne sait pas décoder les directs Twitch en H.264/AAC, par Dream Displays `1.9.6` et son pipeline vidéo natif ;
|
||||
- conserve le bloc-écran 9 × 5, les chaînes enregistrées, l’interface et le protocole réseau ZEVENT 1 sans migration de monde ;
|
||||
- réutilise directement la texture GPU décodée dans le renderer ZEVENT, avec une limite de 720p et deux flux visibles au maximum par client ;
|
||||
- maintient le volume dans la catégorie Disques, l’atténuation jusqu’à 64 blocs et un seul écran audible à la fois ;
|
||||
- réessaie automatiquement les chaînes momentanément indisponibles et affiche désormais un message explicite lorsqu’une chaîne est hors ligne ;
|
||||
- valide le démarrage client natif sur macOS Apple Silicon ainsi que la résolution d’un direct Twitch actif en cinq qualités H.264 jusqu’en 1080p avec audio AAC.
|
||||
|
||||
## `26.2.0-alpha.234`
|
||||
|
||||
Module ajouté temporairement : **ZEVENT `0.0.0-alpha.2`**.
|
||||
|
||||
- ajoute pour le week-end le bloc-écran ZEVENT, une surface multibloc de 9 × 5 qui diffuse une chaîne Twitch officielle dans le monde ;
|
||||
- permet de saisir un nom de chaîne ou une URL Twitch par clic droit, puis enregistre et synchronise la chaîne côté serveur ;
|
||||
- relie le volume à la catégorie Disques, l’atténue jusqu’à 64 blocs et limite chaque client à deux lecteurs visibles ;
|
||||
- ajoute MCEF Modern `0.3.3+mc26.2.jcef146.0.10` uniquement côté client, sans imposer Chromium au serveur dédié ;
|
||||
- conserve les données et protocoles des dix modules Sanctuary existants ; ZEVENT démarre en données v1 et protocole 1, avec mise à jour simultanée du client et du serveur.
|
||||
|
||||
## `26.2.0-alpha.233`
|
||||
|
||||
Modules modifiés : **Sanctuary `0.0.0-alpha.139`** et **Red-Stoner `0.0.0-alpha.20`**.
|
||||
|
||||
- remplace le bloc-écran `redstoner:trophy_panel` par un vrai panneau Vanilla en chêne pâle, posable au sol ou au mur, sans changer l’ID de l’item déjà présent dans les inventaires et recettes ;
|
||||
- ouvre à la première pose un éditeur blanc permettant de rechercher une statistique Vanilla, composer et réordonner jusqu’à seize pages, puis choisir entre noms et valeurs, noms seuls ou valeurs seules ;
|
||||
- verrouille la configuration et la destruction au créateur ou à un opérateur, tout en laissant le clic droit public faire défiler les pages ;
|
||||
- conserve l’ancien bloc uniquement comme alias de migration : chaque plaque déjà posée devient automatiquement un panneau mural en gardant propriétaire, statistiques et page courante ;
|
||||
- répare les classements importés qui montraient les huit premiers caractères de l’UUID : Sanctuary récupère désormais le vrai pseudo depuis le cache local du serveur et utilise `?` plutôt qu’un fragment d’UUID si aucun nom n’est connu ;
|
||||
- ajoute les payloads bornés de l’éditeur, fait passer Red-Stoner aux données v5 et au protocole réseau 9, sans changer les données v15 ni le protocole 24 de Sanctuary. Client et serveur Red-Stoner doivent être mis à jour ensemble.
|
||||
|
||||
## `26.2.0-alpha.232`
|
||||
|
||||
Modules modifiés : **Another World `0.0.0-alpha.58`**, **Sanctuary `0.0.0-alpha.138`** et **Red-Stoner `0.0.0-alpha.19`**.
|
||||
|
||||
- ferme le ticket #146 en enregistrant chaque cliché pris avec un appareil Canon dans le dossier `screenshots` de l’instance, à côté des captures F2 ;
|
||||
- exporte le carré cadré à sa résolution native avec le filtre de la pellicule, sans le réduire aux 128 × 128 pixels de la carte en jeu ;
|
||||
- écrit le PNG hors du thread de rendu et réserve un nom horodaté unique afin d’éviter les freezes comme les écrasements lors de plusieurs clichés rapprochés ;
|
||||
|
||||
- remplace le volume de la cape à motifs par les deux faces opposées de l’îlot UV exact du bouclier, redimensionnées aux proportions 10 × 16 de la cape, afin de corriger la projection et le culling sans afficher le manche ni l’épaisseur du bouclier ;
|
||||
- laisse la touche d’inventaire fermer le terminal tant que la recherche n’est pas focalisée, mais la traite comme du texte dans le champ de recherche afin que la lettre `E` ne ferme plus l’écran ;
|
||||
- utilise les huit lignes du panneau de trophées pour afficher le titre et jusqu’à sept rangs, avec une page de noms suivie d’une page de valeurs pour chaque statistique afin que les longues durées de jeu restent lisibles ;
|
||||
- fait configurer par un œuf d’apparition le classement vanilla des créatures correspondantes tuées, par exemple les zombies tués avec l’œuf de zombie ;
|
||||
- conserve les IDs et les sauvegardes Sanctuary v15/Red-Stoner v4 ; le snapshot client du panneau passant de trois à sept entrées, le protocole Red-Stoner passe de 7 à 8 et exige la mise à jour du client et du serveur.
|
||||
|
||||
## `26.2.0-alpha.231`
|
||||
|
||||
Modules modifiés : **Sanctuary `0.0.0-alpha.137`** et **Red-Stoner `0.0.0-alpha.18`**.
|
||||
|
||||
- transforme le panneau de trophées en plaque verticale de chêne, corrige le texte auparavant masqué et utilise les quatre lignes pour le titre paginé ainsi que les trois places du podium ;
|
||||
- permet de passer à la page suivante d’un clic droit et de revenir à la précédente avec `Maj` + clic droit, sans changer les IDs, les sauvegardes Red-Stoner v4 ni le protocole réseau 7.
|
||||
- garantit sur chaque niveau de la Backroom un coffre par cellule de 3 × 3 chunks dans les nouveaux chunks, au lieu d’un coffre aléatoire réparti entre les cinq niveaux ;
|
||||
- resserre aussi la grille des lits de sortie de 10 × 10 à 3 × 3 chunks sur chacun des cinq niveaux afin qu’ils soient réellement trouvables ;
|
||||
- actualise JEI vers la bêta Fabric `30.29.0.201`, qui corrige un crash avec les mods utilisant l’atlas de textures ;
|
||||
- ne change ni le butin ordinaire des coffres, ni les données Sanctuary v15/Red-Stoner v4, ni leurs protocoles réseau 24/7 ; les nouveaux coffres et lits apparaissent dans les chunks qui n’ont pas encore été générés.
|
||||
|
||||
## `26.2.0-alpha.230`
|
||||
|
||||
Modules modifiés : **Sanctuary `0.0.0-alpha.136`** et **Red-Stoner `0.0.0-alpha.17`**.
|
||||
|
||||
- attribue les demandes de quêtes personnalisées terminées à leur preneur directement sur le panneau et dans les détails au clic, avec son pseudonyme et sa couleur conservés ;
|
||||
- répartit les lits de sortie rares sur chacun des cinq niveaux de la Backroom, avec exactement un lit par cellule de 10 × 10 chunks et par niveau dans les nouveaux chunks ;
|
||||
@@ -16,6 +152,9 @@ Modules modifiés : **Sanctuary `0.0.0-alpha.135`** et **Red-Stoner `0.0.0-alpha
|
||||
- ajoute l’API publique Sanctuary de métriques et de classements persistants par UUID, avec import monotone des statistiques vanilla, podium borné et exclusion des actions créatives, spectateur ou simulées ;
|
||||
- ajoute `redstoner:trophy_panel`, panneau mural configurable par son propriétaire avec un item non consommé, pages publiques, rendu en monde et rafraîchissement serveur limité à une fois par seconde ;
|
||||
- passe Sanctuary aux données v15 et Red-Stoner aux données v4 sans nouveau payload ni changement de leurs protocoles réseau 24 et 7.
|
||||
- corrige la cape blanche décorée en projetant sa couleur et ses motifs avec les sprites de bouclier, aux proportions exactes de la cape ;
|
||||
- permet d’équiper une poule adulte avec l’armure de loup vanilla en écailles de tatou, de la réparer avec des écailles et de la retirer aux cisailles ; l’armure absorbe les dégâts et conserve son usure, ses teintures et ses enchantements ;
|
||||
- conserve les sauvegardes Sanctuary v15 et le protocole réseau 24 : l’armure utilise exclusivement le slot d’équipement vanilla de la poule.
|
||||
|
||||
## `26.2.0-alpha.229`
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Sanctuary 26.2
|
||||
|
||||
Workspace Fabric multi-projet de Sanctuary 2.0. Il construit dix JAR indépendants sur Minecraft 26.2, Fabric Loader 0.19.3, Fabric API 0.159.0+26.2, Loom 1.17, Gradle 9.5.1 et Java 25.
|
||||
Workspace Fabric multi-projet de Sanctuary 2.0. Il construit dix JAR, sur Minecraft 26.2, Fabric Loader 0.19.3, Fabric API 0.159.0+26.2, Loom 1.17, Gradle 9.5.1 et Java 25.
|
||||
|
||||
Auteur : **KOKA99CAB**
|
||||
Licence : **GNU GPL 3.0 or Later** (`GPL-3.0-or-later`)
|
||||
@@ -22,11 +22,33 @@ 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.229`**. Cette release distribue Red-Stoner **`0.0.0-alpha.16`** ; les neuf autres modules conservent leur version issue de l'alpha.228.
|
||||
Version courante du pack distribué : **`26.2.0-alpha.242`**. Cette release conserve les dix modules Sanctuary, notamment Another World **`0.0.0-alpha.58`**, Sanctuary **`0.0.0-alpha.139`** et Red-Stoner **`0.0.0-alpha.20`**, garde ZEVENT entièrement retiré et met à jour MyDisplay **`1.0.0`** vers le quatrième build fourni, avec WaterMedia **`3.0.0.23`** et WaterMedia Binaries **`3.0.0.6`**.
|
||||
|
||||
Le prochain module Sanctuary **`0.0.0-alpha.134`** attribue les quêtes communautaires terminées à leur preneur et répartit les lits de sortie rares sur chacun des cinq niveaux de la Backroom. Chaque niveau garantit désormais un lit par cellule de 10 × 10 chunks dans les nouveaux chunks, sans modifier les sauvegardes, les données v14 ni le protocole réseau 24.
|
||||
L’alpha.242 remplace le troisième JAR MyDisplay par `mydisplay-1.0.0-sanctuary-4.jar`. Cette révision actualise exclusivement le pipeline client de performance, de session et de lecture WaterMedia, sans changer les blocs, les données ni le protocole réseau. Le manifeste reste compatible avec le serveur dédié, les traductions EN/FR/RU sont alignées, les écrans déjà placés restent compatibles et le nom distinct force Packwiz à retirer le build précédent.
|
||||
|
||||
Sanctuary **`0.0.0-alpha.135`** et Red-Stoner **`0.0.0-alpha.17`** ajoutent les classements publics et le panneau mural `redstoner:trophy_panel`. Les statistiques vanilla des anciens joueurs sont importées une fois, les scores restent dans Sanctuary et le panneau ne reçoit que sa page visible ainsi qu’un podium de trois noms maximum. Les données passent respectivement à v15 et v4 sans changer les protocoles réseau 24 et 7.
|
||||
L’alpha.241 remplace le second JAR MyDisplay par `mydisplay-1.0.0-sanctuary-3.jar`. Le rendu garde tous les écrans visibles tout en réduisant la charge client : copie vidéo ARGB vers RGBA par `IntBuffer`, résolution WaterMedia adaptative selon la distance, priorité et budget des uploads, gel de l’image hors champ, distance de rendu limitée à 128 blocs, rendu mural groupé et démarrage échelonné. Les flux Streamlink identiques sont partagés, l’audio muet ne crée plus de décodeur OpenAL/FFmpeg et les captures F2 réinitialisent proprement le tampon en suspendant les uploads. Le manifeste reste compatible avec le serveur dédié, les traductions EN/FR/RU sont alignées et les écrans déjà placés restent compatibles.
|
||||
|
||||
L’alpha.240 remplace le premier JAR MyDisplay par `mydisplay-1.0.0-sanctuary-2.jar`, dont le nom distinct force Packwiz à télécharger le nouveau build et à supprimer le précédent. Cette révision ajoute notamment la sélection côté client d’un écran MyDisplay visé. Le manifeste conserve l’adaptation serveur dédié et les traductions EN/FR/RU restent alignées. Les écrans MyDisplay déjà placés restent compatibles.
|
||||
|
||||
L’alpha.239 distribue MyDisplay côté client et serveur, et réserve WaterMedia ainsi que ses binaires natifs au client. Packwiz retire automatiquement Dream Displays ; ses anciens écrans déjà posés ne sont pas convertis et doivent être remplacés par des écrans MyDisplay.
|
||||
|
||||
L’alpha.238 distribue la seconde révision `dreamdisplays-fabric-26.2-audio-buffer-test-2.jar` côté client et serveur.
|
||||
|
||||
L’alpha.237 remplace Dream Displays `1.9.6` par le premier build de test `1.10.0-dev` destiné à corriger la gestion du tampon audio.
|
||||
|
||||
L’alpha.236 supprime le JAR et le code ZEVENT devenus redondants. Packwiz retire automatiquement `zevent-0.0.0-alpha.3.jar` du client et du serveur, tandis que Dream Displays reste installé des deux côtés. Les écrans `zevent:zevent_screen` et leurs chaînes enregistrées ne sont pas convertibles : ils doivent être remplacés par des écrans Dream Displays. Les sauvegardes et données des dix autres modules restent inchangées.
|
||||
|
||||
L’alpha.235 corrige l’écran noir de ZEVENT. MCEF chargeait correctement l’interface et les liens Twitch, mais son Chromium ne prenait pas en charge les codecs des directs. L’écran utilise désormais le résolveur HLS et le décodeur natif de Dream Displays, conserve ses options et sa synchronisation serveur, réessaie automatiquement un flux indisponible et indique clairement lorsqu’une chaîne est hors ligne. Le volume suit toujours la catégorie Disques, décroît jusqu’à 64 blocs et seul l’écran visible le plus proche reste audible.
|
||||
|
||||
L’alpha.234 ajoute pour le week-end l’écran Twitch ZEVENT de 9 × 5 blocs. Un clic droit permet de choisir une chaîne officielle ; la sélection est enregistrée par le serveur, tandis que l’image et le son sont rendus localement avec MCEF Modern. Le volume suit la catégorie Disques et décroît jusqu’à 64 blocs. Les mondes et les dix modules Sanctuary existants restent compatibles ; le client et le serveur doivent toutefois recevoir le JAR ZEVENT, et seul le client reçoit MCEF.
|
||||
|
||||
L’alpha.233 remplace le bloc-écran des trophées par un véritable panneau Vanilla en chêne pâle, posable au sol ou au mur. Son éditeur permet de rechercher une statistique, composer jusqu’à seize pages et choisir l’affichage des noms et valeurs ; le propriétaire ou un opérateur garde le contrôle de sa configuration tandis que le changement de page reste public. L’item `redstoner:trophy_panel` conserve son ID et les anciens panneaux posés sont migrés automatiquement. Les classements affichent désormais le vrai pseudo connu du joueur au lieu d’un fragment d’UUID. Les sauvegardes Sanctuary restent en données v15/protocole 24 ; Red-Stoner passe aux données v5 et au protocole 9, ce qui exige la même version côté client et serveur.
|
||||
|
||||
L’alpha.232 enregistre automatiquement chaque cliché Canon dans le dossier `screenshots` de l’instance, à côté des captures F2. Le PNG conserve le carré réellement cadré, sa résolution et le filtre de la pellicule ; l’écriture asynchrone et les noms uniques évitent les freezes et les écrasements. Cette release corrige aussi la cape blanche décorée, la touche `E` du terminal et étend le panneau de trophées à huit lignes. Chaque statistique y possède une page de noms puis une page de valeurs ; un œuf d’apparition ajoute directement le classement des créatures correspondantes tuées. Les sauvegardes restent en données Another World v5, Sanctuary v15 et Red-Stoner v4 ; le snapshot du panneau passant à sept entrées, le protocole Red-Stoner passe à 8.
|
||||
|
||||
L’alpha.231 rend les sorties et le butin de la Backroom réellement trouvables sur ses cinq niveaux. Chaque niveau garantit désormais un coffre et un lit de sortie par cellule de 3 × 3 chunks dans les nouveaux chunks, sans ajouter de livres au butin. Le panneau de trophées devient une plaque verticale de chêne avec quatre lignes lisibles : titre et numéro de page, puis les trois places du podium. Un clic droit avance d’une page et `Maj` + clic droit revient à la précédente. JEI passe à la bêta Fabric `30.29.0.201`, qui corrige un crash lié à l’atlas de textures. Les sauvegardes et les chunks déjà générés restent compatibles ; Sanctuary conserve les données v15/protocole 24 et Red-Stoner les données v4/protocole 7.
|
||||
|
||||
L’alpha.230 attribue les quêtes communautaires terminées à leur preneur et répartit les lits de sortie rares sur chacun des cinq niveaux de la Backroom. Elle ajoute aussi les classements publics et le panneau mural `redstoner:trophy_panel`, corrige la projection des couleurs et motifs de la cape blanche avec les sprites de bouclier, et permet d’équiper une poule adulte avec l’armure de loup vanilla en écailles de tatou. Les sauvegardes restent compatibles ; Sanctuary passe aux données v15 et Red-Stoner aux données v4 sans changer leurs protocoles réseau 24 et 7.
|
||||
|
||||
L’alpha.229 ajoute au terminal les tris alphabétique, par quantité croissante et par quantité décroissante. Le tri ne change que sur demande explicite et les rafraîchissements périodiques conservent l’ordre ainsi que les objets sous le curseur. Les sauvegardes, les données Red-Stoner v3 et le protocole réseau 7 restent compatibles.
|
||||
|
||||
@@ -323,9 +345,9 @@ Le guide détaillé est dans [`masterkey/tools/PANORAMA.md`](masterkey/tools/PAN
|
||||
|
||||
## Red-Stoner et photographie
|
||||
|
||||
Red-Stoner possède sept blocs actifs sous son namespace : `eight_bit_computer`, `controller`, `personal_computer`, `drawer`, `particuleur`, `storage_terminal` et `trophy_panel`. Son onglet créatif expose aussi les trois appareils Canon et les seize styles de pellicule.
|
||||
Red-Stoner possède ses appareils sous son namespace ainsi que la paire de panneaux Vanilla `trophy_sign`/`trophy_wall_sign`. L’item historique `redstoner:trophy_panel` est conservé afin que les inventaires et recettes existants restent valides ; il place désormais cette paire, tandis que l’ancien bloc mural du même ID ne subsiste que le temps de sa migration automatique. Son onglet créatif expose le panneau de trophées avec les ordinateurs, le Particuleur et le terminal de stockage.
|
||||
|
||||
Le panneau mural `redstoner:trophy_panel` consomme l’API publique de classement de Sanctuary sans conserver les scores. Le joueur qui le pose peut ajouter des pages statistiques en cliquant avec un item, tandis que tous les visiteurs peuvent faire défiler les pages. Sanctuary importe une fois les statistiques vanilla existantes, garde les classements par UUID — champion déconnecté compris — et fournit seulement un podium borné à trois entrées. Les composants privés de l’item servant d’icône et la base complète des joueurs ne sont jamais synchronisés au client.
|
||||
Le panneau de trophées consomme l’API publique de classement de Sanctuary sans conserver les scores. Sa première pose ouvre un éditeur blanc : le créateur choisit dans le catalogue Vanilla jusqu’à seize statistiques, leur ordre et un affichage noms+valeurs, noms seuls ou valeurs seules. `Maj` + clic droit rouvre ensuite cet éditeur pour le créateur ou un opérateur ; le clic droit ordinaire fait défiler publiquement les pages. La configuration et la destruction sont protégées par UUID. Chaque page utilise les quatre lignes natives du panneau — titre puis trois rangs — et se rafraîchit côté serveur. Sanctuary importe une fois les statistiques Vanilla existantes, conserve les classements par UUID — champion déconnecté compris — et résout les vrais pseudos depuis le cache du serveur au lieu d’afficher un début d’UUID.
|
||||
|
||||
L'ordinateur 8 bits et le contrôleur exécutent la machine virtuelle historique : programme assembleur borné à 4 096 caractères, RAM, inventaire, six ports redstone directionnels, impulsions et sortie comparateur. Le contrôleur dispose de 1 024 cellules de RAM contre 256 pour l'ordinateur. L'instruction destinée à la matrice d'affichage renvoie volontairement une erreur claire tant que ce périphérique reste différé.
|
||||
|
||||
@@ -414,7 +436,7 @@ Le menu pause est lui aussi possédé par Sanctuary. Il ne conserve que Retour a
|
||||
|
||||
## Versions du modpack et mises à jour Modrinth
|
||||
|
||||
`pack_version` dans `gradle.properties` est la version du pack complet. `mod_version=0.0.0-alpha.0` et `module_lifecycle=standby` sont les valeurs par défaut des modules en veille ; les propriétés `<module>_version` et `<module>_lifecycle` n’activent et ne font avancer que le module concerné. Le contrat de distribution est dans `pack/release.json`.
|
||||
`pack_version` dans `gradle.properties` est la version du pack complet. `mod_version=0.0.0-alpha.0` et `module_lifecycle=standby` sont les valeurs par défaut des modules en veille ; les propriétés `<module>_version` et `<module>_lifecycle` n’activent et ne font avancer que le module concerné. Le contrat de distribution est dans `pack/release.json`. Depuis l’alpha.236, le module temporaire `zevent` n’est plus construit ni distribué ; l’alpha.240 utilise le second build MyDisplay fourni et son moteur client WaterMedia à la place de Dream Displays.
|
||||
|
||||
La version globale du pack n’est pas injectée dans les JAR inchangés : leur contenu et leur numéro restent immuables. Le lanceur possède la composition exacte du pack, tandis que chaque manifest conserve la version effective de son module.
|
||||
|
||||
|
||||
@@ -15,6 +15,14 @@ tasks.register("vaultRulesSmoke", JavaExec) {
|
||||
mainClass = "fr.koka99cab.sanctuary26.anotherworld.vault.VaultRulesSmoke"
|
||||
}
|
||||
|
||||
tasks.register("canonScreenshotFilesSmoke", JavaExec) {
|
||||
group = "verification"
|
||||
description = "Checks Canon photo exports use the vanilla screenshots directory and collision-safe names."
|
||||
dependsOn tasks.named("testClasses")
|
||||
classpath = sourceSets.test.runtimeClasspath
|
||||
mainClass = "fr.koka99cab.canon.client.CanonScreenshotFilesSmoke"
|
||||
}
|
||||
|
||||
tasks.named("test") {
|
||||
failOnNoDiscoveredTests = false
|
||||
}
|
||||
@@ -1956,7 +1964,7 @@ tasks.register("verifyAnotherWorldCatalog") {
|
||||
"src/main/resources/assets/redstoner/textures/item/weather_gauge.png",
|
||||
"src/main/resources/data/redstoner/recipe/weather_gauge.json"
|
||||
]
|
||||
if (project.version.toString() != "0.0.0-alpha.57"
|
||||
if (project.version.toString() != "0.0.0-alpha.58"
|
||||
|| sulfurMigration.target?.modules?.anotherworld != "0.0.0-alpha.44"
|
||||
|| sulfurMigration.sulfur_caves?.sanctuary?.dripstone_cell_period != 2
|
||||
|| !javaText.contains('Identifier.parse("sanctuary:sulfur_caves")')
|
||||
@@ -3261,6 +3269,7 @@ tasks.register("verifyAnotherWorldCatalog") {
|
||||
tasks.named("check") {
|
||||
dependsOn tasks.named("verifyAnotherWorldCatalog")
|
||||
dependsOn tasks.named("vaultRulesSmoke")
|
||||
dependsOn tasks.named("canonScreenshotFilesSmoke")
|
||||
}
|
||||
|
||||
tasks.register("verifyClimateBiomeMixer") {
|
||||
@@ -3273,7 +3282,7 @@ tasks.register("verifyClimateBiomeMixer") {
|
||||
"pack/migrations/26.2.0-alpha.188-beta-climate-provinces-to-alpha.189.json"))
|
||||
def mixer = file(
|
||||
"src/main/java/fr/koka99cab/sanctuary26/anotherworld/mixin/MultiNoiseBiomeSourceParameterListPresetMixin.java").text
|
||||
if (project.version.toString() != "0.0.0-alpha.57"
|
||||
if (project.version.toString() != "0.0.0-alpha.58"
|
||||
|| migration.source?.pack_version != "26.2.0-alpha.188"
|
||||
|| migration.target?.pack_version != "26.2.0-alpha.189"
|
||||
|| migration.target?.modules?.anotherworld != "0.0.0-alpha.48"
|
||||
@@ -3315,7 +3324,7 @@ tasks.register("verifyBackroomMaterialIntegration") {
|
||||
"sanctuary/src/main/java/fr/koka99cab/sanctuary26/sanctuary/api/backroom/BackroomMaterialApi.java").text
|
||||
def rootBuild = rootProject.file("build.gradle").text
|
||||
def manifest = new JsonSlurper().parse(file("src/main/resources/fabric.mod.json"))
|
||||
if (project.version.toString() != "0.0.0-alpha.57"
|
||||
if (project.version.toString() != "0.0.0-alpha.58"
|
||||
|| !initializer.contains("BackroomMaterialApi.registerAnomalyMaterial(AnotherWorldBlocks.TURQUOISE_MOSS)")
|
||||
|| !blocks.contains('"turquoise_moss"')
|
||||
|| !api.contains("public static void registerAnomalyMaterial")
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
package fr.koka99cab.canon.client;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.FileAlreadyExistsException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
final class CanonScreenshotFiles {
|
||||
private static final String SCREENSHOT_DIRECTORY = "screenshots";
|
||||
private static final int MAX_SUFFIX = 10_000;
|
||||
|
||||
private CanonScreenshotFiles() {
|
||||
}
|
||||
|
||||
static Path reserve(Path gameDirectory, String timestamp) throws IOException {
|
||||
Path screenshots = gameDirectory.resolve(SCREENSHOT_DIRECTORY);
|
||||
Files.createDirectories(screenshots);
|
||||
for (int suffix = 1; suffix <= MAX_SUFFIX; suffix++) {
|
||||
Path candidate = screenshots.resolve(fileName(timestamp, suffix));
|
||||
try {
|
||||
return Files.createFile(candidate);
|
||||
} catch (FileAlreadyExistsException ignored) {
|
||||
// Keep the vanilla timestamp readable while avoiding same-second collisions.
|
||||
}
|
||||
}
|
||||
throw new IOException("Could not reserve a unique Canon screenshot filename");
|
||||
}
|
||||
|
||||
static String fileName(String timestamp, int suffix) {
|
||||
if (timestamp == null || timestamp.isBlank()) {
|
||||
throw new IllegalArgumentException("timestamp must not be blank");
|
||||
}
|
||||
if (suffix < 1) {
|
||||
throw new IllegalArgumentException("suffix must be positive");
|
||||
}
|
||||
return timestamp + "-canon" + (suffix == 1 ? "" : "_" + suffix) + ".png";
|
||||
}
|
||||
}
|
||||
@@ -8,12 +8,15 @@ import fr.koka99cab.canon.network.CapturePhotoPayload;
|
||||
import fr.koka99cab.canon.photo.PhotoMapPalette;
|
||||
import com.mojang.blaze3d.platform.NativeImage;
|
||||
import com.mojang.blaze3d.platform.Window;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.Screenshot;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.sounds.SoundEvents;
|
||||
import net.minecraft.util.ARGB;
|
||||
import net.minecraft.util.Util;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
|
||||
public final class ClientPhotoCapture {
|
||||
@@ -65,6 +68,7 @@ public final class ClientPhotoCapture {
|
||||
ClientPlayNetworking.send(new CapturePhotoPayload(colors));
|
||||
client.player.getCooldowns().addCooldown(capture.cameraStack(), CanonConstants.CAMERA_COOLDOWN_TICKS);
|
||||
client.player.playSound(SoundEvents.SPYGLASS_USE, 0.7F, 1.35F);
|
||||
exportScreenshot(client, screenshot, capture.style(), offsetX, offsetY, cropWidth, cropHeight);
|
||||
} catch (Exception exception) {
|
||||
CanonMod.LOGGER.warn("Failed to capture photo", exception);
|
||||
if (client.player != null) {
|
||||
@@ -74,6 +78,72 @@ public final class ClientPhotoCapture {
|
||||
});
|
||||
}
|
||||
|
||||
private static void exportScreenshot(
|
||||
Minecraft client,
|
||||
NativeImage screenshot,
|
||||
PhotoStyle style,
|
||||
int offsetX,
|
||||
int offsetY,
|
||||
int cropWidth,
|
||||
int cropHeight
|
||||
) {
|
||||
NativeImage exported = null;
|
||||
Path target = null;
|
||||
try {
|
||||
exported = new NativeImage(cropWidth, cropHeight, false);
|
||||
screenshot.copyRect(exported, offsetX, offsetY, 0, 0, cropWidth, cropHeight, false, false);
|
||||
target = CanonScreenshotFiles.reserve(client.gameDirectory.toPath(), Util.getFilenameFormattedDateTime());
|
||||
} catch (Exception exception) {
|
||||
if (exported != null) exported.close();
|
||||
if (target != null) deleteReservedFile(target);
|
||||
notifyScreenshotFailure(client, exception);
|
||||
return;
|
||||
}
|
||||
|
||||
NativeImage image = exported;
|
||||
Path output = target;
|
||||
Util.ioPool().execute(() -> {
|
||||
try (image) {
|
||||
applyStyle(image, style);
|
||||
image.writeToFile(output);
|
||||
client.execute(() -> {
|
||||
if (client.player != null) {
|
||||
client.player.sendSystemMessage(Component.translatable(
|
||||
"message.canon.camera.screenshot_saved", output.getFileName().toString()));
|
||||
}
|
||||
});
|
||||
} catch (Exception exception) {
|
||||
deleteReservedFile(output);
|
||||
notifyScreenshotFailure(client, exception);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static void applyStyle(NativeImage image, PhotoStyle style) {
|
||||
for (int y = 0; y < image.getHeight(); y++) {
|
||||
for (int x = 0; x < image.getWidth(); x++) {
|
||||
image.setPixel(x, y, style.applyFilter(image.getPixel(x, y)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void notifyScreenshotFailure(Minecraft client, Exception exception) {
|
||||
CanonMod.LOGGER.warn("Failed to save Canon photo in the screenshots directory", exception);
|
||||
client.execute(() -> {
|
||||
if (client.player != null) {
|
||||
client.player.sendSystemMessage(Component.translatable("message.canon.camera.screenshot_failed"));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static void deleteReservedFile(Path target) {
|
||||
try {
|
||||
Files.deleteIfExists(target);
|
||||
} catch (Exception cleanupException) {
|
||||
CanonMod.LOGGER.debug("Could not remove incomplete Canon screenshot {}", target, cleanupException);
|
||||
}
|
||||
}
|
||||
|
||||
private static int clamp(int value, int min, int max) {
|
||||
return Math.max(min, Math.min(max, value));
|
||||
}
|
||||
|
||||
@@ -24,6 +24,8 @@
|
||||
"message.canon.camera.full": "The camera is already full.",
|
||||
"message.canon.camera.captured": "Photo captured.",
|
||||
"message.canon.camera.capture_failed": "Could not capture the current frame.",
|
||||
"message.canon.camera.screenshot_saved": "Photo saved to screenshots: %s",
|
||||
"message.canon.camera.screenshot_failed": "The in-game photo was captured, but its PNG could not be saved to screenshots.",
|
||||
"style.canon.white": "Neutral",
|
||||
"style.canon.white.effect": "Neutral stock with soft paper whites.",
|
||||
"style.canon.light_gray": "Matte",
|
||||
|
||||
@@ -24,6 +24,8 @@
|
||||
"message.canon.camera.full": "L'appareil photo est deja plein.",
|
||||
"message.canon.camera.captured": "Photo prise.",
|
||||
"message.canon.camera.capture_failed": "Impossible de capturer l'image actuelle.",
|
||||
"message.canon.camera.screenshot_saved": "Photo sauvegardee dans screenshots : %s",
|
||||
"message.canon.camera.screenshot_failed": "La photo en jeu est prise, mais son PNG n'a pas pu etre sauvegarde dans screenshots.",
|
||||
"style.canon.white": "Neutre",
|
||||
"style.canon.white.effect": "Rendu neutre avec des blancs doux.",
|
||||
"style.canon.light_gray": "Mat",
|
||||
|
||||
@@ -24,6 +24,8 @@
|
||||
"message.canon.camera.full": "Фотоаппарат уже полностью заряжен.",
|
||||
"message.canon.camera.captured": "Фотография сделана.",
|
||||
"message.canon.camera.capture_failed": "Не удалось снять текущий кадр.",
|
||||
"message.canon.camera.screenshot_saved": "Фотография сохранена в screenshots: %s",
|
||||
"message.canon.camera.screenshot_failed": "Снимок в игре сделан, но PNG не удалось сохранить в screenshots.",
|
||||
"style.canon.white": "Нейтральный",
|
||||
"style.canon.white.effect": "Нейтральная плёнка с мягкими белыми тонами.",
|
||||
"style.canon.light_gray": "Матовый",
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
package fr.koka99cab.canon.client;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
public final class CanonScreenshotFilesSmoke {
|
||||
private CanonScreenshotFilesSmoke() {
|
||||
}
|
||||
|
||||
public static void main(String[] args) throws Exception {
|
||||
String timestamp = "2026-09-03_01.23.45";
|
||||
require(CanonScreenshotFiles.fileName(timestamp, 1).equals("2026-09-03_01.23.45-canon.png"),
|
||||
"first screenshot filename");
|
||||
require(CanonScreenshotFiles.fileName(timestamp, 2).equals("2026-09-03_01.23.45-canon_2.png"),
|
||||
"same-second screenshot filename");
|
||||
|
||||
Path gameDirectory = Files.createTempDirectory("canon-screenshot-smoke-");
|
||||
Path first = CanonScreenshotFiles.reserve(gameDirectory, timestamp);
|
||||
Path second = CanonScreenshotFiles.reserve(gameDirectory, timestamp);
|
||||
require(first.getParent().equals(gameDirectory.resolve("screenshots")),
|
||||
"screenshots directory");
|
||||
require(first.getFileName().toString().equals("2026-09-03_01.23.45-canon.png"),
|
||||
"first reserved screenshot");
|
||||
require(second.getFileName().toString().equals("2026-09-03_01.23.45-canon_2.png"),
|
||||
"collision-safe screenshot");
|
||||
require(Files.isRegularFile(first) && Files.isRegularFile(second),
|
||||
"reserved screenshot files");
|
||||
}
|
||||
|
||||
private static void require(boolean condition, String contract) {
|
||||
if (!condition) throw new IllegalStateException("Invalid Canon screenshot contract: " + contract);
|
||||
}
|
||||
}
|
||||
+1267
-336
File diff suppressed because it is too large
Load Diff
+4
-4
@@ -14,9 +14,9 @@ mod_version=0.0.0-alpha.0
|
||||
module_lifecycle=standby
|
||||
ambiance_version=0.0.0-alpha.22
|
||||
ambiance_lifecycle=active
|
||||
redstoner_version=0.0.0-alpha.17
|
||||
redstoner_version=0.0.0-alpha.20
|
||||
redstoner_lifecycle=active
|
||||
anotherworld_version=0.0.0-alpha.57
|
||||
anotherworld_version=0.0.0-alpha.58
|
||||
anotherworld_lifecycle=active
|
||||
itsalive_version=0.0.0-alpha.38
|
||||
itsalive_lifecycle=active
|
||||
@@ -28,7 +28,7 @@ iliketomoveit_version=0.0.0-alpha.18
|
||||
iliketomoveit_lifecycle=active
|
||||
onlyfun_version=0.0.0-alpha.27
|
||||
onlyfun_lifecycle=active
|
||||
sanctuary_version=0.0.0-alpha.135
|
||||
sanctuary_version=0.0.0-alpha.139
|
||||
sanctuary_lifecycle=active
|
||||
pack_version=26.2.0-alpha.229
|
||||
pack_version=26.2.0-alpha.242
|
||||
maven_group=fr.koka99cab.sanctuary26
|
||||
|
||||
@@ -1261,6 +1261,7 @@ tasks.named("processResources") {
|
||||
|
||||
tasks.named("sourcesJar") {
|
||||
dependsOn tasks.named("generateAgricultureResources")
|
||||
dependsOn tasks.named("generateCanapliaResources")
|
||||
}
|
||||
|
||||
tasks.register("verifyCulinaryReliability") {
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 641 B |
+100
@@ -0,0 +1,100 @@
|
||||
package fr.koka99cab.sanctuary26.onlyfun.client;
|
||||
|
||||
import fr.koka99cab.sanctuary26.onlyfun.item.SkinFaceFireworkExplosions;
|
||||
import fr.koka99cab.sanctuary26.onlyfun.mixin.client.ParticleLifetimeAccessor;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.particle.Particle;
|
||||
import net.minecraft.core.particles.ColorParticleOption;
|
||||
import net.minecraft.core.particles.DustParticleOptions;
|
||||
import net.minecraft.core.particles.ParticleTypes;
|
||||
import net.minecraft.sounds.SoundEvents;
|
||||
import net.minecraft.sounds.SoundSource;
|
||||
import net.minecraft.util.RandomSource;
|
||||
import net.minecraft.world.entity.projectile.FireworkRocketEntity;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
|
||||
/** Paints the baked skin as a firework burst of coloured sparks, facing the player. */
|
||||
public final class SkinFaceFireworkClient {
|
||||
public static final double PIXEL = 1.85D;
|
||||
public static final double LARGE_PIXEL = 2.7D;
|
||||
public static final float SPARK_SIZE = 3.6F;
|
||||
public static final float LARGE_SPARK_SIZE = 5.15F;
|
||||
public static final int SPARK_LIFETIME_TICKS = 38;
|
||||
private static final int SUBPIXELS = 2;
|
||||
|
||||
private SkinFaceFireworkClient() {
|
||||
}
|
||||
|
||||
public static void explode(FireworkRocketEntity rocket) {
|
||||
Minecraft minecraft = Minecraft.getInstance();
|
||||
if (minecraft.level == null || minecraft.particleEngine == null) return;
|
||||
Vec3 origin = rocket.position();
|
||||
RandomSource random = rocket.level().getRandom();
|
||||
minecraft.level.playLocalSound(origin.x, origin.y, origin.z,
|
||||
SoundEvents.FIREWORK_ROCKET_BLAST, SoundSource.AMBIENT,
|
||||
20.0F, 0.95F + random.nextFloat() * 0.1F, true);
|
||||
minecraft.level.addParticle(ColorParticleOption.create(ParticleTypes.FLASH, 0xFFFFFFFF),
|
||||
origin.x, origin.y, origin.z, 0.0D, 0.0D, 0.0D);
|
||||
Vec3 look = facing(minecraft, origin);
|
||||
Vec3 right = look.cross(new Vec3(0.0D, 1.0D, 0.0D));
|
||||
if (right.lengthSqr() < 1.0E-6D) right = new Vec3(1.0D, 0.0D, 0.0D);
|
||||
right = right.normalize();
|
||||
Vec3 up = new Vec3(0.0D, 1.0D, 0.0D);
|
||||
int[] pixels = SkinFaceFireworkExplosions.portrait();
|
||||
int width = SkinFaceFireworkExplosions.WIDTH;
|
||||
int height = SkinFaceFireworkExplosions.portraitHeight();
|
||||
boolean large = largeBurst(rocket);
|
||||
double pixel = burstPixel(large);
|
||||
float sparkSize = burstSparkSize(large);
|
||||
double midX = (width - 1) / 2.0D;
|
||||
double midY = (height - 1) / 2.0D;
|
||||
double step = 1.0D / SUBPIXELS;
|
||||
for (int row = 0; row < height; row++) {
|
||||
for (int col = 0; col < width; col++) {
|
||||
int color = visibleSpark(pixels[row * width + col]);
|
||||
DustParticleOptions dust = new DustParticleOptions(color, sparkSize);
|
||||
for (int subY = 0; subY < SUBPIXELS; subY++) {
|
||||
for (int subX = 0; subX < SUBPIXELS; subX++) {
|
||||
double px = (col - midX) + (subX + 0.5D) * step - 0.5D;
|
||||
double py = (midY - row) - (subY + 0.5D) * step + 0.5D;
|
||||
Vec3 offset = right.scale(px * pixel).add(up.scale(py * pixel));
|
||||
Vec3 pos = origin.add(offset);
|
||||
Vec3 velocity = offset.scale(0.05D);
|
||||
Particle spark = minecraft.particleEngine.createParticle(
|
||||
dust, pos.x, pos.y, pos.z, velocity.x, velocity.y, velocity.z);
|
||||
if (spark != null) {
|
||||
((ParticleLifetimeAccessor) spark).onlyfun$setLifetime(SPARK_LIFETIME_TICKS);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean largeBurst(FireworkRocketEntity rocket) {
|
||||
return (rocket.getUUID().getLeastSignificantBits() & 1L) != 0L;
|
||||
}
|
||||
|
||||
public static double burstPixel(boolean large) {
|
||||
return large ? LARGE_PIXEL : PIXEL;
|
||||
}
|
||||
|
||||
public static float burstSparkSize(boolean large) {
|
||||
return large ? LARGE_SPARK_SIZE : SPARK_SIZE;
|
||||
}
|
||||
|
||||
private static Vec3 facing(Minecraft minecraft, Vec3 origin) {
|
||||
Vec3 camera = minecraft.gameRenderer.mainCamera().position();
|
||||
Vec3 horizontal = new Vec3(origin.x - camera.x, 0.0D, origin.z - camera.z);
|
||||
if (horizontal.lengthSqr() < 1.0E-4D) return new Vec3(0.0D, 0.0D, 1.0D);
|
||||
return horizontal.normalize();
|
||||
}
|
||||
|
||||
private static int visibleSpark(int rgb) {
|
||||
int red = rgb >> 16 & 0xFF;
|
||||
int green = rgb >> 8 & 0xFF;
|
||||
int blue = rgb & 0xFF;
|
||||
if (red + green + blue < 24) return 0x2A2A2A;
|
||||
return rgb;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package fr.koka99cab.sanctuary26.onlyfun.item;
|
||||
|
||||
import java.util.function.Consumer;
|
||||
import net.minecraft.ChatFormatting;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.world.item.FireworkRocketItem;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.item.TooltipFlag;
|
||||
import net.minecraft.world.item.component.TooltipDisplay;
|
||||
|
||||
/** Firework rocket that bursts into the baked Nourra skin face. */
|
||||
public final class NourraFireworkItem extends FireworkRocketItem {
|
||||
public NourraFireworkItem(Properties properties) {
|
||||
super(properties);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void appendHoverText(ItemStack stack, TooltipContext context, TooltipDisplay display,
|
||||
Consumer<Component> tooltip, TooltipFlag flag) {
|
||||
tooltip.accept(Component.translatable("tooltip.onlyfun.nourra_firework").withStyle(ChatFormatting.AQUA));
|
||||
super.appendHoverText(stack, context, display, tooltip, flag);
|
||||
}
|
||||
}
|
||||
+83
@@ -0,0 +1,83 @@
|
||||
package fr.koka99cab.sanctuary26.onlyfun.item;
|
||||
|
||||
import fr.koka99cab.sanctuary26.onlyfun.registry.OnlyFunRegistries;
|
||||
import java.util.List;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.item.component.Fireworks;
|
||||
|
||||
/**
|
||||
* Baked Nourra head (NameMC 6166a439e9354904): inner face, hood overlay, hood top.
|
||||
* The portrait is drawn client-side with firework sparks.
|
||||
*/
|
||||
public final class SkinFaceFireworkExplosions {
|
||||
public static final int WIDTH = 8;
|
||||
public static final int HEIGHT = 8;
|
||||
public static final int TRANSPARENT = -1;
|
||||
/** Inner head front, top-left origin, row-major RGB. */
|
||||
public static final int[] FACE = {
|
||||
0x000000, 0x020202, 0x0A0A0A, 0x111111, 0x111111, 0x0A0A0A, 0x020202, 0x000000,
|
||||
0x020202, 0x0A0A0A, 0x111111, 0x181818, 0x181818, 0x111111, 0x0A0A0A, 0x020202,
|
||||
0x0A0A0A, 0x111111, 0x181818, 0x212121, 0x212121, 0x181818, 0x111111, 0x0A0A0A,
|
||||
0x111111, 0x181818, 0x212121, 0x2B2B2B, 0x2B2B2B, 0x212121, 0x181818, 0x111111,
|
||||
0x111111, 0x000000, 0x000000, 0xFFF0E1, 0xFFF0E1, 0x000000, 0x000000, 0x111111,
|
||||
0x0A0A0A, 0xFFFFFF, 0x4A9DFC, 0xFDE8D7, 0xFFF0E1, 0x4A9DFC, 0xFFFFFF, 0x0A0A0A,
|
||||
0x020202, 0xFFFFFF, 0x4A9DFC, 0xFDE8D7, 0xFDE8D7, 0x4A9DFC, 0xFFFFFF, 0x020202,
|
||||
0x000000, 0xF0C6A5, 0xF7D5B9, 0xFCE1CA, 0xFCE1CA, 0xF7D5B9, 0xF0C6A5, 0x000000
|
||||
};
|
||||
/** Hat/hood overlay on the front; {@link #TRANSPARENT} keeps the inner face visible. */
|
||||
public static final int[] HOOD = {
|
||||
0x000000, 0x0D0D0D, 0xCBCBCB, 0xD8D8D8, 0xD8D8D8, 0xCBCBCB, 0x0D0D0D, 0x000000,
|
||||
0x0D0D0D, 0x707070, 0xD8D8D8, 0xE5E5E5, 0xE5E5E5, 0xD8D8D8, 0x707070, 0x0D0D0D,
|
||||
0xCBCBCB, 0xD8D8D8, 0xFFFFFF, 0x212121, 0x212121, 0x181818, 0xD8D8D8, 0xCBCBCB,
|
||||
0xD8D8D8, 0x181818, 0x212121, 0x2B2B2B, 0x2B2B2B, 0x212121, 0x181818, 0xD8D8D8,
|
||||
0x111111, 0x181818, 0x212121, 0x2B2B2B, TRANSPARENT, TRANSPARENT, TRANSPARENT, 0x111111,
|
||||
0x0A0A0A, 0x111111, 0x181818, TRANSPARENT, TRANSPARENT, TRANSPARENT, TRANSPARENT, TRANSPARENT,
|
||||
0x020202, 0x0A0A0A, TRANSPARENT, TRANSPARENT, TRANSPARENT, TRANSPARENT, TRANSPARENT, TRANSPARENT,
|
||||
TRANSPARENT, 0x020202, TRANSPARENT, TRANSPARENT, TRANSPARENT, TRANSPARENT, TRANSPARENT, TRANSPARENT
|
||||
};
|
||||
/** Front edge of the hood top (skin overlay top rows closest to the face). */
|
||||
public static final int[] HOOD_TOP = {
|
||||
0xBEBEBE, 0xCBCBCB, 0xD8D8D8, 0xD8D8D8, 0xD8D8D8, 0xD8D8D8, 0xCBCBCB, 0xBEBEBE,
|
||||
0xB8B8B8, 0xB8B8B8, 0xCBCBCB, 0xD8D8D8, 0xD8D8D8, 0xCBCBCB, 0xB8B8B8, 0xB8B8B8
|
||||
};
|
||||
|
||||
private SkinFaceFireworkExplosions() {
|
||||
}
|
||||
|
||||
/** Vanilla flight 3 minus half a gunpowder step, so the burst sits between 2 and 3. */
|
||||
public static final int LIFETIME_TRIM_TICKS = 5;
|
||||
|
||||
public static Fireworks defaultFireworks() {
|
||||
return new Fireworks(3, List.of());
|
||||
}
|
||||
|
||||
public static int midFlightLifetime(int vanillaLifetime) {
|
||||
return Math.max(1, vanillaLifetime - LIFETIME_TRIM_TICKS);
|
||||
}
|
||||
|
||||
public static boolean isSkinFaceRocket(ItemStack stack) {
|
||||
return !stack.isEmpty() && stack.is(OnlyFunRegistries.NOURRA_FIREWORK);
|
||||
}
|
||||
|
||||
public static int opaqueHoodPixels() {
|
||||
int count = 0;
|
||||
for (int color : HOOD) {
|
||||
if (color != TRANSPARENT) count++;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
/** Hood top (2 rows) then the 8×8 face with the hood overlay composited. */
|
||||
public static int[] portrait() {
|
||||
int[] pixels = new int[WIDTH * (HEIGHT + 2)];
|
||||
System.arraycopy(HOOD_TOP, 0, pixels, 0, HOOD_TOP.length);
|
||||
for (int index = 0; index < FACE.length; index++) {
|
||||
pixels[HOOD_TOP.length + index] = HOOD[index] == TRANSPARENT ? FACE[index] : HOOD[index];
|
||||
}
|
||||
return pixels;
|
||||
}
|
||||
|
||||
public static int portraitHeight() {
|
||||
return HEIGHT + HOOD_TOP.length / WIDTH;
|
||||
}
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
package fr.koka99cab.sanctuary26.onlyfun.mixin;
|
||||
|
||||
import fr.koka99cab.sanctuary26.onlyfun.item.SkinFaceFireworkExplosions;
|
||||
import net.minecraft.world.entity.projectile.FireworkRocketEntity;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.Shadow;
|
||||
import org.spongepowered.asm.mixin.injection.At;
|
||||
import org.spongepowered.asm.mixin.injection.Inject;
|
||||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
|
||||
|
||||
@Mixin(FireworkRocketEntity.class)
|
||||
public abstract class FireworkRocketLifetimeMixin {
|
||||
@Shadow
|
||||
private int life;
|
||||
@Shadow
|
||||
private int lifetime;
|
||||
|
||||
@Inject(method = "tick", at = @At("HEAD"))
|
||||
private void onlyfun$midFlightLifetime(CallbackInfo callback) {
|
||||
if (this.life != 0) return;
|
||||
FireworkRocketEntity rocket = (FireworkRocketEntity) (Object) this;
|
||||
if (!SkinFaceFireworkExplosions.isSkinFaceRocket(rocket.getItem())) return;
|
||||
this.lifetime = SkinFaceFireworkExplosions.midFlightLifetime(this.lifetime);
|
||||
}
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
package fr.koka99cab.sanctuary26.onlyfun.mixin.client;
|
||||
|
||||
import fr.koka99cab.sanctuary26.onlyfun.client.SkinFaceFireworkClient;
|
||||
import fr.koka99cab.sanctuary26.onlyfun.item.SkinFaceFireworkExplosions;
|
||||
import net.minecraft.world.entity.projectile.FireworkRocketEntity;
|
||||
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(FireworkRocketEntity.class)
|
||||
public abstract class FireworkRocketEntityMixin {
|
||||
@Inject(method = "handleEntityEvent", at = @At("HEAD"), cancellable = true)
|
||||
private void onlyfun$drawSkinFace(byte id, CallbackInfo callback) {
|
||||
if (id != 17) return;
|
||||
FireworkRocketEntity rocket = (FireworkRocketEntity) (Object) this;
|
||||
if (!rocket.level().isClientSide()) return;
|
||||
if (!SkinFaceFireworkExplosions.isSkinFaceRocket(rocket.getItem())) return;
|
||||
SkinFaceFireworkClient.explode(rocket);
|
||||
callback.cancel();
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
package fr.koka99cab.sanctuary26.onlyfun.mixin.client;
|
||||
|
||||
import net.minecraft.client.particle.Particle;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.gen.Accessor;
|
||||
|
||||
@Mixin(Particle.class)
|
||||
public interface ParticleLifetimeAccessor {
|
||||
@Accessor("lifetime")
|
||||
void onlyfun$setLifetime(int lifetime);
|
||||
}
|
||||
+1
@@ -28,6 +28,7 @@ public final class OnlyFunCreativeTab {
|
||||
output.accept(OnlyFunRegistries.CAPE_LOOTBOX);
|
||||
output.accept(OnlyFunRegistries.SPAWN_EGG_LOOTBOX);
|
||||
output.accept(OnlyFunRegistries.ANOMALY_LOOTBOX);
|
||||
output.accept(OnlyFunRegistries.NOURRA_FIREWORK);
|
||||
output.accept(AbjectionRegistries.SHIT_BALL);
|
||||
output.accept(AbjectionRegistries.SHIT_CUBE_ITEM);
|
||||
})
|
||||
|
||||
+10
@@ -3,6 +3,8 @@ package fr.koka99cab.sanctuary26.onlyfun.registry;
|
||||
import fr.koka99cab.sanctuary26.onlyfun.OnlyFunMod;
|
||||
import fr.koka99cab.sanctuary26.onlyfun.block.LuckyBlockBlock;
|
||||
import fr.koka99cab.sanctuary26.onlyfun.item.LootboxItem;
|
||||
import fr.koka99cab.sanctuary26.onlyfun.item.SkinFaceFireworkExplosions;
|
||||
import fr.koka99cab.sanctuary26.onlyfun.item.NourraFireworkItem;
|
||||
import fr.koka99cab.sanctuary26.onlyfun.loot.OnlyFunGenerator;
|
||||
import fr.koka99cab.sanctuary26.onlyfun.loot.OnlyFunSource;
|
||||
import java.util.function.Function;
|
||||
@@ -32,6 +34,9 @@ public final class OnlyFunRegistries {
|
||||
public static final Item CAPE_LOOTBOX = registerLootbox("cape_lootbox", OnlyFunSource.CAPE_LOOTBOX);
|
||||
public static final Item SPAWN_EGG_LOOTBOX = registerLootbox("spawn_egg_lootbox", OnlyFunSource.SPAWN_EGG_LOOTBOX);
|
||||
public static final Item ANOMALY_LOOTBOX = registerLootbox("anomaly_lootbox", OnlyFunSource.ANOMALY_LOOTBOX);
|
||||
public static final Item NOURRA_FIREWORK = registerItem("nourra_firework",
|
||||
properties -> new NourraFireworkItem(properties.stacksTo(64)
|
||||
.component(DataComponents.FIREWORKS, SkinFaceFireworkExplosions.defaultFireworks())));
|
||||
private static boolean initialized;
|
||||
|
||||
private OnlyFunRegistries() {
|
||||
@@ -61,6 +66,11 @@ public final class OnlyFunRegistries {
|
||||
OnlyFunMod.LOGGER.debug("[{}] Item and block registries initialized.", OnlyFunMod.DISPLAY_NAME);
|
||||
}
|
||||
|
||||
private static Item registerItem(String path, Function<Item.Properties, Item> factory) {
|
||||
ResourceKey<Item> key = ResourceKey.create(Registries.ITEM, OnlyFunMod.id(path));
|
||||
return Registry.register(BuiltInRegistries.ITEM, key, factory.apply(new Item.Properties().setId(key)));
|
||||
}
|
||||
|
||||
private static Item registerLootbox(String path, OnlyFunSource source) {
|
||||
ResourceKey<Item> key = ResourceKey.create(Registries.ITEM, OnlyFunMod.id(path));
|
||||
return Registry.register(BuiltInRegistries.ITEM, key,
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"model": {
|
||||
"type": "minecraft:model",
|
||||
"model": "onlyfun:item/nourra_firework"
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,8 @@
|
||||
"item.onlyfun.cape_lootbox": "Cape Lootbox",
|
||||
"item.onlyfun.spawn_egg_lootbox": "Spawn Egg Lootbox",
|
||||
"item.onlyfun.anomaly_lootbox": "Anomaly Lootbox",
|
||||
"item.onlyfun.nourra_firework": "Nourra Firework",
|
||||
"tooltip.onlyfun.nourra_firework": "The burst paints the skin face, at normal or larger size.",
|
||||
"item.onlyfun.quality_gear": "Quality Gear",
|
||||
"item.onlyfun.impossible_tool": "Impossible Tool",
|
||||
"item.onlyfun.impossible_book": "Impossible Book",
|
||||
|
||||
@@ -6,6 +6,8 @@
|
||||
"item.onlyfun.cape_lootbox": "Lootbox de capes",
|
||||
"item.onlyfun.spawn_egg_lootbox": "Lootbox d'œufs",
|
||||
"item.onlyfun.anomaly_lootbox": "Lootbox anomalie",
|
||||
"item.onlyfun.nourra_firework": "Feu d'artifice Nourra",
|
||||
"tooltip.onlyfun.nourra_firework": "L'explosion dessine le visage, en taille normale ou plus grande.",
|
||||
"item.onlyfun.quality_gear": "Équipement de qualité",
|
||||
"item.onlyfun.impossible_tool": "Outil impossible",
|
||||
"item.onlyfun.impossible_book": "Livre impossible",
|
||||
|
||||
@@ -6,6 +6,8 @@
|
||||
"item.onlyfun.cape_lootbox": "Лутбокс плащей",
|
||||
"item.onlyfun.spawn_egg_lootbox": "Лутбокс яиц призыва",
|
||||
"item.onlyfun.anomaly_lootbox": "Лутбокс-аномалия",
|
||||
"item.onlyfun.nourra_firework": "Фейерверк Нурры",
|
||||
"tooltip.onlyfun.nourra_firework": "Взрыв рисует лицо скина обычного или увеличенного размера.",
|
||||
"item.onlyfun.quality_gear": "Качественное снаряжение",
|
||||
"item.onlyfun.impossible_tool": "Невозможный инструмент",
|
||||
"item.onlyfun.impossible_book": "Невозможная книга",
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"parent": "minecraft:item/generated",
|
||||
"textures": {
|
||||
"layer0": "onlyfun:item/nourra_firework"
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 280 B |
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"type": "minecraft:crafting_shapeless",
|
||||
"category": "misc",
|
||||
"ingredients": [
|
||||
"minecraft:firework_rocket",
|
||||
"minecraft:light_blue_dye",
|
||||
"minecraft:paper"
|
||||
],
|
||||
"result": {
|
||||
"count": 1,
|
||||
"id": "onlyfun:nourra_firework"
|
||||
}
|
||||
}
|
||||
@@ -21,6 +21,13 @@
|
||||
"fr.koka99cab.sanctuary26.onlyfun.client.OnlyFunClient"
|
||||
]
|
||||
},
|
||||
"mixins": [
|
||||
"onlyfun.mixins.json",
|
||||
{
|
||||
"config": "onlyfun.client.mixins.json",
|
||||
"environment": "client"
|
||||
}
|
||||
],
|
||||
"depends": {
|
||||
"fabricloader": ">=0.19.3",
|
||||
"minecraft": "~26.2",
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"required": true,
|
||||
"package": "fr.koka99cab.sanctuary26.onlyfun.mixin.client",
|
||||
"compatibilityLevel": "JAVA_25",
|
||||
"client": [
|
||||
"FireworkRocketEntityMixin",
|
||||
"ParticleLifetimeAccessor"
|
||||
],
|
||||
"injectors": {
|
||||
"defaultRequire": 1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"required": true,
|
||||
"package": "fr.koka99cab.sanctuary26.onlyfun.mixin",
|
||||
"compatibilityLevel": "JAVA_25",
|
||||
"mixins": [
|
||||
"FireworkRocketLifetimeMixin"
|
||||
],
|
||||
"injectors": {
|
||||
"defaultRequire": 1
|
||||
}
|
||||
}
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
package fr.koka99cab.sanctuary26.onlyfun.item;
|
||||
|
||||
import fr.koka99cab.sanctuary26.onlyfun.client.SkinFaceFireworkClient;
|
||||
import net.minecraft.SharedConstants;
|
||||
import net.minecraft.server.Bootstrap;
|
||||
import net.minecraft.world.item.component.Fireworks;
|
||||
|
||||
/** Executable check for the baked Nourra face and hood firework contract. */
|
||||
public final class SkinFaceFireworkSmoke {
|
||||
private SkinFaceFireworkSmoke() {
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
SharedConstants.tryDetectVersion();
|
||||
Bootstrap.bootStrap();
|
||||
verify();
|
||||
}
|
||||
|
||||
public static void verify() {
|
||||
require(SkinFaceFireworkExplosions.WIDTH == 8 && SkinFaceFireworkExplosions.HEIGHT == 8,
|
||||
"Face grid is no longer 8×8");
|
||||
require(SkinFaceFireworkExplosions.FACE.length == 64, "Face palette length drifted");
|
||||
require(SkinFaceFireworkExplosions.HOOD.length == 64, "Hood overlay length drifted");
|
||||
require(SkinFaceFireworkExplosions.HOOD_TOP.length == 16, "Hood top strip length drifted");
|
||||
require(SkinFaceFireworkExplosions.FACE[5 * 8 + 2] == 0x4A9DFC
|
||||
&& SkinFaceFireworkExplosions.FACE[5 * 8 + 5] == 0x4A9DFC,
|
||||
"Blue eyes are missing from the inner face");
|
||||
require(SkinFaceFireworkExplosions.FACE[4 * 8 + 3] == 0xFFF0E1,
|
||||
"Skin tone is missing from the baked face");
|
||||
require(SkinFaceFireworkExplosions.HOOD[2] == 0xCBCBCB,
|
||||
"Hood overlay is missing from the top of the face");
|
||||
require(SkinFaceFireworkExplosions.HOOD[5 * 8 + 2] == 0x181818,
|
||||
"Hood no longer covers the left eye");
|
||||
require(SkinFaceFireworkExplosions.HOOD[5 * 8 + 5] == SkinFaceFireworkExplosions.TRANSPARENT,
|
||||
"Hood opening no longer leaves the right eye visible");
|
||||
require(SkinFaceFireworkExplosions.opaqueHoodPixels() == 43,
|
||||
"Opaque hood pixel count drifted");
|
||||
Fireworks fireworks = SkinFaceFireworkExplosions.defaultFireworks();
|
||||
require(fireworks.flightDuration() == 3 && fireworks.explosions().isEmpty(),
|
||||
"Default firework burst contract changed");
|
||||
require(SkinFaceFireworkExplosions.LIFETIME_TRIM_TICKS == 5
|
||||
&& SkinFaceFireworkExplosions.midFlightLifetime(45) == 40,
|
||||
"Mid-flight lifetime trim drifted");
|
||||
int[] portrait = SkinFaceFireworkExplosions.portrait();
|
||||
require(portrait.length == 80 && SkinFaceFireworkExplosions.portraitHeight() == 10,
|
||||
"Portrait grid drifted");
|
||||
require(portrait[16 + 5 * 8 + 5] == 0x4A9DFC,
|
||||
"Right eye is missing from the composited portrait");
|
||||
require(portrait[2] == 0xD8D8D8,
|
||||
"Hood top is missing from the portrait");
|
||||
require(SkinFaceFireworkClient.PIXEL == 1.85D, "Portrait scale drifted");
|
||||
require(SkinFaceFireworkClient.LARGE_PIXEL == 2.7D, "Large portrait scale drifted");
|
||||
require(SkinFaceFireworkClient.burstPixel(false) == SkinFaceFireworkClient.PIXEL
|
||||
&& SkinFaceFireworkClient.burstSparkSize(false) == SkinFaceFireworkClient.SPARK_SIZE,
|
||||
"Normal burst scale drifted");
|
||||
require(SkinFaceFireworkClient.burstPixel(true) == SkinFaceFireworkClient.LARGE_PIXEL
|
||||
&& SkinFaceFireworkClient.burstSparkSize(true) == SkinFaceFireworkClient.LARGE_SPARK_SIZE,
|
||||
"Large burst scale drifted");
|
||||
require(SkinFaceFireworkClient.SPARK_LIFETIME_TICKS == 38, "Spark lifetime drifted");
|
||||
System.out.println("Only Fun Nourra firework: 8×8 face with hood overlay ready.");
|
||||
}
|
||||
|
||||
private static void require(boolean condition, String message) {
|
||||
if (!condition) throw new IllegalStateException(message);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
package fr.koka99cab.sanctuary26.onlyfun.loot;
|
||||
|
||||
import fr.koka99cab.sanctuary26.onlyfun.item.SkinFaceFireworkSmoke;
|
||||
import fr.koka99cab.sanctuary26.onlyfun.shop.OnlyFunShopCatalogSmoke;
|
||||
import fr.koka99cab.sanctuary26.onlyfun.world.CobbleDungeonPlacementSmoke;
|
||||
import java.util.EnumMap;
|
||||
@@ -18,6 +19,7 @@ public final class OnlyFunGeneratorSmoke {
|
||||
Bootstrap.bootStrap();
|
||||
OnlyFunShopCatalogSmoke.verify();
|
||||
CobbleDungeonPlacementSmoke.verify();
|
||||
SkinFaceFireworkSmoke.verify();
|
||||
OnlyFunGenerator.initialize();
|
||||
require(OnlyFunGenerator.ANOMALY_DENOMINATOR == 256, "Anomaly odds changed from 1/256");
|
||||
require(OnlyFunGenerator.PITY_START == 24, "Pity threshold changed");
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
{
|
||||
"schema_version": 1,
|
||||
"status": "released",
|
||||
"source": {
|
||||
"pack_version": "26.2.0-alpha.229",
|
||||
"modules": {
|
||||
"sanctuary": "0.0.0-alpha.132",
|
||||
"redstoner": "0.0.0-alpha.16"
|
||||
}
|
||||
},
|
||||
"target": {
|
||||
"pack_version": "26.2.0-alpha.230",
|
||||
"modules": {
|
||||
"sanctuary": "0.0.0-alpha.136",
|
||||
"redstoner": "0.0.0-alpha.17"
|
||||
}
|
||||
},
|
||||
"sanctuary_polish": {
|
||||
"quest_completion_attribution": true,
|
||||
"backroom_exit_beds_on_every_level": true,
|
||||
"persistent_public_rankings": true,
|
||||
"redstoner_trophy_panel": true,
|
||||
"white_cape_shield_pattern_projection": true,
|
||||
"adult_chicken_wolf_armor": true
|
||||
},
|
||||
"compatibility": {
|
||||
"existing_saves_supported": true,
|
||||
"new_world_required": false,
|
||||
"save_reset_required": false,
|
||||
"persistent_data_formats_changed": true,
|
||||
"network_protocols_changed": false,
|
||||
"sanctuary_data_version": 15,
|
||||
"sanctuary_network_protocol": 24,
|
||||
"redstoner_data_version": 4,
|
||||
"redstoner_network_protocol": 7,
|
||||
"client_server_update_required": true
|
||||
},
|
||||
"distribution": {
|
||||
"pack_release_updated": true,
|
||||
"packwiz_index_refreshed": true,
|
||||
"prism_modrinth_index_updated": true,
|
||||
"shared_client_server_mrpack": true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
{
|
||||
"schema_version": 1,
|
||||
"status": "released",
|
||||
"source": {
|
||||
"pack_version": "26.2.0-alpha.230",
|
||||
"modules": {
|
||||
"sanctuary": "0.0.0-alpha.136",
|
||||
"redstoner": "0.0.0-alpha.17"
|
||||
}
|
||||
},
|
||||
"target": {
|
||||
"pack_version": "26.2.0-alpha.231",
|
||||
"modules": {
|
||||
"sanctuary": "0.0.0-alpha.137",
|
||||
"redstoner": "0.0.0-alpha.18"
|
||||
}
|
||||
},
|
||||
"backroom_landmarks": {
|
||||
"level_count": 5,
|
||||
"loot_chest_cell_size_chunks": 3,
|
||||
"exit_bed_source_cell_size_chunks": 10,
|
||||
"exit_bed_target_cell_size_chunks": 3,
|
||||
"books_added_to_loot": false,
|
||||
"new_chunks_only": true
|
||||
},
|
||||
"trophy_panel": {
|
||||
"wall_plate_model": "oak",
|
||||
"readable_line_count": 4,
|
||||
"page_title_and_three_podium_rows": true,
|
||||
"right_click_next_page": true,
|
||||
"sneak_right_click_previous_page": true
|
||||
},
|
||||
"compatibility": {
|
||||
"existing_saves_supported": true,
|
||||
"new_world_required": false,
|
||||
"save_reset_required": false,
|
||||
"persistent_data_formats_changed": false,
|
||||
"network_protocols_changed": false,
|
||||
"sanctuary_data_version": 15,
|
||||
"sanctuary_network_protocol": 24,
|
||||
"redstoner_data_version": 4,
|
||||
"redstoner_network_protocol": 7,
|
||||
"client_server_update_required": true
|
||||
},
|
||||
"distribution": {
|
||||
"pack_release_updated": true,
|
||||
"packwiz_index_refreshed": true,
|
||||
"prism_modrinth_index_updated": true,
|
||||
"shared_client_server_mrpack": true,
|
||||
"jei": {
|
||||
"source": "30.29.0.199",
|
||||
"target": "30.29.0.201",
|
||||
"packwiz_and_prism_synchronized": true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
{
|
||||
"schema_version": 1,
|
||||
"status": "released",
|
||||
"source": {
|
||||
"pack_version": "26.2.0-alpha.231",
|
||||
"modules": {
|
||||
"anotherworld": "0.0.0-alpha.57",
|
||||
"sanctuary": "0.0.0-alpha.137",
|
||||
"redstoner": "0.0.0-alpha.18"
|
||||
}
|
||||
},
|
||||
"target": {
|
||||
"pack_version": "26.2.0-alpha.232",
|
||||
"modules": {
|
||||
"anotherworld": "0.0.0-alpha.58",
|
||||
"sanctuary": "0.0.0-alpha.138",
|
||||
"redstoner": "0.0.0-alpha.19"
|
||||
}
|
||||
},
|
||||
"fixed_issues": [146],
|
||||
"canon_photo_screenshots": {
|
||||
"directory": "screenshots",
|
||||
"viewfinder_crop_only": true,
|
||||
"film_filter_preserved": true,
|
||||
"full_crop_resolution": true,
|
||||
"asynchronous_png_write": true,
|
||||
"collision_safe_names": true
|
||||
},
|
||||
"alpha231_follow_up": {
|
||||
"migration": "alpha231-follow-up-cape-terminal-trophy.json",
|
||||
"banner_cape_fixed": true,
|
||||
"terminal_inventory_key_fixed": true,
|
||||
"trophy_panel_visible_lines": 8,
|
||||
"trophy_panel_entries_per_view": 7,
|
||||
"spawn_egg_kill_ranking": true
|
||||
},
|
||||
"compatibility": {
|
||||
"existing_saves_supported": true,
|
||||
"new_world_required": false,
|
||||
"save_reset_required": false,
|
||||
"persistent_data_formats_changed": false,
|
||||
"network_protocols_changed": true,
|
||||
"anotherworld_data_version": 5,
|
||||
"anotherworld_network_protocol": 2,
|
||||
"sanctuary_data_version": 15,
|
||||
"sanctuary_network_protocol": 24,
|
||||
"redstoner_data_version": 4,
|
||||
"redstoner_source_network_protocol": 7,
|
||||
"redstoner_target_network_protocol": 8,
|
||||
"client_server_update_required": true
|
||||
},
|
||||
"distribution": {
|
||||
"pack_release_updated": true,
|
||||
"packwiz_index_refreshed": true,
|
||||
"prism_modrinth_index_updated": true,
|
||||
"shared_client_server_mrpack": true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
{
|
||||
"schema_version": 1,
|
||||
"status": "released",
|
||||
"released_in": "26.2.0-alpha.234",
|
||||
"temporary_until": "2026-09-07",
|
||||
"pack": {
|
||||
"source_version": "26.2.0-alpha.233",
|
||||
"target_version": "26.2.0-alpha.234"
|
||||
},
|
||||
"modules": {
|
||||
"zevent": {
|
||||
"source_module_version": null,
|
||||
"target_module_version": "0.0.0-alpha.2",
|
||||
"data_version": 1,
|
||||
"network_protocol": 1,
|
||||
"client_and_server_required": true
|
||||
}
|
||||
},
|
||||
"screen": {
|
||||
"block_id": "zevent:zevent_screen",
|
||||
"width_blocks": 9,
|
||||
"height_blocks": 5,
|
||||
"server_authoritative_channel": true,
|
||||
"maximum_channel_input_length": 128,
|
||||
"maximum_channel_name_length": 25,
|
||||
"maximum_visible_browsers_per_client": 2,
|
||||
"audible_distance_blocks": 64
|
||||
},
|
||||
"dependencies": {
|
||||
"mcef_modern": {
|
||||
"version": "0.3.3+mc26.2.jcef146.0.10",
|
||||
"modrinth_project_id": "YJFfu5pH",
|
||||
"modrinth_version_id": "FOXbUPNd",
|
||||
"environment": "client_only"
|
||||
}
|
||||
},
|
||||
"compatibility": {
|
||||
"existing_saves_supported": true,
|
||||
"new_world_required": false,
|
||||
"save_reset_required": false,
|
||||
"existing_module_data_versions_changed": false,
|
||||
"existing_module_network_protocols_changed": false,
|
||||
"zevent_client_server_update_required": true
|
||||
},
|
||||
"distribution": {
|
||||
"packwiz_index_refreshed": true,
|
||||
"prism_modrinth_index_updated": true,
|
||||
"shared_client_server_mrpack": true,
|
||||
"mcef_client_only": true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
{
|
||||
"schema_version": 1,
|
||||
"status": "released",
|
||||
"released_in": "26.2.0-alpha.235",
|
||||
"temporary_until": "2026-09-07",
|
||||
"pack": {
|
||||
"source_version": "26.2.0-alpha.234",
|
||||
"target_version": "26.2.0-alpha.235"
|
||||
},
|
||||
"modules": {
|
||||
"zevent": {
|
||||
"source_module_version": "0.0.0-alpha.2",
|
||||
"target_module_version": "0.0.0-alpha.3",
|
||||
"data_version": 1,
|
||||
"network_protocol": 1,
|
||||
"client_and_server_required": true
|
||||
}
|
||||
},
|
||||
"playback": {
|
||||
"mcef_removed": true,
|
||||
"native_hls_decoder": true,
|
||||
"h264_video": true,
|
||||
"aac_audio": true,
|
||||
"maximum_quality_height": 720,
|
||||
"automatic_retry_seconds": 30,
|
||||
"offline_feedback": true
|
||||
},
|
||||
"dependencies": {
|
||||
"dream_displays": {
|
||||
"version": "1.9.6",
|
||||
"modrinth_project_id": "4IIKQyBu",
|
||||
"modrinth_version_id": "GxyW2ksW",
|
||||
"environment": "client_and_server"
|
||||
}
|
||||
},
|
||||
"compatibility": {
|
||||
"existing_saves_supported": true,
|
||||
"new_world_required": false,
|
||||
"save_reset_required": false,
|
||||
"data_version_changed": false,
|
||||
"network_protocol_changed": false,
|
||||
"zevent_client_server_update_required": true
|
||||
},
|
||||
"distribution": {
|
||||
"packwiz_index_refreshed": true,
|
||||
"prism_modrinth_index_updated": true,
|
||||
"shared_client_server_mrpack": true,
|
||||
"mcef_removed": true,
|
||||
"dream_displays_required": true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
{
|
||||
"schema_version": 1,
|
||||
"status": "released",
|
||||
"released_in": "26.2.0-alpha.236",
|
||||
"pack": {
|
||||
"source_version": "26.2.0-alpha.235",
|
||||
"target_version": "26.2.0-alpha.236"
|
||||
},
|
||||
"removed_module": {
|
||||
"id": "zevent",
|
||||
"source_module_version": "0.0.0-alpha.3",
|
||||
"target_module_version": null,
|
||||
"source_directory_removed": true,
|
||||
"client_jar_removed": true,
|
||||
"server_jar_removed": true,
|
||||
"registry_ids_removed": [
|
||||
"zevent:zevent_screen"
|
||||
]
|
||||
},
|
||||
"replacement": {
|
||||
"module": "dreamdisplays",
|
||||
"version": "1.9.6",
|
||||
"modrinth_project_id": "4IIKQyBu",
|
||||
"modrinth_version_id": "GxyW2ksW",
|
||||
"client_and_server_required": true,
|
||||
"automatic_screen_conversion": false
|
||||
},
|
||||
"compatibility": {
|
||||
"world_reset_required": false,
|
||||
"world_backup_recommended": true,
|
||||
"existing_zevent_screens_must_be_replaced": true,
|
||||
"remaining_module_data_preserved": true,
|
||||
"remaining_network_protocols_changed": false,
|
||||
"client_and_server_must_remove_zevent": true
|
||||
},
|
||||
"distribution": {
|
||||
"packwiz_index_refreshed": true,
|
||||
"prism_modrinth_index_updated": true,
|
||||
"zevent_jar_absent": true,
|
||||
"dream_displays_required": true,
|
||||
"distributed_module_count": 10
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"schema_version": 1,
|
||||
"status": "released",
|
||||
"released_in": "26.2.0-alpha.237",
|
||||
"pack": {
|
||||
"source_version": "26.2.0-alpha.236",
|
||||
"target_version": "26.2.0-alpha.237"
|
||||
},
|
||||
"dependency": {
|
||||
"id": "dreamdisplays",
|
||||
"source_version": "1.9.6",
|
||||
"target_version": "1.10.0-dev",
|
||||
"target_filename": "dreamdisplays-fabric-26.2-audio-buffer-test.jar",
|
||||
"sha256": "cea9efcea0401ebb44a471f43539c582ffae02fbd9b36d393c7aacdfc198ec74",
|
||||
"client_and_server_required": true,
|
||||
"pinned_test_build": true,
|
||||
"audio_buffer_fix": true
|
||||
},
|
||||
"compatibility": {
|
||||
"world_reset_required": false,
|
||||
"save_migration_required": false,
|
||||
"sanctuary_module_versions_changed": false,
|
||||
"network_protocols_changed": false,
|
||||
"client_and_server_must_update": true
|
||||
},
|
||||
"distribution": {
|
||||
"packwiz_index_refreshed": true,
|
||||
"prism_modrinth_index_updated": true,
|
||||
"old_dreamdisplays_jar_removed": true,
|
||||
"zevent_jar_absent": true,
|
||||
"distributed_module_count": 10
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"schema_version": 1,
|
||||
"status": "released",
|
||||
"released_in": "26.2.0-alpha.238",
|
||||
"pack": {
|
||||
"source_version": "26.2.0-alpha.237",
|
||||
"target_version": "26.2.0-alpha.238"
|
||||
},
|
||||
"dependency": {
|
||||
"id": "dreamdisplays",
|
||||
"source_version": "1.10.0-dev",
|
||||
"target_version": "1.10.0-dev",
|
||||
"source_filename": "dreamdisplays-fabric-26.2-audio-buffer-test.jar",
|
||||
"target_filename": "dreamdisplays-fabric-26.2-audio-buffer-test-2.jar",
|
||||
"sha256": "fae5681de3209dba98d6359fb878e7cf230cde08e96c08fb71394aed35823188",
|
||||
"client_and_server_required": true,
|
||||
"pinned_test_build": true,
|
||||
"audio_buffer_fix": true
|
||||
},
|
||||
"compatibility": {
|
||||
"world_reset_required": false,
|
||||
"save_migration_required": false,
|
||||
"sanctuary_module_versions_changed": false,
|
||||
"network_protocols_changed": false,
|
||||
"client_and_server_must_update": true
|
||||
},
|
||||
"distribution": {
|
||||
"packwiz_index_refreshed": true,
|
||||
"prism_modrinth_index_updated": true,
|
||||
"old_dreamdisplays_jar_removed": true,
|
||||
"zevent_jar_absent": true,
|
||||
"distributed_module_count": 10
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
{
|
||||
"schema_version": 1,
|
||||
"status": "released",
|
||||
"released_in": "26.2.0-alpha.239",
|
||||
"pack": {
|
||||
"source_version": "26.2.0-alpha.238",
|
||||
"target_version": "26.2.0-alpha.239"
|
||||
},
|
||||
"removed_dependencies": [
|
||||
{
|
||||
"id": "dreamdisplays",
|
||||
"version": "1.10.0-dev",
|
||||
"filename": "dreamdisplays-fabric-26.2-audio-buffer-test-2.jar"
|
||||
}
|
||||
],
|
||||
"added_dependencies": [
|
||||
{
|
||||
"id": "mydisplay",
|
||||
"version": "1.0.0",
|
||||
"filename": "mydisplay-1.0.0-sanctuary.jar",
|
||||
"sha256": "22cd63386bc7ba6961f586949f65b4a2a6cb335d75aa155bcdf0a633f1f9d907",
|
||||
"side": "both",
|
||||
"manifest_client_dependencies_optional": true,
|
||||
"languages": ["en_us", "fr_fr", "ru_ru"]
|
||||
},
|
||||
{
|
||||
"id": "watermedia",
|
||||
"version": "3.0.0.23",
|
||||
"filename": "watermedia-3.0.0.23.jar",
|
||||
"sha256": "c3baefc8fbb005e349b3424f7df8f9d89b18524d5f7a358c6df73ca5e1d98b38",
|
||||
"side": "client"
|
||||
},
|
||||
{
|
||||
"id": "watermedia_binaries",
|
||||
"version": "3.0.0.6",
|
||||
"filename": "watermedia-binaries-3.0.0.6.jar",
|
||||
"sha256": "2d9333061c3d2330033fe68837a31f546dbf58984455b0cfec0deeff6b1f121e",
|
||||
"side": "client"
|
||||
}
|
||||
],
|
||||
"compatibility": {
|
||||
"world_reset_required": false,
|
||||
"save_migration_required": false,
|
||||
"sanctuary_module_versions_changed": false,
|
||||
"network_protocols_changed": false,
|
||||
"client_and_server_must_update": true,
|
||||
"existing_dreamdisplays_blocks_converted": false
|
||||
},
|
||||
"distribution": {
|
||||
"packwiz_index_refreshed": true,
|
||||
"prism_modrinth_index_updated": true,
|
||||
"old_dreamdisplays_jar_removed": true,
|
||||
"watermedia_client_only": true,
|
||||
"zevent_jar_absent": true,
|
||||
"distributed_module_count": 10
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"schema_version": 1,
|
||||
"status": "released",
|
||||
"released_in": "26.2.0-alpha.240",
|
||||
"pack": {
|
||||
"source_version": "26.2.0-alpha.239",
|
||||
"target_version": "26.2.0-alpha.240"
|
||||
},
|
||||
"dependency": {
|
||||
"id": "mydisplay",
|
||||
"version": "1.0.0",
|
||||
"source_filename": "mydisplay-1.0.0-sanctuary.jar",
|
||||
"target_filename": "mydisplay-1.0.0-sanctuary-2.jar",
|
||||
"source_sha256": "22cd63386bc7ba6961f586949f65b4a2a6cb335d75aa155bcdf0a633f1f9d907",
|
||||
"target_sha256": "9ff1645731dc2dfdc248e3c07ac923aba9dfe001a1580a981767f948d4f7427f",
|
||||
"side": "both",
|
||||
"manifest_client_dependencies_optional": true,
|
||||
"languages": ["en_us", "fr_fr", "ru_ru"],
|
||||
"screen_picker_added": true
|
||||
},
|
||||
"compatibility": {
|
||||
"world_reset_required": false,
|
||||
"save_migration_required": false,
|
||||
"sanctuary_module_versions_changed": false,
|
||||
"network_protocols_changed": false,
|
||||
"client_and_server_must_update": true,
|
||||
"existing_mydisplay_blocks_compatible": true
|
||||
},
|
||||
"distribution": {
|
||||
"packwiz_index_refreshed": true,
|
||||
"prism_modrinth_index_updated": true,
|
||||
"old_mydisplay_jar_removed": true,
|
||||
"direct_download_dependencies_skipped_by_modrinth_updater": true,
|
||||
"watermedia_client_only": true,
|
||||
"zevent_jar_absent": true,
|
||||
"distributed_module_count": 10
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
{
|
||||
"schema_version": 1,
|
||||
"status": "released",
|
||||
"released_in": "26.2.0-alpha.241",
|
||||
"pack": {
|
||||
"source_version": "26.2.0-alpha.240",
|
||||
"target_version": "26.2.0-alpha.241"
|
||||
},
|
||||
"dependency": {
|
||||
"id": "mydisplay",
|
||||
"version": "1.0.0",
|
||||
"source_filename": "mydisplay-1.0.0-sanctuary-2.jar",
|
||||
"target_filename": "mydisplay-1.0.0-sanctuary-3.jar",
|
||||
"source_sha256": "9ff1645731dc2dfdc248e3c07ac923aba9dfe001a1580a981767f948d4f7427f",
|
||||
"provided_sha256": "45a7dc0f9852ef776bce1ee81f1683b6333e773299b7e5eb96363cf885c8c6ef",
|
||||
"target_sha256": "97a8b7fc79bb68d661e6d0563c54b90179e6a34339c22a433f858cdff6862cbc",
|
||||
"side": "both",
|
||||
"manifest_client_dependencies_optional": true,
|
||||
"languages": ["en_us", "fr_fr", "ru_ru"]
|
||||
},
|
||||
"optimizations": {
|
||||
"argb_to_rgba_int_buffer": true,
|
||||
"distance_based_decode_lod": true,
|
||||
"distant_live_quality_cap_after_five": true,
|
||||
"proximity_prioritized_upload_budget": true,
|
||||
"last_frame_kept_when_deferred": true,
|
||||
"offscreen_cpu_upload_paused": true,
|
||||
"draw_distance_blocks": 128,
|
||||
"single_wall_render_pass": true,
|
||||
"shared_streamlink_by_channel_and_quality": true,
|
||||
"staggered_player_start": true,
|
||||
"muted_audio_decoder_disabled": true,
|
||||
"screenshot_render_buffer_reset": true,
|
||||
"screenshot_upload_pause": true
|
||||
},
|
||||
"compatibility": {
|
||||
"world_reset_required": false,
|
||||
"save_migration_required": false,
|
||||
"sanctuary_module_versions_changed": false,
|
||||
"network_protocols_changed": false,
|
||||
"client_and_server_must_update": true,
|
||||
"existing_mydisplay_blocks_compatible": true,
|
||||
"all_screens_remain_visible": true
|
||||
},
|
||||
"distribution": {
|
||||
"packwiz_index_refreshed": true,
|
||||
"prism_modrinth_index_updated": true,
|
||||
"old_mydisplay_jar_removed": true,
|
||||
"watermedia_client_only": true,
|
||||
"zevent_jar_absent": true,
|
||||
"distributed_module_count": 10
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
{
|
||||
"schema_version": 1,
|
||||
"status": "released",
|
||||
"released_in": "26.2.0-alpha.242",
|
||||
"pack": {
|
||||
"source_version": "26.2.0-alpha.241",
|
||||
"target_version": "26.2.0-alpha.242"
|
||||
},
|
||||
"dependency": {
|
||||
"id": "mydisplay",
|
||||
"version": "1.0.0",
|
||||
"source_filename": "mydisplay-1.0.0-sanctuary-3.jar",
|
||||
"target_filename": "mydisplay-1.0.0-sanctuary-4.jar",
|
||||
"source_sha256": "97a8b7fc79bb68d661e6d0563c54b90179e6a34339c22a433f858cdff6862cbc",
|
||||
"provided_sha256": "4c1bf4aa7140e613ff520b04263cb4733561c2e04b39a21b3cb7783f610119b9",
|
||||
"target_sha256": "6c3bb4e7f562c6730a6cfa23a162a1fa0641aed7105cb6b809bb5a097aa2e559",
|
||||
"side": "both",
|
||||
"manifest_client_dependencies_optional": true,
|
||||
"languages": ["en_us", "fr_fr", "ru_ru"],
|
||||
"changed_classes": [
|
||||
"com.mydisplay.client.media.DisplayPerf",
|
||||
"com.mydisplay.client.media.DisplaySession",
|
||||
"com.mydisplay.client.media.WaterMediaBackend",
|
||||
"com.mydisplay.client.media.WaterMediaBackend$Playback"
|
||||
]
|
||||
},
|
||||
"compatibility": {
|
||||
"world_reset_required": false,
|
||||
"save_migration_required": false,
|
||||
"sanctuary_module_versions_changed": false,
|
||||
"network_protocols_changed": false,
|
||||
"client_and_server_must_update": true,
|
||||
"existing_mydisplay_blocks_compatible": true
|
||||
},
|
||||
"distribution": {
|
||||
"packwiz_index_refreshed": true,
|
||||
"prism_modrinth_index_updated": true,
|
||||
"old_mydisplay_jar_removed": true,
|
||||
"watermedia_client_only": true,
|
||||
"zevent_jar_absent": true,
|
||||
"distributed_module_count": 10
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"schema_version": 1,
|
||||
"status": "released",
|
||||
"released_in": "26.2.0-alpha.232",
|
||||
"modules": {
|
||||
"sanctuary": {
|
||||
"source_module_version": "0.0.0-alpha.137",
|
||||
"target_module_version": "0.0.0-alpha.138",
|
||||
"data_version": 15,
|
||||
"network_protocol": 24
|
||||
},
|
||||
"redstoner": {
|
||||
"source_module_version": "0.0.0-alpha.18",
|
||||
"target_module_version": "0.0.0-alpha.19",
|
||||
"data_version": 4,
|
||||
"source_network_protocol": 7,
|
||||
"target_network_protocol": 8
|
||||
}
|
||||
},
|
||||
"fixes": {
|
||||
"banner_cape_flat_shield_uv_faces": true,
|
||||
"banner_cape_double_sided_culling": true,
|
||||
"terminal_inventory_key_captured_while_searching": true,
|
||||
"trophy_panel_visible_lines": 8,
|
||||
"trophy_panel_entries_per_view": 7,
|
||||
"trophy_panel_names_then_values": true,
|
||||
"spawn_egg_maps_to_entity_killed_statistic": true
|
||||
},
|
||||
"compatibility": {
|
||||
"existing_saves_supported": true,
|
||||
"new_world_required": false,
|
||||
"save_reset_required": false,
|
||||
"persistent_data_formats_changed": false,
|
||||
"network_protocols_changed": true,
|
||||
"client_server_update_required": true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
{
|
||||
"schema_version": 1,
|
||||
"status": "released",
|
||||
"released_in": "26.2.0-alpha.233",
|
||||
"pack": {
|
||||
"source_version": "26.2.0-alpha.232",
|
||||
"target_version": "26.2.0-alpha.233"
|
||||
},
|
||||
"modules": {
|
||||
"sanctuary": {
|
||||
"source_module_version": "0.0.0-alpha.138",
|
||||
"target_module_version": "0.0.0-alpha.139",
|
||||
"data_version": 15,
|
||||
"network_protocol": 24
|
||||
},
|
||||
"redstoner": {
|
||||
"source_module_version": "0.0.0-alpha.19",
|
||||
"target_module_version": "0.0.0-alpha.20",
|
||||
"source_data_version": 4,
|
||||
"target_data_version": 5,
|
||||
"source_network_protocol": 8,
|
||||
"target_network_protocol": 9
|
||||
}
|
||||
},
|
||||
"trophy_sign": {
|
||||
"owner": "redstoner",
|
||||
"item_id_preserved": "redstoner:trophy_panel",
|
||||
"standing_block_id": "redstoner:trophy_sign",
|
||||
"wall_block_id": "redstoner:trophy_wall_sign",
|
||||
"legacy_block_id": "redstoner:trophy_panel",
|
||||
"legacy_blocks_migrate_on_load": true,
|
||||
"vanilla_sign_block_entity": true,
|
||||
"vanilla_wood_type": "pale_oak",
|
||||
"save_key": "redstoner:trophy_signs",
|
||||
"save_data_version": 1,
|
||||
"maximum_signs": 16384,
|
||||
"maximum_pages_per_sign": 16,
|
||||
"editor_opens_on_first_placement": true,
|
||||
"owner_or_operator_configuration": true,
|
||||
"owner_or_operator_breaking": true,
|
||||
"public_right_click_page_cycle": true,
|
||||
"display_modes": ["names_and_values", "names", "values"],
|
||||
"server_revalidates_distance_owner_block_and_metrics": true
|
||||
},
|
||||
"ranking_api": {
|
||||
"client_catalog_built_from_synchronized_registries": true,
|
||||
"catalog_sent_over_network": false,
|
||||
"cached_player_names_repair_uuid_prefix_placeholders": true,
|
||||
"uuid_prefixes_never_used_as_display_fallback": true
|
||||
},
|
||||
"compatibility": {
|
||||
"existing_saves_supported": true,
|
||||
"existing_item_stacks_supported": true,
|
||||
"new_world_required": false,
|
||||
"save_reset_required": false,
|
||||
"monotone_data_migration": true,
|
||||
"client_server_update_required": true
|
||||
},
|
||||
"distribution": {
|
||||
"packwiz_index_refreshed": true,
|
||||
"prism_modrinth_index_updated": true,
|
||||
"shared_client_server_mrpack": true
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,7 @@ Cette commande :
|
||||
|
||||
1. reconstruit les dix modules Sanctuary ;
|
||||
2. remplace leurs anciens JAR dans ce pack ;
|
||||
3. sélectionne la dernière version Modrinth marquée `release` de Fabric API, Simple Voice Chat, Sodium et Iris, ou `beta` pour JEI qui ne publie pas de release 26.2 ; aucune alpha n'est acceptée ;
|
||||
3. sélectionne la dernière version Modrinth marquée `release` de Fabric API, Simple Voice Chat, Sodium et Iris, ou `beta` pour JEI qui ne publie pas de release 26.2 ; aucune alpha n'est acceptée et les dépendances privées en téléchargement direct restent épinglées sans interrogation Modrinth ;
|
||||
4. rafraîchit `index.toml` ;
|
||||
5. exporte `build/distributions/sanctuary-<version>-packwiz.mrpack` ;
|
||||
6. produit `build/distributions/sanctuary-<version>-packwiz-site.zip`, à extraire sur un hébergement HTTPS statique.
|
||||
|
||||
+29
-14
@@ -14,15 +14,15 @@ hash = "41f4f4c88c71d0a23446d5014f7407d55ad477441847099cad24492b032335a0"
|
||||
|
||||
[[files]]
|
||||
file = "mods/ambiance-0.0.0-alpha.22.jar"
|
||||
hash = "b4ba14b9ada5b4c1d97b039ee632a158ae266eba2faefee690e710cc5f0f993e"
|
||||
hash = "960322cd387c47588f6fd76202c0abf2c9993cf0effa82e19f8bb31d86ab07a4"
|
||||
|
||||
[[files]]
|
||||
file = "mods/anotherworld-0.0.0-alpha.57.jar"
|
||||
hash = "4c8a3315ba9db6799fb0f046b45d702d56fba65173abccbd81ab995f56690407"
|
||||
file = "mods/anotherworld-0.0.0-alpha.58.jar"
|
||||
hash = "8936b0a5bf3f122afc752cba5e8d96ac92cfb1458785c3fac13d25f51cb4814c"
|
||||
|
||||
[[files]]
|
||||
file = "mods/events-0.0.0-alpha.0.jar"
|
||||
hash = "e7304da48379eb39737b126431316644250ecbc5b6ec8a439cc12e96a9477fa4"
|
||||
hash = "1fe3fc2f6ca371f8dde44e0bb535b1e70df0e9c4de3f56002f9f3ec6d5826745"
|
||||
|
||||
[[files]]
|
||||
file = "mods/fabric-api.pw.toml"
|
||||
@@ -31,7 +31,7 @@ metafile = true
|
||||
|
||||
[[files]]
|
||||
file = "mods/iliketomoveit-0.0.0-alpha.18.jar"
|
||||
hash = "bd0467d186737b59996a8b1589a7c1837c990890f1b21154aaca614fb042c556"
|
||||
hash = "a964e17e139757f948f67aeae73730c97af2c5932bffcc4d366d084c7cdf53ee"
|
||||
|
||||
[[files]]
|
||||
file = "mods/iris.pw.toml"
|
||||
@@ -40,32 +40,37 @@ metafile = true
|
||||
|
||||
[[files]]
|
||||
file = "mods/itsalive-0.0.0-alpha.38.jar"
|
||||
hash = "dd1869166cfeeb36b19b7e243e7ece32c925d362aa80782076d493decc6e2871"
|
||||
hash = "97a29e1d4875265854d0b4757c6e0c286a35e626df1b8d13b465b85502381309"
|
||||
|
||||
[[files]]
|
||||
file = "mods/jei.pw.toml"
|
||||
hash = "9cc4a91f57effc48b94f6acc1f43067bb8eb99fd1df3397612c55761de9ec2c2"
|
||||
hash = "80d0fa55c1bbc0659e792b047c2bb64c446c2bf92557d9d74777333a8a68a318"
|
||||
metafile = true
|
||||
|
||||
[[files]]
|
||||
file = "mods/masterkey-0.0.0-alpha.9.jar"
|
||||
hash = "46ef9947e82368fbe60d696174b3a3c99960be29432f3ccd7daf2714927b5c12"
|
||||
hash = "95265c4aa4ebb89d5e89e3245917f47a4044db192f88995cfee2bdf63f348b79"
|
||||
|
||||
[[files]]
|
||||
file = "mods/mydisplay.pw.toml"
|
||||
hash = "a5538de6d9c38cf37ff0882451fd025c8a24c7cb250482baa4ca5918d1129b06"
|
||||
metafile = true
|
||||
|
||||
[[files]]
|
||||
file = "mods/onlyfun-0.0.0-alpha.27.jar"
|
||||
hash = "d01271cbccef9c3c5f66e3d56748cd69d953f9eb53d21c8a1575523d40e77c56"
|
||||
hash = "dcd5003390c132f3421c12550f851846548b4d1cc541d3ada0deb35c65bb3f54"
|
||||
|
||||
[[files]]
|
||||
file = "mods/ouch-0.0.0-alpha.11.jar"
|
||||
hash = "ca612f8e660744af81d5a2612f78f8ad61dacf746a22474837e8b52a22bb3204"
|
||||
hash = "08f3355b0240c30a65314e8173f2268a1cf4b0a5ae4dac1766e49939e3e5a256"
|
||||
|
||||
[[files]]
|
||||
file = "mods/redstoner-0.0.0-alpha.16.jar"
|
||||
hash = "e273042c0f7b12ed0a13da3f75f41903a741a29399da353ecd5144e67dcb680e"
|
||||
file = "mods/redstoner-0.0.0-alpha.20.jar"
|
||||
hash = "39f0cba6213e4efcd40b8b72cf7812b56315e1d637803a87a2672b00bc4ee4a0"
|
||||
|
||||
[[files]]
|
||||
file = "mods/sanctuary-0.0.0-alpha.132.jar"
|
||||
hash = "72a946358be6039c706cf52563ffe61ec70477ffb4e7db80e71116758f5bd519"
|
||||
file = "mods/sanctuary-0.0.0-alpha.139.jar"
|
||||
hash = "22a37980bd7b121cbfe65e853a6c6a9f8549bbf4af9b4fdd5f22dbd04df273b0"
|
||||
|
||||
[[files]]
|
||||
file = "mods/simple-voice-chat.pw.toml"
|
||||
@@ -76,3 +81,13 @@ metafile = true
|
||||
file = "mods/sodium.pw.toml"
|
||||
hash = "f06c0b9150da9877b99cb265d037c067e509340d9bf347e02cef2043c110e963"
|
||||
metafile = true
|
||||
|
||||
[[files]]
|
||||
file = "mods/watermedia-binaries.pw.toml"
|
||||
hash = "1b7c4327062eb0f70d815a2461b0ded2a2495eed8fca0fd00d75467a4aa777da"
|
||||
metafile = true
|
||||
|
||||
[[files]]
|
||||
file = "mods/watermedia.pw.toml"
|
||||
hash = "c99e2e5e4d26ec3414e6af55f252091fd728a52f1a97c69a14cacdda7af15a88"
|
||||
metafile = true
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
name = "Just Enough Items (JEI)"
|
||||
filename = "jei-26.2-fabric-30.29.0.199.jar"
|
||||
filename = "jei-26.2-fabric-30.29.0.201.jar"
|
||||
side = "both"
|
||||
pin = true
|
||||
|
||||
[download]
|
||||
url = "https://cdn.modrinth.com/data/u6dRKJwZ/versions/astSvyl6/jei-26.2-fabric-30.29.0.199.jar"
|
||||
url = "https://cdn.modrinth.com/data/u6dRKJwZ/versions/d3HeviSL/jei-26.2-fabric-30.29.0.201.jar"
|
||||
hash-format = "sha512"
|
||||
hash = "2849ff6a4cc91822798f0c70e7749c298b911c1037fc74e9ab0881734b8d5cb6507fb3169b4a240ef6c37eb6f7c573794c46073ff352f160feee245233b2abe5"
|
||||
hash = "2279cb0ec4e41d9eebad80c2ecac76d19c8bc8e5aac69ba13f35b77494625c70d66edc74d59959116012bfb2f464b55806dd69fbf6387f25679b1553953e329a"
|
||||
|
||||
[update]
|
||||
[update.modrinth]
|
||||
mod-id = "u6dRKJwZ"
|
||||
version = "astSvyl6"
|
||||
version = "d3HeviSL"
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
name = "MyDisplay"
|
||||
filename = "mydisplay-1.0.0-sanctuary-4.jar"
|
||||
side = "both"
|
||||
pin = true
|
||||
|
||||
[download]
|
||||
url = "https://download.botsu.cloud/sanctuary/artifacts/mydisplay-1.0.0-sanctuary-4.jar"
|
||||
hash-format = "sha256"
|
||||
hash = "6c3bb4e7f562c6730a6cfa23a162a1fa0641aed7105cb6b809bb5a097aa2e559"
|
||||
@@ -0,0 +1,9 @@
|
||||
name = "WaterMedia: Binaries"
|
||||
filename = "watermedia-binaries-3.0.0.6.jar"
|
||||
side = "client"
|
||||
pin = true
|
||||
|
||||
[download]
|
||||
url = "https://download.botsu.cloud/sanctuary/artifacts/watermedia-binaries-3.0.0.6.jar"
|
||||
hash-format = "sha256"
|
||||
hash = "2d9333061c3d2330033fe68837a31f546dbf58984455b0cfec0deeff6b1f121e"
|
||||
@@ -0,0 +1,9 @@
|
||||
name = "WaterMedia"
|
||||
filename = "watermedia-3.0.0.23.jar"
|
||||
side = "client"
|
||||
pin = true
|
||||
|
||||
[download]
|
||||
url = "https://download.botsu.cloud/sanctuary/artifacts/watermedia-3.0.0.23.jar"
|
||||
hash-format = "sha256"
|
||||
hash = "c3baefc8fbb005e349b3424f7df8f9d89b18524d5f7a358c6df73ca5e1d98b38"
|
||||
@@ -1,12 +1,12 @@
|
||||
name = "Sanctuary 26.2"
|
||||
author = "KOKA99CAB"
|
||||
version = "26.2.0-alpha.229"
|
||||
version = "26.2.0-alpha.242"
|
||||
pack-format = "packwiz:1.1.0"
|
||||
|
||||
[index]
|
||||
file = "index.toml"
|
||||
hash-format = "sha256"
|
||||
hash = "6a5b1811c2dd2b905a67cd817b8e377a8b68589431650775e1d8aabe04ff32e9"
|
||||
hash = "0a55cea4e39c4df93a2d52133295afdf55798e23516f1209cab56d50e5339051"
|
||||
|
||||
[versions]
|
||||
fabric = "0.19.3"
|
||||
|
||||
@@ -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.229",
|
||||
"versionId": "26.2.0-alpha.242",
|
||||
"files": [
|
||||
{
|
||||
"path": "mods/fabric-api-0.159.0+26.2.jar",
|
||||
@@ -21,19 +21,19 @@
|
||||
"fileSize": 2535793
|
||||
},
|
||||
{
|
||||
"path": "mods/jei-26.2-fabric-30.29.0.199.jar",
|
||||
"path": "mods/jei-26.2-fabric-30.29.0.201.jar",
|
||||
"hashes": {
|
||||
"sha1": "1d6ab9269be16f85fb13a34a36b34ad07a9b32a7",
|
||||
"sha512": "2849ff6a4cc91822798f0c70e7749c298b911c1037fc74e9ab0881734b8d5cb6507fb3169b4a240ef6c37eb6f7c573794c46073ff352f160feee245233b2abe5"
|
||||
"sha1": "e82447aff07e87a372cf817bc9c28b0d77b6f55c",
|
||||
"sha512": "2279cb0ec4e41d9eebad80c2ecac76d19c8bc8e5aac69ba13f35b77494625c70d66edc74d59959116012bfb2f464b55806dd69fbf6387f25679b1553953e329a"
|
||||
},
|
||||
"env": {
|
||||
"client": "required",
|
||||
"server": "required"
|
||||
},
|
||||
"downloads": [
|
||||
"https://cdn.modrinth.com/data/u6dRKJwZ/versions/astSvyl6/jei-26.2-fabric-30.29.0.199.jar"
|
||||
"https://cdn.modrinth.com/data/u6dRKJwZ/versions/d3HeviSL/jei-26.2-fabric-30.29.0.201.jar"
|
||||
],
|
||||
"fileSize": 1849188
|
||||
"fileSize": 1860103
|
||||
},
|
||||
{
|
||||
"path": "mods/voicechat-fabric-2.6.22+26.2.jar",
|
||||
@@ -79,6 +79,51 @@
|
||||
"https://cdn.modrinth.com/data/YL57xq9U/versions/oaD6KQls/iris-fabric-1.11.2%2Bmc26.2.jar"
|
||||
],
|
||||
"fileSize": 2820763
|
||||
},
|
||||
{
|
||||
"path": "mods/mydisplay-1.0.0-sanctuary-4.jar",
|
||||
"hashes": {
|
||||
"sha1": "2253da2b8e1b8d0111dc63a0d926022acef2c422",
|
||||
"sha512": "a8fcf35c6d465ef16140a5c699762939aa12e4fccae3891fbbdb38e4f14ba4cfd978193e2ef2c165ecc820927104c5e35b42fd65eeb802a6280dc2faca0e39bd"
|
||||
},
|
||||
"env": {
|
||||
"client": "required",
|
||||
"server": "required"
|
||||
},
|
||||
"downloads": [
|
||||
"https://download.botsu.cloud/sanctuary/artifacts/mydisplay-1.0.0-sanctuary-4.jar"
|
||||
],
|
||||
"fileSize": 112172
|
||||
},
|
||||
{
|
||||
"path": "mods/watermedia-3.0.0.23.jar",
|
||||
"hashes": {
|
||||
"sha1": "a644ab079cc9ed19a62b181e0252be5a9a422166",
|
||||
"sha512": "b2f9c31828285bf7cb039b91e8756b35c2a9e425d614d9c767bdcd752475a7abf4f025d6f6005d74f5c610c298fdadf01e53b29124d1d9fd03554fff71b1993b"
|
||||
},
|
||||
"env": {
|
||||
"client": "required",
|
||||
"server": "unsupported"
|
||||
},
|
||||
"downloads": [
|
||||
"https://download.botsu.cloud/sanctuary/artifacts/watermedia-3.0.0.23.jar"
|
||||
],
|
||||
"fileSize": 3625444
|
||||
},
|
||||
{
|
||||
"path": "mods/watermedia-binaries-3.0.0.6.jar",
|
||||
"hashes": {
|
||||
"sha1": "8f132fc2fec0fe91a9a085c11088f0773da187fc",
|
||||
"sha512": "45b5c1d645f75fb54de4d7e92b54f50372bd9d24c7543b57e6d32ba6b35ce346250bc871ba1a39c8b1988a423d2fca75a4806ae75af502d194455caabb470897"
|
||||
},
|
||||
"env": {
|
||||
"client": "required",
|
||||
"server": "unsupported"
|
||||
},
|
||||
"downloads": [
|
||||
"https://download.botsu.cloud/sanctuary/artifacts/watermedia-binaries-3.0.0.6.jar"
|
||||
],
|
||||
"fileSize": 143725808
|
||||
}
|
||||
],
|
||||
"dependencies": {
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"schema_version": 1,
|
||||
"pack_version": "26.2.0-alpha.229",
|
||||
"pack_version": "26.2.0-alpha.242",
|
||||
"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.229.mrpack",
|
||||
"primary_file": "sanctuary-26.2.0-alpha.242.mrpack",
|
||||
"shared_client_server_pack": true,
|
||||
"update_owner": "packwiz-installer",
|
||||
"check_on_launch": true,
|
||||
|
||||
@@ -58,6 +58,12 @@ def main() -> int:
|
||||
|
||||
for metafile in metafiles:
|
||||
text = metafile.read_text(encoding="utf-8")
|
||||
if "[update.modrinth]" not in text:
|
||||
label = field(text, "name")
|
||||
if not re.search(r"^\s*pin\s*=\s*true\s*$", text, re.MULTILINE):
|
||||
raise RuntimeError(f"Direct-download dependency {label!r} must stay pinned")
|
||||
print(f"{label}: pinned direct download, skipped", flush=True)
|
||||
continue
|
||||
project_id = field(text, "mod-id")
|
||||
current_version = field(text, "version")
|
||||
release, channel = latest_supported(project_id)
|
||||
|
||||
@@ -4,6 +4,7 @@ import fr.koka99cab.sanctuary26.redstoner.api.RedStonerApi;
|
||||
import fr.koka99cab.sanctuary26.redstoner.network.RedStonerNetworking;
|
||||
import fr.koka99cab.sanctuary26.redstoner.registry.RedStonerRegistries;
|
||||
import fr.koka99cab.sanctuary26.redstoner.registry.RedStonerMenus;
|
||||
import fr.koka99cab.sanctuary26.redstoner.trophy.TrophySignService;
|
||||
import fr.koka99cab.sanctuary26.sanctuary.api.server.SanctuaryServerIdentity;
|
||||
import net.fabricmc.api.ModInitializer;
|
||||
import net.minecraft.resources.Identifier;
|
||||
@@ -13,7 +14,7 @@ import org.slf4j.LoggerFactory;
|
||||
public final class RedStonerMod implements ModInitializer {
|
||||
public static final String MOD_ID = "redstoner";
|
||||
public static final String DISPLAY_NAME = "Red-Stoner";
|
||||
public static final int CURRENT_DATA_VERSION = 4;
|
||||
public static final int CURRENT_DATA_VERSION = 5;
|
||||
public static final Logger LOGGER = LoggerFactory.getLogger(MOD_ID);
|
||||
|
||||
public RedStonerMod() {
|
||||
@@ -27,6 +28,7 @@ public final class RedStonerMod implements ModInitializer {
|
||||
public void onInitialize() {
|
||||
RedStonerApi.registerServerIdentityProvider(SanctuaryServerIdentity::get);
|
||||
RedStonerRegistries.initialize();
|
||||
TrophySignService.initialize();
|
||||
RedStonerMenus.initialize();
|
||||
RedStonerNetworking.initialize();
|
||||
LOGGER.info("[{}] Computers, Drawer, Particuleur, Storage Terminal and Trophy Panel ready (data v{}, network v{}).",
|
||||
|
||||
+14
-35
@@ -3,7 +3,6 @@ package fr.koka99cab.sanctuary26.redstoner.block;
|
||||
import com.mojang.serialization.MapCodec;
|
||||
import fr.koka99cab.sanctuary26.redstoner.block.entity.TrophyPanelBlockEntity;
|
||||
import fr.koka99cab.sanctuary26.redstoner.registry.RedStonerRegistries;
|
||||
import fr.koka99cab.sanctuary26.sanctuary.api.ranking.RankingUnit;
|
||||
import java.util.Map;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Direction;
|
||||
@@ -45,10 +44,10 @@ public final class TrophyPanelBlock extends BaseEntityBlock {
|
||||
public static final MapCodec<TrophyPanelBlock> CODEC = simpleCodec(TrophyPanelBlock::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, 13.0D, 16.0D, 16.0D, 16.0D),
|
||||
Direction.SOUTH, Block.box(0.0D, 0.0D, 0.0D, 16.0D, 16.0D, 3.0D),
|
||||
Direction.WEST, Block.box(13.0D, 0.0D, 0.0D, 16.0D, 16.0D, 16.0D),
|
||||
Direction.EAST, Block.box(0.0D, 0.0D, 0.0D, 3.0D, 16.0D, 16.0D)
|
||||
Direction.NORTH, Block.box(0.0D, 0.0D, 14.0D, 16.0D, 16.0D, 16.0D),
|
||||
Direction.SOUTH, Block.box(0.0D, 0.0D, 0.0D, 16.0D, 16.0D, 2.0D),
|
||||
Direction.WEST, Block.box(14.0D, 0.0D, 0.0D, 16.0D, 16.0D, 16.0D),
|
||||
Direction.EAST, Block.box(0.0D, 0.0D, 0.0D, 2.0D, 16.0D, 16.0D)
|
||||
);
|
||||
|
||||
public TrophyPanelBlock(BlockBehaviour.Properties properties) {
|
||||
@@ -120,18 +119,20 @@ public final class TrophyPanelBlock extends BaseEntityBlock {
|
||||
if (level.isClientSide()) return InteractionResult.SUCCESS;
|
||||
if (!(player instanceof ServerPlayer serverPlayer)
|
||||
|| !(level.getBlockEntity(pos) instanceof TrophyPanelBlockEntity panel)) return InteractionResult.CONSUME;
|
||||
if (player.isSecondaryUseActive()) return cycle(panel, serverPlayer);
|
||||
if (player.isSecondaryUseActive()) return cycle(panel, serverPlayer, true);
|
||||
|
||||
boolean operator = serverPlayer.createCommandSourceStack().permissions()
|
||||
.hasPermission(Permissions.COMMANDS_GAMEMASTER);
|
||||
if (!panel.canConfigure(serverPlayer, operator)) {
|
||||
serverPlayer.sendSystemMessage(Component.translatable("message.redstoner.trophy_panel.not_owner"));
|
||||
return InteractionResult.CONSUME;
|
||||
return cycle(panel, serverPlayer, false);
|
||||
}
|
||||
TrophyPanelBlockEntity.ConfigureResult result = panel.configure(serverPlayer, stack);
|
||||
if (result == TrophyPanelBlockEntity.ConfigureResult.NO_METRIC) {
|
||||
return cycle(panel, serverPlayer, false);
|
||||
}
|
||||
String key = switch (result) {
|
||||
case SUCCESS -> "configured";
|
||||
case NO_METRIC -> "no_metric";
|
||||
case NO_METRIC -> throw new IllegalStateException("Handled above");
|
||||
case NEWER_DATA -> "newer_data";
|
||||
};
|
||||
serverPlayer.sendSystemMessage(Component.translatable("message.redstoner.trophy_panel." + key));
|
||||
@@ -144,9 +145,7 @@ public final class TrophyPanelBlock extends BaseEntityBlock {
|
||||
if (level.isClientSide()) return InteractionResult.SUCCESS;
|
||||
if (!(player instanceof ServerPlayer serverPlayer)
|
||||
|| !(level.getBlockEntity(pos) instanceof TrophyPanelBlockEntity panel)) return InteractionResult.CONSUME;
|
||||
if (player.isSecondaryUseActive()) return cycle(panel, serverPlayer);
|
||||
showDetails(panel, serverPlayer);
|
||||
return InteractionResult.SUCCESS_SERVER;
|
||||
return cycle(panel, serverPlayer, player.isSecondaryUseActive());
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -164,8 +163,9 @@ public final class TrophyPanelBlock extends BaseEntityBlock {
|
||||
return state.rotate(mirror.getRotation(state.getValue(FACING)));
|
||||
}
|
||||
|
||||
private static InteractionResult cycle(TrophyPanelBlockEntity panel, ServerPlayer player) {
|
||||
if (!panel.cyclePage()) {
|
||||
private static InteractionResult cycle(TrophyPanelBlockEntity panel, ServerPlayer player, boolean backwards) {
|
||||
boolean changed = backwards ? panel.previousPage() : panel.nextPage();
|
||||
if (!changed) {
|
||||
player.sendSystemMessage(Component.translatable("message.redstoner.trophy_panel.newer_data"));
|
||||
return InteractionResult.CONSUME;
|
||||
}
|
||||
@@ -173,25 +173,4 @@ public final class TrophyPanelBlock extends BaseEntityBlock {
|
||||
panel.pageNumber(), panel.pageCount()));
|
||||
return InteractionResult.SUCCESS_SERVER;
|
||||
}
|
||||
|
||||
private static void showDetails(TrophyPanelBlockEntity panel, ServerPlayer player) {
|
||||
TrophyPanelBlockEntity.ClientSnapshot snapshot = panel.visibleSnapshot();
|
||||
RankingUnit unit = snapshot.unit();
|
||||
ItemStack icon = panel.currentPage().icon();
|
||||
Component metric = icon.isEmpty() ? Component.translatable(snapshot.translationKey())
|
||||
: Component.translatable(snapshot.translationKey(), icon.getHoverName());
|
||||
player.sendSystemMessage(Component.translatable("message.redstoner.trophy_panel.header", metric,
|
||||
panel.pageNumber(), panel.pageCount()));
|
||||
for (TrophyPanelBlockEntity.ClientEntry entry : snapshot.podium()) {
|
||||
player.sendSystemMessage(Component.translatable("message.redstoner.trophy_panel.podium",
|
||||
entry.rank(), entry.name(), unit.format(entry.value())));
|
||||
}
|
||||
if (snapshot.ownerRank() > 0) {
|
||||
player.sendSystemMessage(Component.translatable("message.redstoner.trophy_panel.owner_rank",
|
||||
panel.ownerName(), snapshot.ownerRank(), unit.format(snapshot.ownerValue())));
|
||||
} else {
|
||||
player.sendSystemMessage(Component.translatable("message.redstoner.trophy_panel.owner_unranked",
|
||||
panel.ownerName()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+33
-4
@@ -5,6 +5,7 @@ import com.mojang.serialization.codecs.RecordCodecBuilder;
|
||||
import fr.koka99cab.sanctuary26.redstoner.block.TrophyPanelBlock;
|
||||
import fr.koka99cab.sanctuary26.redstoner.registry.RedStonerRegistries;
|
||||
import fr.koka99cab.sanctuary26.redstoner.trophy.TrophyPanelRules;
|
||||
import fr.koka99cab.sanctuary26.redstoner.trophy.TrophySignService;
|
||||
import fr.koka99cab.sanctuary26.sanctuary.api.ranking.RankingMetric;
|
||||
import fr.koka99cab.sanctuary26.sanctuary.api.ranking.RankingSnapshot;
|
||||
import fr.koka99cab.sanctuary26.sanctuary.api.ranking.RankingUnit;
|
||||
@@ -36,6 +37,7 @@ public final class TrophyPanelBlockEntity extends BlockEntity implements ItemOwn
|
||||
private static final int DATA_VERSION = 1;
|
||||
private static final int REFRESH_INTERVAL_TICKS = 20;
|
||||
private static final String CLIENT_SNAPSHOT_TAG = "VisibleSnapshot";
|
||||
private static final String CLIENT_VALUES_VIEW_TAG = "ValuesView";
|
||||
private static final Codec<Page> PAGE_CODEC = RecordCodecBuilder.create(instance -> instance.group(
|
||||
Identifier.CODEC.fieldOf("metric").forGetter(Page::metricId),
|
||||
ItemStack.OPTIONAL_CODEC.fieldOf("icon").forGetter(Page::icon)
|
||||
@@ -60,7 +62,9 @@ public final class TrophyPanelBlockEntity extends BlockEntity implements ItemOwn
|
||||
private String ownerName = "?";
|
||||
private List<Page> pages = List.of(defaultPage());
|
||||
private int currentPage;
|
||||
private boolean valuesView;
|
||||
private long lastRefreshTick = Long.MIN_VALUE;
|
||||
private boolean migrationAttempted;
|
||||
private ClientSnapshot visibleSnapshot = ClientSnapshot.empty(SanctuaryRankingApi.defaultMetric());
|
||||
|
||||
public TrophyPanelBlockEntity(BlockPos pos, BlockState state) {
|
||||
@@ -70,6 +74,13 @@ public final class TrophyPanelBlockEntity extends BlockEntity implements ItemOwn
|
||||
public static void serverTick(net.minecraft.world.level.Level level, BlockPos pos, BlockState state,
|
||||
TrophyPanelBlockEntity panel) {
|
||||
if (!(level instanceof ServerLevel serverLevel)) return;
|
||||
if (!panel.migrationAttempted) {
|
||||
panel.migrationAttempted = true;
|
||||
if (TrophySignService.migrateLegacy(serverLevel, pos, state.getValue(TrophyPanelBlock.FACING),
|
||||
panel.owner, panel.ownerName, panel.pages.stream().map(Page::metricId).toList(), panel.currentPage)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
long now = level.getGameTime();
|
||||
if (panel.lastRefreshTick != Long.MIN_VALUE && now - panel.lastRefreshTick < REFRESH_INTERVAL_TICKS) return;
|
||||
panel.lastRefreshTick = now;
|
||||
@@ -104,15 +115,27 @@ public final class TrophyPanelBlockEntity extends BlockEntity implements ItemOwn
|
||||
currentPage = Math.max(0, pages.indexOf(pages.stream()
|
||||
.filter(page -> offered.stream().anyMatch(metric -> metric.id().equals(page.metricId())))
|
||||
.findFirst().orElse(pages.getFirst())));
|
||||
valuesView = false;
|
||||
ownerName = cleanName(player.getGameProfile().name());
|
||||
syncPersistent();
|
||||
if (level instanceof ServerLevel serverLevel) refresh(serverLevel);
|
||||
return ConfigureResult.SUCCESS;
|
||||
}
|
||||
|
||||
public boolean cyclePage() {
|
||||
public boolean nextPage() {
|
||||
return movePage(1);
|
||||
}
|
||||
|
||||
public boolean previousPage() {
|
||||
return movePage(-1);
|
||||
}
|
||||
|
||||
private boolean movePage(int offset) {
|
||||
if (!writable() || pages.isEmpty()) return false;
|
||||
currentPage = TrophyPanelRules.nextPage(currentPage, pages.size());
|
||||
TrophyPanelRules.DisplayPage displayPage = TrophyPanelRules.displayPageByOffset(
|
||||
currentPage, valuesView, pages.size(), offset);
|
||||
currentPage = displayPage.metricPage();
|
||||
valuesView = displayPage.valuesView();
|
||||
syncPersistent();
|
||||
if (level instanceof ServerLevel serverLevel) refresh(serverLevel);
|
||||
return true;
|
||||
@@ -123,11 +146,15 @@ public final class TrophyPanelBlockEntity extends BlockEntity implements ItemOwn
|
||||
}
|
||||
|
||||
public int pageNumber() {
|
||||
return currentPage + 1;
|
||||
return TrophyPanelRules.displayPageNumber(currentPage, valuesView, pages.size());
|
||||
}
|
||||
|
||||
public int pageCount() {
|
||||
return pages.size();
|
||||
return TrophyPanelRules.displayPageCount(pages.size());
|
||||
}
|
||||
|
||||
public boolean valuesView() {
|
||||
return valuesView;
|
||||
}
|
||||
|
||||
public String ownerName() {
|
||||
@@ -166,6 +193,7 @@ public final class TrophyPanelBlockEntity extends BlockEntity implements ItemOwn
|
||||
pages = input.read("Pages", PAGE_CODEC.listOf(0, TrophyPanelRules.MAX_PAGES))
|
||||
.map(TrophyPanelBlockEntity::sanitizePages).orElseGet(() -> List.of(defaultPage()));
|
||||
currentPage = Math.clamp(input.getIntOr("CurrentPage", 0), 0, pages.size() - 1);
|
||||
valuesView = input.getBooleanOr(CLIENT_VALUES_VIEW_TAG, false);
|
||||
visibleSnapshot = input.read(CLIENT_SNAPSHOT_TAG, CLIENT_SNAPSHOT_CODEC)
|
||||
.orElseGet(() -> ClientSnapshot.empty(metricFor(currentPage().metricId())));
|
||||
}
|
||||
@@ -179,6 +207,7 @@ public final class TrophyPanelBlockEntity extends BlockEntity implements ItemOwn
|
||||
public CompoundTag getUpdateTag(HolderLookup.Provider lookup) {
|
||||
CompoundTag tag = saveCustomOnly(lookup);
|
||||
tag.store(CLIENT_SNAPSHOT_TAG, CLIENT_SNAPSHOT_CODEC, visibleSnapshot);
|
||||
tag.putBoolean(CLIENT_VALUES_VIEW_TAG, valuesView);
|
||||
return tag;
|
||||
}
|
||||
|
||||
|
||||
@@ -4,10 +4,12 @@ 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.gui.TrophySignEditorScreen;
|
||||
import fr.koka99cab.sanctuary26.redstoner.client.renderer.DrawerBlockEntityRenderer;
|
||||
import fr.koka99cab.sanctuary26.redstoner.client.renderer.TrophyPanelBlockEntityRenderer;
|
||||
import fr.koka99cab.sanctuary26.redstoner.network.OpenComputerPayload;
|
||||
import fr.koka99cab.sanctuary26.redstoner.network.OpenPersonalComputerPayload;
|
||||
import fr.koka99cab.sanctuary26.redstoner.network.OpenTrophySignEditorPayload;
|
||||
import fr.koka99cab.sanctuary26.redstoner.registry.RedStonerMenus;
|
||||
import fr.koka99cab.sanctuary26.redstoner.registry.RedStonerRegistries;
|
||||
import net.fabricmc.api.ClientModInitializer;
|
||||
@@ -33,6 +35,9 @@ public final class RedStonerClient implements ClientModInitializer {
|
||||
context.client().gui.setScreen(new PersonalComputerScreen(payload));
|
||||
}
|
||||
}));
|
||||
ClientPlayNetworking.registerGlobalReceiver(OpenTrophySignEditorPayload.TYPE, (payload, context) ->
|
||||
context.client().execute(() -> context.client().gui.setScreen(
|
||||
new TrophySignEditorScreen(context.client().gui.screen(), payload))));
|
||||
RedStonerMod.LOGGER.info("[{}] Client renderers and laboratory screens ready.", RedStonerMod.DISPLAY_NAME);
|
||||
}
|
||||
|
||||
|
||||
+4
-4
@@ -1,5 +1,6 @@
|
||||
package fr.koka99cab.sanctuary26.redstoner.client.gui;
|
||||
|
||||
import fr.koka99cab.sanctuary26.redstoner.storage.StorageTerminalKeyRules;
|
||||
import fr.koka99cab.sanctuary26.redstoner.storage.StorageTerminalMenu;
|
||||
import fr.koka99cab.sanctuary26.redstoner.storage.StorageTerminalRules;
|
||||
import fr.koka99cab.sanctuary26.redstoner.storage.StorageTerminalSort;
|
||||
@@ -139,10 +140,9 @@ public final class StorageTerminalScreen extends AbstractContainerScreen<Storage
|
||||
|
||||
@Override
|
||||
public boolean keyPressed(KeyEvent event) {
|
||||
if (searchBox != null && searchBox.isFocused() && minecraft != null
|
||||
&& minecraft.options.keyInventory.matches(event)) {
|
||||
clearSearchFocus();
|
||||
}
|
||||
if (StorageTerminalKeyRules.capturesInventoryKey(
|
||||
searchBox != null && searchBox.isFocused(), minecraft != null
|
||||
&& minecraft.options.keyInventory.matches(event))) return true;
|
||||
return super.keyPressed(event);
|
||||
}
|
||||
|
||||
|
||||
+265
@@ -0,0 +1,265 @@
|
||||
package fr.koka99cab.sanctuary26.redstoner.client.gui;
|
||||
|
||||
import fr.koka99cab.sanctuary26.redstoner.network.OpenTrophySignEditorPayload;
|
||||
import fr.koka99cab.sanctuary26.redstoner.network.SaveTrophySignPayload;
|
||||
import fr.koka99cab.sanctuary26.redstoner.trophy.TrophyDisplayMode;
|
||||
import fr.koka99cab.sanctuary26.redstoner.trophy.TrophyPanelRules;
|
||||
import fr.koka99cab.sanctuary26.sanctuary.api.ranking.RankingMetric;
|
||||
import fr.koka99cab.sanctuary26.sanctuary.api.ranking.RankingUnit;
|
||||
import fr.koka99cab.sanctuary26.sanctuary.api.ranking.SanctuaryRankingApi;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
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.components.EditBox;
|
||||
import net.minecraft.client.gui.screens.Screen;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.resources.Identifier;
|
||||
|
||||
/** White, bounded editor inspired by the community quest-sign screen. */
|
||||
public final class TrophySignEditorScreen extends Screen {
|
||||
private static final int VISIBLE_ROWS = 5;
|
||||
private static final int PANEL_WIDTH = 328;
|
||||
private static final int PANEL_HEIGHT = 250;
|
||||
private static final int COLUMN_WIDTH = 148;
|
||||
private static final int GAP = 8;
|
||||
private static final int BACKDROP = 0xD0F1F0EC;
|
||||
private static final int BORDER = 0xFFFFFFFF;
|
||||
private static final int TITLE = 0xFF343434;
|
||||
private static final int MUTED = 0xFF6A6A6A;
|
||||
|
||||
private final Screen parent;
|
||||
private final OpenTrophySignEditorPayload payload;
|
||||
private final List<Identifier> pages;
|
||||
private final List<MetricOption> catalog = new ArrayList<>();
|
||||
private final List<MetricOption> filtered = new ArrayList<>();
|
||||
private final Button[] resultButtons = new Button[VISIBLE_ROWS];
|
||||
private final Button[] pageButtons = new Button[VISIBLE_ROWS];
|
||||
private EditBox search;
|
||||
private Button previousResults;
|
||||
private Button nextResults;
|
||||
private Button previousPages;
|
||||
private Button nextPages;
|
||||
private Button moveUp;
|
||||
private Button moveDown;
|
||||
private Button remove;
|
||||
private Button displayMode;
|
||||
private Button save;
|
||||
private TrophyDisplayMode mode;
|
||||
private int catalogOffset;
|
||||
private int pageOffset;
|
||||
private int selectedPage = -1;
|
||||
|
||||
public TrophySignEditorScreen(Screen parent, OpenTrophySignEditorPayload payload) {
|
||||
super(Component.translatable("screen.redstoner.trophy_sign.title"));
|
||||
this.parent = parent;
|
||||
this.payload = payload;
|
||||
this.pages = new ArrayList<>(payload.pages());
|
||||
this.mode = TrophyDisplayMode.byKey(payload.display());
|
||||
if (!pages.isEmpty()) selectedPage = 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void init() {
|
||||
int x = panelX() + 12;
|
||||
int y = panelY();
|
||||
buildCatalog();
|
||||
search = addRenderableWidget(new EditBox(font, x, y + 38, COLUMN_WIDTH, 20,
|
||||
Component.translatable("screen.redstoner.trophy_sign.search")));
|
||||
search.setMaxLength(64);
|
||||
search.setHint(Component.translatable("screen.redstoner.trophy_sign.search"));
|
||||
search.setResponder(value -> filterCatalog());
|
||||
|
||||
for (int slot = 0; slot < VISIBLE_ROWS; slot++) {
|
||||
int resultSlot = slot;
|
||||
resultButtons[slot] = addRenderableWidget(Button.builder(Component.empty(),
|
||||
button -> addResult(resultSlot)).bounds(x, y + 62 + slot * 22, COLUMN_WIDTH, 20).build());
|
||||
int pageSlot = slot;
|
||||
pageButtons[slot] = addRenderableWidget(Button.builder(Component.empty(),
|
||||
button -> selectPage(pageSlot)).bounds(x + COLUMN_WIDTH + GAP, y + 38 + slot * 22,
|
||||
COLUMN_WIDTH, 20).build());
|
||||
}
|
||||
|
||||
previousResults = addRenderableWidget(Button.builder(Component.literal("‹"), button -> moveCatalog(-VISIBLE_ROWS))
|
||||
.bounds(x, y + 174, 72, 20).build());
|
||||
nextResults = addRenderableWidget(Button.builder(Component.literal("›"), button -> moveCatalog(VISIBLE_ROWS))
|
||||
.bounds(x + 76, y + 174, 72, 20).build());
|
||||
previousPages = addRenderableWidget(Button.builder(Component.literal("‹"), button -> movePageWindow(-VISIBLE_ROWS))
|
||||
.bounds(x + COLUMN_WIDTH + GAP, y + 174, 72, 20).build());
|
||||
nextPages = addRenderableWidget(Button.builder(Component.literal("›"), button -> movePageWindow(VISIBLE_ROWS))
|
||||
.bounds(x + COLUMN_WIDTH + GAP + 76, y + 174, 72, 20).build());
|
||||
|
||||
moveUp = addRenderableWidget(Button.builder(Component.literal("↑"), button -> moveSelected(-1))
|
||||
.bounds(x + COLUMN_WIDTH + GAP, y + 150, 46, 20).build());
|
||||
moveDown = addRenderableWidget(Button.builder(Component.literal("↓"), button -> moveSelected(1))
|
||||
.bounds(x + COLUMN_WIDTH + GAP + 50, y + 150, 46, 20).build());
|
||||
remove = addRenderableWidget(Button.builder(Component.translatable("screen.redstoner.trophy_sign.remove"),
|
||||
button -> removeSelected()).bounds(x + COLUMN_WIDTH + GAP + 100, y + 150, 48, 20).build());
|
||||
|
||||
displayMode = addRenderableWidget(Button.builder(displayLabel(), button -> {
|
||||
mode = mode.next();
|
||||
button.setMessage(displayLabel());
|
||||
}).bounds(x, y + 198, COLUMN_WIDTH * 2 + GAP, 20).build());
|
||||
save = addRenderableWidget(Button.builder(Component.translatable("gui.done"), button -> submit())
|
||||
.bounds(x, y + 224, COLUMN_WIDTH, 20).build());
|
||||
addRenderableWidget(Button.builder(Component.translatable("gui.cancel"), button -> onClose())
|
||||
.bounds(x + COLUMN_WIDTH + GAP, y + 224, COLUMN_WIDTH, 20).build());
|
||||
|
||||
filterCatalog();
|
||||
refreshWidgets();
|
||||
setInitialFocus(search);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void tick() {
|
||||
refreshWidgets();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void extractRenderState(GuiGraphicsExtractor graphics, int mouseX, int mouseY, float tickDelta) {
|
||||
int x = panelX();
|
||||
int y = panelY();
|
||||
graphics.fill(x, y, x + PANEL_WIDTH, y + PANEL_HEIGHT, BACKDROP);
|
||||
graphics.outline(x, y, PANEL_WIDTH, PANEL_HEIGHT, BORDER);
|
||||
graphics.centeredText(font, title, width / 2, y + 10, TITLE);
|
||||
graphics.text(font, Component.translatable("screen.redstoner.trophy_sign.available"),
|
||||
x + 12, y + 27, MUTED);
|
||||
graphics.text(font, Component.translatable("screen.redstoner.trophy_sign.pages",
|
||||
pages.size(), TrophyPanelRules.MAX_PAGES), x + 12 + COLUMN_WIDTH + GAP, y + 27, MUTED);
|
||||
super.extractRenderState(graphics, mouseX, mouseY, tickDelta);
|
||||
}
|
||||
|
||||
private void buildCatalog() {
|
||||
catalog.clear();
|
||||
for (RankingMetric metric : SanctuaryRankingApi.configurableMetrics()) {
|
||||
Component label = SanctuaryRankingApi.displayName(metric);
|
||||
catalog.add(new MetricOption(metric, label, (label.getString() + " " + metric.id())
|
||||
.toLowerCase(Locale.ROOT)));
|
||||
}
|
||||
catalog.sort(Comparator.comparing(option -> option.label().getString(), String.CASE_INSENSITIVE_ORDER));
|
||||
}
|
||||
|
||||
private void filterCatalog() {
|
||||
if (search == null) return;
|
||||
String query = search.getValue().strip().toLowerCase(Locale.ROOT);
|
||||
filtered.clear();
|
||||
for (MetricOption option : catalog) if (query.isEmpty() || option.searchText().contains(query)) filtered.add(option);
|
||||
catalogOffset = 0;
|
||||
refreshWidgets();
|
||||
}
|
||||
|
||||
private void addResult(int slot) {
|
||||
int index = catalogOffset + slot;
|
||||
if (index < 0 || index >= filtered.size()) return;
|
||||
Identifier id = filtered.get(index).metric().id();
|
||||
int existing = pages.indexOf(id);
|
||||
if (existing >= 0) {
|
||||
selectedPage = existing;
|
||||
ensureSelectedVisible();
|
||||
} else if (pages.size() < TrophyPanelRules.MAX_PAGES) {
|
||||
pages.add(id);
|
||||
selectedPage = pages.size() - 1;
|
||||
ensureSelectedVisible();
|
||||
}
|
||||
refreshWidgets();
|
||||
}
|
||||
|
||||
private void selectPage(int slot) {
|
||||
int index = pageOffset + slot;
|
||||
if (index >= 0 && index < pages.size()) selectedPage = index;
|
||||
refreshWidgets();
|
||||
}
|
||||
|
||||
private void moveSelected(int offset) {
|
||||
int target = selectedPage + offset;
|
||||
if (selectedPage < 0 || target < 0 || target >= pages.size()) return;
|
||||
Identifier page = pages.remove(selectedPage);
|
||||
pages.add(target, page);
|
||||
selectedPage = target;
|
||||
ensureSelectedVisible();
|
||||
refreshWidgets();
|
||||
}
|
||||
|
||||
private void removeSelected() {
|
||||
if (selectedPage < 0 || selectedPage >= pages.size()) return;
|
||||
pages.remove(selectedPage);
|
||||
selectedPage = pages.isEmpty() ? -1 : Math.min(selectedPage, pages.size() - 1);
|
||||
ensureSelectedVisible();
|
||||
refreshWidgets();
|
||||
}
|
||||
|
||||
private void moveCatalog(int amount) {
|
||||
int maximum = Math.max(0, filtered.size() - VISIBLE_ROWS);
|
||||
catalogOffset = Math.clamp(catalogOffset + amount, 0, maximum);
|
||||
refreshWidgets();
|
||||
}
|
||||
|
||||
private void movePageWindow(int amount) {
|
||||
int maximum = Math.max(0, pages.size() - VISIBLE_ROWS);
|
||||
pageOffset = Math.clamp(pageOffset + amount, 0, maximum);
|
||||
refreshWidgets();
|
||||
}
|
||||
|
||||
private void ensureSelectedVisible() {
|
||||
if (selectedPage < pageOffset) pageOffset = selectedPage;
|
||||
if (selectedPage >= pageOffset + VISIBLE_ROWS) pageOffset = selectedPage - VISIBLE_ROWS + 1;
|
||||
pageOffset = Math.max(0, pageOffset);
|
||||
}
|
||||
|
||||
private void refreshWidgets() {
|
||||
if (save == null) return;
|
||||
for (int slot = 0; slot < VISIBLE_ROWS; slot++) {
|
||||
int resultIndex = catalogOffset + slot;
|
||||
Button result = resultButtons[slot];
|
||||
result.visible = resultIndex < filtered.size();
|
||||
if (result.visible) {
|
||||
MetricOption option = filtered.get(resultIndex);
|
||||
result.setMessage(Component.literal(pages.contains(option.metric().id()) ? "✓ " : "+ ")
|
||||
.append(option.label()));
|
||||
result.active = pages.contains(option.metric().id()) || pages.size() < TrophyPanelRules.MAX_PAGES;
|
||||
}
|
||||
|
||||
int pageIndex = pageOffset + slot;
|
||||
Button page = pageButtons[slot];
|
||||
page.visible = pageIndex < pages.size();
|
||||
if (page.visible) page.setMessage(Component.literal(pageIndex == selectedPage ? "▶ " : "")
|
||||
.append(Component.literal((pageIndex + 1) + ". ")).append(metricLabel(pages.get(pageIndex))));
|
||||
}
|
||||
previousResults.active = catalogOffset > 0;
|
||||
nextResults.active = catalogOffset + VISIBLE_ROWS < filtered.size();
|
||||
previousPages.active = pageOffset > 0;
|
||||
nextPages.active = pageOffset + VISIBLE_ROWS < pages.size();
|
||||
moveUp.active = selectedPage > 0;
|
||||
moveDown.active = selectedPage >= 0 && selectedPage + 1 < pages.size();
|
||||
remove.active = selectedPage >= 0;
|
||||
save.active = !pages.isEmpty() && ClientPlayNetworking.canSend(SaveTrophySignPayload.TYPE);
|
||||
}
|
||||
|
||||
private Component metricLabel(Identifier id) {
|
||||
RankingMetric metric = SanctuaryRankingApi.metric(id).orElseGet(() ->
|
||||
new RankingMetric(id, "metric.sanctuary.ranking.unknown", RankingUnit.COUNT));
|
||||
return SanctuaryRankingApi.displayName(metric);
|
||||
}
|
||||
|
||||
private Component displayLabel() {
|
||||
return Component.translatable("screen.redstoner.trophy_sign.display",
|
||||
Component.translatable("screen.redstoner.trophy_sign.display." + mode.key()));
|
||||
}
|
||||
|
||||
private void submit() {
|
||||
if (pages.isEmpty() || !ClientPlayNetworking.canSend(SaveTrophySignPayload.TYPE)) return;
|
||||
ClientPlayNetworking.send(new SaveTrophySignPayload(payload.pos(), pages, mode.key()));
|
||||
onClose();
|
||||
}
|
||||
|
||||
private int panelX() { return (width - PANEL_WIDTH) / 2; }
|
||||
private int panelY() { return Math.max(2, (height - PANEL_HEIGHT) / 2); }
|
||||
|
||||
@Override public void onClose() { if (minecraft != null) minecraft.gui.setScreen(parent); }
|
||||
@Override public boolean isPauseScreen() { return false; }
|
||||
|
||||
private record MetricOption(RankingMetric metric, Component label, String searchText) {}
|
||||
}
|
||||
+34
-44
@@ -5,28 +5,31 @@ import com.mojang.math.Axis;
|
||||
import fr.koka99cab.sanctuary26.redstoner.block.TrophyPanelBlock;
|
||||
import fr.koka99cab.sanctuary26.redstoner.block.entity.TrophyPanelBlockEntity;
|
||||
import fr.koka99cab.sanctuary26.redstoner.client.renderer.state.TrophyPanelRenderState;
|
||||
import fr.koka99cab.sanctuary26.redstoner.trophy.TrophyPanelRules;
|
||||
import fr.koka99cab.sanctuary26.sanctuary.api.ranking.RankingMetric;
|
||||
import fr.koka99cab.sanctuary26.sanctuary.api.ranking.RankingUnit;
|
||||
import fr.koka99cab.sanctuary26.sanctuary.api.ranking.SanctuaryRankingApi;
|
||||
import java.util.Arrays;
|
||||
import net.minecraft.client.gui.Font;
|
||||
import net.minecraft.client.renderer.SubmitNodeCollector;
|
||||
import net.minecraft.client.renderer.blockentity.BlockEntityRenderer;
|
||||
import net.minecraft.client.renderer.blockentity.BlockEntityRendererProvider;
|
||||
import net.minecraft.client.renderer.feature.ModelFeatureRenderer;
|
||||
import net.minecraft.client.renderer.item.ItemModelResolver;
|
||||
import net.minecraft.client.renderer.state.level.CameraRenderState;
|
||||
import net.minecraft.client.renderer.texture.OverlayTexture;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.util.FormattedCharSequence;
|
||||
import net.minecraft.world.item.ItemDisplayContext;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
|
||||
public final class TrophyPanelBlockEntityRenderer
|
||||
implements BlockEntityRenderer<TrophyPanelBlockEntity, TrophyPanelRenderState> {
|
||||
private final ItemModelResolver itemModelResolver;
|
||||
private static final int MAX_TEXT_WIDTH = 90;
|
||||
private static final float TEXT_SCALE = 0.010416667F;
|
||||
private static final float[] LINE_HEIGHTS = {0.86F, 0.75F, 0.64F, 0.53F, 0.42F, 0.31F, 0.20F, 0.09F};
|
||||
private static final int TEXT_COLOR = 0xFF3D281A;
|
||||
private final Font font;
|
||||
|
||||
public TrophyPanelBlockEntityRenderer(BlockEntityRendererProvider.Context context) {
|
||||
itemModelResolver = context.itemModelResolver();
|
||||
font = context.font();
|
||||
}
|
||||
|
||||
@@ -40,65 +43,52 @@ public final class TrophyPanelBlockEntityRenderer
|
||||
float tickProgress, Vec3 cameraPos, ModelFeatureRenderer.CrumblingOverlay crumblingOverlay) {
|
||||
BlockEntityRenderer.super.extractRenderState(panel, state, tickProgress, cameraPos, crumblingOverlay);
|
||||
state.facing = panel.getBlockState().getValue(TrophyPanelBlock.FACING);
|
||||
state.icon.clear();
|
||||
Arrays.fill(state.lines, Component.empty());
|
||||
ItemStack icon = panel.currentPage().icon();
|
||||
state.hasIcon = !icon.isEmpty();
|
||||
if (state.hasIcon) {
|
||||
itemModelResolver.updateForTopItem(state.icon, icon, ItemDisplayContext.FIXED,
|
||||
panel.level(), panel, (int) panel.getBlockPos().asLong());
|
||||
}
|
||||
|
||||
TrophyPanelBlockEntity.ClientSnapshot snapshot = panel.visibleSnapshot();
|
||||
RankingUnit unit = snapshot.unit();
|
||||
state.metric = state.hasIcon ? Component.translatable(snapshot.translationKey(), icon.getHoverName())
|
||||
: Component.translatable(snapshot.translationKey());
|
||||
Component metric = SanctuaryRankingApi.displayName(
|
||||
new RankingMetric(snapshot.metricId(), snapshot.translationKey(), unit), icon);
|
||||
state.lines[0] = Component.translatable("block.redstoner.trophy_panel.page_title",
|
||||
panel.pageNumber(), panel.pageCount(), metric);
|
||||
if (snapshot.podium().isEmpty()) {
|
||||
state.champion = Component.translatable("block.redstoner.trophy_panel.no_score");
|
||||
state.lines[1] = Component.translatable("block.redstoner.trophy_panel.no_score");
|
||||
} else {
|
||||
TrophyPanelBlockEntity.ClientEntry champion = snapshot.podium().getFirst();
|
||||
state.champion = Component.translatable("block.redstoner.trophy_panel.champion",
|
||||
champion.name(), unit.format(champion.value()));
|
||||
for (int index = 0; index < TrophyPanelRules.ENTRIES_PER_VIEW; index++) {
|
||||
if (index >= snapshot.podium().size()) {
|
||||
break;
|
||||
}
|
||||
TrophyPanelBlockEntity.ClientEntry entry = snapshot.podium().get(index);
|
||||
state.lines[index + 1] = panel.valuesView()
|
||||
? Component.translatable("block.redstoner.trophy_panel.value",
|
||||
entry.rank(), unit.format(entry.value()))
|
||||
: Component.translatable("block.redstoner.trophy_panel.player",
|
||||
entry.rank(), entry.name());
|
||||
}
|
||||
}
|
||||
state.owner = snapshot.ownerRank() > 0
|
||||
? Component.translatable("block.redstoner.trophy_panel.owner_rank", panel.ownerName(),
|
||||
snapshot.ownerRank(), unit.format(snapshot.ownerValue()))
|
||||
: Component.translatable("block.redstoner.trophy_panel.owner_unranked", panel.ownerName());
|
||||
state.page = Component.literal(panel.pageNumber() + "/" + panel.pageCount());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void submit(TrophyPanelRenderState state, PoseStack poseStack,
|
||||
SubmitNodeCollector submitNodeCollector, CameraRenderState cameraRenderState) {
|
||||
float yRotation = -state.facing.toYRot();
|
||||
if (state.hasIcon) submitIcon(state, poseStack, submitNodeCollector, yRotation);
|
||||
submitText(state.metric, 0.72F, 0.0065F, state, poseStack, submitNodeCollector, yRotation, 0xFFE8B4);
|
||||
submitText(state.champion, 0.52F, 0.0060F, state, poseStack, submitNodeCollector, yRotation, 0xFFFFFF);
|
||||
submitText(state.owner, 0.36F, 0.0055F, state, poseStack, submitNodeCollector, yRotation, 0xB8D9FF);
|
||||
submitText(state.page, 0.20F, 0.0050F, state, poseStack, submitNodeCollector, yRotation, 0xA0A0A0);
|
||||
for (int index = 0; index < TrophyPanelRenderState.LINE_COUNT; index++) {
|
||||
submitText(state.lines[index], LINE_HEIGHTS[index], state, poseStack, submitNodeCollector, yRotation);
|
||||
}
|
||||
}
|
||||
|
||||
private void submitIcon(TrophyPanelRenderState state, PoseStack poseStack,
|
||||
SubmitNodeCollector submitNodeCollector, float yRotation) {
|
||||
poseStack.pushPose();
|
||||
poseStack.translate(0.5F, 0.89F, 0.5F);
|
||||
poseStack.mulPose(Axis.YP.rotationDegrees(yRotation));
|
||||
poseStack.translate(0.0F, 0.0F, -0.33F);
|
||||
poseStack.scale(0.18F, 0.18F, 0.18F);
|
||||
state.icon.submit(poseStack, submitNodeCollector, state.lightCoords, OverlayTexture.NO_OVERLAY, 0);
|
||||
poseStack.popPose();
|
||||
}
|
||||
|
||||
private void submitText(Component component, float y, float scale, TrophyPanelRenderState state,
|
||||
PoseStack poseStack, SubmitNodeCollector submitNodeCollector, float yRotation, int color) {
|
||||
FormattedCharSequence text = component.getVisualOrderText();
|
||||
private void submitText(Component component, float y, TrophyPanelRenderState state,
|
||||
PoseStack poseStack, SubmitNodeCollector submitNodeCollector, float yRotation) {
|
||||
FormattedCharSequence text = font.split(component, MAX_TEXT_WIDTH).stream().findFirst()
|
||||
.orElse(FormattedCharSequence.EMPTY);
|
||||
float x = -font.width(text) / 2.0F;
|
||||
poseStack.pushPose();
|
||||
poseStack.translate(0.5F, y, 0.5F);
|
||||
poseStack.mulPose(Axis.YP.rotationDegrees(yRotation));
|
||||
poseStack.translate(0.0F, 0.0F, -0.331F);
|
||||
poseStack.scale(scale, -scale, scale);
|
||||
poseStack.scale(TEXT_SCALE, -TEXT_SCALE, TEXT_SCALE);
|
||||
submitNodeCollector.submitText(poseStack, x, 0.0F, text, false, Font.DisplayMode.POLYGON_OFFSET,
|
||||
color, state.lightCoords, 0, 0x30181818);
|
||||
state.lightCoords, TEXT_COLOR, 0, 0);
|
||||
poseStack.popPose();
|
||||
}
|
||||
}
|
||||
|
||||
+7
-7
@@ -1,16 +1,16 @@
|
||||
package fr.koka99cab.sanctuary26.redstoner.client.renderer.state;
|
||||
|
||||
import fr.koka99cab.sanctuary26.redstoner.trophy.TrophyPanelRules;
|
||||
import net.minecraft.client.renderer.blockentity.state.BlockEntityRenderState;
|
||||
import net.minecraft.client.renderer.item.ItemStackRenderState;
|
||||
import net.minecraft.core.Direction;
|
||||
import net.minecraft.network.chat.Component;
|
||||
|
||||
public final class TrophyPanelRenderState extends BlockEntityRenderState {
|
||||
public final ItemStackRenderState icon = new ItemStackRenderState();
|
||||
public static final int LINE_COUNT = TrophyPanelRules.LINE_COUNT;
|
||||
public final Component[] lines = new Component[LINE_COUNT];
|
||||
public Direction facing = Direction.NORTH;
|
||||
public boolean hasIcon;
|
||||
public Component metric = Component.empty();
|
||||
public Component champion = Component.empty();
|
||||
public Component owner = Component.empty();
|
||||
public Component page = Component.empty();
|
||||
|
||||
public TrophyPanelRenderState() {
|
||||
java.util.Arrays.fill(lines, Component.empty());
|
||||
}
|
||||
}
|
||||
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
package fr.koka99cab.sanctuary26.redstoner.network;
|
||||
|
||||
import fr.koka99cab.sanctuary26.redstoner.RedStonerMod;
|
||||
import fr.koka99cab.sanctuary26.redstoner.trophy.TrophyDisplayMode;
|
||||
import fr.koka99cab.sanctuary26.redstoner.trophy.TrophyPanelRules;
|
||||
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.resources.Identifier;
|
||||
|
||||
/** Opens the white trophy-sign editor with only its small current configuration. */
|
||||
public record OpenTrophySignEditorPayload(BlockPos pos, List<Identifier> pages, String display)
|
||||
implements CustomPacketPayload {
|
||||
public static final Type<OpenTrophySignEditorPayload> TYPE =
|
||||
new Type<>(RedStonerMod.id("s2c/open_trophy_sign_editor"));
|
||||
public static final StreamCodec<RegistryFriendlyByteBuf, OpenTrophySignEditorPayload> STREAM_CODEC =
|
||||
StreamCodec.of(OpenTrophySignEditorPayload::write, OpenTrophySignEditorPayload::read);
|
||||
|
||||
public OpenTrophySignEditorPayload {
|
||||
pos = pos == null ? BlockPos.ZERO : pos.immutable();
|
||||
pages = TrophyPanelRules.sanitizeMetricIds(pages);
|
||||
display = TrophyDisplayMode.byKey(display).key();
|
||||
}
|
||||
|
||||
private static OpenTrophySignEditorPayload read(RegistryFriendlyByteBuf buffer) {
|
||||
BlockPos pos = buffer.readBlockPos();
|
||||
int count = boundedCount(buffer.readVarInt());
|
||||
List<Identifier> pages = new ArrayList<>(count);
|
||||
for (int index = 0; index < count; index++) pages.add(Identifier.parse(buffer.readUtf(256)));
|
||||
return new OpenTrophySignEditorPayload(pos, pages, buffer.readUtf(24));
|
||||
}
|
||||
|
||||
private static void write(RegistryFriendlyByteBuf buffer, OpenTrophySignEditorPayload payload) {
|
||||
buffer.writeBlockPos(payload.pos);
|
||||
buffer.writeVarInt(payload.pages.size());
|
||||
for (Identifier page : payload.pages) buffer.writeUtf(page.toString(), 256);
|
||||
buffer.writeUtf(payload.display, 24);
|
||||
}
|
||||
|
||||
private static int boundedCount(int count) {
|
||||
if (count < 0 || count > TrophyPanelRules.MAX_PAGES) {
|
||||
throw new IllegalArgumentException("Invalid trophy-sign page count: " + count);
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
@Override public Type<? extends CustomPacketPayload> type() { return TYPE; }
|
||||
}
|
||||
+8
-1
@@ -4,6 +4,7 @@ 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.trophy.TrophySignService;
|
||||
import fr.koka99cab.sanctuary26.sanctuary.api.atlas.SanctuaryAtlasLandmarks;
|
||||
import net.fabricmc.fabric.api.networking.v1.PayloadTypeRegistry;
|
||||
import net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking;
|
||||
@@ -12,7 +13,7 @@ import net.minecraft.world.entity.player.Player;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
|
||||
public final class RedStonerNetworking {
|
||||
public static final int PROTOCOL_VERSION = 7;
|
||||
public static final int PROTOCOL_VERSION = 9;
|
||||
private static boolean initialized;
|
||||
|
||||
private RedStonerNetworking() {
|
||||
@@ -25,6 +26,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(OpenTrophySignEditorPayload.TYPE,
|
||||
OpenTrophySignEditorPayload.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 +36,8 @@ public final class RedStonerNetworking {
|
||||
AcknowledgePersonalComputerIntroPayload.STREAM_CODEC);
|
||||
PayloadTypeRegistry.serverboundPlay().register(ShutdownPersonalComputerPayload.TYPE,
|
||||
ShutdownPersonalComputerPayload.STREAM_CODEC);
|
||||
PayloadTypeRegistry.serverboundPlay().register(SaveTrophySignPayload.TYPE,
|
||||
SaveTrophySignPayload.STREAM_CODEC);
|
||||
ServerPlayNetworking.registerGlobalReceiver(SaveComputerProgramPayload.TYPE, (payload, context) ->
|
||||
context.server().execute(() -> saveProgram(context.player(), payload)));
|
||||
ServerPlayNetworking.registerGlobalReceiver(PlaceAtlasLandmarkPayload.TYPE, (payload, context) ->
|
||||
@@ -43,6 +48,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(SaveTrophySignPayload.TYPE, (payload, context) ->
|
||||
context.server().execute(() -> TrophySignService.save(context.player(), payload)));
|
||||
RedStonerMod.LOGGER.debug("[{}] Networking contract v{} registered.", RedStonerMod.DISPLAY_NAME, PROTOCOL_VERSION);
|
||||
}
|
||||
|
||||
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
package fr.koka99cab.sanctuary26.redstoner.network;
|
||||
|
||||
import fr.koka99cab.sanctuary26.redstoner.RedStonerMod;
|
||||
import fr.koka99cab.sanctuary26.redstoner.trophy.TrophyDisplayMode;
|
||||
import fr.koka99cab.sanctuary26.redstoner.trophy.TrophyPanelRules;
|
||||
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.resources.Identifier;
|
||||
|
||||
/** Bounded editor submission; ownership, distance, block and metrics are all revalidated server-side. */
|
||||
public record SaveTrophySignPayload(BlockPos pos, List<Identifier> pages, String display)
|
||||
implements CustomPacketPayload {
|
||||
public static final Type<SaveTrophySignPayload> TYPE =
|
||||
new Type<>(RedStonerMod.id("c2s/save_trophy_sign"));
|
||||
public static final StreamCodec<RegistryFriendlyByteBuf, SaveTrophySignPayload> STREAM_CODEC =
|
||||
StreamCodec.of(SaveTrophySignPayload::write, SaveTrophySignPayload::read);
|
||||
|
||||
public SaveTrophySignPayload {
|
||||
pos = pos == null ? BlockPos.ZERO : pos.immutable();
|
||||
pages = TrophyPanelRules.sanitizeMetricIds(pages);
|
||||
display = TrophyDisplayMode.byKey(display).key();
|
||||
}
|
||||
|
||||
private static SaveTrophySignPayload read(RegistryFriendlyByteBuf buffer) {
|
||||
BlockPos pos = buffer.readBlockPos();
|
||||
int count = boundedCount(buffer.readVarInt());
|
||||
List<Identifier> pages = new ArrayList<>(count);
|
||||
for (int index = 0; index < count; index++) pages.add(Identifier.parse(buffer.readUtf(256)));
|
||||
return new SaveTrophySignPayload(pos, pages, buffer.readUtf(24));
|
||||
}
|
||||
|
||||
private static void write(RegistryFriendlyByteBuf buffer, SaveTrophySignPayload payload) {
|
||||
buffer.writeBlockPos(payload.pos);
|
||||
buffer.writeVarInt(payload.pages.size());
|
||||
for (Identifier page : payload.pages) buffer.writeUtf(page.toString(), 256);
|
||||
buffer.writeUtf(payload.display, 24);
|
||||
}
|
||||
|
||||
private static int boundedCount(int count) {
|
||||
if (count < 0 || count > TrophyPanelRules.MAX_PAGES) {
|
||||
throw new IllegalArgumentException("Invalid trophy-sign page count: " + count);
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
@Override public Type<? extends CustomPacketPayload> type() { return TYPE; }
|
||||
}
|
||||
+37
-2
@@ -12,8 +12,10 @@ import fr.koka99cab.sanctuary26.redstoner.block.entity.DrawerBlockEntity;
|
||||
import fr.koka99cab.sanctuary26.redstoner.block.entity.PersonalComputerBlockEntity;
|
||||
import fr.koka99cab.sanctuary26.redstoner.block.entity.ParticuleurBlockEntity;
|
||||
import fr.koka99cab.sanctuary26.redstoner.block.entity.TrophyPanelBlockEntity;
|
||||
import fr.koka99cab.sanctuary26.redstoner.trophy.TrophySignItem;
|
||||
import java.util.function.Function;
|
||||
import net.fabricmc.fabric.api.event.player.AttackBlockCallback;
|
||||
import net.fabricmc.fabric.api.object.builder.v1.block.entity.FabricBlockEntityType;
|
||||
import net.fabricmc.fabric.api.object.builder.v1.block.entity.FabricBlockEntityTypeBuilder;
|
||||
import net.minecraft.core.Registry;
|
||||
import net.minecraft.core.registries.BuiltInRegistries;
|
||||
@@ -28,10 +30,15 @@ import net.minecraft.world.item.CreativeModeTab;
|
||||
import net.minecraft.world.item.Item;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.level.block.Block;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.block.SoundType;
|
||||
import net.minecraft.world.level.block.StandingSignBlock;
|
||||
import net.minecraft.world.level.block.WallSignBlock;
|
||||
import net.minecraft.world.level.block.entity.BlockEntityType;
|
||||
import net.minecraft.world.level.block.entity.BlockEntityTypes;
|
||||
import net.minecraft.world.level.block.state.BlockBehaviour;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.block.state.properties.WoodType;
|
||||
import net.minecraft.world.level.material.MapColor;
|
||||
|
||||
public final class RedStonerRegistries {
|
||||
@@ -79,6 +86,18 @@ public final class RedStonerRegistries {
|
||||
BlockBehaviour.Properties.of().mapColor(MapColor.WOOD).strength(2.0F).sound(SoundType.WOOD)
|
||||
.requiresCorrectToolForDrops().noOcclusion().setId(blockKey(RedStonerMod.id("trophy_panel")))
|
||||
);
|
||||
public static final StandingSignBlock TROPHY_SIGN = registerBlock(
|
||||
RedStonerMod.id("trophy_sign"),
|
||||
properties -> new StandingSignBlock(WoodType.PALE_OAK, properties),
|
||||
BlockBehaviour.Properties.ofFullCopy(Blocks.PALE_OAK_SIGN).mapColor(MapColor.SNOW)
|
||||
.setId(blockKey(RedStonerMod.id("trophy_sign")))
|
||||
);
|
||||
public static final WallSignBlock TROPHY_WALL_SIGN = registerBlock(
|
||||
RedStonerMod.id("trophy_wall_sign"),
|
||||
properties -> new WallSignBlock(WoodType.PALE_OAK, properties),
|
||||
BlockBehaviour.Properties.ofFullCopy(Blocks.PALE_OAK_WALL_SIGN).mapColor(MapColor.SNOW)
|
||||
.setId(blockKey(RedStonerMod.id("trophy_wall_sign")))
|
||||
);
|
||||
|
||||
public static final ComputerBlock LEGACY_EIGHT_BIT_COMPUTER = registerBlock(
|
||||
LEGACY_COMPUTER_ID, ComputerBlock::new, computerProperties(LEGACY_COMPUTER_ID, MapColor.COLOR_RED));
|
||||
@@ -100,7 +119,7 @@ public final class RedStonerRegistries {
|
||||
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 TROPHY_PANEL_ITEM = registerBlockItem(RedStonerMod.id("trophy_panel"), TROPHY_PANEL);
|
||||
public static final Item TROPHY_PANEL_ITEM = registerTrophySignItem();
|
||||
|
||||
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);
|
||||
@@ -153,8 +172,16 @@ public final class RedStonerRegistries {
|
||||
return;
|
||||
}
|
||||
initialized = true;
|
||||
FabricBlockEntityType signType = (FabricBlockEntityType) (Object) BlockEntityTypes.SIGN;
|
||||
signType.addValidBlock(TROPHY_SIGN);
|
||||
signType.addValidBlock(TROPHY_WALL_SIGN);
|
||||
registerDrawerAttack();
|
||||
RedStonerMod.LOGGER.info("[{}] Registered seven active blocks and five legacy block aliases.", RedStonerMod.DISPLAY_NAME);
|
||||
RedStonerMod.LOGGER.info("[{}] Registered the vanilla trophy-sign pair and retained the old panel for migration.",
|
||||
RedStonerMod.DISPLAY_NAME);
|
||||
}
|
||||
|
||||
public static boolean isTrophySign(Block block) {
|
||||
return block == TROPHY_SIGN || block == TROPHY_WALL_SIGN;
|
||||
}
|
||||
|
||||
public static boolean isController(BlockState state) {
|
||||
@@ -199,6 +226,14 @@ public final class RedStonerRegistries {
|
||||
new BlockItem(block, new Item.Properties().useBlockDescriptionPrefix().setId(key)));
|
||||
}
|
||||
|
||||
private static Item registerTrophySignItem() {
|
||||
Identifier id = RedStonerMod.id("trophy_panel");
|
||||
ResourceKey<Item> key = ResourceKey.create(Registries.ITEM, id);
|
||||
return Registry.register(BuiltInRegistries.ITEM, key,
|
||||
new TrophySignItem(TROPHY_SIGN, TROPHY_WALL_SIGN,
|
||||
new Item.Properties().useBlockDescriptionPrefix().setId(key)));
|
||||
}
|
||||
|
||||
private static ResourceKey<Block> blockKey(Identifier id) {
|
||||
return ResourceKey.create(Registries.BLOCK, id);
|
||||
}
|
||||
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
package fr.koka99cab.sanctuary26.redstoner.storage;
|
||||
|
||||
/** Keyboard routing shared by the terminal screen and its deterministic smoke test. */
|
||||
public final class StorageTerminalKeyRules {
|
||||
private StorageTerminalKeyRules() {}
|
||||
|
||||
public static boolean capturesInventoryKey(boolean searchFocused, boolean inventoryKey) {
|
||||
return searchFocused && inventoryKey;
|
||||
}
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
package fr.koka99cab.sanctuary26.redstoner.trophy;
|
||||
|
||||
/** Stable, persisted choice for the three ranking rows of a vanilla trophy sign. */
|
||||
public enum TrophyDisplayMode {
|
||||
NAMES_AND_VALUES("names_and_values"),
|
||||
NAMES("names"),
|
||||
VALUES("values");
|
||||
|
||||
private final String key;
|
||||
|
||||
TrophyDisplayMode(String key) {
|
||||
this.key = key;
|
||||
}
|
||||
|
||||
public String key() {
|
||||
return key;
|
||||
}
|
||||
|
||||
public TrophyDisplayMode next() {
|
||||
TrophyDisplayMode[] modes = values();
|
||||
return modes[(ordinal() + 1) % modes.length];
|
||||
}
|
||||
|
||||
public static TrophyDisplayMode byKey(String key) {
|
||||
for (TrophyDisplayMode mode : values()) if (mode.key.equals(key)) return mode;
|
||||
return NAMES_AND_VALUES;
|
||||
}
|
||||
}
|
||||
+46
-1
@@ -9,6 +9,12 @@ import net.minecraft.resources.Identifier;
|
||||
/** Pure bounded rules shared by the trophy panel and its smoke tests. */
|
||||
public final class TrophyPanelRules {
|
||||
public static final int MAX_PAGES = 16;
|
||||
public static final int SIGN_LINE_COUNT = 4;
|
||||
public static final int SIGN_ENTRIES_PER_PAGE = SIGN_LINE_COUNT - 1;
|
||||
/** Legacy plate dimensions kept only while old block entities migrate. */
|
||||
public static final int LINE_COUNT = 8;
|
||||
public static final int ENTRIES_PER_VIEW = LINE_COUNT - 1;
|
||||
private static final int VIEWS_PER_METRIC = 2;
|
||||
|
||||
private TrophyPanelRules() {}
|
||||
|
||||
@@ -17,7 +23,36 @@ public final class TrophyPanelRules {
|
||||
}
|
||||
|
||||
public static int nextPage(int currentPage, int pageCount) {
|
||||
return pageCount <= 0 ? 0 : Math.floorMod(currentPage + 1, pageCount);
|
||||
return pageByOffset(currentPage, pageCount, 1);
|
||||
}
|
||||
|
||||
public static int previousPage(int currentPage, int pageCount) {
|
||||
return pageByOffset(currentPage, pageCount, -1);
|
||||
}
|
||||
|
||||
public static int pageByOffset(int currentPage, int pageCount, int offset) {
|
||||
return pageCount <= 0 ? 0 : Math.floorMod(currentPage + offset, pageCount);
|
||||
}
|
||||
|
||||
public static DisplayPage displayPageByOffset(int metricPage, boolean valuesView,
|
||||
int metricCount, int offset) {
|
||||
int safeMetricCount = Math.max(1, metricCount);
|
||||
int safeMetricPage = Math.clamp(metricPage, 0, safeMetricCount - 1);
|
||||
int currentDisplayPage = safeMetricPage * VIEWS_PER_METRIC + (valuesView ? 1 : 0);
|
||||
int displayPage = Math.floorMod(currentDisplayPage + offset,
|
||||
safeMetricCount * VIEWS_PER_METRIC);
|
||||
return new DisplayPage(displayPage / VIEWS_PER_METRIC,
|
||||
displayPage % VIEWS_PER_METRIC == 1);
|
||||
}
|
||||
|
||||
public static int displayPageNumber(int metricPage, boolean valuesView, int metricCount) {
|
||||
int safeMetricCount = Math.max(1, metricCount);
|
||||
return Math.clamp(metricPage, 0, safeMetricCount - 1) * VIEWS_PER_METRIC
|
||||
+ (valuesView ? 2 : 1);
|
||||
}
|
||||
|
||||
public static int displayPageCount(int metricCount) {
|
||||
return Math.max(1, metricCount) * VIEWS_PER_METRIC;
|
||||
}
|
||||
|
||||
public static List<Identifier> mergeMetricIds(List<Identifier> current, List<Identifier> offered) {
|
||||
@@ -26,4 +61,14 @@ public final class TrophyPanelRules {
|
||||
if (offered != null) merged.addAll(offered);
|
||||
return List.copyOf(new ArrayList<>(merged).subList(0, Math.min(MAX_PAGES, merged.size())));
|
||||
}
|
||||
|
||||
public static List<Identifier> sanitizeMetricIds(List<Identifier> requested) {
|
||||
LinkedHashSet<Identifier> unique = new LinkedHashSet<>();
|
||||
if (requested != null) {
|
||||
requested.stream().filter(java.util.Objects::nonNull).limit(MAX_PAGES).forEach(unique::add);
|
||||
}
|
||||
return List.copyOf(unique);
|
||||
}
|
||||
|
||||
public record DisplayPage(int metricPage, boolean valuesView) {}
|
||||
}
|
||||
|
||||
+180
@@ -0,0 +1,180 @@
|
||||
package fr.koka99cab.sanctuary26.redstoner.trophy;
|
||||
|
||||
import com.mojang.serialization.Codec;
|
||||
import com.mojang.serialization.codecs.RecordCodecBuilder;
|
||||
import fr.koka99cab.sanctuary26.redstoner.RedStonerMod;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.world.level.Level;
|
||||
import net.minecraft.world.level.saveddata.SavedData;
|
||||
import net.minecraft.world.level.saveddata.SavedDataType;
|
||||
|
||||
/** Versioned, server-owned configurations for the vanilla block entities used by trophy signs. */
|
||||
public final class TrophySignData extends SavedData {
|
||||
public static final int DATA_VERSION = 1;
|
||||
public static final String SAVE_KEY = "trophy_signs";
|
||||
public static final int MAX_SIGNS = 16_384;
|
||||
|
||||
private static final Codec<Config> CONFIG_CODEC = RecordCodecBuilder.create(instance -> instance.group(
|
||||
Codec.STRING.fieldOf("dimension").forGetter(Config::dimension),
|
||||
Codec.LONG.fieldOf("pos").forGetter(Config::pos),
|
||||
Codec.STRING.fieldOf("owner").forGetter(Config::owner),
|
||||
Codec.STRING.optionalFieldOf("owner_name", "?").forGetter(Config::ownerName),
|
||||
Identifier.CODEC.listOf(0, TrophyPanelRules.MAX_PAGES).optionalFieldOf("pages", List.of())
|
||||
.forGetter(Config::pages),
|
||||
Codec.INT.optionalFieldOf("current_page", 0).forGetter(Config::currentPage),
|
||||
Codec.STRING.optionalFieldOf("display", TrophyDisplayMode.NAMES_AND_VALUES.key())
|
||||
.forGetter(Config::display),
|
||||
Codec.BOOL.optionalFieldOf("configured", false).forGetter(Config::configured)
|
||||
).apply(instance, Config::new));
|
||||
private static final Codec<TrophySignData> CODEC = RecordCodecBuilder.create(instance -> instance.group(
|
||||
Codec.INT.optionalFieldOf("data_version", DATA_VERSION).forGetter(data -> data.dataVersion),
|
||||
CONFIG_CODEC.listOf().optionalFieldOf("signs", List.of()).forGetter(TrophySignData::entriesForCodec)
|
||||
).apply(instance, TrophySignData::new));
|
||||
private static final SavedDataType<TrophySignData> TYPE = new SavedDataType<>(
|
||||
RedStonerMod.id(SAVE_KEY), TrophySignData::new, CODEC, null);
|
||||
|
||||
private int dataVersion;
|
||||
private final List<Config> signs;
|
||||
|
||||
public TrophySignData() {
|
||||
this(DATA_VERSION, List.of());
|
||||
}
|
||||
|
||||
private TrophySignData(int dataVersion, List<Config> signs) {
|
||||
this.dataVersion = dataVersion;
|
||||
this.signs = new ArrayList<>(signs == null ? List.of()
|
||||
: signs.stream().limit(MAX_SIGNS).toList());
|
||||
}
|
||||
|
||||
public static TrophySignData get(MinecraftServer server) {
|
||||
ServerLevel overworld = server.getLevel(Level.OVERWORLD);
|
||||
if (overworld == null) throw new IllegalStateException("Overworld unavailable for trophy-sign data");
|
||||
TrophySignData data = overworld.getDataStorage().computeIfAbsent(TYPE);
|
||||
data.upgradeIfRequired();
|
||||
return data;
|
||||
}
|
||||
|
||||
public boolean writable() {
|
||||
return dataVersion <= DATA_VERSION;
|
||||
}
|
||||
|
||||
public List<Config> signs() {
|
||||
return List.copyOf(signs);
|
||||
}
|
||||
|
||||
public Optional<Config> signAt(ServerLevel level, BlockPos pos) {
|
||||
return signAt(dimension(level), pos.asLong());
|
||||
}
|
||||
|
||||
public Optional<Config> signAt(String dimension, long pos) {
|
||||
return signs.stream().filter(sign -> sign.dimension().equals(dimension) && sign.pos() == pos).findFirst();
|
||||
}
|
||||
|
||||
public boolean add(Config config) {
|
||||
if (!writable() || signs.size() >= MAX_SIGNS
|
||||
|| signAt(config.dimension(), config.pos()).isPresent()) return false;
|
||||
signs.add(config);
|
||||
setDirty();
|
||||
return true;
|
||||
}
|
||||
|
||||
public boolean replace(Config replacement) {
|
||||
if (!writable()) return false;
|
||||
for (int index = 0; index < signs.size(); index++) {
|
||||
Config current = signs.get(index);
|
||||
if (!current.dimension().equals(replacement.dimension()) || current.pos() != replacement.pos()) continue;
|
||||
signs.set(index, replacement);
|
||||
setDirty();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean remove(Config config) {
|
||||
if (!writable()) return false;
|
||||
boolean removed = signs.remove(config);
|
||||
if (removed) setDirty();
|
||||
return removed;
|
||||
}
|
||||
|
||||
private List<Config> entriesForCodec() {
|
||||
return List.copyOf(signs);
|
||||
}
|
||||
|
||||
private void upgradeIfRequired() {
|
||||
if (dataVersion < DATA_VERSION) {
|
||||
int previous = dataVersion;
|
||||
dataVersion = DATA_VERSION;
|
||||
setDirty();
|
||||
RedStonerMod.LOGGER.info("[{}] Migrated trophy-sign data from v{} to v{}.",
|
||||
RedStonerMod.DISPLAY_NAME, previous, dataVersion);
|
||||
} else if (dataVersion > DATA_VERSION) {
|
||||
RedStonerMod.LOGGER.warn("[{}] Trophy-sign data v{} is newer than supported v{}.",
|
||||
RedStonerMod.DISPLAY_NAME, dataVersion, DATA_VERSION);
|
||||
}
|
||||
}
|
||||
|
||||
public static String dimension(ServerLevel level) {
|
||||
return level.dimension().identifier().toString();
|
||||
}
|
||||
|
||||
public record Config(String dimension, long pos, String owner, String ownerName,
|
||||
List<Identifier> pages, int currentPage, String display, boolean configured) {
|
||||
public Config {
|
||||
dimension = clean(dimension, 128, "minecraft:overworld");
|
||||
owner = cleanUuid(owner);
|
||||
ownerName = clean(ownerName, 32, "?");
|
||||
pages = TrophyPanelRules.sanitizeMetricIds(pages);
|
||||
currentPage = pages.isEmpty() ? 0 : Math.clamp(currentPage, 0, pages.size() - 1);
|
||||
display = TrophyDisplayMode.byKey(display).key();
|
||||
configured = configured && !pages.isEmpty();
|
||||
}
|
||||
|
||||
public static Config draft(ServerLevel level, BlockPos pos, UUID ownerId, String ownerName) {
|
||||
return new Config(TrophySignData.dimension(level), pos.asLong(), ownerId.toString(), ownerName,
|
||||
List.of(), 0, TrophyDisplayMode.NAMES_AND_VALUES.key(), false);
|
||||
}
|
||||
|
||||
public UUID ownerId() {
|
||||
return UUID.fromString(owner);
|
||||
}
|
||||
|
||||
public TrophyDisplayMode displayMode() {
|
||||
return TrophyDisplayMode.byKey(display);
|
||||
}
|
||||
|
||||
public Identifier currentMetric() {
|
||||
return pages.get(Math.clamp(currentPage, 0, pages.size() - 1));
|
||||
}
|
||||
|
||||
public Config withConfiguration(List<Identifier> requestedPages, TrophyDisplayMode mode,
|
||||
String retainedOwnerName) {
|
||||
List<Identifier> sanitized = TrophyPanelRules.sanitizeMetricIds(requestedPages);
|
||||
return new Config(dimension, pos, owner, retainedOwnerName, sanitized, 0, mode.key(), true);
|
||||
}
|
||||
|
||||
public Config withCurrentPage(int page) {
|
||||
return new Config(dimension, pos, owner, ownerName, pages, page, display, configured);
|
||||
}
|
||||
}
|
||||
|
||||
private static String clean(String value, int maximum, String fallback) {
|
||||
String clean = value == null || value.isBlank() ? fallback : value.strip();
|
||||
return clean.substring(0, Math.min(maximum, clean.length()));
|
||||
}
|
||||
|
||||
private static String cleanUuid(String value) {
|
||||
try {
|
||||
return UUID.fromString(value).toString();
|
||||
} catch (IllegalArgumentException | NullPointerException ignored) {
|
||||
return new UUID(0L, 0L).toString();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package fr.koka99cab.sanctuary26.redstoner.trophy;
|
||||
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.item.SignItem;
|
||||
import net.minecraft.world.level.Level;
|
||||
import net.minecraft.world.level.block.Block;
|
||||
import net.minecraft.world.level.block.entity.SignBlockEntity;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
|
||||
/** The historical trophy-panel item now places an ordinary standing or wall sign. */
|
||||
public final class TrophySignItem extends SignItem {
|
||||
public TrophySignItem(Block standing, Block wall, Properties properties) {
|
||||
super(standing, wall, properties);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean updateCustomBlockEntityTag(BlockPos pos, Level level, Player player,
|
||||
ItemStack stack, BlockState state) {
|
||||
if (level instanceof ServerLevel serverLevel && player instanceof ServerPlayer serverPlayer
|
||||
&& level.getBlockEntity(pos) instanceof SignBlockEntity sign) {
|
||||
TrophySignService.activatePlacedSign(serverPlayer, serverLevel, pos, sign);
|
||||
}
|
||||
// The bounded trophy editor replaces the vanilla text editor.
|
||||
return true;
|
||||
}
|
||||
}
|
||||
+271
@@ -0,0 +1,271 @@
|
||||
package fr.koka99cab.sanctuary26.redstoner.trophy;
|
||||
|
||||
import fr.koka99cab.sanctuary26.redstoner.network.OpenTrophySignEditorPayload;
|
||||
import fr.koka99cab.sanctuary26.redstoner.network.SaveTrophySignPayload;
|
||||
import fr.koka99cab.sanctuary26.redstoner.registry.RedStonerRegistries;
|
||||
import fr.koka99cab.sanctuary26.sanctuary.api.ranking.RankingMetric;
|
||||
import fr.koka99cab.sanctuary26.sanctuary.api.ranking.RankingSnapshot;
|
||||
import fr.koka99cab.sanctuary26.sanctuary.api.ranking.RankingUnit;
|
||||
import fr.koka99cab.sanctuary26.sanctuary.api.ranking.SanctuaryRankingApi;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import net.fabricmc.fabric.api.event.lifecycle.v1.ServerTickEvents;
|
||||
import net.fabricmc.fabric.api.event.player.PlayerBlockBreakEvents;
|
||||
import net.fabricmc.fabric.api.event.player.UseBlockCallback;
|
||||
import net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking;
|
||||
import net.minecraft.ChatFormatting;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Direction;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.server.permissions.Permissions;
|
||||
import net.minecraft.world.InteractionHand;
|
||||
import net.minecraft.world.InteractionResult;
|
||||
import net.minecraft.world.level.block.Block;
|
||||
import net.minecraft.world.level.block.SignBlock;
|
||||
import net.minecraft.world.level.block.WallSignBlock;
|
||||
import net.minecraft.world.level.block.entity.SignBlockEntity;
|
||||
import net.minecraft.world.level.block.entity.SignText;
|
||||
|
||||
/** Server-authoritative ownership, paging, editing and refresh behavior for white vanilla trophy signs. */
|
||||
public final class TrophySignService {
|
||||
private static final int REFRESH_INTERVAL_TICKS = 20;
|
||||
private static final int MAX_INSPECTIONS_PER_REFRESH = 128;
|
||||
private static int tickCounter;
|
||||
private static int refreshCursor;
|
||||
private static boolean initialized;
|
||||
|
||||
private TrophySignService() {}
|
||||
|
||||
public static void initialize() {
|
||||
if (initialized) return;
|
||||
initialized = true;
|
||||
UseBlockCallback.EVENT.register((player, level, hand, hit) -> {
|
||||
BlockPos pos = hit.getBlockPos();
|
||||
if (hand != InteractionHand.MAIN_HAND
|
||||
|| !RedStonerRegistries.isTrophySign(level.getBlockState(pos).getBlock())
|
||||
|| !(level.getBlockEntity(pos) instanceof SignBlockEntity sign)) {
|
||||
return InteractionResult.PASS;
|
||||
}
|
||||
if (level.isClientSide()) return InteractionResult.SUCCESS;
|
||||
return player instanceof ServerPlayer serverPlayer
|
||||
? interact(serverPlayer, (ServerLevel) level, pos, sign)
|
||||
: InteractionResult.PASS;
|
||||
});
|
||||
PlayerBlockBreakEvents.BEFORE.register((level, player, pos, state, blockEntity) -> {
|
||||
if (!(level instanceof ServerLevel serverLevel) || !RedStonerRegistries.isTrophySign(state.getBlock())) {
|
||||
return true;
|
||||
}
|
||||
TrophySignData.Config config = TrophySignData.get(serverLevel.getServer())
|
||||
.signAt(serverLevel, pos).orElse(null);
|
||||
if (config == null || config.ownerId().equals(player.getUUID())) return true;
|
||||
if (player instanceof ServerPlayer serverPlayer && isOperator(serverPlayer)) return true;
|
||||
if (player instanceof ServerPlayer serverPlayer) message(serverPlayer, "protected");
|
||||
return false;
|
||||
});
|
||||
PlayerBlockBreakEvents.AFTER.register((level, player, pos, state, blockEntity) -> {
|
||||
if (!(level instanceof ServerLevel serverLevel) || !RedStonerRegistries.isTrophySign(state.getBlock())) {
|
||||
return;
|
||||
}
|
||||
TrophySignData data = TrophySignData.get(serverLevel.getServer());
|
||||
data.signAt(serverLevel, pos).ifPresent(data::remove);
|
||||
});
|
||||
ServerTickEvents.END_SERVER_TICK.register(TrophySignService::tick);
|
||||
}
|
||||
|
||||
/** Creates a blank owned sign and immediately opens its custom editor. */
|
||||
public static void activatePlacedSign(ServerPlayer player, ServerLevel level, BlockPos pos,
|
||||
SignBlockEntity sign) {
|
||||
TrophySignData data = TrophySignData.get(level.getServer());
|
||||
TrophySignData.Config existing = data.signAt(level, pos).orElse(null);
|
||||
if (existing == null) {
|
||||
TrophySignData.Config draft = TrophySignData.Config.draft(level, pos, player.getUUID(),
|
||||
player.getGameProfile().name());
|
||||
if (!data.add(draft)) {
|
||||
message(player, data.writable() ? "registry_full" : "newer_data");
|
||||
return;
|
||||
}
|
||||
existing = draft;
|
||||
}
|
||||
writeBlankSign(level, sign);
|
||||
openEditor(player, existing);
|
||||
}
|
||||
|
||||
private static InteractionResult interact(ServerPlayer player, ServerLevel level, BlockPos pos,
|
||||
SignBlockEntity sign) {
|
||||
TrophySignData data = TrophySignData.get(level.getServer());
|
||||
TrophySignData.Config config = data.signAt(level, pos).orElse(null);
|
||||
if (config == null) {
|
||||
activatePlacedSign(player, level, pos, sign);
|
||||
return InteractionResult.SUCCESS_SERVER;
|
||||
}
|
||||
boolean canConfigure = TrophyPanelRules.canConfigure(config.ownerId(), player.getUUID(), isOperator(player));
|
||||
if (!config.configured() || player.isSecondaryUseActive()) {
|
||||
if (canConfigure) openEditor(player, config);
|
||||
else message(player, "not_owner");
|
||||
return InteractionResult.SUCCESS_SERVER;
|
||||
}
|
||||
int page = TrophyPanelRules.nextPage(config.currentPage(), config.pages().size());
|
||||
TrophySignData.Config advanced = config.withCurrentPage(page);
|
||||
if (!data.replace(advanced)) {
|
||||
message(player, "newer_data");
|
||||
return InteractionResult.CONSUME;
|
||||
}
|
||||
writeSign(level, sign, advanced);
|
||||
message(player, "page", advanced.currentPage() + 1, advanced.pages().size());
|
||||
return InteractionResult.SUCCESS_SERVER;
|
||||
}
|
||||
|
||||
public static void save(ServerPlayer player, SaveTrophySignPayload payload) {
|
||||
if (player.distanceToSqr(net.minecraft.world.phys.Vec3.atCenterOf(payload.pos())) > 64.0D) {
|
||||
message(player, "too_far");
|
||||
return;
|
||||
}
|
||||
ServerLevel level = player.level();
|
||||
if (!RedStonerRegistries.isTrophySign(level.getBlockState(payload.pos()).getBlock())
|
||||
|| !(level.getBlockEntity(payload.pos()) instanceof SignBlockEntity sign)) {
|
||||
message(player, "missing");
|
||||
return;
|
||||
}
|
||||
TrophySignData data = TrophySignData.get(level.getServer());
|
||||
TrophySignData.Config config = data.signAt(level, payload.pos()).orElse(null);
|
||||
if (config == null || !TrophyPanelRules.canConfigure(config.ownerId(), player.getUUID(), isOperator(player))) {
|
||||
message(player, "not_owner");
|
||||
return;
|
||||
}
|
||||
List<Identifier> pages = TrophyPanelRules.sanitizeMetricIds(payload.pages()).stream()
|
||||
.filter(SanctuaryRankingApi::isConfigurableMetric).toList();
|
||||
if (pages.isEmpty() || pages.size() != payload.pages().size()) {
|
||||
message(player, "invalid");
|
||||
return;
|
||||
}
|
||||
TrophySignData.Config configured = config.withConfiguration(pages,
|
||||
TrophyDisplayMode.byKey(payload.display()), config.ownerName());
|
||||
if (!data.replace(configured)) {
|
||||
message(player, "newer_data");
|
||||
return;
|
||||
}
|
||||
writeSign(level, sign, configured);
|
||||
message(player, "saved");
|
||||
}
|
||||
|
||||
public static boolean migrateLegacy(ServerLevel level, BlockPos pos, Direction facing, UUID owner,
|
||||
String ownerName, List<Identifier> requestedPages, int currentPage) {
|
||||
TrophySignData data = TrophySignData.get(level.getServer());
|
||||
if (!data.writable()) return false;
|
||||
UUID retainedOwner = owner == null ? new UUID(0L, 0L) : owner;
|
||||
TrophySignData.Config previous = data.signAt(level, pos).orElse(null);
|
||||
if (previous == null && data.signs().size() >= TrophySignData.MAX_SIGNS) return false;
|
||||
List<Identifier> pages = TrophyPanelRules.sanitizeMetricIds(requestedPages).stream()
|
||||
.filter(SanctuaryRankingApi::isConfigurableMetric).toList();
|
||||
if (pages.isEmpty()) pages = List.of(SanctuaryRankingApi.defaultMetric().id());
|
||||
var state = RedStonerRegistries.TROPHY_WALL_SIGN.defaultBlockState()
|
||||
.setValue(WallSignBlock.FACING, facing)
|
||||
.setValue(SignBlock.WATERLOGGED, false);
|
||||
if (!state.canSurvive(level, pos) || !level.setBlock(pos, state, Block.UPDATE_ALL)) return false;
|
||||
TrophySignData.Config migrated = new TrophySignData.Config(TrophySignData.dimension(level), pos.asLong(),
|
||||
retainedOwner.toString(), ownerName, pages, currentPage, TrophyDisplayMode.NAMES_AND_VALUES.key(), true);
|
||||
boolean stored = previous == null ? data.add(migrated) : data.replace(migrated);
|
||||
if (!stored) return false;
|
||||
if (level.getBlockEntity(pos) instanceof SignBlockEntity sign) writeSign(level, sign, migrated);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static void openEditor(ServerPlayer player, TrophySignData.Config config) {
|
||||
if (!ServerPlayNetworking.canSend(player, OpenTrophySignEditorPayload.TYPE)) {
|
||||
message(player, "client_required");
|
||||
return;
|
||||
}
|
||||
ServerPlayNetworking.send(player, new OpenTrophySignEditorPayload(
|
||||
BlockPos.of(config.pos()), config.pages(), config.display()));
|
||||
}
|
||||
|
||||
private static void tick(MinecraftServer server) {
|
||||
if (++tickCounter < REFRESH_INTERVAL_TICKS) return;
|
||||
tickCounter = 0;
|
||||
TrophySignData data = TrophySignData.get(server);
|
||||
List<TrophySignData.Config> signs = data.signs();
|
||||
if (signs.isEmpty()) {
|
||||
refreshCursor = 0;
|
||||
return;
|
||||
}
|
||||
int inspections = Math.min(MAX_INSPECTIONS_PER_REFRESH, signs.size());
|
||||
for (int offset = 0; offset < inspections; offset++) {
|
||||
TrophySignData.Config config = signs.get(Math.floorMod(refreshCursor + offset, signs.size()));
|
||||
ServerLevel level = level(server, config.dimension());
|
||||
BlockPos pos = BlockPos.of(config.pos());
|
||||
if (level == null || !level.isLoaded(pos)) continue;
|
||||
if (!RedStonerRegistries.isTrophySign(level.getBlockState(pos).getBlock())
|
||||
|| !(level.getBlockEntity(pos) instanceof SignBlockEntity sign)) {
|
||||
data.remove(config);
|
||||
continue;
|
||||
}
|
||||
if (config.configured()) writeSign(level, sign, config);
|
||||
else writeBlankSign(level, sign);
|
||||
}
|
||||
refreshCursor = Math.floorMod(refreshCursor + inspections, Math.max(1, signs.size()));
|
||||
}
|
||||
|
||||
private static void writeBlankSign(ServerLevel level, SignBlockEntity sign) {
|
||||
writeText(level, sign, new SignText());
|
||||
}
|
||||
|
||||
private static void writeSign(ServerLevel level, SignBlockEntity sign, TrophySignData.Config config) {
|
||||
Identifier metricId = config.currentMetric();
|
||||
RankingSnapshot snapshot = SanctuaryRankingApi.snapshot(level.getServer(), metricId, config.ownerId());
|
||||
RankingMetric metric = snapshot.metric();
|
||||
RankingUnit unit = metric.unit();
|
||||
SignText text = new SignText().setMessage(0,
|
||||
Component.translatable("block.redstoner.trophy_sign.page_title",
|
||||
config.currentPage() + 1, config.pages().size(), SanctuaryRankingApi.displayName(metric))
|
||||
.withStyle(ChatFormatting.DARK_GRAY, ChatFormatting.BOLD));
|
||||
if (snapshot.podium().isEmpty()) {
|
||||
text = text.setMessage(1, Component.translatable("block.redstoner.trophy_sign.no_score")
|
||||
.withStyle(ChatFormatting.GRAY));
|
||||
} else {
|
||||
for (int index = 0; index < TrophyPanelRules.SIGN_ENTRIES_PER_PAGE
|
||||
&& index < snapshot.podium().size(); index++) {
|
||||
RankingSnapshot.Entry entry = snapshot.podium().get(index);
|
||||
text = text.setMessage(index + 1, rankingLine(config.displayMode(), entry, unit));
|
||||
}
|
||||
}
|
||||
writeText(level, sign, text);
|
||||
}
|
||||
|
||||
private static Component rankingLine(TrophyDisplayMode mode, RankingSnapshot.Entry entry, RankingUnit unit) {
|
||||
return switch (mode) {
|
||||
case NAMES_AND_VALUES -> Component.translatable("block.redstoner.trophy_sign.names_and_values",
|
||||
entry.rank(), entry.playerName(), unit.format(entry.value()));
|
||||
case NAMES -> Component.translatable("block.redstoner.trophy_sign.names",
|
||||
entry.rank(), entry.playerName());
|
||||
case VALUES -> Component.translatable("block.redstoner.trophy_sign.values",
|
||||
entry.rank(), unit.format(entry.value()));
|
||||
};
|
||||
}
|
||||
|
||||
private static void writeText(ServerLevel level, SignBlockEntity sign, SignText text) {
|
||||
sign.setText(text, true);
|
||||
sign.setText(text, false);
|
||||
sign.setWaxed(true);
|
||||
sign.setChanged();
|
||||
level.sendBlockUpdated(sign.getBlockPos(), sign.getBlockState(), sign.getBlockState(), 3);
|
||||
}
|
||||
|
||||
private static ServerLevel level(MinecraftServer server, String dimension) {
|
||||
for (ServerLevel level : server.getAllLevels()) {
|
||||
if (TrophySignData.dimension(level).equals(dimension)) return level;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static boolean isOperator(ServerPlayer player) {
|
||||
return player.createCommandSourceStack().permissions().hasPermission(Permissions.COMMANDS_GAMEMASTER);
|
||||
}
|
||||
|
||||
private static void message(ServerPlayer player, String key, Object... args) {
|
||||
player.sendSystemMessage(Component.translatable("message.redstoner.trophy_sign." + key, args));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"variants": {
|
||||
"rotation=0": { "model": "redstoner:block/trophy_sign_rot_0" },
|
||||
"rotation=1": { "model": "redstoner:block/trophy_sign_rot_1" },
|
||||
"rotation=2": { "model": "redstoner:block/trophy_sign_rot_2" },
|
||||
"rotation=3": { "model": "redstoner:block/trophy_sign_rot_3" },
|
||||
"rotation=4": { "model": "redstoner:block/trophy_sign_rot_0", "y": 90 },
|
||||
"rotation=5": { "model": "redstoner:block/trophy_sign_rot_1", "y": 90 },
|
||||
"rotation=6": { "model": "redstoner:block/trophy_sign_rot_2", "y": 90 },
|
||||
"rotation=7": { "model": "redstoner:block/trophy_sign_rot_3", "y": 90 },
|
||||
"rotation=8": { "model": "redstoner:block/trophy_sign_rot_0", "y": 180 },
|
||||
"rotation=9": { "model": "redstoner:block/trophy_sign_rot_1", "y": 180 },
|
||||
"rotation=10": { "model": "redstoner:block/trophy_sign_rot_2", "y": 180 },
|
||||
"rotation=11": { "model": "redstoner:block/trophy_sign_rot_3", "y": 180 },
|
||||
"rotation=12": { "model": "redstoner:block/trophy_sign_rot_0", "y": 270 },
|
||||
"rotation=13": { "model": "redstoner:block/trophy_sign_rot_1", "y": 270 },
|
||||
"rotation=14": { "model": "redstoner:block/trophy_sign_rot_2", "y": 270 },
|
||||
"rotation=15": { "model": "redstoner:block/trophy_sign_rot_3", "y": 270 }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"variants": {
|
||||
"facing=east": { "model": "redstoner:block/trophy_wall_sign", "y": 270 },
|
||||
"facing=north": { "model": "redstoner:block/trophy_wall_sign", "y": 180 },
|
||||
"facing=south": { "model": "redstoner:block/trophy_wall_sign" },
|
||||
"facing=west": { "model": "redstoner:block/trophy_wall_sign", "y": 90 }
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"model": {
|
||||
"type": "minecraft:model",
|
||||
"model": "redstoner:block/trophy_panel"
|
||||
"model": "redstoner:item/trophy_sign"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,6 +64,36 @@
|
||||
"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.",
|
||||
"block.redstoner.trophy_panel": "Trophy Panel",
|
||||
"block.redstoner.trophy_sign": "Trophy Sign",
|
||||
"block.redstoner.trophy_wall_sign": "Wall Trophy Sign",
|
||||
"block.redstoner.trophy_sign.page_title": "[%s/%s] %s",
|
||||
"block.redstoner.trophy_sign.names_and_values": "#%s %s · %s",
|
||||
"block.redstoner.trophy_sign.names": "#%s %s",
|
||||
"block.redstoner.trophy_sign.values": "#%s %s",
|
||||
"block.redstoner.trophy_sign.no_score": "No score yet",
|
||||
"screen.redstoner.trophy_sign.title": "Configure Trophy Sign",
|
||||
"screen.redstoner.trophy_sign.search": "Search for a statistic…",
|
||||
"screen.redstoner.trophy_sign.available": "Available statistics",
|
||||
"screen.redstoner.trophy_sign.pages": "Pages %s/%s",
|
||||
"screen.redstoner.trophy_sign.remove": "Remove",
|
||||
"screen.redstoner.trophy_sign.display": "Display: %s",
|
||||
"screen.redstoner.trophy_sign.display.names_and_values": "names and values",
|
||||
"screen.redstoner.trophy_sign.display.names": "names only",
|
||||
"screen.redstoner.trophy_sign.display.values": "values only",
|
||||
"message.redstoner.trophy_sign.not_owner": "Only the creator or an operator can configure this sign.",
|
||||
"message.redstoner.trophy_sign.protected": "Only the creator or an operator can break this sign.",
|
||||
"message.redstoner.trophy_sign.saved": "Trophy sign configured.",
|
||||
"message.redstoner.trophy_sign.page": "Page %s/%s",
|
||||
"message.redstoner.trophy_sign.invalid": "The configuration contains an invalid statistic.",
|
||||
"message.redstoner.trophy_sign.missing": "This trophy sign no longer exists.",
|
||||
"message.redstoner.trophy_sign.too_far": "You are too far away from this sign.",
|
||||
"message.redstoner.trophy_sign.client_required": "An up-to-date Red-Stoner client is required to configure this sign.",
|
||||
"message.redstoner.trophy_sign.registry_full": "This world has reached its trophy-sign limit.",
|
||||
"message.redstoner.trophy_sign.newer_data": "This sign data comes from a newer version and remains read-only.",
|
||||
"block.redstoner.trophy_panel.page_title": "[%s/%s] %s",
|
||||
"block.redstoner.trophy_panel.podium": "#%s %s · %s",
|
||||
"block.redstoner.trophy_panel.player": "#%s %s",
|
||||
"block.redstoner.trophy_panel.value": "#%s %s",
|
||||
"block.redstoner.trophy_panel.no_score": "No score yet",
|
||||
"block.redstoner.trophy_panel.champion": "#1 %s · %s",
|
||||
"block.redstoner.trophy_panel.owner_rank": "%s · #%s · %s",
|
||||
|
||||
@@ -64,6 +64,36 @@
|
||||
"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é.",
|
||||
"block.redstoner.trophy_panel": "Panneau de trophées",
|
||||
"block.redstoner.trophy_sign": "Panneau de trophées",
|
||||
"block.redstoner.trophy_wall_sign": "Panneau mural de trophées",
|
||||
"block.redstoner.trophy_sign.page_title": "[%s/%s] %s",
|
||||
"block.redstoner.trophy_sign.names_and_values": "#%s %s · %s",
|
||||
"block.redstoner.trophy_sign.names": "#%s %s",
|
||||
"block.redstoner.trophy_sign.values": "#%s %s",
|
||||
"block.redstoner.trophy_sign.no_score": "Aucun score",
|
||||
"screen.redstoner.trophy_sign.title": "Configurer le panneau de trophées",
|
||||
"screen.redstoner.trophy_sign.search": "Rechercher une statistique…",
|
||||
"screen.redstoner.trophy_sign.available": "Statistiques disponibles",
|
||||
"screen.redstoner.trophy_sign.pages": "Pages %s/%s",
|
||||
"screen.redstoner.trophy_sign.remove": "Retirer",
|
||||
"screen.redstoner.trophy_sign.display": "Affichage : %s",
|
||||
"screen.redstoner.trophy_sign.display.names_and_values": "noms et valeurs",
|
||||
"screen.redstoner.trophy_sign.display.names": "noms seulement",
|
||||
"screen.redstoner.trophy_sign.display.values": "valeurs seulement",
|
||||
"message.redstoner.trophy_sign.not_owner": "Seul le créateur ou un opérateur peut configurer ce panneau.",
|
||||
"message.redstoner.trophy_sign.protected": "Seul le créateur ou un opérateur peut casser ce panneau.",
|
||||
"message.redstoner.trophy_sign.saved": "Panneau de trophées configuré.",
|
||||
"message.redstoner.trophy_sign.page": "Page %s/%s",
|
||||
"message.redstoner.trophy_sign.invalid": "La configuration contient une statistique invalide.",
|
||||
"message.redstoner.trophy_sign.missing": "Ce panneau de trophées n’existe plus.",
|
||||
"message.redstoner.trophy_sign.too_far": "Tu es trop loin de ce panneau.",
|
||||
"message.redstoner.trophy_sign.client_required": "Un client Red-Stoner à jour est requis pour configurer ce panneau.",
|
||||
"message.redstoner.trophy_sign.registry_full": "La limite de panneaux de trophées du monde est atteinte.",
|
||||
"message.redstoner.trophy_sign.newer_data": "Ces données de panneau proviennent d’une version plus récente et restent en lecture seule.",
|
||||
"block.redstoner.trophy_panel.page_title": "[%s/%s] %s",
|
||||
"block.redstoner.trophy_panel.podium": "#%s %s · %s",
|
||||
"block.redstoner.trophy_panel.player": "#%s %s",
|
||||
"block.redstoner.trophy_panel.value": "#%s %s",
|
||||
"block.redstoner.trophy_panel.no_score": "Aucun score",
|
||||
"block.redstoner.trophy_panel.champion": "#1 %s · %s",
|
||||
"block.redstoner.trophy_panel.owner_rank": "%s · #%s · %s",
|
||||
|
||||
@@ -64,6 +64,36 @@
|
||||
"screen.redstoner.storage_terminal.error.too_large": "Сеть слишком велика (не более 128 хранилищ).",
|
||||
"screen.redstoner.storage_terminal.error.locked": "В сети есть запертое хранилище.",
|
||||
"block.redstoner.trophy_panel": "Панель трофеев",
|
||||
"block.redstoner.trophy_sign": "Табличка трофеев",
|
||||
"block.redstoner.trophy_wall_sign": "Настенная табличка трофеев",
|
||||
"block.redstoner.trophy_sign.page_title": "[%s/%s] %s",
|
||||
"block.redstoner.trophy_sign.names_and_values": "#%s %s · %s",
|
||||
"block.redstoner.trophy_sign.names": "#%s %s",
|
||||
"block.redstoner.trophy_sign.values": "#%s %s",
|
||||
"block.redstoner.trophy_sign.no_score": "Результатов пока нет",
|
||||
"screen.redstoner.trophy_sign.title": "Настройка таблички трофеев",
|
||||
"screen.redstoner.trophy_sign.search": "Найти статистику…",
|
||||
"screen.redstoner.trophy_sign.available": "Доступная статистика",
|
||||
"screen.redstoner.trophy_sign.pages": "Страницы %s/%s",
|
||||
"screen.redstoner.trophy_sign.remove": "Удалить",
|
||||
"screen.redstoner.trophy_sign.display": "Отображение: %s",
|
||||
"screen.redstoner.trophy_sign.display.names_and_values": "имена и значения",
|
||||
"screen.redstoner.trophy_sign.display.names": "только имена",
|
||||
"screen.redstoner.trophy_sign.display.values": "только значения",
|
||||
"message.redstoner.trophy_sign.not_owner": "Настраивать эту табличку может только создатель или оператор.",
|
||||
"message.redstoner.trophy_sign.protected": "Сломать эту табличку может только создатель или оператор.",
|
||||
"message.redstoner.trophy_sign.saved": "Табличка трофеев настроена.",
|
||||
"message.redstoner.trophy_sign.page": "Страница %s/%s",
|
||||
"message.redstoner.trophy_sign.invalid": "Настройка содержит недопустимую статистику.",
|
||||
"message.redstoner.trophy_sign.missing": "Этой таблички трофеев больше нет.",
|
||||
"message.redstoner.trophy_sign.too_far": "Вы слишком далеко от этой таблички.",
|
||||
"message.redstoner.trophy_sign.client_required": "Для настройки нужен обновлённый клиент Red-Stoner.",
|
||||
"message.redstoner.trophy_sign.registry_full": "Достигнут предел табличек трофеев для этого мира.",
|
||||
"message.redstoner.trophy_sign.newer_data": "Данные таблички созданы более новой версией и доступны только для чтения.",
|
||||
"block.redstoner.trophy_panel.page_title": "[%s/%s] %s",
|
||||
"block.redstoner.trophy_panel.podium": "#%s %s · %s",
|
||||
"block.redstoner.trophy_panel.player": "#%s %s",
|
||||
"block.redstoner.trophy_panel.value": "#%s %s",
|
||||
"block.redstoner.trophy_panel.no_score": "Результатов пока нет",
|
||||
"block.redstoner.trophy_panel.champion": "#1 %s · %s",
|
||||
"block.redstoner.trophy_panel.owner_rank": "%s · #%s · %s",
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
{
|
||||
"parent": "minecraft:block/block",
|
||||
"textures": {
|
||||
"particle": "minecraft:block/dark_oak_planks",
|
||||
"wood": "minecraft:block/dark_oak_planks",
|
||||
"frame": "minecraft:block/gold_block",
|
||||
"screen": "minecraft:block/black_concrete"
|
||||
"particle": "minecraft:block/oak_planks",
|
||||
"wood": "minecraft:block/oak_planks"
|
||||
},
|
||||
"elements": [
|
||||
{
|
||||
"from": [0, 0, 13],
|
||||
"from": [0, 0, 14],
|
||||
"to": [16, 16, 16],
|
||||
"faces": {
|
||||
"north": { "texture": "#wood" },
|
||||
@@ -18,31 +16,6 @@
|
||||
"up": { "texture": "#wood" },
|
||||
"down": { "texture": "#wood" }
|
||||
}
|
||||
},
|
||||
{
|
||||
"from": [1.5, 1.5, 12.5],
|
||||
"to": [14.5, 14.5, 13],
|
||||
"faces": { "north": { "texture": "#screen" } }
|
||||
},
|
||||
{
|
||||
"from": [0.5, 0.5, 12.25],
|
||||
"to": [15.5, 1.5, 13],
|
||||
"faces": { "north": { "texture": "#frame" } }
|
||||
},
|
||||
{
|
||||
"from": [0.5, 14.5, 12.25],
|
||||
"to": [15.5, 15.5, 13],
|
||||
"faces": { "north": { "texture": "#frame" } }
|
||||
},
|
||||
{
|
||||
"from": [0.5, 1.5, 12.25],
|
||||
"to": [1.5, 14.5, 13],
|
||||
"faces": { "north": { "texture": "#frame" } }
|
||||
},
|
||||
{
|
||||
"from": [14.5, 1.5, 12.25],
|
||||
"to": [15.5, 14.5, 13],
|
||||
"faces": { "north": { "texture": "#frame" } }
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"parent": "minecraft:block/template_sign_rot_0",
|
||||
"textures": {
|
||||
"all": "minecraft:block/pale_oak_sign",
|
||||
"particle": "minecraft:block/pale_oak_planks"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"parent": "minecraft:block/template_sign_rot_1",
|
||||
"textures": {
|
||||
"all": "minecraft:block/pale_oak_sign",
|
||||
"particle": "minecraft:block/pale_oak_planks"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"parent": "minecraft:block/template_sign_rot_2",
|
||||
"textures": {
|
||||
"all": "minecraft:block/pale_oak_sign",
|
||||
"particle": "minecraft:block/pale_oak_planks"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"parent": "minecraft:block/template_sign_rot_3",
|
||||
"textures": {
|
||||
"all": "minecraft:block/pale_oak_sign",
|
||||
"particle": "minecraft:block/pale_oak_planks"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"parent": "minecraft:block/template_wall_sign",
|
||||
"textures": {
|
||||
"all": "minecraft:block/pale_oak_sign",
|
||||
"particle": "minecraft:block/pale_oak_planks"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"parent": "minecraft:item/generated",
|
||||
"textures": {
|
||||
"layer0": "minecraft:item/pale_oak_sign"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"replace": false,
|
||||
"values": ["redstoner:trophy_sign", "redstoner:trophy_wall_sign"]
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"replace": false,
|
||||
"values": ["redstoner:trophy_sign"]
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"replace": false,
|
||||
"values": ["redstoner:trophy_wall_sign"]
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"replace": false,
|
||||
"values": ["redstoner:trophy_panel"]
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"type": "minecraft:block",
|
||||
"pools": [
|
||||
{
|
||||
"conditions": [
|
||||
{ "condition": "minecraft:survives_explosion" }
|
||||
],
|
||||
"entries": [
|
||||
{ "type": "minecraft:item", "name": "redstoner:trophy_panel" }
|
||||
],
|
||||
"rolls": 1
|
||||
}
|
||||
],
|
||||
"random_sequence": "redstoner:blocks/trophy_sign"
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"type": "minecraft:block",
|
||||
"pools": [
|
||||
{
|
||||
"conditions": [
|
||||
{ "condition": "minecraft:survives_explosion" }
|
||||
],
|
||||
"entries": [
|
||||
{ "type": "minecraft:item", "name": "redstoner:trophy_panel" }
|
||||
],
|
||||
"rolls": 1
|
||||
}
|
||||
],
|
||||
"random_sequence": "redstoner:blocks/trophy_wall_sign"
|
||||
}
|
||||
@@ -1,17 +1,10 @@
|
||||
{
|
||||
"type": "minecraft:crafting_shaped",
|
||||
"category": "redstone",
|
||||
"pattern": [
|
||||
"NGN",
|
||||
"GRG",
|
||||
"NSN"
|
||||
"type": "minecraft:crafting_shapeless",
|
||||
"category": "building",
|
||||
"ingredients": [
|
||||
"#minecraft:signs",
|
||||
"minecraft:white_dye"
|
||||
],
|
||||
"key": {
|
||||
"N": "minecraft:gold_nugget",
|
||||
"G": "minecraft:black_stained_glass_pane",
|
||||
"R": "minecraft:redstone_lamp",
|
||||
"S": "minecraft:oak_sign"
|
||||
},
|
||||
"result": {
|
||||
"id": "redstoner:trophy_panel",
|
||||
"count": 1
|
||||
|
||||
@@ -24,14 +24,14 @@
|
||||
"java": ">=25",
|
||||
"fabric-api": ">=0.154.2+26.2",
|
||||
"anotherworld": "*",
|
||||
"sanctuary": "*"
|
||||
"sanctuary": ">=0.0.0-alpha.139"
|
||||
},
|
||||
"custom": {
|
||||
"sanctuary26": {
|
||||
"system_type": "gameplay-system",
|
||||
"namespace": "redstoner",
|
||||
"data_version": 4,
|
||||
"network_protocol": 7,
|
||||
"data_version": 5,
|
||||
"network_protocol": 9,
|
||||
"lifecycle": "${lifecycle}",
|
||||
"pack_version": "${pack_version}",
|
||||
"descriptions": {
|
||||
|
||||
+10
@@ -27,6 +27,7 @@ public final class StorageTerminalSmoke {
|
||||
verifyPagination();
|
||||
verifyAggregationAndTransfers();
|
||||
verifySearch();
|
||||
verifySearchKeyboardRouting();
|
||||
verifySorting();
|
||||
verifyStableLayout();
|
||||
}
|
||||
@@ -145,6 +146,15 @@ public final class StorageTerminalSmoke {
|
||||
"Terminal search retained an unrelated item");
|
||||
}
|
||||
|
||||
private static void verifySearchKeyboardRouting() {
|
||||
require(StorageTerminalKeyRules.capturesInventoryKey(true, true),
|
||||
"The inventory key escaped the focused terminal search field");
|
||||
require(!StorageTerminalKeyRules.capturesInventoryKey(false, true),
|
||||
"The inventory key no longer closes an unfocused terminal");
|
||||
require(!StorageTerminalKeyRules.capturesInventoryKey(true, false),
|
||||
"The terminal captured an unrelated search-field key");
|
||||
}
|
||||
|
||||
private static Holder<Item> stackable(Item item) {
|
||||
return new Holder.Direct<>(item, DataComponentMap.builder()
|
||||
.set(DataComponents.MAX_STACK_SIZE, 64).build());
|
||||
|
||||
+25
@@ -15,6 +15,28 @@ public final class TrophyPanelRulesSmoke {
|
||||
require(TrophyPanelRules.canConfigure(owner, visitor, true), "An operator cannot repair a panel");
|
||||
require(TrophyPanelRules.nextPage(0, 3) == 1 && TrophyPanelRules.nextPage(2, 3) == 0,
|
||||
"Public page cycling does not wrap");
|
||||
require(TrophyPanelRules.previousPage(0, 3) == 2 && TrophyPanelRules.previousPage(2, 3) == 1,
|
||||
"Reverse public page cycling does not wrap");
|
||||
require(TrophyPanelRules.pageByOffset(4, 0, 1) == 0,
|
||||
"An empty panel produced an invalid page index");
|
||||
TrophyPanelRules.DisplayPage values = TrophyPanelRules.displayPageByOffset(0, false, 2, 1);
|
||||
TrophyPanelRules.DisplayPage secondMetric = TrophyPanelRules.displayPageByOffset(
|
||||
values.metricPage(), values.valuesView(), 2, 1);
|
||||
TrophyPanelRules.DisplayPage wrapped = TrophyPanelRules.displayPageByOffset(0, false, 2, -1);
|
||||
require(values.metricPage() == 0 && values.valuesView()
|
||||
&& secondMetric.metricPage() == 1 && !secondMetric.valuesView()
|
||||
&& wrapped.metricPage() == 1 && wrapped.valuesView(),
|
||||
"Name and value views do not cycle as consecutive public pages");
|
||||
require(TrophyPanelRules.LINE_COUNT == 8 && TrophyPanelRules.ENTRIES_PER_VIEW == 7
|
||||
&& TrophyPanelRules.displayPageNumber(1, true, 2) == 4
|
||||
&& TrophyPanelRules.displayPageCount(2) == 4,
|
||||
"The panel does not use eight lines or number both metric views correctly");
|
||||
require(TrophyPanelRules.SIGN_LINE_COUNT == 4 && TrophyPanelRules.SIGN_ENTRIES_PER_PAGE == 3,
|
||||
"The vanilla trophy sign must reserve one title line and three ranking lines");
|
||||
require(TrophyDisplayMode.byKey("names") == TrophyDisplayMode.NAMES
|
||||
&& TrophyDisplayMode.NAMES.next() == TrophyDisplayMode.VALUES
|
||||
&& TrophyDisplayMode.byKey("invalid") == TrophyDisplayMode.NAMES_AND_VALUES,
|
||||
"Persisted trophy-sign display modes are not stable or safely defaulted");
|
||||
|
||||
Identifier first = Identifier.parse("sanctuary:first");
|
||||
Identifier second = Identifier.parse("sanctuary:second");
|
||||
@@ -25,6 +47,9 @@ public final class TrophyPanelRulesSmoke {
|
||||
require(merged.get(0).equals(first) && merged.get(1).equals(second),
|
||||
"Existing page order was not preserved");
|
||||
require(merged.stream().distinct().count() == merged.size(), "Duplicate metric pages were retained");
|
||||
List<Identifier> sanitized = TrophyPanelRules.sanitizeMetricIds(List.of(first, second, first));
|
||||
require(sanitized.equals(List.of(first, second)),
|
||||
"The trophy editor did not preserve page order while removing duplicates");
|
||||
}
|
||||
|
||||
private static void require(boolean condition, String message) {
|
||||
|
||||
@@ -35,6 +35,7 @@ réelle matérialisée reçoit un identifiant persistant : elle peut passer du r
|
||||
Une perte associée à un joueur lui reste réservée pendant 24 000 ticks avant de pouvoir rejoindre
|
||||
les tirages historiques partagés ; une perte sans propriétaire est immédiatement partagée.
|
||||
|
||||
Chaque niveau génère exactement un coffre par cellule de 3 × 3 chunks dans les nouveaux chunks.
|
||||
Tous les coffres vanilla de la Backroom, générés ou posés, deviennent des nœuds de butin renouvelés
|
||||
toutes les 12 000 ticks lors de leur prochaine ouverture. Le renouvellement rend au registre les
|
||||
objets perdus non pris, efface le contenu ordinaire et produit le plus souvent du vide ou des
|
||||
@@ -75,9 +76,9 @@ numérotés de haut en bas afin qu’une chute fasse toujours avancer vers le ni
|
||||
Les ouvertures entre deux niveaux sont calculées une seule fois puis appliquées à la fois au sol
|
||||
supérieur et au plafond inférieur : une chute annoncée ne débouche donc pas sur un bloc fermé. Les
|
||||
imperfections, morceaux suspendus et raccords volontairement cassés font partie de l’esthétique,
|
||||
mais les portes des couloirs restent traversables. Les lits de sortie restent rares, mais chacun
|
||||
des cinq niveaux en génère exactement un par cellule de 10 × 10 chunks, sur un plancher praticable
|
||||
propre à sa zone. Il n’est donc plus nécessaire de remonter jusqu’aux Archives pour ressortir.
|
||||
mais les portes des couloirs restent traversables. Chacun des cinq niveaux génère exactement un lit
|
||||
de sortie par cellule de 3 × 3 chunks, sur un plancher praticable propre à sa zone. Il n’est donc
|
||||
plus nécessaire de parcourir une centaine de chunks ou de remonter jusqu’aux Archives pour ressortir.
|
||||
|
||||
La Backroom ne possède ni ciel, ni soleil, ni lune, ni étoiles, ni lumière ambiante. Son biome ne
|
||||
déclare aucune créature et le serveur retire aussi toute entité de catégorie monstre qui y serait
|
||||
|
||||
+85
-14
@@ -124,6 +124,22 @@ tasks.register("capeSyncPayloadSmoke", JavaExec) {
|
||||
mainClass = "fr.koka99cab.sanctuary26.sanctuary.network.CapeSyncPayloadSmoke"
|
||||
}
|
||||
|
||||
tasks.register("capeModelSmoke", JavaExec) {
|
||||
group = "verification"
|
||||
description = "Checks the double-sided shield-pattern plane used by banner capes."
|
||||
dependsOn tasks.named("testClasses")
|
||||
classpath = sourceSets.test.runtimeClasspath
|
||||
mainClass = "fr.koka99cab.sanctuary26.sanctuary.client.SanctuaryBannerCapeModelSmoke"
|
||||
}
|
||||
|
||||
tasks.register("chickenArmorSmoke", JavaExec) {
|
||||
group = "verification"
|
||||
description = "Checks that only adult unarmored chickens accept armadillo-scute armor."
|
||||
dependsOn tasks.named("testClasses")
|
||||
classpath = sourceSets.test.runtimeClasspath
|
||||
mainClass = "fr.koka99cab.sanctuary26.sanctuary.gameplay.SanctuaryChickenArmorSmoke"
|
||||
}
|
||||
|
||||
tasks.register("inventorySortSmoke", JavaExec) {
|
||||
group = "verification"
|
||||
description = "Checks that storage stacks merge and sort while the hotbar row stays out of the sort."
|
||||
@@ -199,7 +215,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.135"
|
||||
if (project.version.toString() != "0.0.0-alpha.139"
|
||||
|| !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")
|
||||
@@ -2915,7 +2931,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.135"
|
||||
if (project.version.toString() != "0.0.0-alpha.139"
|
||||
|| 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
|
||||
@@ -3081,7 +3097,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.135"
|
||||
if (project.version.toString() != "0.0.0-alpha.139"
|
||||
|| 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
|
||||
@@ -3119,7 +3135,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.135"
|
||||
if (project.version.toString() != "0.0.0-alpha.139"
|
||||
|| 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]
|
||||
@@ -3150,7 +3166,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.135"
|
||||
if (project.version.toString() != "0.0.0-alpha.139"
|
||||
|| migration.target?.pack_version != "26.2.0-alpha.195"
|
||||
|| migration.target?.modules?.sanctuary != "0.0.0-alpha.111"
|
||||
|| migration.compatibility?.sanctuary_data_version != 11
|
||||
@@ -3181,7 +3197,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.135"
|
||||
if (project.version.toString() != "0.0.0-alpha.139"
|
||||
|| migration.target?.modules?.sanctuary != "0.0.0-alpha.112"
|
||||
|| migration.mailbox?.id != "sanctuary:delivery_box"
|
||||
|| migration.mailbox?.horizontal_facing != true
|
||||
@@ -3209,7 +3225,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.135"
|
||||
if (project.version.toString() != "0.0.0-alpha.139"
|
||||
|| !transfer.contains("shouldAppendOverflow")
|
||||
|| !transfer.contains("isVanillaStorageRange")
|
||||
|| !transfer.contains("hotbarStart")
|
||||
@@ -3255,7 +3271,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.135"
|
||||
if (project.version.toString() != "0.0.0-alpha.139"
|
||||
|| migration.target?.modules?.sanctuary != "0.0.0-alpha.113"
|
||||
|| migration.quest_requests?.save_key != "quest_requests"
|
||||
|| migration.quest_requests?.data_version != 1
|
||||
@@ -3317,7 +3333,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.135"
|
||||
if (project.version.toString() != "0.0.0-alpha.139"
|
||||
|| 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"
|
||||
@@ -3386,7 +3402,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.135"
|
||||
if (project.version.toString() != "0.0.0-alpha.139"
|
||||
|| 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"
|
||||
@@ -3465,7 +3481,7 @@ tasks.register("verifyCraftableBannerCape") {
|
||||
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.135"
|
||||
if (project.version.toString() != "0.0.0-alpha.139"
|
||||
|| migration.target?.modules?.sanctuary != "0.0.0-alpha.130"
|
||||
|| migration.white_cape?.crafting_material_count != 6
|
||||
|| migration.white_cape?.maximum_pattern_layers != 6
|
||||
@@ -3493,6 +3509,57 @@ tasks.named("verifySanctuary") {
|
||||
dependsOn tasks.named("verifyCraftableBannerCape")
|
||||
}
|
||||
|
||||
tasks.register("verifyFinalSanctuaryTickets") {
|
||||
group = "verification"
|
||||
description = "Checks the shield-mapped banner cape and armadillo-scute chicken armor contracts."
|
||||
inputs.files(
|
||||
file("src/main/java/fr/koka99cab/sanctuary26/sanctuary/client/SanctuaryBannerCapeLayer.java"),
|
||||
file("src/main/java/fr/koka99cab/sanctuary26/sanctuary/client/SanctuaryChickenArmorLayer.java"),
|
||||
file("src/main/java/fr/koka99cab/sanctuary26/sanctuary/gameplay/SanctuaryChickenArmor.java"),
|
||||
file("src/main/java/fr/koka99cab/sanctuary26/sanctuary/mixin/AnimalChickenArmorMixin.java"),
|
||||
file("src/main/java/fr/koka99cab/sanctuary26/sanctuary/mixin/LivingEntityChickenArmorMixin.java"),
|
||||
file("src/main/resources/sanctuary.mixins.json"))
|
||||
inputs.files(fileTree("src/main/resources/assets/sanctuary/lang") { include "*.json" })
|
||||
doLast {
|
||||
def capeRenderer = file(
|
||||
"src/main/java/fr/koka99cab/sanctuary26/sanctuary/client/SanctuaryBannerCapeLayer.java").text
|
||||
def armorLayer = file(
|
||||
"src/main/java/fr/koka99cab/sanctuary26/sanctuary/client/SanctuaryChickenArmorLayer.java").text
|
||||
def armorRules = file(
|
||||
"src/main/java/fr/koka99cab/sanctuary26/sanctuary/gameplay/SanctuaryChickenArmor.java").text
|
||||
def armorDamage = file(
|
||||
"src/main/java/fr/koka99cab/sanctuary26/sanctuary/mixin/LivingEntityChickenArmorMixin.java").text
|
||||
def mixins = new JsonSlurper().parse(file("src/main/resources/sanctuary.mixins.json"))
|
||||
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.139"
|
||||
|| !capeRenderer.contains("SHIELD_FACE_WIDTH = 12.0F")
|
||||
|| !capeRenderer.contains("SHIELD_FACE_HEIGHT = 22.0F")
|
||||
|| !capeRenderer.contains("CAPE_WIDTH / SHIELD_FACE_WIDTH")
|
||||
|| !capeRenderer.contains("CAPE_HEIGHT / SHIELD_FACE_HEIGHT")
|
||||
|| !capeRenderer.contains('addOrReplaceChild("front"')
|
||||
|| !capeRenderer.contains('addOrReplaceChild("back"')
|
||||
|| !capeRenderer.contains("Set.of(Direction.NORTH)")
|
||||
|| !capeRenderer.contains("model, state, false, appearance.baseColor()")
|
||||
|| !armorLayer.contains("EquipmentClientInfo.LayerType.WOLF_BODY")
|
||||
|| !armorRules.contains("held.is(Items.WOLF_ARMOR)")
|
||||
|| !armorRules.contains("DamageTypeTags.BYPASSES_WOLF_ARMOR")
|
||||
|| !armorDamage.contains("armor.hurtAndBreak")
|
||||
|| !mixins.mixins.contains("AnimalChickenArmorMixin")
|
||||
|| !mixins.mixins.contains("LivingEntityChickenArmorMixin")
|
||||
|| !mixins.client.contains("client.ChickenRendererMixin")
|
||||
|| !mixins.client.contains("client.ChickenRenderStateMixin")
|
||||
|| locales.any { !it.containsKey("tooltip.sanctuary.chicken_armor") }) {
|
||||
throw new GradleException("Sanctuary alpha.136 final-ticket contract is incomplete")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tasks.named("verifySanctuary") {
|
||||
dependsOn tasks.named("verifyFinalSanctuaryTickets")
|
||||
}
|
||||
|
||||
tasks.register("verifyAlphaIndev") {
|
||||
group = "verification"
|
||||
description = "Checks the alpha.122 finite Indev world, migration and attribution contract."
|
||||
@@ -3520,7 +3587,7 @@ tasks.register("verifyAlphaIndev") {
|
||||
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.135"
|
||||
if (project.version.toString() != "0.0.0-alpha.139"
|
||||
|| migration.target?.modules?.sanctuary != "0.0.0-alpha.122"
|
||||
|| migration.alpha_world?.dimension != "sanctuary:alpha"
|
||||
|| migration.alpha_world?.legacy_dimension != "portals:alpha"
|
||||
@@ -3580,7 +3647,7 @@ tasks.register("verifyBackroom") {
|
||||
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.135"
|
||||
if (project.version.toString() != "0.0.0-alpha.139"
|
||||
|| migration.source_data_version != 13 || migration.target_data_version != 14
|
||||
|| migration.integration_modules?.anotherworld?.source != "0.0.0-alpha.56"
|
||||
|| migration.integration_modules?.anotherworld?.target != "0.0.0-alpha.57"
|
||||
@@ -3606,10 +3673,12 @@ tasks.register("verifyBackroom") {
|
||||
|| !chest.contains("REFRESH_TICKS = 12_000L") || !chest.contains("ordinal == 3")
|
||||
|| !architecture.contains("PLAYABLE_RADIUS = 2_000")
|
||||
|| !architecture.contains("BackroomMaterialApi.anomalyMaterial()")
|
||||
|| !architecture.contains("placeLootLandmark")
|
||||
|| !layout.contains("WORLD_MIN_Y = -64") || !layout.contains("WORLD_MAX_Y_EXCLUSIVE = 384")
|
||||
|| !layout.contains("LEVEL_0(295, 383)") || !layout.contains("LEVEL_4(-64, 27)")
|
||||
|| !layout.contains("mazeConnected") || !layout.contains("RoomCorner")
|
||||
|| !layout.contains("EXIT_BED_CELL_SIZE_CHUNKS = 10")
|
||||
|| !layout.contains("LOOT_CHEST_CELL_SIZE_CHUNKS = 3") || !layout.contains("lootChestChunk")
|
||||
|| !layout.contains("EXIT_BED_CELL_SIZE_CHUNKS = 3")
|
||||
|| !layout.contains("exitBedChunk") || !layout.contains("exitBedFloor")
|
||||
|| !materialApi.contains("registerAnomalyMaterial")
|
||||
|| locales.any { !it.containsKey("dimension.sanctuary.backroom")
|
||||
@@ -3640,6 +3709,8 @@ tasks.named("check") {
|
||||
dependsOn tasks.named("alphaIndevGeneratorSmoke")
|
||||
dependsOn tasks.named("capeJoinSyncSmoke")
|
||||
dependsOn tasks.named("capeSyncPayloadSmoke")
|
||||
dependsOn tasks.named("capeModelSmoke")
|
||||
dependsOn tasks.named("chickenArmorSmoke")
|
||||
dependsOn tasks.named("inventorySortSmoke")
|
||||
dependsOn tasks.named("entityPolicySmoke")
|
||||
dependsOn tasks.named("verifyAlphaIndev")
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user