Compare commits
51
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0ad9763666 | ||
|
|
0196b6efa5 | ||
|
|
3bdc5fe241 | ||
|
|
789faeb708 | ||
|
|
3491158785 | ||
|
|
c0229d4ea9 | ||
|
|
ddaf71c7cd | ||
|
|
7e1ed85307 | ||
|
|
5840621acf | ||
|
|
974cea20b5 | ||
|
|
81a98ed197 | ||
|
|
b022be5739 | ||
|
|
27b29c1340 | ||
|
|
274d4c1ffa | ||
|
|
f217bb3661 | ||
|
|
5100091b17 | ||
|
|
398d94e7d1 | ||
|
|
0743ff33a6 | ||
|
|
bfda90f4f4 | ||
|
|
a698140963 | ||
|
|
88ec91e9f4 | ||
|
|
e1d48e9017 | ||
|
|
cce58c27c8 | ||
|
|
1a9263d273 | ||
|
|
4a74b6dfed | ||
|
|
06da8fbd2b | ||
|
|
bd747afbe0 | ||
|
|
7e459e3615 | ||
|
|
67220fa0c7 | ||
|
|
a345db5aeb | ||
|
|
0bd60d01cc | ||
|
|
94e6cf380d | ||
|
|
43962f4857 | ||
|
|
5af573deb4 | ||
|
|
e552bd68fd | ||
|
|
69dd9f919d | ||
|
|
2d212ae6a1 | ||
|
|
2b614ce5a8 | ||
|
|
99ac5fc3a7 | ||
|
|
ab82c3929e | ||
|
|
4820c736f4 | ||
|
|
a16347140a | ||
|
|
75fa88cfc5 | ||
|
|
f04aeb02b2 | ||
|
|
4ca42cb18a | ||
|
|
985e8b9192 | ||
|
|
1d8ab0b30a | ||
|
|
083c3449d5 | ||
|
|
51d630037d | ||
|
|
341f1eb8e5 | ||
|
|
10b606d01d |
@@ -8,6 +8,79 @@ Ce journal suit les **versions du modpack**. Lorsqu'un seul module change, sa ve
|
||||
|
||||
## Non publié
|
||||
|
||||
Aucun changement non publié.
|
||||
|
||||
## `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 ;
|
||||
- conserve les IDs, les données Sanctuary v14 et le protocole réseau 24 ; les chunks Backroom déjà générés restent inchangés.
|
||||
- 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`
|
||||
|
||||
Module modifié : **Red-Stoner `0.0.0-alpha.16`**.
|
||||
|
||||
- ajoute au terminal les tris alphabétique, par quantité croissante et par quantité décroissante, appliqués explicitement sans déplacer les objets sous le curseur lors des rafraîchissements périodiques ;
|
||||
|
||||
## `26.2.0-alpha.228`
|
||||
|
||||
Modules modifiés : **Sanctuary `0.0.0-alpha.132`** et **Red-Stoner `0.0.0-alpha.15`**.
|
||||
|
||||
- étend `sanctuary:backroom` à toute la hauteur Y -64 → 383 et empile cinq niveaux thématiques de haut en bas : seuil modulaire, archives, ville, services et glitch ;
|
||||
- remplace les volumes ouverts par des pièces et couloirs raccordés, quatre angles de salle déclinés en cinq matériaux, des bibliothèques immenses, des tours plus serrées, des autoroutes continues et de rares parcs ;
|
||||
- ajoute au niveau inférieur une grille de prismes 3D et un labyrinthe déterministe connecté, avec portes physiquement ouvertes, salles fusionnées et trous volontaires vers le vide ;
|
||||
- fait correspondre exactement chaque ouverture entre le sol supérieur et le plafond inférieur, place une torche récupérable à chaque arrivée et raréfie fortement les lits de sortie ;
|
||||
- supprime le ciel, les astres, la lumière ambiante et tous les monstres, y compris ceux injectés par un autre système ;
|
||||
- conserve les IDs, les données Sanctuary v14 et le protocole réseau 24 ; seuls les nouveaux chunks Backroom utilisent la nouvelle architecture complète ;
|
||||
- rend le champ de recherche du terminal inactif à l’ouverture puis focalisable au clic, libère le focus au clic extérieur et laisse toujours la touche d’inventaire fermer le terminal ;
|
||||
- permet à la recherche du terminal de retrouver une boîte de Shulker par le nom ou l’identifiant d’un objet qu’elle contient, tout en conservant la boîte comme résultat afin que son aperçu vanilla reste directement consultable ;
|
||||
|
||||
## `26.2.0-alpha.227`
|
||||
|
||||
Modules modifiés : **Sanctuary `0.0.0-alpha.131`** et **Another World `0.0.0-alpha.57`**.
|
||||
|
||||
- ajoute la dimension persistante `sanctuary:backroom`, accessible en s’accroupissant devant un lit et quittée uniquement par un lit, avec arrivées variées dans une zone architecturale de 2 000 blocs ;
|
||||
@@ -16,6 +89,7 @@ Modules modifiés : **Sanctuary `0.0.0-alpha.131`** et **Another World `0.0.0-al
|
||||
- transforme tous les coffres de la dimension en butin renouvelable, pauvre la plupart du temps, qui privilégie les pertes personnelles dans les premiers coffres avant les pertes anciennes partagées ;
|
||||
- génère des villes, bureaux, plaines, agrégats flottants et pièces visuellement retournées à partir de palettes composées, en réservant les matériaux d’Another World aux anomalies et les blocs précieux à de rares cachettes ;
|
||||
- fait enregistrer la mousse turquoise par Another World via l’API publique de matériaux Backroom, sans dépendance circulaire ;
|
||||
- actualise Fabric API vers `0.159.0+26.2` et JEI vers `30.29.0.199`, avec empreintes Packwiz et Prism synchronisées ;
|
||||
- migre Sanctuary vers les données v14 sans changer le protocole réseau 24, sans réinitialiser les sauvegardes ni effacer les constructions.
|
||||
|
||||
## `26.2.0-alpha.226`
|
||||
|
||||
@@ -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.158.0+26.2, Loom 1.17, Gradle 9.5.1 et Java 25.
|
||||
Workspace Fabric multi-projet de Sanctuary 2.0. Il construit les dix JAR du pack et le mod événementiel ZEVENT distribué à part, 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`)
|
||||
@@ -12,17 +12,32 @@ Licence : **GNU GPL 3.0 or Later** (`GPL-3.0-or-later`)
|
||||
| `anotherworld` | Another World | choses matérielles | matière, photographie, instruments météo et 108 biomes natifs |
|
||||
| `itsalive` | It's Alive ! | vivant | Creepers, créatures, agriculture et gameplay serveur complet de Canaplia |
|
||||
| `ambiance` | Ambiance | perception | atmosphère, météo, lumières dynamiques, audio positionnel, calendrier et composition visuelle accessible |
|
||||
| `redstoner` | Red-Stoner | calcul et signaux | ordinateurs, identité serveur, terminal de stockage, Drawer et Particuleur |
|
||||
| `redstoner` | Red-Stoner | calcul et signaux | ordinateurs, identité serveur, terminal de stockage, panneau de trophées, Drawer et Particuleur |
|
||||
| `iliketomoveit` | I Like To Move It | déplacement | portails, véhicules, grappin, poulies et waystones locaux |
|
||||
| `ouch` | Ouch | combat et destruction | arsenal actif (6 armes) et TNT météo |
|
||||
| `onlyfun` | Only Fun | règles ludiques absurdes | lootboxes, Lucky Block, raretés et règles absurdes |
|
||||
| `sanctuary` | Sanctuary | progression et aventure | progression globale, Atlas et monde principal flottant |
|
||||
| `sanctuary` | Sanctuary | progression et aventure | progression globale, classements publics, Atlas et monde principal flottant |
|
||||
| `events` | Events | événements serveur | socle vide |
|
||||
| `masterkey` | MasterKey | administration et diagnostic | laboratoire minimal |
|
||||
| `zevent` | ZEVENT | diffusion Twitch temporaire | écran mural 9×5, hors pack principal |
|
||||
|
||||
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.226`**. Cette release distribue Sanctuary **`0.0.0-alpha.130`** ; les neuf autres modules conservent leur version issue de l'alpha.225.
|
||||
Version courante du pack distribué : **`26.2.0-alpha.233`**. Cette release distribue Another World **`0.0.0-alpha.58`**, Sanctuary **`0.0.0-alpha.139`** et Red-Stoner **`0.0.0-alpha.20`** ; les sept autres modules conservent leur version issue de l'alpha.231.
|
||||
|
||||
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.
|
||||
|
||||
L’alpha.228 reconstruit la Backroom sur cinq niveaux empilés entre Y -64 et Y 383. Le monde devient entièrement noir et sans monstres ; l’arrivée est signalée par une torche récupérable. Les nouveaux chunks composent seuils modulaires, bibliothèques et bureaux, ville dense avec autoroutes et parcs, couloirs techniques, puis une grille 3D et un labyrinthe connecté dont certains trous mènent au vide. Red-Stoner corrige aussi le focus du terminal et permet de retrouver une boîte de Shulker par son contenu. Les chunks déjà générés et les sauvegardes existantes restent compatibles.
|
||||
|
||||
L’alpha.227 ajoute la dimension persistante `sanctuary:backroom`. On y entre accroupi devant un lit et on n’en ressort que par un lit. L’équipement d’entrée est transféré atomiquement vers la Mailbox, tandis que les objets perdus ou abandonnés dans la dimension alimentent des coffres renouvelables sans duplication. Les anciennes sauvegardes sont migrées vers Sanctuary data v14 sans réinitialisation ; le protocole réseau reste en version 24.
|
||||
|
||||
L’alpha.225 corrige quatre tickets : posséder un coffre-fort devient obligatoire pour en braquer un autre, les familiers temporaires ne donnent plus de butin, d’expérience, de lootbox ni de progression de quête, les seize variantes Canaplia survivent à tout leur cycle de culture, et le Magic Carpet retrouve la texture historique du Totem de Notch à la place de la laine violette. Les IDs, sauvegardes, formats de données et protocoles restent compatibles.
|
||||
|
||||
@@ -40,7 +55,7 @@ L’alpha.220 transforme le terminal de stockage en véritable menu de conteneur
|
||||
|
||||
L’alpha.219 porte l'île finie Minecraft Indev dans `sanctuary:alpha` : 128 × 128 × 64 blocs déterministes, plages, cavernes, minerais, fluides, végétation et maison historique, le tout borné sans chunks parasites. Le cadre 4 × 5 en cobblestone moussue est maintenant actif et crée une paire persistante à une arrivée sûre hors de la maison. Ambiance peut enfin composer localement Golden Days Base puis Alpha uniquement dans cette dimension, sans redistribuer leurs textures et en restaurant exactement la pile précédente. Voir le [contrat Alpha](sanctuary/ALPHA.md), le [contrat des portails](iliketomoveit/PORTALS.md) et le [guide Golden Days](ambiance/GOLDEN_DAYS.md).
|
||||
|
||||
L’alpha.218 ajoute le terminal de stockage Red-Stoner : cette demi-dalle verticale se fixe sur un coffre, un coffre piégé ou un baril et donne accès, par pages défilantes de 54 types, à un réseau contigu borné à 128 conteneurs chargés. Les retraits et dépôts sont validés par le serveur, les composants d’items restent strictement séparés et le terminal endgame est exclu du Shop, du Black Market et des récompenses Only Fun.
|
||||
L’alpha.218 ajoute le terminal de stockage Red-Stoner : cette demi-dalle verticale se fixe sur un coffre, un coffre piégé ou un baril et donne accès, par pages défilantes de 54 types, à un réseau contigu borné à 128 conteneurs chargés. Les retraits et dépôts sont validés par le serveur, les composants d’items restent strictement séparés et le terminal endgame est exclu du Shop, du Black Market et des récompenses Only Fun. La recherche retrouve aussi une boîte de Shulker d’après son contenu direct ; la boîte reste affichée dans la grille afin que son aperçu vanilla soit consultable sans modifier les règles de retrait. Un bouton permet enfin de trier explicitement les résultats par nom, quantité croissante ou quantité décroissante.
|
||||
|
||||
L’alpha.217 tentait de compenser la hauteur trop grave du disque blanc par un facteur OpenAL de `4/3`. Cette compensation est retirée par l’alpha.221, qui corrige directement la conversion stéréo vers mono.
|
||||
|
||||
@@ -313,7 +328,9 @@ Le guide détaillé est dans [`masterkey/tools/PANORAMA.md`](masterkey/tools/PAN
|
||||
|
||||
## Red-Stoner et photographie
|
||||
|
||||
Red-Stoner possède quatre blocs actifs sous son namespace : `eight_bit_computer`, `controller`, `personal_computer` et `drawer`. 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 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é.
|
||||
|
||||
@@ -402,7 +419,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`. Le sous-projet `zevent` reste volontairement hors de `moduleIds` et des archives du pack : son JAR et sa dépendance cliente MCEF sont distribués séparément pour l’événement.
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
+1282
-282
File diff suppressed because it is too large
Load Diff
+7
-5
@@ -6,7 +6,7 @@ org.gradle.configuration-cache=false
|
||||
minecraft_version=26.2
|
||||
loader_version=0.19.3
|
||||
loom_version=1.17-SNAPSHOT
|
||||
fabric_api_version=0.158.0+26.2
|
||||
fabric_api_version=0.159.0+26.2
|
||||
|
||||
# Sanctuary 26.2
|
||||
# Standby modules stay at zero until their migration really begins.
|
||||
@@ -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.13
|
||||
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,9 @@ 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.131
|
||||
sanctuary_version=0.0.0-alpha.139
|
||||
sanctuary_lifecycle=active
|
||||
pack_version=26.2.0-alpha.226
|
||||
zevent_version=0.0.0-alpha.1
|
||||
zevent_lifecycle=active
|
||||
pack_version=26.2.0-alpha.233
|
||||
maven_group=fr.koka99cab.sanctuary26
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
{
|
||||
"schema_version": 1,
|
||||
"status": "released",
|
||||
"source": {
|
||||
"pack_version": "26.2.0-alpha.226",
|
||||
"modules": {
|
||||
"anotherworld": "0.0.0-alpha.56",
|
||||
"sanctuary": "0.0.0-alpha.130"
|
||||
}
|
||||
},
|
||||
"target": {
|
||||
"pack_version": "26.2.0-alpha.227",
|
||||
"modules": {
|
||||
"anotherworld": "0.0.0-alpha.57",
|
||||
"sanctuary": "0.0.0-alpha.131"
|
||||
}
|
||||
},
|
||||
"backroom": {
|
||||
"owner": "sanctuary",
|
||||
"dimension": "sanctuary:backroom",
|
||||
"entry_requires_sneaking_at_bed": true,
|
||||
"exit_requires_bed": true,
|
||||
"arrival_radius_blocks": 2000,
|
||||
"entry_inventory_to_mailbox": true,
|
||||
"entry_transfer_atomic": true,
|
||||
"entry_transfer_idempotent": true,
|
||||
"lost_items_persisted": true,
|
||||
"lost_item_registry_bounded": true,
|
||||
"renewable_chests": true,
|
||||
"personal_losses_prioritized": true,
|
||||
"shared_old_losses_supported": true,
|
||||
"anotherworld_material_api": "BackroomMaterialApi"
|
||||
},
|
||||
"compatibility": {
|
||||
"existing_saves_supported": true,
|
||||
"new_world_required": false,
|
||||
"save_reset_required": false,
|
||||
"sanctuary_source_data_version": 13,
|
||||
"sanctuary_target_data_version": 14,
|
||||
"sanctuary_network_protocol": 24,
|
||||
"network_protocol_changed": false,
|
||||
"client_server_update_required": true
|
||||
},
|
||||
"distribution": {
|
||||
"pack_release_updated": true,
|
||||
"packwiz_index_refreshed": true,
|
||||
"prism_modrinth_index_updated": true,
|
||||
"shared_client_server_mrpack": true,
|
||||
"fabric_api": {
|
||||
"source": "0.158.0+26.2",
|
||||
"target": "0.159.0+26.2",
|
||||
"modrinth_version_id": "BgeCGgGZ"
|
||||
},
|
||||
"jei": {
|
||||
"source": "30.29.0.198",
|
||||
"target": "30.29.0.199",
|
||||
"modrinth_version_id": "astSvyl6"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
{
|
||||
"schema_version": 1,
|
||||
"status": "released",
|
||||
"source": {
|
||||
"pack_version": "26.2.0-alpha.227",
|
||||
"modules": {
|
||||
"redstoner": "0.0.0-alpha.13",
|
||||
"sanctuary": "0.0.0-alpha.131"
|
||||
}
|
||||
},
|
||||
"target": {
|
||||
"pack_version": "26.2.0-alpha.228",
|
||||
"modules": {
|
||||
"redstoner": "0.0.0-alpha.15",
|
||||
"sanctuary": "0.0.0-alpha.132"
|
||||
}
|
||||
},
|
||||
"backroom": {
|
||||
"level_count": 5,
|
||||
"minimum_y": -64,
|
||||
"maximum_y_exclusive": 384,
|
||||
"entry_floor_y": 367,
|
||||
"monsters_disabled": true,
|
||||
"arrival_torch": true,
|
||||
"matching_vertical_transitions": true,
|
||||
"connected_maze": true,
|
||||
"dense_city_and_highways": true,
|
||||
"three_dimensional_brick_grid": true,
|
||||
"intentional_void_holes": true
|
||||
},
|
||||
"storage_terminal": {
|
||||
"click_to_focus": true,
|
||||
"inventory_key_closes": true,
|
||||
"shulker_contents_searchable": true,
|
||||
"shulker_remains_result": true,
|
||||
"vanilla_shulker_preview_preserved": true
|
||||
},
|
||||
"compatibility": {
|
||||
"existing_saves_supported": true,
|
||||
"new_world_required": false,
|
||||
"save_reset_required": false,
|
||||
"persistent_data_formats_changed": false,
|
||||
"network_protocols_changed": false,
|
||||
"redstoner_data_version": 3,
|
||||
"redstoner_network_protocol": 7,
|
||||
"sanctuary_data_version": 14,
|
||||
"sanctuary_network_protocol": 24,
|
||||
"client_server_update_required": true,
|
||||
"existing_backroom_chunks_preserved": true,
|
||||
"new_backroom_generation_applies_to_new_chunks": true
|
||||
},
|
||||
"distribution": {
|
||||
"pack_release_updated": true,
|
||||
"packwiz_index_refreshed": true,
|
||||
"prism_modrinth_index_updated": true,
|
||||
"shared_client_server_mrpack": true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
{
|
||||
"schema_version": 1,
|
||||
"status": "released",
|
||||
"source": {
|
||||
"pack_version": "26.2.0-alpha.228",
|
||||
"modules": {
|
||||
"redstoner": "0.0.0-alpha.15"
|
||||
}
|
||||
},
|
||||
"target": {
|
||||
"pack_version": "26.2.0-alpha.229",
|
||||
"modules": {
|
||||
"redstoner": "0.0.0-alpha.16"
|
||||
}
|
||||
},
|
||||
"storage_terminal_sorting": {
|
||||
"owner": "redstoner",
|
||||
"modes": [
|
||||
"alphabetical",
|
||||
"count_ascending",
|
||||
"count_descending"
|
||||
],
|
||||
"server_authoritative": true,
|
||||
"explicit_user_action": true,
|
||||
"returns_to_first_page": true,
|
||||
"periodic_refresh_preserves_order": true,
|
||||
"periodic_refresh_preserves_hovered_stack": true
|
||||
},
|
||||
"compatibility": {
|
||||
"existing_saves_supported": true,
|
||||
"new_world_required": false,
|
||||
"save_reset_required": false,
|
||||
"persistent_data_formats_changed": false,
|
||||
"network_protocols_changed": false,
|
||||
"redstoner_data_version": 3,
|
||||
"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,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,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
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"schema_version": 1,
|
||||
"status": "pending_release",
|
||||
"status": "released",
|
||||
"module": "sanctuary",
|
||||
"source_module_version": "0.0.0-alpha.130",
|
||||
"target_module_version": "0.0.0-alpha.131",
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"schema_version": 1,
|
||||
"status": "released",
|
||||
"modules": {
|
||||
"sanctuary": {
|
||||
"source_module_version": "0.0.0-alpha.134",
|
||||
"target_module_version": "0.0.0-alpha.135",
|
||||
"source_data_version": 14,
|
||||
"target_data_version": 15
|
||||
},
|
||||
"redstoner": {
|
||||
"source_module_version": "0.0.0-alpha.16",
|
||||
"target_module_version": "0.0.0-alpha.17",
|
||||
"source_data_version": 3,
|
||||
"target_data_version": 4
|
||||
}
|
||||
},
|
||||
"migration": {
|
||||
"kind": "monotone_addition",
|
||||
"ranking_save_key": "sanctuary:rankings",
|
||||
"vanilla_statistics_imported_once": true,
|
||||
"player_identity": "uuid",
|
||||
"ranking_metric_index_version": 1,
|
||||
"trophy_panel_id": "redstoner:trophy_panel",
|
||||
"trophy_panel_data_version": 1,
|
||||
"existing_redstoner_block_entities_unchanged": true
|
||||
},
|
||||
"compatibility": {
|
||||
"existing_saves_supported": true,
|
||||
"new_world_required": false,
|
||||
"save_reset_required": false,
|
||||
"network_protocols_changed": false,
|
||||
"newer_ranking_data_is_read_only": true,
|
||||
"newer_panel_data_is_read_only": true
|
||||
}
|
||||
}
|
||||
+15
-15
@@ -14,24 +14,24 @@ hash = "41f4f4c88c71d0a23446d5014f7407d55ad477441847099cad24492b032335a0"
|
||||
|
||||
[[files]]
|
||||
file = "mods/ambiance-0.0.0-alpha.22.jar"
|
||||
hash = "0e862db1c5efb5c0745b82f991d5480f4305822c3d3f53ed38e2d2d47efabee1"
|
||||
hash = "c72a87d3ea187e65f3d459d18954bfc557deeb741deea143e610604a3af5985f"
|
||||
|
||||
[[files]]
|
||||
file = "mods/anotherworld-0.0.0-alpha.56.jar"
|
||||
hash = "cec8ed493f3c86e4c4481081be110382407f7a5c631b7cf27dd80f80c6f2e92a"
|
||||
file = "mods/anotherworld-0.0.0-alpha.58.jar"
|
||||
hash = "51f0122c222317fd65a642f8568a3d822732a874ecd5e4433592ae96e2a535a8"
|
||||
|
||||
[[files]]
|
||||
file = "mods/events-0.0.0-alpha.0.jar"
|
||||
hash = "16bf9fc194988086f51ba6987546b6aa5bc634fe8fcb118b88540ed16991049c"
|
||||
hash = "1c1623d4d40973346d3143657461e916e38142a867e7a6cf0da054ad998dbb0e"
|
||||
|
||||
[[files]]
|
||||
file = "mods/fabric-api.pw.toml"
|
||||
hash = "df032a6b051d0262cf24945c43e0e6b915e8cc1d62af62fe933932e7f35960c4"
|
||||
hash = "b8ea5f403a0cdfbfd5ab1704bb61278e8dcf31b525db9c242b22047852d34f85"
|
||||
metafile = true
|
||||
|
||||
[[files]]
|
||||
file = "mods/iliketomoveit-0.0.0-alpha.18.jar"
|
||||
hash = "94044341de08c9b89e0c1e6af6857da64b472fadbc94051ab76fc3cf1a0f0006"
|
||||
hash = "6a38653f4898e2fe2df6f8935b516073185f7c0d3913164e769ebafcc487b318"
|
||||
|
||||
[[files]]
|
||||
file = "mods/iris.pw.toml"
|
||||
@@ -40,32 +40,32 @@ metafile = true
|
||||
|
||||
[[files]]
|
||||
file = "mods/itsalive-0.0.0-alpha.38.jar"
|
||||
hash = "a42885703439646687ac281b1c124d1cddb3ca0595a41d930733b94bd8f1aa2f"
|
||||
hash = "0e40259acf98cf3d64dfdebe3441ffb3a09aa64de6cf1b2c8dcaa4af7c8d0e82"
|
||||
|
||||
[[files]]
|
||||
file = "mods/jei.pw.toml"
|
||||
hash = "cffb13bae33cd8df605c62e7fa66089428dc684e0ab973f662a8a710c45c306d"
|
||||
hash = "80d0fa55c1bbc0659e792b047c2bb64c446c2bf92557d9d74777333a8a68a318"
|
||||
metafile = true
|
||||
|
||||
[[files]]
|
||||
file = "mods/masterkey-0.0.0-alpha.9.jar"
|
||||
hash = "979c14e0a527be63688e97af8349be07b027d3149107c3e9e728a2b1e1ec65ff"
|
||||
hash = "b42b25f1eabc0b1e7660f0071e8f105d82ceb078b4bd9b7c570df8ce2c59601c"
|
||||
|
||||
[[files]]
|
||||
file = "mods/onlyfun-0.0.0-alpha.27.jar"
|
||||
hash = "023ba80a8f3326a6b534b343753d5630d3be0700d25613ea21057cbfe44fdc73"
|
||||
hash = "4e2c95c54fa3f591d3e1b6f7454211caaa4fb5f7ef31cd5b6aa477d03be18e2e"
|
||||
|
||||
[[files]]
|
||||
file = "mods/ouch-0.0.0-alpha.11.jar"
|
||||
hash = "2c15c9d0adeb315b2c3a6796f27c9dc62dd33365f35ea584552d97500b833eb8"
|
||||
hash = "8dcec6870fba091adda5df2a2f719e97d0f5dcf86f14c5f99035c7aebaf6c119"
|
||||
|
||||
[[files]]
|
||||
file = "mods/redstoner-0.0.0-alpha.13.jar"
|
||||
hash = "753835558c1037e39ca669737190013bd59a3ab16f7d5156c52e940c54ff9c60"
|
||||
file = "mods/redstoner-0.0.0-alpha.20.jar"
|
||||
hash = "669ca9a357874448c68ca45fdf26d827d5094b1ef1ee07f948a564b33cc1a79c"
|
||||
|
||||
[[files]]
|
||||
file = "mods/sanctuary-0.0.0-alpha.130.jar"
|
||||
hash = "8906be26de369903a0d5152d80a95a1c47b3468e4afca17ab4683b8300ad69a9"
|
||||
file = "mods/sanctuary-0.0.0-alpha.139.jar"
|
||||
hash = "62d06cfe57badc5295fdcaf1ad9d0eb6d7ba67345bdc5fc8ca6d689817f70c01"
|
||||
|
||||
[[files]]
|
||||
file = "mods/simple-voice-chat.pw.toml"
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
name = "Fabric API"
|
||||
filename = "fabric-api-0.158.0+26.2.jar"
|
||||
filename = "fabric-api-0.159.0+26.2.jar"
|
||||
side = "both"
|
||||
pin = true
|
||||
|
||||
[download]
|
||||
url = "https://cdn.modrinth.com/data/P7dR8mSH/versions/NqwNSxwA/fabric-api-0.158.0%2B26.2.jar"
|
||||
url = "https://cdn.modrinth.com/data/P7dR8mSH/versions/BgeCGgGZ/fabric-api-0.159.0%2B26.2.jar"
|
||||
hash-format = "sha512"
|
||||
hash = "4c2c1ebe74ffd54875a01ff371b53ba3d8674ac98d561f7dae02a96d3d37fbdbc5f5abc6e820f73b6154d6f873ddd05a442b0998ed2d456863dc0ad972e040a6"
|
||||
hash = "4d5d9c3daafe19be187f638f2d6de3db26420b8720ef5a266cfa81f79d9019cab8c86e6d47478bbb0b0ee0b8f769204b72c843caa2e4aabd94adc41f3dafb518"
|
||||
|
||||
[update]
|
||||
[update.modrinth]
|
||||
mod-id = "P7dR8mSH"
|
||||
version = "NqwNSxwA"
|
||||
version = "BgeCGgGZ"
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
name = "Just Enough Items (JEI)"
|
||||
filename = "jei-26.2-fabric-30.29.0.198.jar"
|
||||
filename = "jei-26.2-fabric-30.29.0.201.jar"
|
||||
side = "both"
|
||||
pin = true
|
||||
|
||||
[download]
|
||||
url = "https://cdn.modrinth.com/data/u6dRKJwZ/versions/67Dvn8sL/jei-26.2-fabric-30.29.0.198.jar"
|
||||
url = "https://cdn.modrinth.com/data/u6dRKJwZ/versions/d3HeviSL/jei-26.2-fabric-30.29.0.201.jar"
|
||||
hash-format = "sha512"
|
||||
hash = "06131c9528549840e07ddc9634e23454cd454380d7749a23d818f0efe0dacc02874e967ceb74a24ee82efb13e0fe2df4026f988a68f54f890586967e91dcaa5b"
|
||||
hash = "2279cb0ec4e41d9eebad80c2ecac76d19c8bc8e5aac69ba13f35b77494625c70d66edc74d59959116012bfb2f464b55806dd69fbf6387f25679b1553953e329a"
|
||||
|
||||
[update]
|
||||
[update.modrinth]
|
||||
mod-id = "u6dRKJwZ"
|
||||
version = "67Dvn8sL"
|
||||
version = "d3HeviSL"
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
name = "Sanctuary 26.2"
|
||||
author = "KOKA99CAB"
|
||||
version = "26.2.0-alpha.226"
|
||||
version = "26.2.0-alpha.233"
|
||||
pack-format = "packwiz:1.1.0"
|
||||
|
||||
[index]
|
||||
file = "index.toml"
|
||||
hash-format = "sha256"
|
||||
hash = "d3b929a596081adc3960866a2f939d4155d1b67ecb22bfe6050931a7d1d0f0a2"
|
||||
hash = "c666b031c2ac27a8f01cc4d12d6e471ea7471e9e5e7ce02567ee8a6649f9142a"
|
||||
|
||||
[versions]
|
||||
fabric = "0.19.3"
|
||||
|
||||
@@ -3,37 +3,37 @@
|
||||
"game": "minecraft",
|
||||
"name": "Sanctuary 26.2",
|
||||
"summary": "Sanctuary 26.2 — progression, exploration, worldgen and multiplayer systems by KOKA99CAB.",
|
||||
"versionId": "26.2.0-alpha.226",
|
||||
"versionId": "26.2.0-alpha.233",
|
||||
"files": [
|
||||
{
|
||||
"path": "mods/fabric-api-0.158.0+26.2.jar",
|
||||
"path": "mods/fabric-api-0.159.0+26.2.jar",
|
||||
"hashes": {
|
||||
"sha1": "7d0fb560579d28b933bdbc36b47449efe78d3bda",
|
||||
"sha512": "4c2c1ebe74ffd54875a01ff371b53ba3d8674ac98d561f7dae02a96d3d37fbdbc5f5abc6e820f73b6154d6f873ddd05a442b0998ed2d456863dc0ad972e040a6"
|
||||
"sha1": "5307b87f8336f27e8f963263ac514f4bc6175d90",
|
||||
"sha512": "4d5d9c3daafe19be187f638f2d6de3db26420b8720ef5a266cfa81f79d9019cab8c86e6d47478bbb0b0ee0b8f769204b72c843caa2e4aabd94adc41f3dafb518"
|
||||
},
|
||||
"env": {
|
||||
"client": "required",
|
||||
"server": "required"
|
||||
},
|
||||
"downloads": [
|
||||
"https://cdn.modrinth.com/data/P7dR8mSH/versions/NqwNSxwA/fabric-api-0.158.0%2B26.2.jar"
|
||||
"https://cdn.modrinth.com/data/P7dR8mSH/versions/BgeCGgGZ/fabric-api-0.159.0%2B26.2.jar"
|
||||
],
|
||||
"fileSize": 2535765
|
||||
"fileSize": 2535793
|
||||
},
|
||||
{
|
||||
"path": "mods/jei-26.2-fabric-30.29.0.198.jar",
|
||||
"path": "mods/jei-26.2-fabric-30.29.0.201.jar",
|
||||
"hashes": {
|
||||
"sha1": "652e964c71a699fdfdd96cda265ea703b13c6ef4",
|
||||
"sha512": "06131c9528549840e07ddc9634e23454cd454380d7749a23d818f0efe0dacc02874e967ceb74a24ee82efb13e0fe2df4026f988a68f54f890586967e91dcaa5b"
|
||||
"sha1": "e82447aff07e87a372cf817bc9c28b0d77b6f55c",
|
||||
"sha512": "2279cb0ec4e41d9eebad80c2ecac76d19c8bc8e5aac69ba13f35b77494625c70d66edc74d59959116012bfb2f464b55806dd69fbf6387f25679b1553953e329a"
|
||||
},
|
||||
"env": {
|
||||
"client": "required",
|
||||
"server": "required"
|
||||
},
|
||||
"downloads": [
|
||||
"https://cdn.modrinth.com/data/u6dRKJwZ/versions/67Dvn8sL/jei-26.2-fabric-30.29.0.198.jar"
|
||||
"https://cdn.modrinth.com/data/u6dRKJwZ/versions/d3HeviSL/jei-26.2-fabric-30.29.0.201.jar"
|
||||
],
|
||||
"fileSize": 1839279
|
||||
"fileSize": 1860103
|
||||
},
|
||||
{
|
||||
"path": "mods/voicechat-fabric-2.6.22+26.2.jar",
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"schema_version": 1,
|
||||
"pack_version": "26.2.0-alpha.226",
|
||||
"pack_version": "26.2.0-alpha.233",
|
||||
"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.226.mrpack",
|
||||
"primary_file": "sanctuary-26.2.0-alpha.233.mrpack",
|
||||
"shared_client_server_pack": true,
|
||||
"update_owner": "packwiz-installer",
|
||||
"check_on_launch": true,
|
||||
|
||||
@@ -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 = 3;
|
||||
public static final int CURRENT_DATA_VERSION = 5;
|
||||
public static final Logger LOGGER = LoggerFactory.getLogger(MOD_ID);
|
||||
|
||||
public RedStonerMod() {
|
||||
@@ -27,9 +28,10 @@ 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 and Storage Terminal ready (data v{}, network v{}).",
|
||||
LOGGER.info("[{}] Computers, Drawer, Particuleur, Storage Terminal and Trophy Panel ready (data v{}, network v{}).",
|
||||
DISPLAY_NAME, CURRENT_DATA_VERSION, RedStonerNetworking.PROTOCOL_VERSION);
|
||||
}
|
||||
}
|
||||
|
||||
+176
@@ -0,0 +1,176 @@
|
||||
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 java.util.Map;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Direction;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.server.permissions.Permissions;
|
||||
import net.minecraft.util.RandomSource;
|
||||
import net.minecraft.world.InteractionHand;
|
||||
import net.minecraft.world.InteractionResult;
|
||||
import net.minecraft.world.entity.LivingEntity;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.item.context.BlockPlaceContext;
|
||||
import net.minecraft.world.level.BlockGetter;
|
||||
import net.minecraft.world.level.Level;
|
||||
import net.minecraft.world.level.LevelReader;
|
||||
import net.minecraft.world.level.ScheduledTickAccess;
|
||||
import net.minecraft.world.level.block.BaseEntityBlock;
|
||||
import net.minecraft.world.level.block.Block;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.block.Mirror;
|
||||
import net.minecraft.world.level.block.RenderShape;
|
||||
import net.minecraft.world.level.block.Rotation;
|
||||
import net.minecraft.world.level.block.entity.BlockEntity;
|
||||
import net.minecraft.world.level.block.entity.BlockEntityTicker;
|
||||
import net.minecraft.world.level.block.entity.BlockEntityType;
|
||||
import net.minecraft.world.level.block.state.BlockBehaviour;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.block.state.StateDefinition;
|
||||
import net.minecraft.world.level.block.state.properties.BlockStateProperties;
|
||||
import net.minecraft.world.level.block.state.properties.EnumProperty;
|
||||
import net.minecraft.world.phys.BlockHitResult;
|
||||
import net.minecraft.world.phys.shapes.CollisionContext;
|
||||
import net.minecraft.world.phys.shapes.VoxelShape;
|
||||
|
||||
/** Wall-mounted, server-authoritative trophy and ranking panel. */
|
||||
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, 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) {
|
||||
super(properties);
|
||||
registerDefaultState(stateDefinition.any().setValue(FACING, Direction.NORTH));
|
||||
}
|
||||
|
||||
@Override
|
||||
public MapCodec<TrophyPanelBlock> codec() {
|
||||
return CODEC;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BlockEntity newBlockEntity(BlockPos pos, BlockState state) {
|
||||
return new TrophyPanelBlockEntity(pos, state);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T extends BlockEntity> BlockEntityTicker<T> getTicker(Level level, BlockState state,
|
||||
BlockEntityType<T> type) {
|
||||
if (level.isClientSide()) return null;
|
||||
return createTickerHelper(type, RedStonerRegistries.TROPHY_PANEL_BLOCK_ENTITY,
|
||||
TrophyPanelBlockEntity::serverTick);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected RenderShape getRenderShape(BlockState state) {
|
||||
return RenderShape.MODEL;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BlockState getStateForPlacement(BlockPlaceContext context) {
|
||||
Direction facing = context.getClickedFace();
|
||||
if (!facing.getAxis().isHorizontal()) return null;
|
||||
BlockState state = defaultBlockState().setValue(FACING, facing);
|
||||
return state.canSurvive(context.getLevel(), context.getClickedPos()) ? state : null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean canSurvive(BlockState state, LevelReader level, BlockPos pos) {
|
||||
Direction facing = state.getValue(FACING);
|
||||
BlockPos support = pos.relative(facing.getOpposite());
|
||||
return level.getBlockState(support).isFaceSturdy(level, support, facing);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected BlockState updateShape(BlockState state, LevelReader level, ScheduledTickAccess ticks,
|
||||
BlockPos pos, Direction direction, BlockPos neighborPos, BlockState neighborState, RandomSource random) {
|
||||
return direction == state.getValue(FACING).getOpposite() && !state.canSurvive(level, pos)
|
||||
? Blocks.AIR.defaultBlockState()
|
||||
: super.updateShape(state, level, ticks, pos, direction, neighborPos, neighborState, random);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected VoxelShape getShape(BlockState state, BlockGetter level, BlockPos pos, CollisionContext context) {
|
||||
return SHAPES.get(state.getValue(FACING));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setPlacedBy(Level level, BlockPos pos, BlockState state, LivingEntity placer, ItemStack stack) {
|
||||
super.setPlacedBy(level, pos, state, placer, stack);
|
||||
if (placer instanceof ServerPlayer player
|
||||
&& level.getBlockEntity(pos) instanceof TrophyPanelBlockEntity panel) panel.claim(player);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected InteractionResult useItemOn(ItemStack stack, BlockState state, Level level, BlockPos pos,
|
||||
Player player, InteractionHand hand, BlockHitResult hit) {
|
||||
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, true);
|
||||
|
||||
boolean operator = serverPlayer.createCommandSourceStack().permissions()
|
||||
.hasPermission(Permissions.COMMANDS_GAMEMASTER);
|
||||
if (!panel.canConfigure(serverPlayer, operator)) {
|
||||
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 -> throw new IllegalStateException("Handled above");
|
||||
case NEWER_DATA -> "newer_data";
|
||||
};
|
||||
serverPlayer.sendSystemMessage(Component.translatable("message.redstoner.trophy_panel." + key));
|
||||
return InteractionResult.SUCCESS_SERVER;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected InteractionResult useWithoutItem(BlockState state, Level level, BlockPos pos,
|
||||
Player player, BlockHitResult hit) {
|
||||
if (level.isClientSide()) return InteractionResult.SUCCESS;
|
||||
if (!(player instanceof ServerPlayer serverPlayer)
|
||||
|| !(level.getBlockEntity(pos) instanceof TrophyPanelBlockEntity panel)) return InteractionResult.CONSUME;
|
||||
return cycle(panel, serverPlayer, player.isSecondaryUseActive());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void createBlockStateDefinition(StateDefinition.Builder<Block, BlockState> builder) {
|
||||
builder.add(FACING);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected BlockState rotate(BlockState state, Rotation rotation) {
|
||||
return state.setValue(FACING, rotation.rotate(state.getValue(FACING)));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected BlockState mirror(BlockState state, Mirror mirror) {
|
||||
return state.rotate(mirror.getRotation(state.getValue(FACING)));
|
||||
}
|
||||
|
||||
private static InteractionResult 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;
|
||||
}
|
||||
player.sendSystemMessage(Component.translatable("message.redstoner.trophy_panel.page",
|
||||
panel.pageNumber(), panel.pageCount()));
|
||||
return InteractionResult.SUCCESS_SERVER;
|
||||
}
|
||||
}
|
||||
+303
@@ -0,0 +1,303 @@
|
||||
package fr.koka99cab.sanctuary26.redstoner.block.entity;
|
||||
|
||||
import com.mojang.serialization.Codec;
|
||||
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;
|
||||
import fr.koka99cab.sanctuary26.sanctuary.api.ranking.SanctuaryRankingApi;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.HolderLookup;
|
||||
import net.minecraft.core.UUIDUtil;
|
||||
import net.minecraft.nbt.CompoundTag;
|
||||
import net.minecraft.network.protocol.Packet;
|
||||
import net.minecraft.network.protocol.game.ClientGamePacketListener;
|
||||
import net.minecraft.network.protocol.game.ClientboundBlockEntityDataPacket;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.world.entity.ItemOwner;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.level.block.Block;
|
||||
import net.minecraft.world.level.block.entity.BlockEntity;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.storage.ValueInput;
|
||||
import net.minecraft.world.level.storage.ValueOutput;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
|
||||
/** Persistent panel configuration plus a transient, bounded client-visible ranking snapshot. */
|
||||
public final class TrophyPanelBlockEntity extends BlockEntity implements ItemOwner {
|
||||
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)
|
||||
).apply(instance, Page::new));
|
||||
private static final Codec<ClientEntry> CLIENT_ENTRY_CODEC = RecordCodecBuilder.create(instance -> instance.group(
|
||||
Codec.STRING.fieldOf("name").forGetter(ClientEntry::name),
|
||||
Codec.LONG.fieldOf("value").forGetter(ClientEntry::value),
|
||||
Codec.INT.fieldOf("rank").forGetter(ClientEntry::rank)
|
||||
).apply(instance, ClientEntry::new));
|
||||
private static final Codec<ClientSnapshot> CLIENT_SNAPSHOT_CODEC = RecordCodecBuilder.create(instance -> instance.group(
|
||||
Identifier.CODEC.fieldOf("metric").forGetter(ClientSnapshot::metricId),
|
||||
Codec.STRING.fieldOf("translation_key").forGetter(ClientSnapshot::translationKey),
|
||||
Codec.STRING.fieldOf("unit").forGetter(ClientSnapshot::unitKey),
|
||||
CLIENT_ENTRY_CODEC.listOf(0, RankingSnapshot.MAX_PODIUM_SIZE).fieldOf("podium")
|
||||
.forGetter(ClientSnapshot::podium),
|
||||
Codec.LONG.fieldOf("owner_value").forGetter(ClientSnapshot::ownerValue),
|
||||
Codec.INT.fieldOf("owner_rank").forGetter(ClientSnapshot::ownerRank)
|
||||
).apply(instance, ClientSnapshot::new));
|
||||
|
||||
private int dataVersion = DATA_VERSION;
|
||||
private UUID owner;
|
||||
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) {
|
||||
super(RedStonerRegistries.TROPHY_PANEL_BLOCK_ENTITY, pos, state);
|
||||
}
|
||||
|
||||
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;
|
||||
panel.refresh(serverLevel);
|
||||
}
|
||||
|
||||
public void claim(ServerPlayer player) {
|
||||
if (!writable() || owner != null) return;
|
||||
owner = player.getUUID();
|
||||
ownerName = cleanName(player.getGameProfile().name());
|
||||
syncPersistent();
|
||||
}
|
||||
|
||||
public boolean canConfigure(ServerPlayer player, boolean operator) {
|
||||
return writable() && TrophyPanelRules.canConfigure(owner, player.getUUID(), operator);
|
||||
}
|
||||
|
||||
public ConfigureResult configure(ServerPlayer player, ItemStack stack) {
|
||||
if (!writable()) return ConfigureResult.NEWER_DATA;
|
||||
List<RankingMetric> offered = SanctuaryRankingApi.itemMetrics(stack);
|
||||
if (offered.isEmpty()) return ConfigureResult.NO_METRIC;
|
||||
|
||||
List<Identifier> currentIds = pages.stream().map(Page::metricId).toList();
|
||||
List<Identifier> mergedIds = TrophyPanelRules.mergeMetricIds(currentIds,
|
||||
offered.stream().map(RankingMetric::id).toList());
|
||||
List<Page> next = new ArrayList<>();
|
||||
for (Identifier id : mergedIds) {
|
||||
Page existing = pages.stream().filter(page -> page.metricId().equals(id)).findFirst().orElse(null);
|
||||
next.add(existing == null ? new Page(id, stack.copyWithCount(1)) : existing);
|
||||
}
|
||||
pages = List.copyOf(next);
|
||||
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 nextPage() {
|
||||
return movePage(1);
|
||||
}
|
||||
|
||||
public boolean previousPage() {
|
||||
return movePage(-1);
|
||||
}
|
||||
|
||||
private boolean movePage(int offset) {
|
||||
if (!writable() || pages.isEmpty()) return false;
|
||||
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;
|
||||
}
|
||||
|
||||
public Page currentPage() {
|
||||
return pages.get(Math.clamp(currentPage, 0, pages.size() - 1));
|
||||
}
|
||||
|
||||
public int pageNumber() {
|
||||
return TrophyPanelRules.displayPageNumber(currentPage, valuesView, pages.size());
|
||||
}
|
||||
|
||||
public int pageCount() {
|
||||
return TrophyPanelRules.displayPageCount(pages.size());
|
||||
}
|
||||
|
||||
public boolean valuesView() {
|
||||
return valuesView;
|
||||
}
|
||||
|
||||
public String ownerName() {
|
||||
return ownerName;
|
||||
}
|
||||
|
||||
public ClientSnapshot visibleSnapshot() {
|
||||
return visibleSnapshot;
|
||||
}
|
||||
|
||||
private void refresh(ServerLevel level) {
|
||||
Page page = currentPage();
|
||||
RankingSnapshot snapshot = SanctuaryRankingApi.snapshot(level.getServer(), page.metricId(), owner);
|
||||
ClientSnapshot next = ClientSnapshot.from(snapshot);
|
||||
if (next.equals(visibleSnapshot)) return;
|
||||
visibleSnapshot = next;
|
||||
level.sendBlockUpdated(worldPosition, getBlockState(), getBlockState(), Block.UPDATE_CLIENTS);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void saveAdditional(ValueOutput output) {
|
||||
super.saveAdditional(output);
|
||||
output.putInt("DataVersion", dataVersion);
|
||||
if (owner != null) output.store("Owner", UUIDUtil.CODEC, owner);
|
||||
output.putString("OwnerName", ownerName);
|
||||
output.store("Pages", PAGE_CODEC.listOf(0, TrophyPanelRules.MAX_PAGES), pages);
|
||||
output.putInt("CurrentPage", currentPage);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void loadAdditional(ValueInput input) {
|
||||
super.loadAdditional(input);
|
||||
dataVersion = Math.max(DATA_VERSION, input.getIntOr("DataVersion", 0));
|
||||
owner = input.read("Owner", UUIDUtil.CODEC).orElse(null);
|
||||
ownerName = cleanName(input.getStringOr("OwnerName", "?"));
|
||||
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())));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Packet<ClientGamePacketListener> getUpdatePacket() {
|
||||
return ClientboundBlockEntityDataPacket.create(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
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;
|
||||
}
|
||||
|
||||
@Override
|
||||
public net.minecraft.world.level.Level level() {
|
||||
return level;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Vec3 position() {
|
||||
return Vec3.atCenterOf(worldPosition);
|
||||
}
|
||||
|
||||
@Override
|
||||
public float getVisualRotationYInDegrees() {
|
||||
return getBlockState().getValue(TrophyPanelBlock.FACING).toYRot();
|
||||
}
|
||||
|
||||
private void syncPersistent() {
|
||||
setChanged();
|
||||
if (level != null) level.sendBlockUpdated(worldPosition, getBlockState(), getBlockState(), Block.UPDATE_CLIENTS);
|
||||
}
|
||||
|
||||
private boolean writable() {
|
||||
return dataVersion <= DATA_VERSION;
|
||||
}
|
||||
|
||||
private static List<Page> sanitizePages(List<Page> loaded) {
|
||||
List<Page> valid = loaded.stream().filter(page -> SanctuaryRankingApi.metric(page.metricId()).isPresent())
|
||||
.limit(TrophyPanelRules.MAX_PAGES).toList();
|
||||
return valid.isEmpty() ? List.of(defaultPage()) : List.copyOf(valid);
|
||||
}
|
||||
|
||||
private static Page defaultPage() {
|
||||
return new Page(SanctuaryRankingApi.defaultMetric().id(), ItemStack.EMPTY);
|
||||
}
|
||||
|
||||
private static RankingMetric metricFor(Identifier id) {
|
||||
return SanctuaryRankingApi.metric(id).orElse(SanctuaryRankingApi.defaultMetric());
|
||||
}
|
||||
|
||||
private static String cleanName(String name) {
|
||||
if (name == null || name.isBlank()) return "?";
|
||||
String clean = name.strip();
|
||||
return clean.length() > 32 ? clean.substring(0, 32) : clean;
|
||||
}
|
||||
|
||||
public enum ConfigureResult { SUCCESS, NO_METRIC, NEWER_DATA }
|
||||
|
||||
public record Page(Identifier metricId, ItemStack icon) {
|
||||
public Page {
|
||||
metricId = metricId == null ? SanctuaryRankingApi.defaultMetric().id() : metricId;
|
||||
// The registry item is enough for an icon. Player components (containers, names, lore) never persist or sync.
|
||||
icon = icon == null || icon.isEmpty() ? ItemStack.EMPTY : new ItemStack(icon.getItem());
|
||||
}
|
||||
}
|
||||
|
||||
public record ClientEntry(String name, long value, int rank) {
|
||||
public ClientEntry {
|
||||
name = cleanName(name);
|
||||
value = Math.max(0L, value);
|
||||
rank = Math.max(0, rank);
|
||||
}
|
||||
}
|
||||
|
||||
public record ClientSnapshot(Identifier metricId, String translationKey, String unitKey,
|
||||
List<ClientEntry> podium, long ownerValue, int ownerRank) {
|
||||
public ClientSnapshot {
|
||||
translationKey = translationKey == null || translationKey.isBlank()
|
||||
? "metric.sanctuary.ranking.unknown" : translationKey;
|
||||
unitKey = RankingUnit.byKey(unitKey).key();
|
||||
podium = podium == null ? List.of() : List.copyOf(podium.stream()
|
||||
.limit(RankingSnapshot.MAX_PODIUM_SIZE).toList());
|
||||
ownerValue = Math.max(0L, ownerValue);
|
||||
ownerRank = Math.max(0, ownerRank);
|
||||
}
|
||||
|
||||
static ClientSnapshot from(RankingSnapshot snapshot) {
|
||||
return new ClientSnapshot(snapshot.metric().id(), snapshot.metric().translationKey(),
|
||||
snapshot.metric().unit().key(), snapshot.podium().stream()
|
||||
.map(entry -> new ClientEntry(entry.playerName(), entry.value(), entry.rank())).toList(),
|
||||
snapshot.owner().value(), snapshot.owner().rank());
|
||||
}
|
||||
|
||||
static ClientSnapshot empty(RankingMetric metric) {
|
||||
return new ClientSnapshot(metric.id(), metric.translationKey(), metric.unit().key(), List.of(), 0L, 0);
|
||||
}
|
||||
|
||||
public RankingUnit unit() {
|
||||
return RankingUnit.byKey(unitKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,9 +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;
|
||||
@@ -19,6 +22,8 @@ public final class RedStonerClient implements ClientModInitializer {
|
||||
public void onInitializeClient() {
|
||||
BlockEntityRendererRegistry.register(RedStonerRegistries.DRAWER_BLOCK_ENTITY, DrawerBlockEntityRenderer::new);
|
||||
BlockEntityRendererRegistry.register(RedStonerRegistries.LEGACY_DRAWER_BLOCK_ENTITY, DrawerBlockEntityRenderer::new);
|
||||
BlockEntityRendererRegistry.register(RedStonerRegistries.TROPHY_PANEL_BLOCK_ENTITY,
|
||||
TrophyPanelBlockEntityRenderer::new);
|
||||
MenuScreens.register(RedStonerMenus.STORAGE_TERMINAL, StorageTerminalScreen::new);
|
||||
ClientPlayNetworking.registerGlobalReceiver(OpenComputerPayload.TYPE, (payload, context) ->
|
||||
context.client().execute(() -> context.client().gui.setScreen(new ComputerScreen(payload))));
|
||||
@@ -30,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);
|
||||
}
|
||||
|
||||
|
||||
+45
-6
@@ -1,13 +1,17 @@
|
||||
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;
|
||||
import fr.koka99cab.sanctuary26.redstoner.storage.StorageTerminalStatus;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import net.minecraft.ChatFormatting;
|
||||
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.components.Tooltip;
|
||||
import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen;
|
||||
import net.minecraft.client.input.KeyEvent;
|
||||
import net.minecraft.client.input.MouseButtonEvent;
|
||||
@@ -28,8 +32,10 @@ public final class StorageTerminalScreen extends AbstractContainerScreen<Storage
|
||||
private static final int SCROLL_WIDTH = 4;
|
||||
private int requestedPage;
|
||||
private EditBox searchBox;
|
||||
private Button sortButton;
|
||||
private String searchValue = "";
|
||||
private String sentSearchValue = "";
|
||||
private StorageTerminalSort sortMode = StorageTerminalSort.ALPHABETICAL;
|
||||
|
||||
public StorageTerminalScreen(StorageTerminalMenu menu, Inventory inventory, Component title) {
|
||||
super(menu, inventory, title, 176, 222);
|
||||
@@ -40,13 +46,16 @@ public final class StorageTerminalScreen extends AbstractContainerScreen<Storage
|
||||
protected void init() {
|
||||
super.init();
|
||||
titleLabelY = 6;
|
||||
searchBox = addRenderableWidget(new EditBox(font, leftPos + 8, topPos + 2, 126, 14,
|
||||
searchBox = addRenderableWidget(new EditBox(font, leftPos + 8, topPos + 2, 96, 14,
|
||||
Component.translatable("screen.redstoner.storage_terminal.search")));
|
||||
searchBox.setMaxLength(StorageTerminalRules.MAX_SEARCH_LENGTH);
|
||||
searchBox.setHint(Component.translatable("screen.redstoner.storage_terminal.search"));
|
||||
searchBox.setValue(searchValue);
|
||||
searchBox.setResponder(this::searchChanged);
|
||||
setInitialFocus(searchBox);
|
||||
sortButton = addRenderableWidget(Button.builder(sortButtonLabel(), button -> requestSort())
|
||||
.bounds(leftPos + 108, topPos + 2, 34, 14)
|
||||
.tooltip(Tooltip.create(sortTooltip()))
|
||||
.build());
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -114,6 +123,10 @@ public final class StorageTerminalScreen extends AbstractContainerScreen<Storage
|
||||
|
||||
@Override
|
||||
public boolean mouseClicked(MouseButtonEvent event, boolean doubleClick) {
|
||||
if (searchBox != null && searchBox.isFocused()
|
||||
&& !searchBox.isMouseOver(event.x(), event.y())) {
|
||||
clearSearchFocus();
|
||||
}
|
||||
if (event.button() == 0
|
||||
&& isHovering(SCROLL_X - 2, TERMINAL_TOP, SCROLL_WIDTH + 4, TERMINAL_HEIGHT,
|
||||
event.x(), event.y())) {
|
||||
@@ -127,10 +140,9 @@ public final class StorageTerminalScreen extends AbstractContainerScreen<Storage
|
||||
|
||||
@Override
|
||||
public boolean keyPressed(KeyEvent event) {
|
||||
if (searchBox != null && searchBox.isFocused() && event.key() != 256) {
|
||||
searchBox.keyPressed(event);
|
||||
return true;
|
||||
}
|
||||
if (StorageTerminalKeyRules.capturesInventoryKey(
|
||||
searchBox != null && searchBox.isFocused(), minecraft != null
|
||||
&& minecraft.options.keyInventory.matches(event))) return true;
|
||||
return super.keyPressed(event);
|
||||
}
|
||||
|
||||
@@ -148,6 +160,28 @@ public final class StorageTerminalScreen extends AbstractContainerScreen<Storage
|
||||
}
|
||||
}
|
||||
|
||||
private void requestSort() {
|
||||
sortMode = sortMode.next();
|
||||
requestedPage = 0;
|
||||
sortButton.setMessage(sortButtonLabel());
|
||||
sortButton.setTooltip(Tooltip.create(sortTooltip()));
|
||||
sendMenuButton(StorageTerminalMenu.SORT_BUTTON_BASE + sortMode.ordinal());
|
||||
}
|
||||
|
||||
private Component sortButtonLabel() {
|
||||
return Component.literal(switch (sortMode) {
|
||||
case ALPHABETICAL -> "A→Z";
|
||||
case COUNT_ASCENDING -> "1↑";
|
||||
case COUNT_DESCENDING -> "1↓";
|
||||
});
|
||||
}
|
||||
|
||||
private Component sortTooltip() {
|
||||
return Component.translatable("screen.redstoner.storage_terminal.sort",
|
||||
Component.translatable("screen.redstoner.storage_terminal.sort."
|
||||
+ sortMode.translationSuffix()));
|
||||
}
|
||||
|
||||
private void searchChanged(String value) {
|
||||
String previous = sentSearchValue;
|
||||
searchValue = value;
|
||||
@@ -173,6 +207,11 @@ public final class StorageTerminalScreen extends AbstractContainerScreen<Storage
|
||||
}
|
||||
}
|
||||
|
||||
private void clearSearchFocus() {
|
||||
searchBox.setFocused(false);
|
||||
setFocused(null);
|
||||
}
|
||||
|
||||
private void extractScrollBar(GuiGraphicsExtractor graphics) {
|
||||
int x = leftPos + SCROLL_X;
|
||||
int y = topPos + TERMINAL_TOP;
|
||||
|
||||
+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) {}
|
||||
}
|
||||
+94
@@ -0,0 +1,94 @@
|
||||
package fr.koka99cab.sanctuary26.redstoner.client.renderer;
|
||||
|
||||
import com.mojang.blaze3d.vertex.PoseStack;
|
||||
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.state.level.CameraRenderState;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.util.FormattedCharSequence;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
|
||||
public final class TrophyPanelBlockEntityRenderer
|
||||
implements BlockEntityRenderer<TrophyPanelBlockEntity, TrophyPanelRenderState> {
|
||||
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) {
|
||||
font = context.font();
|
||||
}
|
||||
|
||||
@Override
|
||||
public TrophyPanelRenderState createRenderState() {
|
||||
return new TrophyPanelRenderState();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void extractRenderState(TrophyPanelBlockEntity panel, TrophyPanelRenderState state,
|
||||
float tickProgress, Vec3 cameraPos, ModelFeatureRenderer.CrumblingOverlay crumblingOverlay) {
|
||||
BlockEntityRenderer.super.extractRenderState(panel, state, tickProgress, cameraPos, crumblingOverlay);
|
||||
state.facing = panel.getBlockState().getValue(TrophyPanelBlock.FACING);
|
||||
Arrays.fill(state.lines, Component.empty());
|
||||
ItemStack icon = panel.currentPage().icon();
|
||||
TrophyPanelBlockEntity.ClientSnapshot snapshot = panel.visibleSnapshot();
|
||||
RankingUnit unit = snapshot.unit();
|
||||
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.lines[1] = Component.translatable("block.redstoner.trophy_panel.no_score");
|
||||
} else {
|
||||
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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void submit(TrophyPanelRenderState state, PoseStack poseStack,
|
||||
SubmitNodeCollector submitNodeCollector, CameraRenderState cameraRenderState) {
|
||||
float yRotation = -state.facing.toYRot();
|
||||
for (int index = 0; index < TrophyPanelRenderState.LINE_COUNT; index++) {
|
||||
submitText(state.lines[index], LINE_HEIGHTS[index], state, poseStack, submitNodeCollector, yRotation);
|
||||
}
|
||||
}
|
||||
|
||||
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(TEXT_SCALE, -TEXT_SCALE, TEXT_SCALE);
|
||||
submitNodeCollector.submitText(poseStack, x, 0.0F, text, false, Font.DisplayMode.POLYGON_OFFSET,
|
||||
state.lightCoords, TEXT_COLOR, 0, 0);
|
||||
poseStack.popPose();
|
||||
}
|
||||
}
|
||||
+16
@@ -0,0 +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.core.Direction;
|
||||
import net.minecraft.network.chat.Component;
|
||||
|
||||
public final class TrophyPanelRenderState extends BlockEntityRenderState {
|
||||
public static final int LINE_COUNT = TrophyPanelRules.LINE_COUNT;
|
||||
public final Component[] lines = new Component[LINE_COUNT];
|
||||
public Direction facing = Direction.NORTH;
|
||||
|
||||
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; }
|
||||
}
|
||||
+48
-1
@@ -6,12 +6,16 @@ import fr.koka99cab.sanctuary26.redstoner.block.DrawerBlock;
|
||||
import fr.koka99cab.sanctuary26.redstoner.block.PersonalComputerBlock;
|
||||
import fr.koka99cab.sanctuary26.redstoner.block.ParticuleurBlock;
|
||||
import fr.koka99cab.sanctuary26.redstoner.block.StorageTerminalBlock;
|
||||
import fr.koka99cab.sanctuary26.redstoner.block.TrophyPanelBlock;
|
||||
import fr.koka99cab.sanctuary26.redstoner.block.entity.ComputerBlockEntity;
|
||||
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;
|
||||
@@ -26,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 {
|
||||
@@ -71,6 +80,24 @@ public final class RedStonerRegistries {
|
||||
BlockBehaviour.Properties.of().mapColor(MapColor.COLOR_GRAY).strength(4.0F, 6.0F).sound(SoundType.METAL)
|
||||
.requiresCorrectToolForDrops().noOcclusion().setId(blockKey(RedStonerMod.id("storage_terminal")))
|
||||
);
|
||||
public static final TrophyPanelBlock TROPHY_PANEL = registerBlock(
|
||||
RedStonerMod.id("trophy_panel"),
|
||||
TrophyPanelBlock::new,
|
||||
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));
|
||||
@@ -92,6 +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 = 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);
|
||||
@@ -115,6 +143,8 @@ public final class RedStonerRegistries {
|
||||
RedStonerMod.id("particuleur"), ParticuleurBlockEntity::new, PARTICULEUR);
|
||||
public static final BlockEntityType<ParticuleurBlockEntity> LEGACY_PARTICULEUR_BLOCK_ENTITY = registerBlockEntity(
|
||||
LEGACY_PARTICULEUR_ID, ParticuleurBlockEntity::new, LEGACY_PARTICULEUR);
|
||||
public static final BlockEntityType<TrophyPanelBlockEntity> TROPHY_PANEL_BLOCK_ENTITY = registerBlockEntity(
|
||||
RedStonerMod.id("trophy_panel"), TrophyPanelBlockEntity::new, TROPHY_PANEL);
|
||||
|
||||
public static final CreativeModeTab CREATIVE_TAB = Registry.register(
|
||||
BuiltInRegistries.CREATIVE_MODE_TAB,
|
||||
@@ -127,6 +157,7 @@ public final class RedStonerRegistries {
|
||||
output.accept(CONTROLLER_ITEM);
|
||||
output.accept(PARTICULEUR_ITEM);
|
||||
output.accept(STORAGE_TERMINAL_ITEM);
|
||||
output.accept(TROPHY_PANEL_ITEM);
|
||||
})
|
||||
.build()
|
||||
);
|
||||
@@ -141,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 six 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) {
|
||||
@@ -187,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;
|
||||
}
|
||||
}
|
||||
+6
-1
@@ -27,6 +27,7 @@ public final class StorageTerminalMenu extends AbstractContainerMenu {
|
||||
public static final int PAGE_BUTTON_BASE = 10;
|
||||
public static final int SEARCH_CLEAR_BUTTON = 100;
|
||||
public static final int SEARCH_BACKSPACE_BUTTON = 101;
|
||||
public static final int SORT_BUTTON_BASE = 200;
|
||||
public static final int SEARCH_CODEPOINT_BUTTON_BASE = 1_000;
|
||||
private static final int PLAYER_INVENTORY_Y = 140;
|
||||
private static final int REFRESH_INTERVAL = 20;
|
||||
@@ -49,6 +50,7 @@ public final class StorageTerminalMenu extends AbstractContainerMenu {
|
||||
private final StorageTerminalLayout layout = new StorageTerminalLayout();
|
||||
private List<StorageTerminalInventory.Entry> entries = List.of();
|
||||
private String searchQuery = "";
|
||||
private StorageTerminalSort sortMode = StorageTerminalSort.ALPHABETICAL;
|
||||
private int refreshCooldown = REFRESH_INTERVAL;
|
||||
|
||||
public StorageTerminalMenu(int containerId, Inventory inventory) {
|
||||
@@ -131,6 +133,9 @@ public final class StorageTerminalMenu extends AbstractContainerMenu {
|
||||
if (searchQuery.isEmpty()) return true;
|
||||
searchQuery = searchQuery.substring(0,
|
||||
searchQuery.offsetByCodePoints(searchQuery.length(), -1));
|
||||
} else if (buttonId >= SORT_BUTTON_BASE
|
||||
&& buttonId < SORT_BUTTON_BASE + StorageTerminalSort.values().length) {
|
||||
sortMode = StorageTerminalSort.values()[buttonId - SORT_BUTTON_BASE];
|
||||
} else {
|
||||
int codePoint = buttonId - SEARCH_CODEPOINT_BUTTON_BASE;
|
||||
if (!isSearchCodePoint(codePoint)) return false;
|
||||
@@ -211,7 +216,7 @@ public final class StorageTerminalMenu extends AbstractContainerMenu {
|
||||
if (serverPlayer == null || terminalPos == null) return;
|
||||
discovery = StorageTerminalNetwork.discover(serverPlayer, terminalPos);
|
||||
List<StorageTerminalInventory.Entry> liveEntries =
|
||||
StorageTerminalNetwork.filteredEntries(discovery, searchQuery);
|
||||
sortMode.order(StorageTerminalNetwork.filteredEntries(discovery, searchQuery));
|
||||
layout.update(liveEntries, resetOrder);
|
||||
|
||||
int page = discovery.status() == StorageTerminalStatus.READY
|
||||
|
||||
+21
-4
@@ -5,12 +5,16 @@ import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.regex.Pattern;
|
||||
import net.minecraft.core.component.DataComponents;
|
||||
import net.minecraft.core.registries.BuiltInRegistries;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.world.item.BlockItem;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.level.block.ShulkerBoxBlock;
|
||||
|
||||
/** Server-side matching for the bounded terminal search field. */
|
||||
public final class StorageTerminalSearch {
|
||||
private static final int SHULKER_SLOT_COUNT = 27;
|
||||
private static final Pattern COMBINING_MARKS = Pattern.compile("\\p{M}+");
|
||||
private static final Pattern NON_ALPHANUMERIC = Pattern.compile("[^\\p{L}\\p{N}]+");
|
||||
|
||||
@@ -38,14 +42,27 @@ public final class StorageTerminalSearch {
|
||||
}
|
||||
|
||||
private static boolean matches(ItemStack stack, String[] terms) {
|
||||
StringBuilder searchable = new StringBuilder(stack.getHoverName().getString());
|
||||
Identifier id = BuiltInRegistries.ITEM.getKey(stack.getItem());
|
||||
if (id != null) searchable.append(' ').append(id);
|
||||
searchable.append(' ').append(stack.getItem().getDescriptionId());
|
||||
StringBuilder searchable = new StringBuilder();
|
||||
appendSearchableText(searchable, stack);
|
||||
if (stack.getItem() instanceof BlockItem blockItem
|
||||
&& blockItem.getBlock() instanceof ShulkerBoxBlock) {
|
||||
var contents = stack.get(DataComponents.CONTAINER);
|
||||
if (contents != null) {
|
||||
contents.nonEmptyItemCopyStream().limit(SHULKER_SLOT_COUNT).forEach(contained ->
|
||||
appendSearchableText(searchable, contained));
|
||||
}
|
||||
}
|
||||
String normalized = normalize(searchable.toString());
|
||||
for (String term : terms) {
|
||||
if (!normalized.contains(term)) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private static void appendSearchableText(StringBuilder searchable, ItemStack stack) {
|
||||
searchable.append(' ').append(stack.getHoverName().getString());
|
||||
Identifier id = BuiltInRegistries.ITEM.getKey(stack.getItem());
|
||||
if (id != null) searchable.append(' ').append(id);
|
||||
searchable.append(' ').append(stack.getItem().getDescriptionId());
|
||||
}
|
||||
}
|
||||
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
package fr.koka99cab.sanctuary26.redstoner.storage;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import net.minecraft.core.registries.BuiltInRegistries;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
|
||||
/** Explicit server-side ordering modes for terminal entries. */
|
||||
public enum StorageTerminalSort {
|
||||
ALPHABETICAL("alphabetical"),
|
||||
COUNT_ASCENDING("count_ascending"),
|
||||
COUNT_DESCENDING("count_descending");
|
||||
|
||||
private final String translationSuffix;
|
||||
|
||||
StorageTerminalSort(String translationSuffix) {
|
||||
this.translationSuffix = translationSuffix;
|
||||
}
|
||||
|
||||
public String translationSuffix() {
|
||||
return translationSuffix;
|
||||
}
|
||||
|
||||
public StorageTerminalSort next() {
|
||||
StorageTerminalSort[] values = values();
|
||||
return values[(ordinal() + 1) % values.length];
|
||||
}
|
||||
|
||||
public List<StorageTerminalInventory.Entry> order(
|
||||
List<StorageTerminalInventory.Entry> entries) {
|
||||
List<StorageTerminalInventory.Entry> ordered = new ArrayList<>(entries);
|
||||
ordered.sort(comparator());
|
||||
return List.copyOf(ordered);
|
||||
}
|
||||
|
||||
private Comparator<StorageTerminalInventory.Entry> comparator() {
|
||||
Comparator<StorageTerminalInventory.Entry> alphabetical = Comparator
|
||||
.comparing((StorageTerminalInventory.Entry entry) -> alphabeticalName(entry.stack()))
|
||||
.thenComparing(entry -> itemId(entry.stack()));
|
||||
return switch (this) {
|
||||
case ALPHABETICAL -> alphabetical;
|
||||
case COUNT_ASCENDING -> Comparator
|
||||
.comparingInt(StorageTerminalInventory.Entry::total).thenComparing(alphabetical);
|
||||
case COUNT_DESCENDING -> Comparator
|
||||
.comparingInt(StorageTerminalInventory.Entry::total).reversed().thenComparing(alphabetical);
|
||||
};
|
||||
}
|
||||
|
||||
private static String alphabeticalName(ItemStack stack) {
|
||||
return StorageTerminalSearch.normalize(stack.getHoverName().getString());
|
||||
}
|
||||
|
||||
private static String itemId(ItemStack stack) {
|
||||
Identifier id = BuiltInRegistries.ITEM.getKey(stack.getItem());
|
||||
return id == null ? "" : id.toString();
|
||||
}
|
||||
}
|
||||
+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;
|
||||
}
|
||||
}
|
||||
+74
@@ -0,0 +1,74 @@
|
||||
package fr.koka99cab.sanctuary26.redstoner.trophy;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
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() {}
|
||||
|
||||
public static boolean canConfigure(UUID owner, UUID actor, boolean operator) {
|
||||
return operator || owner != null && owner.equals(actor);
|
||||
}
|
||||
|
||||
public static int nextPage(int currentPage, int 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) {
|
||||
LinkedHashSet<Identifier> merged = new LinkedHashSet<>();
|
||||
if (current != null) merged.addAll(current);
|
||||
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,8 @@
|
||||
{
|
||||
"variants": {
|
||||
"facing=north": { "model": "redstoner:block/trophy_panel" },
|
||||
"facing=east": { "model": "redstoner:block/trophy_panel", "y": 90 },
|
||||
"facing=south": { "model": "redstoner:block/trophy_panel", "y": 180 },
|
||||
"facing=west": { "model": "redstoner:block/trophy_panel", "y": 270 }
|
||||
}
|
||||
}
|
||||
@@ -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 }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"model": {
|
||||
"type": "minecraft:model",
|
||||
"model": "redstoner:item/trophy_sign"
|
||||
}
|
||||
}
|
||||
@@ -55,8 +55,56 @@
|
||||
"screen.redstoner.storage_terminal.search": "Search…",
|
||||
"screen.redstoner.storage_terminal.no_results": "No items found.",
|
||||
"screen.redstoner.storage_terminal.summary": "%s/%s · %s blocks · %s types",
|
||||
"screen.redstoner.storage_terminal.sort": "Sort: %s",
|
||||
"screen.redstoner.storage_terminal.sort.alphabetical": "alphabetical",
|
||||
"screen.redstoner.storage_terminal.sort.count_ascending": "quantity, ascending",
|
||||
"screen.redstoner.storage_terminal.sort.count_descending": "quantity, descending",
|
||||
"screen.redstoner.storage_terminal.error.invalid_terminal": "The terminal is no longer accessible.",
|
||||
"screen.redstoner.storage_terminal.error.disconnected": "No connected chest or barrel.",
|
||||
"screen.redstoner.storage_terminal.error.too_large": "Network too large (maximum 128 storage blocks).",
|
||||
"screen.redstoner.storage_terminal.error.locked": "The network contains a locked storage block."
|
||||
"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",
|
||||
"block.redstoner.trophy_panel.owner_unranked": "%s · unranked",
|
||||
"message.redstoner.trophy_panel.not_owner": "Only the owner or an operator can configure this panel.",
|
||||
"message.redstoner.trophy_panel.configured": "Compatible pages added; the item was not consumed.",
|
||||
"message.redstoner.trophy_panel.no_metric": "This item has no compatible statistic.",
|
||||
"message.redstoner.trophy_panel.newer_data": "This panel uses a newer data format and remains read-only.",
|
||||
"message.redstoner.trophy_panel.page": "Page %s/%s",
|
||||
"message.redstoner.trophy_panel.header": "%s · page %s/%s",
|
||||
"message.redstoner.trophy_panel.podium": "#%s %s · %s",
|
||||
"message.redstoner.trophy_panel.owner_rank": "%s: rank #%s · %s",
|
||||
"message.redstoner.trophy_panel.owner_unranked": "%s: no score"
|
||||
}
|
||||
|
||||
@@ -55,8 +55,56 @@
|
||||
"screen.redstoner.storage_terminal.search": "Rechercher…",
|
||||
"screen.redstoner.storage_terminal.no_results": "Aucun objet trouvé.",
|
||||
"screen.redstoner.storage_terminal.summary": "%s/%s · %s blocs · %s types",
|
||||
"screen.redstoner.storage_terminal.sort": "Tri : %s",
|
||||
"screen.redstoner.storage_terminal.sort.alphabetical": "alphabétique",
|
||||
"screen.redstoner.storage_terminal.sort.count_ascending": "quantité croissante",
|
||||
"screen.redstoner.storage_terminal.sort.count_descending": "quantité décroissante",
|
||||
"screen.redstoner.storage_terminal.error.invalid_terminal": "Le terminal n'est plus accessible.",
|
||||
"screen.redstoner.storage_terminal.error.disconnected": "Aucun coffre ou baril connecté.",
|
||||
"screen.redstoner.storage_terminal.error.too_large": "Réseau trop grand (128 stockages maximum).",
|
||||
"screen.redstoner.storage_terminal.error.locked": "Le réseau contient un stockage verrouillé."
|
||||
"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",
|
||||
"block.redstoner.trophy_panel.owner_unranked": "%s · non classé",
|
||||
"message.redstoner.trophy_panel.not_owner": "Seul le propriétaire ou un opérateur peut configurer ce panneau.",
|
||||
"message.redstoner.trophy_panel.configured": "Pages compatibles ajoutées ; l’objet n’a pas été consommé.",
|
||||
"message.redstoner.trophy_panel.no_metric": "Cet objet ne fournit aucune statistique compatible.",
|
||||
"message.redstoner.trophy_panel.newer_data": "Ce panneau utilise un format de données plus récent et reste en lecture seule.",
|
||||
"message.redstoner.trophy_panel.page": "Page %s/%s",
|
||||
"message.redstoner.trophy_panel.header": "%s · page %s/%s",
|
||||
"message.redstoner.trophy_panel.podium": "#%s %s · %s",
|
||||
"message.redstoner.trophy_panel.owner_rank": "%s : rang #%s · %s",
|
||||
"message.redstoner.trophy_panel.owner_unranked": "%s : aucun score"
|
||||
}
|
||||
|
||||
@@ -55,8 +55,56 @@
|
||||
"screen.redstoner.storage_terminal.search": "Поиск…",
|
||||
"screen.redstoner.storage_terminal.no_results": "Предметы не найдены.",
|
||||
"screen.redstoner.storage_terminal.summary": "%s/%s · блоков: %s · типов: %s",
|
||||
"screen.redstoner.storage_terminal.sort": "Сортировка: %s",
|
||||
"screen.redstoner.storage_terminal.sort.alphabetical": "по алфавиту",
|
||||
"screen.redstoner.storage_terminal.sort.count_ascending": "по возрастанию количества",
|
||||
"screen.redstoner.storage_terminal.sort.count_descending": "по убыванию количества",
|
||||
"screen.redstoner.storage_terminal.error.invalid_terminal": "Терминал больше недоступен.",
|
||||
"screen.redstoner.storage_terminal.error.disconnected": "Нет подключённых сундуков или бочек.",
|
||||
"screen.redstoner.storage_terminal.error.too_large": "Сеть слишком велика (не более 128 хранилищ).",
|
||||
"screen.redstoner.storage_terminal.error.locked": "В сети есть запертое хранилище."
|
||||
"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",
|
||||
"block.redstoner.trophy_panel.owner_unranked": "%s · без места",
|
||||
"message.redstoner.trophy_panel.not_owner": "Настраивать эту панель может только владелец или оператор.",
|
||||
"message.redstoner.trophy_panel.configured": "Совместимые страницы добавлены; предмет не был израсходован.",
|
||||
"message.redstoner.trophy_panel.no_metric": "У этого предмета нет совместимой статистики.",
|
||||
"message.redstoner.trophy_panel.newer_data": "Панель использует более новый формат данных и доступна только для чтения.",
|
||||
"message.redstoner.trophy_panel.page": "Страница %s/%s",
|
||||
"message.redstoner.trophy_panel.header": "%s · страница %s/%s",
|
||||
"message.redstoner.trophy_panel.podium": "#%s %s · %s",
|
||||
"message.redstoner.trophy_panel.owner_rank": "%s: место #%s · %s",
|
||||
"message.redstoner.trophy_panel.owner_unranked": "%s: результата нет"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"parent": "minecraft:block/block",
|
||||
"textures": {
|
||||
"particle": "minecraft:block/oak_planks",
|
||||
"wood": "minecraft:block/oak_planks"
|
||||
},
|
||||
"elements": [
|
||||
{
|
||||
"from": [0, 0, 14],
|
||||
"to": [16, 16, 16],
|
||||
"faces": {
|
||||
"north": { "texture": "#wood" },
|
||||
"south": { "texture": "#wood" },
|
||||
"west": { "texture": "#wood" },
|
||||
"east": { "texture": "#wood" },
|
||||
"up": { "texture": "#wood" },
|
||||
"down": { "texture": "#wood" }
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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"]
|
||||
}
|
||||
@@ -1 +1 @@
|
||||
{"replace":false,"values":["redstoner:eight_bit_computer","redstoner:controller","redstoner:personal_computer","redstoner:particuleur","redstoner:storage_terminal","redstone_computer:redstone_computer","redstone_computer:redstone_controller","redstone_computer:personal_computer","particuleur:particuleur"]}
|
||||
{"replace":false,"values":["redstoner:eight_bit_computer","redstoner:controller","redstoner:personal_computer","redstoner:particuleur","redstoner:storage_terminal","redstoner:trophy_panel","redstone_computer:redstone_computer","redstone_computer:redstone_controller","redstone_computer:personal_computer","particuleur:particuleur"]}
|
||||
|
||||
@@ -1 +1 @@
|
||||
{"replace":false,"values":["redstoner:eight_bit_computer","redstoner:controller","redstoner:personal_computer","redstoner:particuleur","redstoner:storage_terminal","redstone_computer:redstone_computer","redstone_computer:redstone_controller","redstone_computer:personal_computer","particuleur:particuleur"]}
|
||||
{"replace":false,"values":["redstoner:eight_bit_computer","redstoner:controller","redstoner:personal_computer","redstoner:particuleur","redstoner:storage_terminal","redstoner:trophy_panel","redstone_computer:redstone_computer","redstone_computer:redstone_controller","redstone_computer:personal_computer","particuleur:particuleur"]}
|
||||
|
||||
@@ -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": [
|
||||
{
|
||||
"rolls": 1,
|
||||
"entries": [
|
||||
{ "type": "minecraft:item", "name": "redstoner:trophy_panel" }
|
||||
],
|
||||
"conditions": [
|
||||
{ "condition": "minecraft:survives_explosion" }
|
||||
]
|
||||
}
|
||||
],
|
||||
"random_sequence": "redstoner:blocks/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"
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"type": "minecraft:crafting_shapeless",
|
||||
"category": "building",
|
||||
"ingredients": [
|
||||
"#minecraft:signs",
|
||||
"minecraft:white_dye"
|
||||
],
|
||||
"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": 3,
|
||||
"network_protocol": 7,
|
||||
"data_version": 5,
|
||||
"network_protocol": 9,
|
||||
"lifecycle": "${lifecycle}",
|
||||
"pack_version": "${pack_version}",
|
||||
"descriptions": {
|
||||
|
||||
+67
@@ -14,6 +14,7 @@ import net.minecraft.world.SimpleContainer;
|
||||
import net.minecraft.world.item.Item;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.item.Items;
|
||||
import net.minecraft.world.item.component.ItemContainerContents;
|
||||
|
||||
public final class StorageTerminalSmoke {
|
||||
private StorageTerminalSmoke() {
|
||||
@@ -26,6 +27,8 @@ public final class StorageTerminalSmoke {
|
||||
verifyPagination();
|
||||
verifyAggregationAndTransfers();
|
||||
verifySearch();
|
||||
verifySearchKeyboardRouting();
|
||||
verifySorting();
|
||||
verifyStableLayout();
|
||||
}
|
||||
|
||||
@@ -115,6 +118,14 @@ public final class StorageTerminalSmoke {
|
||||
ItemStack russian = new ItemStack(stackable(Items.REDSTONE), 3);
|
||||
russian.set(DataComponents.CUSTOM_NAME, Component.literal("Красный камень"));
|
||||
container.setItem(2, russian);
|
||||
Holder<Item> shulkerItem = new Holder.Direct<>(Items.SHULKER_BOX, DataComponentMap.builder()
|
||||
.set(DataComponents.MAX_STACK_SIZE, 1).build());
|
||||
ItemStack shulker = new ItemStack(shulkerItem);
|
||||
ItemStack nestedDiamond = new ItemStack(stackable(Items.DIAMOND), 7);
|
||||
nestedDiamond.set(DataComponents.CUSTOM_NAME, Component.literal("Trésor intérieur"));
|
||||
shulker.set(DataComponents.CONTAINER, ItemContainerContents.fromItems(List.of(
|
||||
nestedDiamond, new ItemStack(stackable(Items.OAK_LOG), 32))));
|
||||
container.setItem(3, shulker);
|
||||
|
||||
List<StorageTerminalInventory.Entry> entries = StorageTerminalInventory.aggregate(List.of(container));
|
||||
require(StorageTerminalSearch.filter(entries, "minecraft:cobblestone").size() == 1,
|
||||
@@ -125,15 +136,71 @@ public final class StorageTerminalSmoke {
|
||||
"Terminal search did not ignore accents in a custom name");
|
||||
require(StorageTerminalSearch.filter(entries, "красный камень").size() == 1,
|
||||
"Terminal search did not preserve non-Latin keyboard input");
|
||||
require(StorageTerminalSearch.filter(entries, "minecraft:oak_log").size() == 1,
|
||||
"Terminal search did not find an item id inside a Shulker box");
|
||||
require(StorageTerminalSearch.filter(entries, "tresor interieur oak log").size() == 1,
|
||||
"Terminal search did not index the composed contents of a Shulker box");
|
||||
require(StorageTerminalSearch.filter(entries, "shulker box").size() == 1,
|
||||
"Searching Shulker contents broke matching against the box itself");
|
||||
require(StorageTerminalSearch.filter(entries, "missing").isEmpty(),
|
||||
"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());
|
||||
}
|
||||
|
||||
private static void verifySorting() {
|
||||
ItemStack eclair = new ItemStack(stackable(Items.STONE));
|
||||
eclair.set(DataComponents.CUSTOM_NAME, Component.literal("Éclair"));
|
||||
ItemStack banana = new ItemStack(stackable(Items.DIRT));
|
||||
banana.set(DataComponents.CUSTOM_NAME, Component.literal("Banane"));
|
||||
ItemStack zebra = new ItemStack(stackable(Items.DIAMOND));
|
||||
zebra.set(DataComponents.CUSTOM_NAME, Component.literal("Zèbre"));
|
||||
List<StorageTerminalInventory.Entry> entries = List.of(
|
||||
new StorageTerminalInventory.Entry(eclair, 64),
|
||||
new StorageTerminalInventory.Entry(banana, 8),
|
||||
new StorageTerminalInventory.Entry(zebra, 2));
|
||||
|
||||
require(names(StorageTerminalSort.ALPHABETICAL.order(entries))
|
||||
.equals(List.of("Banane", "Éclair", "Zèbre")),
|
||||
"Alphabetical terminal sorting ignored names or accent folding");
|
||||
require(names(StorageTerminalSort.COUNT_ASCENDING.order(entries))
|
||||
.equals(List.of("Zèbre", "Banane", "Éclair")),
|
||||
"Ascending terminal quantity sorting is incorrect");
|
||||
require(names(StorageTerminalSort.COUNT_DESCENDING.order(entries))
|
||||
.equals(List.of("Éclair", "Banane", "Zèbre")),
|
||||
"Descending terminal quantity sorting is incorrect");
|
||||
require(StorageTerminalSort.ALPHABETICAL.next() == StorageTerminalSort.COUNT_ASCENDING
|
||||
&& StorageTerminalSort.COUNT_ASCENDING.next() == StorageTerminalSort.COUNT_DESCENDING
|
||||
&& StorageTerminalSort.COUNT_DESCENDING.next() == StorageTerminalSort.ALPHABETICAL,
|
||||
"Terminal sorting modes do not cycle deterministically");
|
||||
|
||||
StorageTerminalLayout stable = new StorageTerminalLayout();
|
||||
stable.update(StorageTerminalSort.COUNT_DESCENDING.order(entries), true);
|
||||
List<StorageTerminalInventory.Entry> changedCounts = List.of(
|
||||
new StorageTerminalInventory.Entry(eclair, 1),
|
||||
new StorageTerminalInventory.Entry(banana, 80),
|
||||
new StorageTerminalInventory.Entry(zebra, 2));
|
||||
stable.update(StorageTerminalSort.COUNT_DESCENDING.order(changedCounts), false);
|
||||
require(names(stable.page(0).subList(0, 3)).equals(List.of("Éclair", "Banane", "Zèbre")),
|
||||
"A periodic quantity refresh moved terminal entries under the cursor");
|
||||
}
|
||||
|
||||
private static List<String> names(List<StorageTerminalInventory.Entry> entries) {
|
||||
return entries.stream().map(entry -> entry.stack().getHoverName().getString()).toList();
|
||||
}
|
||||
|
||||
private static void verifyStableLayout() {
|
||||
StorageTerminalInventory.Entry stone = new StorageTerminalInventory.Entry(
|
||||
new ItemStack(stackable(Items.STONE)), 64);
|
||||
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
package fr.koka99cab.sanctuary26.redstoner.trophy;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import net.minecraft.resources.Identifier;
|
||||
|
||||
public final class TrophyPanelRulesSmoke {
|
||||
private TrophyPanelRulesSmoke() {}
|
||||
|
||||
public static void main(String[] args) {
|
||||
UUID owner = UUID.randomUUID();
|
||||
UUID visitor = UUID.randomUUID();
|
||||
require(TrophyPanelRules.canConfigure(owner, owner, false), "The owner cannot configure their panel");
|
||||
require(!TrophyPanelRules.canConfigure(owner, visitor, false), "A visitor can configure another panel");
|
||||
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");
|
||||
List<Identifier> offered = java.util.stream.IntStream.range(0, TrophyPanelRules.MAX_PAGES + 5)
|
||||
.mapToObj(index -> Identifier.parse("sanctuary:metric_" + index)).toList();
|
||||
List<Identifier> merged = TrophyPanelRules.mergeMetricIds(List.of(first, second, first), offered);
|
||||
require(merged.size() == TrophyPanelRules.MAX_PAGES, "Panel pages are not bounded");
|
||||
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) {
|
||||
if (!condition) throw new IllegalStateException(message);
|
||||
}
|
||||
}
|
||||
+38
-10
@@ -22,6 +22,9 @@ Sa zone architecturale forme un disque de 2 000 blocs autour de l’origine.
|
||||
- Toute autre téléportation vers la Backroom est refusée. Une téléportation forcée vers l’extérieur
|
||||
reste possible pour l’administration, mais replace d’abord le butin porté dans le registre : elle
|
||||
ne constitue jamais une extraction réussie.
|
||||
- Chaque arrivée se fait sur le dernier plancher du niveau 0. Le serveur libère exactement les deux
|
||||
blocs nécessaires au joueur et pose une torche adjacente : elle peut être récupérée et devient le
|
||||
premier outil d’exploration dans une dimension autrement noire.
|
||||
|
||||
## Objets perdus et coffres
|
||||
|
||||
@@ -32,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
|
||||
@@ -44,19 +48,43 @@ Pendant une expédition, chaque coffre distinct est un essai personnel. Le premi
|
||||
de proposer une perte récente du joueur, le deuxième 90 %, le troisième 100 %, puis les suivants
|
||||
65 %. Sans perte personnelle disponible, le coffre suit simplement son tirage général.
|
||||
|
||||
## Architecture
|
||||
## Architecture verticale
|
||||
|
||||
Les plans sont déterministes par grandes zones et utilisent cinq familles composées :
|
||||
La liste [Normal Levels I](https://backrooms-wiki.wikidot.com/normal-levels-i) sert uniquement de
|
||||
référence d’ambiance et de vocabulaire. Les plans, palettes et règles ci-dessous sont originaux au
|
||||
pack ; aucun texte ni visuel du wiki n’est embarqué.
|
||||
|
||||
- mégalopoles en béton, verre, pierre et voirie ;
|
||||
- bureaux superposés en bois, clay, textile et éléments techniques ;
|
||||
- plaines naturelles avec arbres et rares ruines ;
|
||||
- agrégats de plateformes et de pièces flottantes reliées verticalement ;
|
||||
- pièces construites visuellement à 90° ou à l’envers, sans modifier la gravité globale.
|
||||
La dimension couvre maintenant toute la hauteur de **Y = -64 à Y = 383**. Ses cinq niveaux sont
|
||||
numérotés de haut en bas afin qu’une chute fasse toujours avancer vers le niveau suivant :
|
||||
|
||||
Les matériaux très marqués d’Another World n’apparaissent que ponctuellement aux anomalies. Aucun
|
||||
bloc précieux ne sert de matériau structurel ; de très rares blocs de diamant sont cachés dans les
|
||||
fondations.
|
||||
- **niveau 0 — Seuil, Y 295 à 383** : cinq plans de pièces jaunes et sableuses, avec couloirs,
|
||||
cloisons à 90°, colonnes et quatre angles de salle. Chaque angle possède cinq variantes de
|
||||
matériaux ; des trous rares relient les plans superposés ;
|
||||
- **niveau 1 — Archives, Y 206 à 294** : quatre étages de bureaux et de bibliothèques pouvant
|
||||
couvrir plusieurs chunks, avec longues rangées de rayonnages, grandes fenêtres intérieures,
|
||||
salles de lecture et cloisons ouvertes ;
|
||||
- **niveau 2 — Ville, Y 117 à 205** : bâtiments rapprochés séparés par de petites ruelles,
|
||||
autoroutes orthogonales continues, tours à plusieurs étages et rares parcs enfermés sous le
|
||||
plafond de la dimension ;
|
||||
- **niveau 3 — Services, Y 28 à 116** : cinq plans de couloirs techniques en briques, grilles et
|
||||
béton, dont toutes les jonctions de portes restent physiquement ouvertes ;
|
||||
- **niveau 4 — Glitch, Y -64 à 27** : prismes de briques en grille 3D autour d’un toit où l’on peut
|
||||
tomber, puis réseau déterministe de salles et couloirs. Chaque cellule du labyrinthe conserve une
|
||||
route ouverte vers son origine ; certaines cloisons disparaissent pour fusionner des salles de
|
||||
tailles différentes. De rares trous dans le dernier plancher mènent au vrai vide.
|
||||
|
||||
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. 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
|
||||
injectée par un autre système. Les matériaux très marqués d’Another World n’apparaissent que
|
||||
ponctuellement aux anomalies. Aucun bloc précieux ne sert de matériau structurel ; de très rares
|
||||
blocs de diamant restent cachés dans les fondations.
|
||||
|
||||
## Exploitation
|
||||
|
||||
|
||||
+106
-15
@@ -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.131"
|
||||
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")
|
||||
@@ -1208,7 +1224,7 @@ tasks.register("verifySanctuary") {
|
||||
}
|
||||
|
||||
def manifest = new JsonSlurper().parse(file("src/main/resources/fabric.mod.json"))
|
||||
if (manifest.custom?.sanctuary26?.data_version != 14
|
||||
if (manifest.custom?.sanctuary26?.data_version != 15
|
||||
|| manifest.custom?.sanctuary26?.network_protocol != 24
|
||||
|| manifest.depends?.jei != ">=30.24.0.176"
|
||||
|| manifest.depends?."fabric-api" != ">=0.155.0+26.2"
|
||||
@@ -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.131"
|
||||
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.131"
|
||||
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.131"
|
||||
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.131"
|
||||
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.131"
|
||||
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.131"
|
||||
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.131"
|
||||
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
|
||||
@@ -3267,6 +3283,8 @@ tasks.register("verifyQuestRequests") {
|
||||
|| !data.contains('SAVE_KEY = "quest_requests"') || !data.contains("DATA_VERSION = 3")
|
||||
|| !service.contains("PlayerBlockBreakEvents.BEFORE")
|
||||
|| !service.contains("player.blockPosition().distSqr(payload.pos()) > 64.0D")
|
||||
|| !service.contains('"sign.sanctuary.quest_request.completed_by"')
|
||||
|| !service.contains('"message.sanctuary.quest_request.details.completed_by"')
|
||||
|| !network.contains("PROTOCOL_VERSION = 24")
|
||||
|| !network.contains("QuestRequestSubmitPayload.ID")
|
||||
|| !network.contains("QuestRequestAcceptPayload.ID")
|
||||
@@ -3276,6 +3294,8 @@ tasks.register("verifyQuestRequests") {
|
||||
!values.containsKey("block.sanctuary.quest_request_sign")
|
||||
|| !values.containsKey("screen.sanctuary.quest_request.title")
|
||||
|| !values.containsKey("message.sanctuary.quest_request.claimed")
|
||||
|| !values.containsKey("message.sanctuary.quest_request.details.completed_by")
|
||||
|| !values.containsKey("sign.sanctuary.quest_request.completed_by")
|
||||
}) {
|
||||
throw new GradleException("Sanctuary alpha.113 quest-request contract is incomplete")
|
||||
}
|
||||
@@ -3313,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.131"
|
||||
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"
|
||||
@@ -3382,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.131"
|
||||
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"
|
||||
@@ -3461,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.131"
|
||||
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
|
||||
@@ -3489,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."
|
||||
@@ -3516,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.131"
|
||||
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"
|
||||
@@ -3550,12 +3621,15 @@ tasks.register("verifyBackroom") {
|
||||
file("src/main/java/fr/koka99cab/sanctuary26/sanctuary/api/backroom/BackroomMaterialApi.java"),
|
||||
file("src/main/java/fr/koka99cab/sanctuary26/sanctuary/shop/DeliveryBoxBlockEntity.java"),
|
||||
file("src/main/java/fr/koka99cab/sanctuary26/sanctuary/worldgen/feature/BackroomArchitectureFeature.java"),
|
||||
file("src/main/java/fr/koka99cab/sanctuary26/sanctuary/worldgen/feature/BackroomLayout.java"),
|
||||
file("src/main/resources/data/minecraft/worldgen/world_preset/normal.json"),
|
||||
file("src/main/resources/data/sanctuary/worldgen/world_preset/overworld.json"),
|
||||
file("src/main/resources/data/sanctuary/worldgen/world_preset/sanctuary.json"))
|
||||
doLast {
|
||||
def migration = new JsonSlurper().parse(rootProject.file("pack/migrations/sanctuary-backroom-data-v14.json"))
|
||||
def dimensionType = new JsonSlurper().parse(file("src/main/resources/data/sanctuary/dimension_type/backroom.json"))
|
||||
def noiseSettings = new JsonSlurper().parse(file("src/main/resources/data/sanctuary/worldgen/noise_settings/backroom.json"))
|
||||
def biome = new JsonSlurper().parse(file("src/main/resources/data/sanctuary/worldgen/biome/backroom.json"))
|
||||
def presets = [
|
||||
file("src/main/resources/data/minecraft/worldgen/world_preset/normal.json"),
|
||||
file("src/main/resources/data/sanctuary/worldgen/world_preset/overworld.json"),
|
||||
@@ -3567,12 +3641,13 @@ tasks.register("verifyBackroom") {
|
||||
def chest = file("src/main/java/fr/koka99cab/sanctuary26/sanctuary/backroom/BackroomChestService.java").text
|
||||
def mailbox = file("src/main/java/fr/koka99cab/sanctuary26/sanctuary/shop/DeliveryBoxBlockEntity.java").text
|
||||
def architecture = file("src/main/java/fr/koka99cab/sanctuary26/sanctuary/worldgen/feature/BackroomArchitectureFeature.java").text
|
||||
def layout = file("src/main/java/fr/koka99cab/sanctuary26/sanctuary/worldgen/feature/BackroomLayout.java").text
|
||||
def materialApi = file(
|
||||
"src/main/java/fr/koka99cab/sanctuary26/sanctuary/api/backroom/BackroomMaterialApi.java").text
|
||||
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.131"
|
||||
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"
|
||||
@@ -3580,6 +3655,12 @@ tasks.register("verifyBackroom") {
|
||||
|| presets.any { it.dimensions?.get("sanctuary:backroom") == null }
|
||||
|| dimensionType.attributes?.get("minecraft:gameplay/bed_rule")?.can_set_spawn != "never"
|
||||
|| dimensionType.attributes?.get("minecraft:gameplay/bed_rule")?.can_sleep != "never"
|
||||
|| dimensionType.min_y != -64 || dimensionType.height != 448 || dimensionType.logical_height != 448
|
||||
|| dimensionType.ambient_light != 0.0 || dimensionType.has_ceiling != true
|
||||
|| dimensionType.attributes?.get("minecraft:visual/fog_color") != "#000000"
|
||||
|| noiseSettings.noise?.min_y != -64 || noiseSettings.noise?.height != 448
|
||||
|| noiseSettings.disable_mob_generation != true
|
||||
|| biome.creature_spawn_probability != 0.0 || !biome.spawners?.monster?.isEmpty()
|
||||
|| !data.contains("MAX_LOST_ITEMS = 16_384") || !data.contains("MAX_CHEST_STATES = 65_536")
|
||||
|| !data.contains("PERSONAL_RESERVATION_TICKS = 24_000L")
|
||||
|| !data.contains("indexOfLoss(marker)") || !data.contains("BACKROOM_LOSS_COMPONENT")
|
||||
@@ -3587,17 +3668,25 @@ tasks.register("verifyBackroom") {
|
||||
|| !service.contains("ARRIVAL_RADIUS = 2_000") || !service.contains("insertTransaction")
|
||||
|| !service.contains("AUTHORIZED_TRANSITIONS")
|
||||
|| !service.contains("level -> !isBackroom(level)")
|
||||
|| !service.contains("MobCategory.MONSTER") || !service.contains("Blocks.TORCH")
|
||||
|| !mailbox.contains("MAX_PROCESSED_TRANSACTIONS = 128")
|
||||
|| !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("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")
|
||||
|| !it.containsKey("message.sanctuary.backroom.bed_entry_only")
|
||||
|| !it.containsKey("message.sanctuary.backroom.registry_busy")
|
||||
|| !it.containsKey("message.sanctuary.backroom.mailbox_full")
|
||||
|| !it.containsKey("message.sanctuary.backroom.exited_with_losses") }) {
|
||||
throw new GradleException("Sanctuary alpha.131 Backroom contract is incomplete")
|
||||
throw new GradleException("Sanctuary alpha.134 Backroom contract is incomplete")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3620,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")
|
||||
|
||||
@@ -2,6 +2,7 @@ package fr.koka99cab.sanctuary26.sanctuary;
|
||||
|
||||
import fr.koka99cab.sanctuary26.sanctuary.api.atlas.SanctuaryAtlasLandmarks;
|
||||
import fr.koka99cab.sanctuary26.sanctuary.api.server.SanctuaryServerIdentity;
|
||||
import fr.koka99cab.sanctuary26.sanctuary.api.ranking.SanctuaryRankingApi;
|
||||
import fr.koka99cab.sanctuary26.sanctuary.network.SanctuaryNetworking;
|
||||
import fr.koka99cab.sanctuary26.sanctuary.gameplay.SanctuaryBulkActions;
|
||||
import fr.koka99cab.sanctuary26.sanctuary.gameplay.SanctuaryInventoryCapacity;
|
||||
@@ -35,7 +36,7 @@ import org.slf4j.LoggerFactory;
|
||||
public final class SanctuaryMod implements ModInitializer {
|
||||
public static final String MOD_ID = "sanctuary";
|
||||
public static final String DISPLAY_NAME = "Sanctuary";
|
||||
public static final int CURRENT_DATA_VERSION = 14;
|
||||
public static final int CURRENT_DATA_VERSION = 15;
|
||||
public static final Logger LOGGER = LoggerFactory.getLogger(MOD_ID);
|
||||
|
||||
public SanctuaryMod() {
|
||||
@@ -48,6 +49,7 @@ public final class SanctuaryMod implements ModInitializer {
|
||||
@Override
|
||||
public void onInitialize() {
|
||||
SanctuaryServerIdentity.register();
|
||||
SanctuaryRankingApi.initialize();
|
||||
SanctuaryDimensions.initialize();
|
||||
SanctuaryDensityFunctions.register();
|
||||
SanctuaryFeatures.register();
|
||||
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
package fr.koka99cab.sanctuary26.sanctuary.api.ranking;
|
||||
|
||||
import java.util.Objects;
|
||||
import net.minecraft.resources.Identifier;
|
||||
|
||||
/** Stable public description registered by gameplay modules, independent from SanctuaryStat ranks. */
|
||||
public record RankingMetric(Identifier id, String translationKey, RankingUnit unit) {
|
||||
public static final int MAX_IDENTIFIER_LENGTH = 256;
|
||||
public static final int MAX_TRANSLATION_KEY_LENGTH = 128;
|
||||
|
||||
public RankingMetric {
|
||||
Objects.requireNonNull(id, "id");
|
||||
Objects.requireNonNull(unit, "unit");
|
||||
if (id.toString().length() > MAX_IDENTIFIER_LENGTH) {
|
||||
throw new IllegalArgumentException("Ranking identifier must contain at most 256 characters");
|
||||
}
|
||||
translationKey = translationKey == null ? "" : translationKey.trim();
|
||||
if (translationKey.isEmpty() || translationKey.length() > MAX_TRANSLATION_KEY_LENGTH) {
|
||||
throw new IllegalArgumentException("Ranking translation key must contain 1-128 characters");
|
||||
}
|
||||
}
|
||||
}
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
package fr.koka99cab.sanctuary26.sanctuary.api.ranking;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import net.minecraft.resources.Identifier;
|
||||
|
||||
/** Server snapshot intentionally bounded to one visible metric and seven panel rows. */
|
||||
public record RankingSnapshot(RankingMetric metric, List<Entry> podium, Placement owner) {
|
||||
public static final int MAX_PODIUM_SIZE = 7;
|
||||
|
||||
public RankingSnapshot {
|
||||
podium = podium == null ? List.of() : List.copyOf(podium.stream().limit(MAX_PODIUM_SIZE).toList());
|
||||
owner = owner == null ? Placement.unranked() : owner;
|
||||
}
|
||||
|
||||
public static RankingSnapshot empty(RankingMetric metric) {
|
||||
return new RankingSnapshot(metric, List.of(), Placement.unranked());
|
||||
}
|
||||
|
||||
public record Entry(UUID playerId, String playerName, long value, int rank) {
|
||||
public Entry {
|
||||
playerName = sanitizeName(playerName);
|
||||
value = Math.max(0L, value);
|
||||
rank = Math.max(0, rank);
|
||||
}
|
||||
}
|
||||
|
||||
public record Placement(long value, int rank) {
|
||||
public Placement {
|
||||
value = Math.max(0L, value);
|
||||
rank = Math.max(0, rank);
|
||||
}
|
||||
|
||||
public static Placement unranked() {
|
||||
return new Placement(0L, 0);
|
||||
}
|
||||
}
|
||||
|
||||
private static String sanitizeName(String value) {
|
||||
if (value == null || value.isBlank()) return "?";
|
||||
String clean = value.strip();
|
||||
return clean.length() > 32 ? clean.substring(0, 32) : clean;
|
||||
}
|
||||
}
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
package fr.koka99cab.sanctuary26.sanctuary.api.ranking;
|
||||
|
||||
/** Public, bounded display formats accepted by server ranking metrics. */
|
||||
public enum RankingUnit {
|
||||
COUNT("count"),
|
||||
TICKS("ticks"),
|
||||
CENTIMETERS("centimeters");
|
||||
|
||||
private final String key;
|
||||
|
||||
RankingUnit(String key) {
|
||||
this.key = key;
|
||||
}
|
||||
|
||||
public String key() {
|
||||
return key;
|
||||
}
|
||||
|
||||
public static RankingUnit byKey(String key) {
|
||||
for (RankingUnit unit : values()) {
|
||||
if (unit.key.equals(key)) return unit;
|
||||
}
|
||||
return COUNT;
|
||||
}
|
||||
|
||||
/** Compact, locale-neutral representation suitable for signs and chat snapshots. */
|
||||
public String format(long rawValue) {
|
||||
long value = Math.max(0L, rawValue);
|
||||
return switch (this) {
|
||||
case TICKS -> formatDuration(value / 20L);
|
||||
case CENTIMETERS -> value >= 100_000L ? (value / 100_000L) + " km" : (value / 100L) + " m";
|
||||
case COUNT -> compact(value);
|
||||
};
|
||||
}
|
||||
|
||||
private static String formatDuration(long seconds) {
|
||||
long hours = seconds / 3_600L;
|
||||
long minutes = seconds % 3_600L / 60L;
|
||||
if (hours > 0L) return hours + " h " + minutes + " min";
|
||||
return minutes + " min";
|
||||
}
|
||||
|
||||
private static String compact(long value) {
|
||||
if (value >= 1_000_000_000L) return value / 1_000_000_000L + "G";
|
||||
if (value >= 1_000_000L) return value / 1_000_000L + "M";
|
||||
if (value >= 1_000L) return value / 1_000L + "K";
|
||||
return Long.toString(value);
|
||||
}
|
||||
}
|
||||
+129
@@ -0,0 +1,129 @@
|
||||
package fr.koka99cab.sanctuary26.sanctuary.api.ranking;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents;
|
||||
import net.fabricmc.fabric.api.networking.v1.ServerPlayConnectionEvents;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
|
||||
/**
|
||||
* Public server-authoritative ranking API. Gameplay modules may register stable metric descriptions and
|
||||
* increment them for real survival/adventure players; Red-Stoner only consumes bounded snapshots.
|
||||
*/
|
||||
public final class SanctuaryRankingApi {
|
||||
private static final Map<Identifier, RankingMetric> METRICS = new ConcurrentHashMap<>();
|
||||
private static final RankingMetric DEFAULT_METRIC = VanillaRankingMetric.descriptor(
|
||||
VanillaRankingMetric.playTimeId());
|
||||
private static boolean initialized;
|
||||
|
||||
private SanctuaryRankingApi() {}
|
||||
|
||||
public static void initialize() {
|
||||
if (initialized) return;
|
||||
initialized = true;
|
||||
register(DEFAULT_METRIC);
|
||||
ServerLifecycleEvents.SERVER_STARTED.register(server -> {
|
||||
SanctuaryRankingData data = SanctuaryRankingData.get(server);
|
||||
data.importVanillaStats(server);
|
||||
data.resolveCachedNames(server);
|
||||
});
|
||||
ServerPlayConnectionEvents.JOIN.register((handler, sender, server) -> {
|
||||
SanctuaryRankingData data = SanctuaryRankingData.get(server);
|
||||
data.updateName(handler.player.getUUID(), handler.player.getGameProfile().name());
|
||||
synchronizeObserved(server, METRICS.keySet());
|
||||
});
|
||||
}
|
||||
|
||||
public static RankingMetric register(RankingMetric metric) {
|
||||
RankingMetric previous = METRICS.putIfAbsent(metric.id(), metric);
|
||||
if (previous != null && !previous.equals(metric)) {
|
||||
throw new IllegalStateException("Ranking metric already registered with another contract: " + metric.id());
|
||||
}
|
||||
return previous == null ? metric : previous;
|
||||
}
|
||||
|
||||
public static RankingMetric defaultMetric() {
|
||||
return DEFAULT_METRIC;
|
||||
}
|
||||
|
||||
public static Optional<RankingMetric> metric(Identifier id) {
|
||||
RankingMetric registered = METRICS.get(id);
|
||||
if (registered != null) return Optional.of(registered);
|
||||
return VanillaRankingMetric.parse(id).map(source -> {
|
||||
RankingMetric descriptor = VanillaRankingMetric.descriptor(id);
|
||||
METRICS.putIfAbsent(id, descriptor);
|
||||
return descriptor;
|
||||
});
|
||||
}
|
||||
|
||||
public static List<RankingMetric> itemMetrics(ItemStack icon) {
|
||||
List<RankingMetric> metrics = VanillaRankingMetric.itemMetrics(icon);
|
||||
metrics.forEach(SanctuaryRankingApi::register);
|
||||
return metrics;
|
||||
}
|
||||
|
||||
/**
|
||||
* Complete client-safe catalog used by configuration screens. The catalog is rebuilt from
|
||||
* synchronized vanilla registries, so it never has to be transferred in one large payload.
|
||||
*/
|
||||
public static List<RankingMetric> configurableMetrics() {
|
||||
return VanillaRankingMetric.configurableMetrics(METRICS.values());
|
||||
}
|
||||
|
||||
/** True when a submitted identifier resolves to a registered or existing vanilla statistic. */
|
||||
public static boolean isConfigurableMetric(Identifier id) {
|
||||
return id != null && (METRICS.containsKey(id) || VanillaRankingMetric.isAvailable(id));
|
||||
}
|
||||
|
||||
/** Localized label for a metric configured from its representative item. */
|
||||
public static Component displayName(RankingMetric metric, ItemStack icon) {
|
||||
return VanillaRankingMetric.displayName(metric, icon);
|
||||
}
|
||||
|
||||
/** Localized label resolved directly from the metric target, without requiring a representative item. */
|
||||
public static Component displayName(RankingMetric metric) {
|
||||
return VanillaRankingMetric.displayName(metric, ItemStack.EMPTY);
|
||||
}
|
||||
|
||||
public static boolean eligible(ServerPlayer player) {
|
||||
return isRealOnlinePlayer(player) && !player.isCreative() && !player.isSpectator();
|
||||
}
|
||||
|
||||
public static void increment(ServerPlayer player, Identifier metricId, long amount) {
|
||||
if (!eligible(player) || amount <= 0L || metric(metricId).isEmpty()) return;
|
||||
SanctuaryRankingData.get(player.level().getServer()).increment(player.getUUID(),
|
||||
player.getGameProfile().name(), metricId, amount);
|
||||
}
|
||||
|
||||
public static RankingSnapshot snapshot(MinecraftServer server, Identifier metricId, UUID ownerId) {
|
||||
RankingMetric metric = metric(metricId).orElse(DEFAULT_METRIC);
|
||||
synchronizeObserved(server, List.of(metric.id()));
|
||||
return SanctuaryRankingData.get(server).snapshot(metric, ownerId);
|
||||
}
|
||||
|
||||
public static void synchronizeObserved(MinecraftServer server, Collection<Identifier> metricIds) {
|
||||
if (server == null || metricIds == null || metricIds.isEmpty()) return;
|
||||
SanctuaryRankingData data = SanctuaryRankingData.get(server);
|
||||
for (ServerPlayer player : server.getPlayerList().getPlayers()) {
|
||||
for (Identifier metricId : metricIds) {
|
||||
VanillaRankingMetric.Source source = VanillaRankingMetric.parse(metricId).orElse(null);
|
||||
if (source == null) continue;
|
||||
data.observeVanilla(player.getUUID(), player.getGameProfile().name(), metricId,
|
||||
VanillaRankingMetric.read(player, source), eligible(player));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isRealOnlinePlayer(ServerPlayer player) {
|
||||
return player != null && player.getClass() == ServerPlayer.class
|
||||
&& player.level().getServer().getPlayerList().getPlayer(player.getUUID()) == player;
|
||||
}
|
||||
}
|
||||
+321
@@ -0,0 +1,321 @@
|
||||
package fr.koka99cab.sanctuary26.sanctuary.api.ranking;
|
||||
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParser;
|
||||
import com.mojang.serialization.Codec;
|
||||
import com.mojang.serialization.codecs.RecordCodecBuilder;
|
||||
import fr.koka99cab.sanctuary26.sanctuary.SanctuaryMod;
|
||||
import java.io.IOException;
|
||||
import java.io.Reader;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Stream;
|
||||
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;
|
||||
import net.minecraft.world.level.storage.LevelResource;
|
||||
|
||||
/** Versioned UUID/metric index; Red-Stoner never owns or duplicates these scores. */
|
||||
final class SanctuaryRankingData extends SavedData {
|
||||
static final String SAVE_KEY = "rankings";
|
||||
static final int DATA_VERSION = 1;
|
||||
static final long MAX_VALUE = Long.MAX_VALUE / 4L;
|
||||
private static final int MAX_METRICS_PER_PLAYER = 16_384;
|
||||
|
||||
private static final Codec<Score> SCORE_CODEC = RecordCodecBuilder.create(instance -> instance.group(
|
||||
Codec.LONG.fieldOf("value").forGetter(Score::value),
|
||||
Codec.LONG.fieldOf("reached_order").forGetter(Score::reachedOrder)
|
||||
).apply(instance, Score::new));
|
||||
private static final Codec<Profile> PROFILE_CODEC = RecordCodecBuilder.create(instance -> instance.group(
|
||||
Codec.STRING.optionalFieldOf("last_name", "?").forGetter(Profile::lastName),
|
||||
Codec.unboundedMap(Codec.STRING, SCORE_CODEC).optionalFieldOf("metrics", Map.of())
|
||||
.forGetter(Profile::metrics),
|
||||
Codec.unboundedMap(Codec.STRING, Codec.LONG).optionalFieldOf("vanilla_observed", Map.of())
|
||||
.forGetter(Profile::vanillaObserved)
|
||||
).apply(instance, Profile::new));
|
||||
private static final Codec<SanctuaryRankingData> CODEC = RecordCodecBuilder.create(instance -> instance.group(
|
||||
Codec.INT.optionalFieldOf("data_version", DATA_VERSION).forGetter(data -> data.dataVersion),
|
||||
Codec.BOOL.optionalFieldOf("vanilla_stats_imported", false).forGetter(data -> data.vanillaStatsImported),
|
||||
Codec.LONG.optionalFieldOf("next_reached_order", 1L).forGetter(data -> data.nextReachedOrder),
|
||||
Codec.unboundedMap(Codec.STRING, PROFILE_CODEC).optionalFieldOf("players", Map.of())
|
||||
.forGetter(data -> data.players)
|
||||
).apply(instance, SanctuaryRankingData::new));
|
||||
private static final SavedDataType<SanctuaryRankingData> TYPE = new SavedDataType<>(
|
||||
SanctuaryMod.id(SAVE_KEY), SanctuaryRankingData::new, CODEC, null);
|
||||
|
||||
private int dataVersion;
|
||||
private boolean vanillaStatsImported;
|
||||
private long nextReachedOrder;
|
||||
private final Map<String, Profile> players;
|
||||
|
||||
SanctuaryRankingData() {
|
||||
this(DATA_VERSION, false, 1L, Map.of());
|
||||
}
|
||||
|
||||
SanctuaryRankingData(int dataVersion, boolean vanillaStatsImported, long nextReachedOrder,
|
||||
Map<String, Profile> players) {
|
||||
this.dataVersion = dataVersion;
|
||||
this.vanillaStatsImported = vanillaStatsImported;
|
||||
this.nextReachedOrder = Math.max(1L, nextReachedOrder);
|
||||
this.players = new HashMap<>();
|
||||
players.forEach((player, profile) -> {
|
||||
try {
|
||||
UUID.fromString(player);
|
||||
this.players.put(player, sanitize(profile));
|
||||
} catch (IllegalArgumentException ignored) {
|
||||
// A malformed external key is ignored rather than poisoning the complete save.
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
static SanctuaryRankingData get(MinecraftServer server) {
|
||||
ServerLevel overworld = server.getLevel(Level.OVERWORLD);
|
||||
if (overworld == null) throw new IllegalStateException("Overworld unavailable for ranking data");
|
||||
SanctuaryRankingData data = overworld.getDataStorage().computeIfAbsent(TYPE);
|
||||
if (data.dataVersion < DATA_VERSION) {
|
||||
data.dataVersion = DATA_VERSION;
|
||||
data.setDirty();
|
||||
} else if (data.dataVersion > DATA_VERSION) {
|
||||
SanctuaryMod.LOGGER.warn("[{}] Ranking data v{} is newer than supported v{}; writes are disabled.",
|
||||
SanctuaryMod.DISPLAY_NAME, data.dataVersion, DATA_VERSION);
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
boolean writable() {
|
||||
return dataVersion <= DATA_VERSION;
|
||||
}
|
||||
|
||||
void updateName(UUID playerId, String name) {
|
||||
if (!writable()) return;
|
||||
String key = playerId.toString();
|
||||
Profile current = players.getOrDefault(key, new Profile(cleanName(name), Map.of(), Map.of()));
|
||||
String clean = cleanName(name);
|
||||
if (current.lastName().equals(clean)) return;
|
||||
players.put(key, new Profile(clean, current.metrics(), current.vanillaObserved()));
|
||||
setDirty();
|
||||
}
|
||||
|
||||
void recordValue(UUID playerId, String name, Identifier metricId, long rawValue) {
|
||||
if (!writable() || rawValue < 0L) return;
|
||||
String key = playerId.toString();
|
||||
Profile current = players.getOrDefault(key, new Profile(cleanName(name), Map.of(), Map.of()));
|
||||
Map<String, Score> metrics = new HashMap<>(current.metrics());
|
||||
String metricKey = metricId.toString();
|
||||
Score previous = metrics.get(metricKey);
|
||||
long value = Math.min(MAX_VALUE, rawValue);
|
||||
if (previous != null && previous.value() >= value) {
|
||||
updateName(playerId, name);
|
||||
return;
|
||||
}
|
||||
if (previous == null && metrics.size() >= MAX_METRICS_PER_PLAYER) return;
|
||||
metrics.put(metricKey, new Score(value, takeReachedOrder()));
|
||||
players.put(key, new Profile(cleanName(name), metrics, current.vanillaObserved()));
|
||||
setDirty();
|
||||
}
|
||||
|
||||
void observeVanilla(UUID playerId, String name, Identifier metricId, long rawValue, boolean credited) {
|
||||
if (!writable() || rawValue < 0L) return;
|
||||
String key = playerId.toString();
|
||||
Profile current = players.getOrDefault(key, new Profile(cleanName(name), Map.of(), Map.of()));
|
||||
Map<String, Long> observed = new HashMap<>(current.vanillaObserved());
|
||||
String metricKey = metricId.toString();
|
||||
Long previous = observed.get(metricKey);
|
||||
if (previous == null && observed.size() >= MAX_METRICS_PER_PLAYER) return;
|
||||
long value = Math.min(MAX_VALUE, rawValue);
|
||||
if (previous == null || previous.longValue() != value || !current.lastName().equals(cleanName(name))) {
|
||||
observed.put(metricKey, value);
|
||||
players.put(key, new Profile(cleanName(name), current.metrics(), observed));
|
||||
setDirty();
|
||||
}
|
||||
long creditedDelta = previous == null ? value : Math.max(0L, value - previous);
|
||||
if (credited && creditedDelta > 0L) {
|
||||
increment(playerId, name, metricId, creditedDelta);
|
||||
}
|
||||
}
|
||||
|
||||
void increment(UUID playerId, String name, Identifier metricId, long amount) {
|
||||
if (!writable() || amount <= 0L) return;
|
||||
long current = score(playerId, metricId);
|
||||
long value = current >= MAX_VALUE - amount ? MAX_VALUE : current + amount;
|
||||
recordValue(playerId, name, metricId, value);
|
||||
}
|
||||
|
||||
long score(UUID playerId, Identifier metricId) {
|
||||
Profile profile = players.get(playerId.toString());
|
||||
Score score = profile == null ? null : profile.metrics().get(metricId.toString());
|
||||
return score == null ? 0L : score.value();
|
||||
}
|
||||
|
||||
RankingSnapshot snapshot(RankingMetric metric, UUID ownerId) {
|
||||
List<Candidate> candidates = new ArrayList<>();
|
||||
players.forEach((player, profile) -> {
|
||||
Score score = profile.metrics().get(metric.id().toString());
|
||||
if (score == null || score.value() <= 0L) return;
|
||||
try {
|
||||
candidates.add(new Candidate(UUID.fromString(player), profile.lastName(), score));
|
||||
} catch (IllegalArgumentException ignored) {
|
||||
// Already filtered during load, retained as defensive parsing.
|
||||
}
|
||||
});
|
||||
candidates.sort(Comparator.comparingLong((Candidate candidate) -> candidate.score().value()).reversed()
|
||||
.thenComparingLong(candidate -> candidate.score().reachedOrder())
|
||||
.thenComparing(candidate -> candidate.playerId().toString()));
|
||||
|
||||
List<RankingSnapshot.Entry> podium = new ArrayList<>();
|
||||
RankingSnapshot.Placement owner = RankingSnapshot.Placement.unranked();
|
||||
long previousValue = -1L;
|
||||
int rank = 0;
|
||||
for (int index = 0; index < candidates.size(); index++) {
|
||||
Candidate candidate = candidates.get(index);
|
||||
if (candidate.score().value() != previousValue) {
|
||||
rank = index + 1;
|
||||
previousValue = candidate.score().value();
|
||||
}
|
||||
if (podium.size() < RankingSnapshot.MAX_PODIUM_SIZE) {
|
||||
podium.add(new RankingSnapshot.Entry(candidate.playerId(), candidate.name(),
|
||||
candidate.score().value(), rank));
|
||||
}
|
||||
if (candidate.playerId().equals(ownerId)) {
|
||||
owner = new RankingSnapshot.Placement(candidate.score().value(), rank);
|
||||
}
|
||||
}
|
||||
return new RankingSnapshot(metric, podium, owner);
|
||||
}
|
||||
|
||||
void importVanillaStats(MinecraftServer server) {
|
||||
if (!writable() || vanillaStatsImported) return;
|
||||
Path statsDirectory = server.getWorldPath(LevelResource.PLAYER_STATS_DIR);
|
||||
if (Files.isDirectory(statsDirectory)) {
|
||||
try (Stream<Path> files = Files.list(statsDirectory)) {
|
||||
files.filter(path -> path.getFileName().toString().endsWith(".json"))
|
||||
.sorted().forEach(path -> importVanillaFile(server, path));
|
||||
} catch (IOException exception) {
|
||||
SanctuaryMod.LOGGER.warn("[{}] Could not import existing vanilla statistics.",
|
||||
SanctuaryMod.DISPLAY_NAME, exception);
|
||||
return;
|
||||
}
|
||||
}
|
||||
vanillaStatsImported = true;
|
||||
setDirty();
|
||||
}
|
||||
|
||||
void importVanillaFile(Path file) {
|
||||
importVanillaFile(null, file);
|
||||
}
|
||||
|
||||
private void importVanillaFile(MinecraftServer server, Path file) {
|
||||
String fileName = file.getFileName().toString();
|
||||
String idText = fileName.substring(0, fileName.length() - ".json".length());
|
||||
UUID playerId;
|
||||
try {
|
||||
playerId = UUID.fromString(idText);
|
||||
} catch (IllegalArgumentException ignored) {
|
||||
return;
|
||||
}
|
||||
try (Reader reader = Files.newBufferedReader(file, StandardCharsets.UTF_8)) {
|
||||
String resolvedName = cachedName(server, playerId);
|
||||
JsonObject root = JsonParser.parseReader(reader).getAsJsonObject();
|
||||
JsonObject stats = root.has("stats") && root.get("stats").isJsonObject()
|
||||
? root.getAsJsonObject("stats") : new JsonObject();
|
||||
for (Map.Entry<String, JsonElement> typeEntry : stats.entrySet()) {
|
||||
Identifier type = Identifier.tryParse(typeEntry.getKey());
|
||||
if (type == null || !typeEntry.getValue().isJsonObject()) continue;
|
||||
for (Map.Entry<String, JsonElement> valueEntry : typeEntry.getValue().getAsJsonObject().entrySet()) {
|
||||
Identifier target = Identifier.tryParse(valueEntry.getKey());
|
||||
if (target == null || !valueEntry.getValue().isJsonPrimitive()
|
||||
|| !valueEntry.getValue().getAsJsonPrimitive().isNumber()) continue;
|
||||
Identifier metricId = VanillaRankingMetric.id(type, target);
|
||||
long value = Math.max(0L, valueEntry.getValue().getAsLong());
|
||||
recordValue(playerId, resolvedName, metricId, value);
|
||||
observeVanilla(playerId, resolvedName, metricId, value, false);
|
||||
}
|
||||
}
|
||||
} catch (RuntimeException | IOException exception) {
|
||||
SanctuaryMod.LOGGER.warn("[{}] Ignoring unreadable vanilla statistic file {}.",
|
||||
SanctuaryMod.DISPLAY_NAME, file.getFileName(), exception);
|
||||
}
|
||||
}
|
||||
|
||||
/** Repairs the old eight-character UUID placeholders from the server's local profile cache. */
|
||||
void resolveCachedNames(MinecraftServer server) {
|
||||
if (!writable() || server == null) return;
|
||||
boolean changed = false;
|
||||
for (Map.Entry<String, Profile> entry : new ArrayList<>(players.entrySet())) {
|
||||
UUID playerId = UUID.fromString(entry.getKey());
|
||||
Profile profile = entry.getValue();
|
||||
if (!isUuidFallback(profile.lastName(), playerId)) continue;
|
||||
String resolved = cachedName(server, playerId);
|
||||
players.put(entry.getKey(), new Profile(resolved, profile.metrics(), profile.vanillaObserved()));
|
||||
changed = true;
|
||||
}
|
||||
if (changed) setDirty();
|
||||
}
|
||||
|
||||
private long takeReachedOrder() {
|
||||
long current = nextReachedOrder;
|
||||
if (nextReachedOrder < Long.MAX_VALUE) nextReachedOrder++;
|
||||
return current;
|
||||
}
|
||||
|
||||
private static Profile sanitize(Profile profile) {
|
||||
Map<String, Score> metrics = new HashMap<>();
|
||||
profile.metrics().entrySet().stream().limit(MAX_METRICS_PER_PLAYER).forEach(entry -> {
|
||||
if (Identifier.tryParse(entry.getKey()) != null) metrics.put(entry.getKey(), entry.getValue());
|
||||
});
|
||||
Map<String, Long> observed = new HashMap<>();
|
||||
profile.vanillaObserved().entrySet().stream().limit(MAX_METRICS_PER_PLAYER).forEach(entry -> {
|
||||
if (Identifier.tryParse(entry.getKey()) != null) {
|
||||
observed.put(entry.getKey(), Math.clamp(entry.getValue(), 0L, MAX_VALUE));
|
||||
}
|
||||
});
|
||||
return new Profile(cleanName(profile.lastName()), metrics, observed);
|
||||
}
|
||||
|
||||
private static String cleanName(String value) {
|
||||
if (value == null || value.isBlank()) return "?";
|
||||
String clean = value.strip();
|
||||
return clean.length() > 32 ? clean.substring(0, 32) : clean;
|
||||
}
|
||||
|
||||
private static String cachedName(MinecraftServer server, UUID playerId) {
|
||||
if (server == null) return "?";
|
||||
return server.services().nameToIdCache().get(playerId)
|
||||
.map(profile -> cleanName(profile.name())).orElse("?");
|
||||
}
|
||||
|
||||
private static boolean isUuidFallback(String name, UUID playerId) {
|
||||
if (name == null || name.isBlank() || name.equals("?")) return true;
|
||||
String uuid = playerId.toString();
|
||||
return (name.length() == 8 || name.length() == 32)
|
||||
&& uuid.regionMatches(true, 0, name, 0, name.length());
|
||||
}
|
||||
|
||||
record Score(long value, long reachedOrder) {
|
||||
Score {
|
||||
value = Math.clamp(value, 0L, MAX_VALUE);
|
||||
reachedOrder = Math.max(1L, reachedOrder);
|
||||
}
|
||||
}
|
||||
|
||||
record Profile(String lastName, Map<String, Score> metrics, Map<String, Long> vanillaObserved) {
|
||||
Profile {
|
||||
lastName = cleanName(lastName);
|
||||
metrics = metrics == null ? Map.of() : Map.copyOf(metrics);
|
||||
vanillaObserved = vanillaObserved == null ? Map.of() : Map.copyOf(vanillaObserved);
|
||||
}
|
||||
}
|
||||
|
||||
private record Candidate(UUID playerId, String name, Score score) {}
|
||||
}
|
||||
+186
@@ -0,0 +1,186 @@
|
||||
package fr.koka99cab.sanctuary26.sanctuary.api.ranking;
|
||||
|
||||
import fr.koka99cab.sanctuary26.sanctuary.SanctuaryMod;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import net.minecraft.core.registries.BuiltInRegistries;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.stats.Stats;
|
||||
import net.minecraft.world.item.BlockItem;
|
||||
import net.minecraft.world.item.Item;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.item.SpawnEggItem;
|
||||
|
||||
/** Stable encoding and server reader for persisted vanilla statistic identifiers. */
|
||||
final class VanillaRankingMetric {
|
||||
private static final String PREFIX = "vanilla/";
|
||||
private static final List<String> ITEM_FAMILIES = List.of(
|
||||
"mined", "crafted", "used", "broken", "picked_up", "dropped");
|
||||
private static final List<String> ENTITY_FAMILIES = List.of("killed", "killed_by");
|
||||
private static final List<Identifier> TIME_STATISTICS = List.of(
|
||||
Stats.PLAY_TIME, Stats.TOTAL_WORLD_TIME, Stats.TIME_SINCE_DEATH,
|
||||
Stats.TIME_SINCE_REST, Stats.CROUCH_TIME);
|
||||
private static final List<Identifier> DISTANCE_STATISTICS = List.of(
|
||||
Stats.WALK_ONE_CM, Stats.CROUCH_ONE_CM, Stats.SPRINT_ONE_CM,
|
||||
Stats.WALK_ON_WATER_ONE_CM, Stats.FALL_ONE_CM, Stats.CLIMB_ONE_CM,
|
||||
Stats.FLY_ONE_CM, Stats.WALK_UNDER_WATER_ONE_CM, Stats.MINECART_ONE_CM,
|
||||
Stats.BOAT_ONE_CM, Stats.PIG_ONE_CM, Stats.HAPPY_GHAST_ONE_CM,
|
||||
Stats.HORSE_ONE_CM, Stats.AVIATE_ONE_CM, Stats.SWIM_ONE_CM,
|
||||
Stats.STRIDER_ONE_CM, Stats.NAUTILUS_ONE_CM);
|
||||
|
||||
private VanillaRankingMetric() {}
|
||||
|
||||
static Identifier id(Identifier type, Identifier target) {
|
||||
return SanctuaryMod.id(PREFIX + type.getNamespace() + "/" + type.getPath() + "/"
|
||||
+ target.getNamespace() + "/" + target.getPath());
|
||||
}
|
||||
|
||||
static Identifier playTimeId() {
|
||||
return id(Identifier.withDefaultNamespace("custom"), Stats.PLAY_TIME);
|
||||
}
|
||||
|
||||
static RankingMetric descriptor(Identifier id) {
|
||||
Source source = parse(id).orElse(null);
|
||||
if (source == null) {
|
||||
return new RankingMetric(id, "metric.sanctuary.ranking.unknown", RankingUnit.COUNT);
|
||||
}
|
||||
boolean custom = source.type().getPath().equals("custom");
|
||||
boolean playTime = custom && source.target().equals(Stats.PLAY_TIME);
|
||||
RankingUnit unit = custom && TIME_STATISTICS.contains(source.target()) ? RankingUnit.TICKS
|
||||
: custom && DISTANCE_STATISTICS.contains(source.target()) ? RankingUnit.CENTIMETERS
|
||||
: RankingUnit.COUNT;
|
||||
return new RankingMetric(id, "metric.sanctuary.ranking."
|
||||
+ (playTime ? "play_time" : source.type().getPath()), unit);
|
||||
}
|
||||
|
||||
static List<RankingMetric> configurableMetrics(Collection<RankingMetric> registered) {
|
||||
Map<Identifier, RankingMetric> metrics = new LinkedHashMap<>();
|
||||
if (registered != null) registered.forEach(metric -> metrics.put(metric.id(), metric));
|
||||
for (Identifier target : BuiltInRegistries.CUSTOM_STAT) {
|
||||
RankingMetric metric = descriptor(id(Identifier.withDefaultNamespace("custom"), target));
|
||||
metrics.putIfAbsent(metric.id(), metric);
|
||||
}
|
||||
for (BlockItem blockItem : BuiltInRegistries.ITEM.stream()
|
||||
.filter(BlockItem.class::isInstance).map(BlockItem.class::cast).toList()) {
|
||||
Identifier target = BuiltInRegistries.BLOCK.getKey(blockItem.getBlock());
|
||||
RankingMetric metric = descriptor(id(Identifier.withDefaultNamespace("mined"), target));
|
||||
metrics.putIfAbsent(metric.id(), metric);
|
||||
}
|
||||
for (Item item : BuiltInRegistries.ITEM) {
|
||||
Identifier target = BuiltInRegistries.ITEM.getKey(item);
|
||||
for (String family : ITEM_FAMILIES) {
|
||||
if (family.equals("mined")) continue;
|
||||
RankingMetric metric = descriptor(id(Identifier.withDefaultNamespace(family), target));
|
||||
metrics.putIfAbsent(metric.id(), metric);
|
||||
}
|
||||
}
|
||||
for (var entityType : BuiltInRegistries.ENTITY_TYPE) {
|
||||
Identifier target = BuiltInRegistries.ENTITY_TYPE.getKey(entityType);
|
||||
for (String family : ENTITY_FAMILIES) {
|
||||
RankingMetric metric = descriptor(id(Identifier.withDefaultNamespace(family), target));
|
||||
metrics.putIfAbsent(metric.id(), metric);
|
||||
}
|
||||
}
|
||||
return List.copyOf(metrics.values());
|
||||
}
|
||||
|
||||
static List<RankingMetric> itemMetrics(ItemStack icon) {
|
||||
if (icon == null || icon.isEmpty()) return List.of();
|
||||
Item item = icon.getItem();
|
||||
if (item instanceof SpawnEggItem) {
|
||||
var entityType = SpawnEggItem.getType(icon);
|
||||
if (entityType == null) return List.of();
|
||||
Identifier entityId = BuiltInRegistries.ENTITY_TYPE.getKey(entityType);
|
||||
return List.of(new RankingMetric(id(Identifier.withDefaultNamespace("killed"), entityId),
|
||||
"metric.sanctuary.ranking.killed", RankingUnit.COUNT));
|
||||
}
|
||||
Identifier itemId = BuiltInRegistries.ITEM.getKey(item);
|
||||
List<RankingMetric> metrics = new ArrayList<>();
|
||||
for (String family : ITEM_FAMILIES) {
|
||||
if (family.equals("mined") && !(item instanceof BlockItem)) continue;
|
||||
Identifier metricId = id(Identifier.withDefaultNamespace(family), itemId);
|
||||
metrics.add(new RankingMetric(metricId, "metric.sanctuary.ranking." + family, RankingUnit.COUNT));
|
||||
}
|
||||
return List.copyOf(metrics);
|
||||
}
|
||||
|
||||
static Component displayName(RankingMetric metric, ItemStack icon) {
|
||||
Source source = parse(metric.id()).orElse(null);
|
||||
if (source == null) return Component.translatable(metric.translationKey());
|
||||
String family = source.type().getPath();
|
||||
if (family.equals("custom")) {
|
||||
return source.target().equals(Stats.PLAY_TIME)
|
||||
? Component.translatable(metric.translationKey())
|
||||
: Component.translatable(source.target().toLanguageKey("stat"));
|
||||
}
|
||||
Component target = switch (family) {
|
||||
case "mined" -> BuiltInRegistries.BLOCK.getOptional(source.target())
|
||||
.<Component>map(block -> block.getName()).orElseGet(() -> iconName(icon));
|
||||
case "killed", "killed_by" -> BuiltInRegistries.ENTITY_TYPE.getOptional(source.target())
|
||||
.<Component>map(type -> type.getDescription()).orElseGet(() -> iconName(icon));
|
||||
default -> BuiltInRegistries.ITEM.getOptional(source.target())
|
||||
.<Component>map(item -> new ItemStack(item).getHoverName()).orElseGet(() -> iconName(icon));
|
||||
};
|
||||
return Component.translatable(metric.translationKey(), target);
|
||||
}
|
||||
|
||||
static boolean isAvailable(Identifier metricId) {
|
||||
Source source = parse(metricId).orElse(null);
|
||||
if (source == null) return false;
|
||||
return switch (source.type().getPath()) {
|
||||
case "custom" -> BuiltInRegistries.CUSTOM_STAT.getOptional(source.target()).isPresent();
|
||||
case "mined" -> BuiltInRegistries.BLOCK.getOptional(source.target()).isPresent();
|
||||
case "crafted", "used", "broken", "picked_up", "dropped" ->
|
||||
BuiltInRegistries.ITEM.getOptional(source.target()).isPresent();
|
||||
case "killed", "killed_by" -> BuiltInRegistries.ENTITY_TYPE.getOptional(source.target()).isPresent();
|
||||
default -> false;
|
||||
};
|
||||
}
|
||||
|
||||
static Optional<Source> parse(Identifier metricId) {
|
||||
if (!metricId.getNamespace().equals(SanctuaryMod.MOD_ID)
|
||||
|| !metricId.getPath().startsWith(PREFIX)) return Optional.empty();
|
||||
String[] parts = metricId.getPath().split("/", 5);
|
||||
if (parts.length != 5) return Optional.empty();
|
||||
Identifier type = Identifier.tryParse(parts[1] + ":" + parts[2]);
|
||||
Identifier target = Identifier.tryParse(parts[3] + ":" + parts[4]);
|
||||
return type == null || target == null ? Optional.empty() : Optional.of(new Source(type, target));
|
||||
}
|
||||
|
||||
static long read(ServerPlayer player, Source source) {
|
||||
String family = source.type().getPath();
|
||||
Identifier target = source.target();
|
||||
return switch (family) {
|
||||
case "mined" -> BuiltInRegistries.BLOCK.getOptional(target)
|
||||
.map(block -> (long) player.getStats().getValue(Stats.BLOCK_MINED, block)).orElse(0L);
|
||||
case "crafted" -> BuiltInRegistries.ITEM.getOptional(target)
|
||||
.map(item -> (long) player.getStats().getValue(Stats.ITEM_CRAFTED, item)).orElse(0L);
|
||||
case "used" -> BuiltInRegistries.ITEM.getOptional(target)
|
||||
.map(item -> (long) player.getStats().getValue(Stats.ITEM_USED, item)).orElse(0L);
|
||||
case "broken" -> BuiltInRegistries.ITEM.getOptional(target)
|
||||
.map(item -> (long) player.getStats().getValue(Stats.ITEM_BROKEN, item)).orElse(0L);
|
||||
case "picked_up" -> BuiltInRegistries.ITEM.getOptional(target)
|
||||
.map(item -> (long) player.getStats().getValue(Stats.ITEM_PICKED_UP, item)).orElse(0L);
|
||||
case "dropped" -> BuiltInRegistries.ITEM.getOptional(target)
|
||||
.map(item -> (long) player.getStats().getValue(Stats.ITEM_DROPPED, item)).orElse(0L);
|
||||
case "killed" -> BuiltInRegistries.ENTITY_TYPE.getOptional(target)
|
||||
.map(type -> (long) player.getStats().getValue(Stats.ENTITY_KILLED, type)).orElse(0L);
|
||||
case "killed_by" -> BuiltInRegistries.ENTITY_TYPE.getOptional(target)
|
||||
.map(type -> (long) player.getStats().getValue(Stats.ENTITY_KILLED_BY, type)).orElse(0L);
|
||||
case "custom" -> (long) player.getStats().getValue(Stats.CUSTOM, target);
|
||||
default -> 0L;
|
||||
};
|
||||
}
|
||||
|
||||
private static Component iconName(ItemStack icon) {
|
||||
return icon == null || icon.isEmpty() ? Component.literal("?") : icon.getHoverName();
|
||||
}
|
||||
|
||||
record Source(Identifier type, Identifier target) {}
|
||||
}
|
||||
+32
-10
@@ -3,12 +3,15 @@ package fr.koka99cab.sanctuary26.sanctuary.backroom;
|
||||
import fr.koka99cab.sanctuary26.sanctuary.SanctuaryMod;
|
||||
import fr.koka99cab.sanctuary26.sanctuary.shop.ShopData;
|
||||
import fr.koka99cab.sanctuary26.sanctuary.world.SanctuaryDimensions;
|
||||
import fr.koka99cab.sanctuary26.sanctuary.worldgen.feature.BackroomArchitectureFeature;
|
||||
import fr.koka99cab.sanctuary26.sanctuary.worldgen.feature.BackroomLayout;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import net.fabricmc.fabric.api.networking.v1.ServerPlayConnectionEvents;
|
||||
import net.fabricmc.fabric.api.command.v2.CommandRegistrationCallback;
|
||||
import net.fabricmc.fabric.api.event.lifecycle.v1.ServerEntityEvents;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.commands.CommandSourceStack;
|
||||
@@ -18,8 +21,9 @@ import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.util.Mth;
|
||||
import net.minecraft.util.RandomSource;
|
||||
import net.minecraft.world.entity.MobCategory;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.level.levelgen.Heightmap;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.portal.TeleportTransition;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
|
||||
@@ -36,6 +40,9 @@ public final class BackroomService {
|
||||
if (initialized) return;
|
||||
initialized = true;
|
||||
ServerPlayConnectionEvents.JOIN.register((handler, sender, server) -> reconcile(handler.player));
|
||||
ServerEntityEvents.ENTITY_LOAD.register((entity, level) -> {
|
||||
if (isBackroom(level) && entity.getType().getCategory() == MobCategory.MONSTER) entity.discard();
|
||||
});
|
||||
CommandRegistrationCallback.EVENT.register((dispatcher, registryAccess, environment) -> {
|
||||
dispatcher.register(Commands.literal("backroom")
|
||||
.requires(Commands.hasPermission(Commands.LEVEL_GAMEMASTERS))
|
||||
@@ -101,6 +108,7 @@ public final class BackroomService {
|
||||
data.finishEntry(player.getUUID());
|
||||
|
||||
BlockPos arrival = chooseArrival(destination, player.getUUID(), data);
|
||||
placeArrivalTorch(destination, arrival);
|
||||
data.beginExpedition(player.getUUID(), arrival.asLong());
|
||||
TeleportTransition transition = new TeleportTransition(destination, Vec3.atBottomCenterOf(arrival),
|
||||
Vec3.ZERO, player.getYRot(), player.getXRot(),
|
||||
@@ -166,15 +174,14 @@ public final class BackroomService {
|
||||
private static BlockPos chooseArrival(ServerLevel level, UUID player, BackroomData data) {
|
||||
RandomSource random = level.getRandom();
|
||||
List<Long> recent = data.recentArrivals(player);
|
||||
int arrivalY = BackroomLayout.ENTRY_FLOOR_Y + 1;
|
||||
BlockPos best = null;
|
||||
for (int attempt = 0; attempt < ARRIVAL_ATTEMPTS; attempt++) {
|
||||
double angle = random.nextDouble() * Math.PI * 2.0D;
|
||||
double radius = Math.sqrt(random.nextDouble()) * ARRIVAL_RADIUS;
|
||||
int x = Mth.floor(Math.cos(angle) * radius);
|
||||
int z = Mth.floor(Math.sin(angle) * radius);
|
||||
int y = Mth.clamp(level.getHeight(Heightmap.Types.MOTION_BLOCKING_NO_LEAVES, x, z),
|
||||
level.getMinY() + 2, level.getMaxY() - 3);
|
||||
BlockPos candidate = new BlockPos(x, y, z);
|
||||
BlockPos candidate = new BlockPos(x, arrivalY, z);
|
||||
best = candidate;
|
||||
if (farFromRecent(candidate, recent) && safe(level, candidate)) return candidate;
|
||||
}
|
||||
@@ -182,9 +189,7 @@ public final class BackroomService {
|
||||
if (fallback != null) return fallback;
|
||||
fallback = findSafeNear(level, BlockPos.ZERO, 48);
|
||||
if (fallback != null) return fallback;
|
||||
int originY = Mth.clamp(level.getHeight(Heightmap.Types.MOTION_BLOCKING_NO_LEAVES, 0, 0),
|
||||
level.getMinY() + 2, level.getMaxY() - 3);
|
||||
return new BlockPos(0, originY, 0);
|
||||
return new BlockPos(8, arrivalY, 8);
|
||||
}
|
||||
|
||||
private static boolean farFromRecent(BlockPos candidate, List<Long> recent) {
|
||||
@@ -207,15 +212,32 @@ public final class BackroomService {
|
||||
if (ring > 0 && Math.abs(dx) != ring && Math.abs(dz) != ring) continue;
|
||||
int x = center.getX() + dx, z = center.getZ() + dz;
|
||||
if ((long) x * x + (long) z * z > (long) ARRIVAL_RADIUS * ARRIVAL_RADIUS) continue;
|
||||
int y = Mth.clamp(level.getHeight(Heightmap.Types.MOTION_BLOCKING_NO_LEAVES, x, z),
|
||||
level.getMinY() + 2, level.getMaxY() - 3);
|
||||
BlockPos candidate = new BlockPos(x, y, z);
|
||||
BlockPos candidate = new BlockPos(x, BackroomLayout.ENTRY_FLOOR_Y + 1, z);
|
||||
if (safe(level, candidate)) return candidate;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static void placeArrivalTorch(ServerLevel level, BlockPos arrival) {
|
||||
level.setBlock(arrival.below(), Blocks.BIRCH_PLANKS.defaultBlockState(), 3);
|
||||
level.setBlock(arrival, Blocks.AIR.defaultBlockState(), 3);
|
||||
level.setBlock(arrival.above(), Blocks.AIR.defaultBlockState(), 3);
|
||||
for (var direction : net.minecraft.core.Direction.Plane.HORIZONTAL) {
|
||||
BlockPos torch = arrival.relative(direction);
|
||||
if (!BackroomArchitectureFeature.withinPlayableArea(torch.getX(), torch.getZ())) continue;
|
||||
if (level.getBlockState(torch).isAir()
|
||||
&& level.getBlockState(torch.below()).isCollisionShapeFullBlock(level, torch.below())) {
|
||||
level.setBlock(torch, Blocks.TORCH.defaultBlockState(), 3);
|
||||
return;
|
||||
}
|
||||
}
|
||||
BlockPos torch = arrival.east();
|
||||
if (!BackroomArchitectureFeature.withinPlayableArea(torch.getX(), torch.getZ())) torch = arrival.west();
|
||||
level.setBlock(torch.below(), Blocks.BIRCH_PLANKS.defaultBlockState(), 3);
|
||||
level.setBlock(torch, Blocks.TORCH.defaultBlockState(), 3);
|
||||
}
|
||||
|
||||
private static void notify(ServerPlayer player, String translationKey) {
|
||||
player.sendOverlayMessage(Component.translatable(translationKey));
|
||||
}
|
||||
|
||||
+22
-6
@@ -2,6 +2,7 @@ package fr.koka99cab.sanctuary26.sanctuary.client;
|
||||
|
||||
import com.mojang.blaze3d.vertex.PoseStack;
|
||||
import fr.koka99cab.sanctuary26.sanctuary.SanctuaryMod;
|
||||
import java.util.Set;
|
||||
import net.fabricmc.fabric.api.client.rendering.v1.LivingEntityRenderLayerRegistrationCallback;
|
||||
import net.fabricmc.fabric.api.client.rendering.v1.ModelLayerRegistry;
|
||||
import net.minecraft.client.model.geom.ModelLayerLocation;
|
||||
@@ -23,6 +24,7 @@ import net.minecraft.client.renderer.entity.state.AvatarRenderState;
|
||||
import net.minecraft.client.renderer.texture.OverlayTexture;
|
||||
import net.minecraft.client.resources.model.EquipmentAssetManager;
|
||||
import net.minecraft.client.resources.model.EquipmentClientInfo;
|
||||
import net.minecraft.core.Direction;
|
||||
import net.minecraft.core.component.DataComponents;
|
||||
import net.minecraft.world.entity.EntityTypes;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
@@ -30,6 +32,10 @@ import net.minecraft.world.item.equipment.Equippable;
|
||||
|
||||
/** Renders a banner-decorated cape with vanilla cape physics and banner pattern sprites. */
|
||||
public final class SanctuaryBannerCapeLayer extends RenderLayer<AvatarRenderState, PlayerModel> {
|
||||
private static final float SHIELD_FACE_WIDTH = 12.0F;
|
||||
private static final float SHIELD_FACE_HEIGHT = 22.0F;
|
||||
private static final float CAPE_WIDTH = 10.0F;
|
||||
private static final float CAPE_HEIGHT = 16.0F;
|
||||
private static final ModelLayerLocation MODEL_LAYER =
|
||||
new ModelLayerLocation(SanctuaryMod.id("banner_cape"), "main");
|
||||
private final PlayerCapeModel model;
|
||||
@@ -61,17 +67,27 @@ public final class SanctuaryBannerCapeLayer extends RenderLayer<AvatarRenderStat
|
||||
helper.register(new SanctuaryBannerCapeLayer(parent, context));
|
||||
}
|
||||
|
||||
private static LayerDefinition createLayer() {
|
||||
static LayerDefinition createLayer() {
|
||||
MeshDefinition mesh = PlayerModel.createMesh(CubeDeformation.NONE, false);
|
||||
PartDefinition root = mesh.getRoot().clearRecursively();
|
||||
PartDefinition body = root.getChild("body");
|
||||
body.addOrReplaceChild("cape", CubeListBuilder.create().texOffs(0, 0)
|
||||
.addBox(-5.0F, 0.0F, -1.0F, 10.0F, 16.0F, 1.0F,
|
||||
CubeDeformation.NONE, 2.0F, 2.5F),
|
||||
PartPose.offsetAndRotation(0.0F, 0.0F, 2.0F, 0.0F, (float) Math.PI, 0.0F));
|
||||
PartDefinition cape = body.addOrReplaceChild("cape", CubeListBuilder.create(),
|
||||
PartPose.offsetAndRotation(0.0F, 0.0F, 2.0F, 0.0F, (float) Math.PI, 0.0F)
|
||||
.scaled(CAPE_WIDTH / SHIELD_FACE_WIDTH, CAPE_HEIGHT / SHIELD_FACE_HEIGHT, 1.0F));
|
||||
cape.addOrReplaceChild("front", shieldPatternFace(), PartPose.offset(0.0F, 0.0F, -1.0F));
|
||||
cape.addOrReplaceChild("back", shieldPatternFace(),
|
||||
PartPose.offsetAndRotation(0.0F, 0.0F, -1.0F, 0.0F, (float) Math.PI, 0.0F));
|
||||
return LayerDefinition.create(mesh, 64, 64);
|
||||
}
|
||||
|
||||
private static CubeListBuilder shieldPatternFace() {
|
||||
// Shield patterns paint this exact 12x22 UV island. Two opposed planes keep
|
||||
// the cloth visible from both sides without rendering the shield thickness or handle.
|
||||
return CubeListBuilder.create().texOffs(1, 1)
|
||||
.addBox(-6.0F, 0.0F, 0.0F, SHIELD_FACE_WIDTH, SHIELD_FACE_HEIGHT, 0.0F,
|
||||
Set.of(Direction.NORTH));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void submit(PoseStack poseStack, SubmitNodeCollector collector, int packedLight,
|
||||
AvatarRenderState state, float yRot, float xRot) {
|
||||
@@ -85,7 +101,7 @@ public final class SanctuaryBannerCapeLayer extends RenderLayer<AvatarRenderStat
|
||||
poseStack.translate(0.0F, -0.053125F, 0.06875F);
|
||||
}
|
||||
BannerRenderer.submitPatterns(context.getSprites(), poseStack, collector, packedLight,
|
||||
OverlayTexture.NO_OVERLAY, model, state, true, appearance.baseColor(),
|
||||
OverlayTexture.NO_OVERLAY, model, state, false, appearance.baseColor(),
|
||||
appearance.patterns(), null);
|
||||
poseStack.popPose();
|
||||
}
|
||||
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
package fr.koka99cab.sanctuary26.sanctuary.client;
|
||||
|
||||
import com.mojang.blaze3d.vertex.PoseStack;
|
||||
import fr.koka99cab.sanctuary26.sanctuary.SanctuaryMod;
|
||||
import net.fabricmc.fabric.api.client.rendering.v1.LivingEntityRenderLayerRegistrationCallback;
|
||||
import net.fabricmc.fabric.api.client.rendering.v1.ModelLayerRegistry;
|
||||
import net.minecraft.client.model.animal.chicken.ChickenModel;
|
||||
import net.minecraft.client.model.geom.ModelLayerLocation;
|
||||
import net.minecraft.client.renderer.SubmitNodeCollector;
|
||||
import net.minecraft.client.renderer.entity.ChickenRenderer;
|
||||
import net.minecraft.client.renderer.entity.EntityRendererProvider;
|
||||
import net.minecraft.client.renderer.entity.RenderLayerParent;
|
||||
import net.minecraft.client.renderer.entity.layers.EquipmentLayerRenderer;
|
||||
import net.minecraft.client.renderer.entity.layers.RenderLayer;
|
||||
import net.minecraft.client.renderer.entity.state.ChickenRenderState;
|
||||
import net.minecraft.client.resources.model.EquipmentClientInfo;
|
||||
import net.minecraft.core.component.DataComponents;
|
||||
import net.minecraft.world.entity.EntityTypes;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.item.equipment.Equippable;
|
||||
|
||||
/** Renders vanilla armadillo-scute equipment around an adult chicken's torso. */
|
||||
public final class SanctuaryChickenArmorLayer extends RenderLayer<ChickenRenderState, ChickenModel> {
|
||||
private static final ModelLayerLocation MODEL_LAYER =
|
||||
new ModelLayerLocation(SanctuaryMod.id("chicken_armor"), "main");
|
||||
private final SanctuaryChickenArmorModel model;
|
||||
private final EquipmentLayerRenderer equipmentRenderer;
|
||||
|
||||
private SanctuaryChickenArmorLayer(RenderLayerParent<ChickenRenderState, ChickenModel> parent,
|
||||
EntityRendererProvider.Context context) {
|
||||
super(parent);
|
||||
model = SanctuaryChickenArmorModel.bake(context.bakeLayer(MODEL_LAYER));
|
||||
equipmentRenderer = context.getEquipmentRenderer();
|
||||
}
|
||||
|
||||
public static void initialize() {
|
||||
ModelLayerRegistry.registerModelLayer(MODEL_LAYER, SanctuaryChickenArmorModel::createLayer);
|
||||
LivingEntityRenderLayerRegistrationCallback.EVENT.register((entityType, renderer, helper, context) -> {
|
||||
if (entityType == EntityTypes.CHICKEN && renderer instanceof ChickenRenderer chickenRenderer) {
|
||||
register(helper, chickenRenderer, context);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@SuppressWarnings({"unchecked", "rawtypes"})
|
||||
private static void register(LivingEntityRenderLayerRegistrationCallback.RegistrationHelper helper,
|
||||
ChickenRenderer renderer, EntityRendererProvider.Context context) {
|
||||
RenderLayerParent<ChickenRenderState, ChickenModel> parent =
|
||||
(RenderLayerParent<ChickenRenderState, ChickenModel>) (RenderLayerParent) renderer;
|
||||
helper.register(new SanctuaryChickenArmorLayer(parent, context));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void submit(PoseStack poseStack, SubmitNodeCollector collector, int packedLight,
|
||||
ChickenRenderState state, float yRot, float xRot) {
|
||||
if (state.isBaby || state.isInvisibleToPlayer) return;
|
||||
ItemStack armor = ((SanctuaryChickenArmorRenderState) state).sanctuary$chickenArmor();
|
||||
Equippable equippable = armor.get(DataComponents.EQUIPPABLE);
|
||||
if (equippable == null || equippable.assetId().isEmpty()) return;
|
||||
equipmentRenderer.renderLayers(EquipmentClientInfo.LayerType.WOLF_BODY,
|
||||
equippable.assetId().get(), model, state, armor, poseStack, collector,
|
||||
packedLight, state.outlineColor);
|
||||
}
|
||||
}
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
package fr.koka99cab.sanctuary26.sanctuary.client;
|
||||
|
||||
import net.minecraft.client.model.EntityModel;
|
||||
import net.minecraft.client.model.geom.ModelPart;
|
||||
import net.minecraft.client.model.geom.PartPose;
|
||||
import net.minecraft.client.model.geom.builders.CubeDeformation;
|
||||
import net.minecraft.client.model.geom.builders.CubeListBuilder;
|
||||
import net.minecraft.client.model.geom.builders.LayerDefinition;
|
||||
import net.minecraft.client.model.geom.builders.MeshDefinition;
|
||||
import net.minecraft.client.renderer.entity.state.ChickenRenderState;
|
||||
|
||||
/** A thin torso shell using the vanilla armadillo-scute equipment UVs. */
|
||||
public final class SanctuaryChickenArmorModel extends EntityModel<ChickenRenderState> {
|
||||
private SanctuaryChickenArmorModel(ModelPart root) {
|
||||
super(root);
|
||||
}
|
||||
|
||||
public static SanctuaryChickenArmorModel bake(ModelPart root) {
|
||||
return new SanctuaryChickenArmorModel(root);
|
||||
}
|
||||
|
||||
public static LayerDefinition createLayer() {
|
||||
MeshDefinition mesh = new MeshDefinition();
|
||||
mesh.getRoot().addOrReplaceChild("body",
|
||||
CubeListBuilder.create().texOffs(18, 14)
|
||||
.addBox(-3.0F, -4.0F, -3.0F, 6.0F, 8.0F, 6.0F,
|
||||
new CubeDeformation(0.45F)),
|
||||
PartPose.offsetAndRotation(0.0F, 16.0F, 0.0F,
|
||||
(float) (Math.PI / 2.0D), 0.0F, 0.0F));
|
||||
return LayerDefinition.create(mesh, 64, 32);
|
||||
}
|
||||
}
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
package fr.koka99cab.sanctuary26.sanctuary.client;
|
||||
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
|
||||
/** Mixin-backed bridge carrying chicken body armor into the extracted render state. */
|
||||
public interface SanctuaryChickenArmorRenderState {
|
||||
ItemStack sanctuary$chickenArmor();
|
||||
|
||||
void sanctuary$setChickenArmor(ItemStack armor);
|
||||
}
|
||||
@@ -120,6 +120,7 @@ public final class SanctuaryClient implements ClientModInitializer {
|
||||
SanctuaryHealthPreview.register();
|
||||
CelestialSpyglassSelection.register();
|
||||
SanctuaryBannerCapeLayer.initialize();
|
||||
SanctuaryChickenArmorLayer.initialize();
|
||||
EntityRendererRegistry.register(SanctuaryRegistries.SEAT, NoopRenderer::new);
|
||||
ClientTickEvents.END_CLIENT_TICK.register(SanctuaryClient::tick);
|
||||
ClientPlayNetworking.registerGlobalReceiver(ProgressionSnapshotPayload.ID, (payload, context) ->
|
||||
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
package fr.koka99cab.sanctuary26.sanctuary.gameplay;
|
||||
|
||||
import net.minecraft.tags.DamageTypeTags;
|
||||
import net.minecraft.world.damagesource.DamageSource;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.item.Items;
|
||||
|
||||
/** Shared server rules for fitting vanilla armadillo-scute armor to adult chickens. */
|
||||
public final class SanctuaryChickenArmor {
|
||||
private SanctuaryChickenArmor() {
|
||||
}
|
||||
|
||||
public static boolean canEquip(ItemStack equipped, ItemStack held, boolean baby) {
|
||||
return !baby && equipped.isEmpty() && held.is(Items.WOLF_ARMOR);
|
||||
}
|
||||
|
||||
public static boolean canRepair(ItemStack equipped, ItemStack held) {
|
||||
return equipped.is(Items.WOLF_ARMOR) && equipped.isDamaged()
|
||||
&& equipped.isValidRepairItem(held);
|
||||
}
|
||||
|
||||
public static boolean absorbs(ItemStack equipped, DamageSource source) {
|
||||
return equipped.is(Items.WOLF_ARMOR) && !source.is(DamageTypeTags.BYPASSES_WOLF_ARMOR);
|
||||
}
|
||||
}
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
package fr.koka99cab.sanctuary26.sanctuary.mixin;
|
||||
|
||||
import fr.koka99cab.sanctuary26.sanctuary.gameplay.SanctuaryChickenArmor;
|
||||
import net.minecraft.sounds.SoundEvents;
|
||||
import net.minecraft.util.Mth;
|
||||
import net.minecraft.world.InteractionHand;
|
||||
import net.minecraft.world.InteractionResult;
|
||||
import net.minecraft.world.entity.EquipmentSlot;
|
||||
import net.minecraft.world.entity.animal.Animal;
|
||||
import net.minecraft.world.entity.animal.chicken.Chicken;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.injection.At;
|
||||
import org.spongepowered.asm.mixin.injection.Inject;
|
||||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
|
||||
|
||||
/** Lets adult chickens wear and repair the vanilla armadillo-scute wolf armor. */
|
||||
@Mixin(Animal.class)
|
||||
abstract class AnimalChickenArmorMixin {
|
||||
@Inject(method = "mobInteract", at = @At("HEAD"), cancellable = true)
|
||||
private void sanctuary$handleChickenArmor(Player player, InteractionHand hand,
|
||||
CallbackInfoReturnable<InteractionResult> cir) {
|
||||
if (!((Object) this instanceof Chicken chicken)) return;
|
||||
ItemStack held = player.getItemInHand(hand);
|
||||
ItemStack equipped = chicken.getItemBySlot(EquipmentSlot.BODY);
|
||||
if (SanctuaryChickenArmor.canEquip(equipped, held, chicken.isBaby())) {
|
||||
chicken.setItemSlot(EquipmentSlot.BODY, held.copyWithCount(1));
|
||||
chicken.setGuaranteedDrop(EquipmentSlot.BODY);
|
||||
held.consume(1, player);
|
||||
chicken.playSound(SoundEvents.ARMOR_EQUIP_WOLF.value());
|
||||
cir.setReturnValue(InteractionResult.SUCCESS);
|
||||
return;
|
||||
}
|
||||
if (SanctuaryChickenArmor.canRepair(equipped, held)) {
|
||||
held.consume(1, player);
|
||||
int repairUnit = Mth.ceil(equipped.getMaxDamage() * 0.125F);
|
||||
equipped.setDamageValue(Math.max(0, equipped.getDamageValue() - repairUnit));
|
||||
chicken.playSound(SoundEvents.WOLF_ARMOR_REPAIR);
|
||||
cir.setReturnValue(InteractionResult.SUCCESS);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,9 +2,11 @@ package fr.koka99cab.sanctuary26.sanctuary.mixin;
|
||||
|
||||
import fr.koka99cab.sanctuary26.sanctuary.gameplay.SanctuaryCompanionPowers;
|
||||
import java.util.function.Consumer;
|
||||
import net.minecraft.ChatFormatting;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.world.item.Item;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.item.Items;
|
||||
import net.minecraft.world.item.SpawnEggItem;
|
||||
import net.minecraft.world.item.TooltipFlag;
|
||||
import net.minecraft.world.item.component.TooltipDisplay;
|
||||
@@ -21,5 +23,9 @@ abstract class ItemTooltipMixin {
|
||||
if (stack.getItem() instanceof SpawnEggItem) {
|
||||
SanctuaryCompanionPowers.appendPowerTooltip(stack, tooltip);
|
||||
}
|
||||
if (stack.is(Items.WOLF_ARMOR)) {
|
||||
tooltip.accept(Component.translatable("tooltip.sanctuary.chicken_armor")
|
||||
.withStyle(ChatFormatting.GRAY));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
package fr.koka99cab.sanctuary26.sanctuary.mixin;
|
||||
|
||||
import fr.koka99cab.sanctuary26.sanctuary.gameplay.SanctuaryChickenArmor;
|
||||
import net.minecraft.core.particles.ItemParticleOption;
|
||||
import net.minecraft.core.particles.ParticleTypes;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.sounds.SoundEvents;
|
||||
import net.minecraft.util.Mth;
|
||||
import net.minecraft.world.damagesource.DamageSource;
|
||||
import net.minecraft.world.entity.Crackiness;
|
||||
import net.minecraft.world.entity.EquipmentSlot;
|
||||
import net.minecraft.world.entity.LivingEntity;
|
||||
import net.minecraft.world.entity.animal.chicken.Chicken;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.item.Items;
|
||||
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;
|
||||
|
||||
/** Gives armored chickens the same damage-absorption contract as armored wolves. */
|
||||
@Mixin(LivingEntity.class)
|
||||
abstract class LivingEntityChickenArmorMixin {
|
||||
@Inject(method = "actuallyHurt", at = @At("HEAD"), cancellable = true)
|
||||
private void sanctuary$absorbChickenDamage(ServerLevel level, DamageSource source,
|
||||
float damage, CallbackInfo ci) {
|
||||
if (!((Object) this instanceof Chicken chicken)) return;
|
||||
ItemStack armor = chicken.getItemBySlot(EquipmentSlot.BODY);
|
||||
if (!SanctuaryChickenArmor.absorbs(armor, source)) return;
|
||||
|
||||
int damageBefore = armor.getDamageValue();
|
||||
int maxDamage = armor.getMaxDamage();
|
||||
armor.hurtAndBreak(Mth.ceil(damage), chicken, EquipmentSlot.BODY);
|
||||
chicken.playSound(SoundEvents.WOLF_ARMOR_DAMAGE);
|
||||
if (Crackiness.WOLF_ARMOR.byDamage(damageBefore, maxDamage)
|
||||
!= Crackiness.WOLF_ARMOR.byDamage(chicken.getItemBySlot(EquipmentSlot.BODY))) {
|
||||
chicken.playSound(SoundEvents.WOLF_ARMOR_CRACK);
|
||||
level.sendParticles(new ItemParticleOption(ParticleTypes.ITEM,
|
||||
Items.ARMADILLO_SCUTE), chicken.getX(), chicken.getY() + 0.5D,
|
||||
chicken.getZ(), 12, 0.2D, 0.15D, 0.2D, 0.08D);
|
||||
}
|
||||
ci.cancel();
|
||||
}
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
package fr.koka99cab.sanctuary26.sanctuary.mixin.client;
|
||||
|
||||
import fr.koka99cab.sanctuary26.sanctuary.client.SanctuaryChickenArmorRenderState;
|
||||
import net.minecraft.client.renderer.entity.state.ChickenRenderState;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.Unique;
|
||||
|
||||
/** Stores synchronized chicken body armor on the client render state. */
|
||||
@Mixin(ChickenRenderState.class)
|
||||
abstract class ChickenRenderStateMixin implements SanctuaryChickenArmorRenderState {
|
||||
@Unique private ItemStack sanctuary$chickenArmor = ItemStack.EMPTY;
|
||||
|
||||
@Override
|
||||
public ItemStack sanctuary$chickenArmor() {
|
||||
return sanctuary$chickenArmor;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sanctuary$setChickenArmor(ItemStack armor) {
|
||||
sanctuary$chickenArmor = armor == null ? ItemStack.EMPTY : armor;
|
||||
}
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
package fr.koka99cab.sanctuary26.sanctuary.mixin.client;
|
||||
|
||||
import fr.koka99cab.sanctuary26.sanctuary.client.SanctuaryChickenArmorRenderState;
|
||||
import net.minecraft.client.renderer.entity.ChickenRenderer;
|
||||
import net.minecraft.client.renderer.entity.state.ChickenRenderState;
|
||||
import net.minecraft.world.entity.EquipmentSlot;
|
||||
import net.minecraft.world.entity.animal.chicken.Chicken;
|
||||
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;
|
||||
|
||||
/** Copies vanilla-synchronized body equipment into each chicken render state. */
|
||||
@Mixin(ChickenRenderer.class)
|
||||
abstract class ChickenRendererMixin {
|
||||
@Inject(method = "extractRenderState", at = @At("TAIL"))
|
||||
private void sanctuary$extractChickenArmor(Chicken chicken, ChickenRenderState state,
|
||||
float partialTick, CallbackInfo ci) {
|
||||
((SanctuaryChickenArmorRenderState) state).sanctuary$setChickenArmor(
|
||||
chicken.getItemBySlot(EquipmentSlot.BODY).copy());
|
||||
}
|
||||
}
|
||||
+19
-4
@@ -425,8 +425,11 @@ public final class QuestRequestService {
|
||||
"message.sanctuary.quest_request.details.reward_legacy").withStyle(ChatFormatting.GRAY));
|
||||
QuestRequestData.Claim claim = request.claim().orElse(null);
|
||||
if (request.completed()) {
|
||||
player.sendSystemMessage(Component.translatable("message.sanctuary.quest_request.details.completed")
|
||||
.withStyle(ChatFormatting.GREEN));
|
||||
Component completion = claim == null
|
||||
? Component.translatable("message.sanctuary.quest_request.details.completed")
|
||||
: Component.translatable("message.sanctuary.quest_request.details.completed_by",
|
||||
claimantName(claim));
|
||||
player.sendSystemMessage(completion.copy().withStyle(ChatFormatting.GREEN));
|
||||
} else if (claim != null) {
|
||||
Component claimant = Component.literal(claim.playerName())
|
||||
.withStyle(style -> style.withColor(claim.playerColor()));
|
||||
@@ -444,6 +447,11 @@ public final class QuestRequestService {
|
||||
return Component.translatable("message.sanctuary.quest_request.action." + action);
|
||||
}
|
||||
|
||||
private static Component claimantName(QuestRequestData.Claim claim) {
|
||||
return Component.literal(claim.playerName())
|
||||
.withStyle(style -> style.withColor(claim.playerColor()));
|
||||
}
|
||||
|
||||
private static Component describeTarget(QuestRequestData.Request request) {
|
||||
if (PAY.equals(request.action()) || PLAYER.equals(request.targetKind())) {
|
||||
return Component.literal(request.target());
|
||||
@@ -485,13 +493,20 @@ public final class QuestRequestService {
|
||||
if (request.completed()) {
|
||||
text = text.setMessage(2, Component.translatable("sign.sanctuary.quest_request.completed")
|
||||
.withStyle(ChatFormatting.DARK_GRAY));
|
||||
QuestRequestData.Claim claim = request.claim().orElse(null);
|
||||
text = text.setMessage(3, claim == null
|
||||
? Component.translatable("sign.sanctuary.quest_request.details")
|
||||
.withStyle(ChatFormatting.GRAY)
|
||||
: Component.translatable("sign.sanctuary.quest_request.completed_by", claimantName(claim))
|
||||
.withStyle(ChatFormatting.GRAY));
|
||||
} else if (request.claim().isPresent()) {
|
||||
text = text.setMessage(2, Component.translatable("sign.sanctuary.quest_request.claimed")
|
||||
.withStyle(ChatFormatting.DARK_GRAY));
|
||||
} else text = text.setMessage(2, Component.translatable("sign.sanctuary.quest_request.available")
|
||||
.withStyle(ChatFormatting.DARK_GRAY));
|
||||
text = text.setMessage(3, Component.translatable("sign.sanctuary.quest_request.details")
|
||||
.withStyle(ChatFormatting.GRAY));
|
||||
if (!request.completed()) text = text.setMessage(3,
|
||||
Component.translatable("sign.sanctuary.quest_request.details")
|
||||
.withStyle(ChatFormatting.GRAY));
|
||||
}
|
||||
sign.setText(text, request.front());
|
||||
sign.setWaxed(true);
|
||||
|
||||
+392
-137
@@ -1,8 +1,9 @@
|
||||
package fr.koka99cab.sanctuary26.sanctuary.worldgen.feature;
|
||||
|
||||
import com.mojang.serialization.Codec;
|
||||
import fr.koka99cab.sanctuary26.sanctuary.SanctuaryMod;
|
||||
import fr.koka99cab.sanctuary26.sanctuary.api.backroom.BackroomMaterialApi;
|
||||
import fr.koka99cab.sanctuary26.sanctuary.worldgen.feature.BackroomLayout.BackroomLevel;
|
||||
import fr.koka99cab.sanctuary26.sanctuary.worldgen.feature.BackroomLayout.RoomCorner;
|
||||
import java.util.Random;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Direction;
|
||||
@@ -19,20 +20,41 @@ import net.minecraft.world.level.levelgen.feature.FeaturePlaceContext;
|
||||
import net.minecraft.world.level.levelgen.feature.configurations.NoneFeatureConfiguration;
|
||||
|
||||
/**
|
||||
* Deterministic, chunk-local architecture for the persistent Backroom.
|
||||
* Macro cells select authored palettes; randomness only varies plans inside a coherent family.
|
||||
* Five deterministic Backroom levels stacked from Y=384 to Y=-64. Generation is chunk-local,
|
||||
* while every wall, doorway, highway and hole is derived from world coordinates so chunk seams
|
||||
* remain open and coherent.
|
||||
*/
|
||||
public final class BackroomArchitectureFeature extends Feature<NoneFeatureConfiguration> {
|
||||
public static final int PLAYABLE_RADIUS = 2_000;
|
||||
private static final int CHUNK_SIZE = 16;
|
||||
private static final int MACRO_CHUNKS = 8;
|
||||
private static final int[] THRESHOLD_DECKS = {295, 313, 331, 349, 367};
|
||||
private static final int[] ARCHIVE_DECKS = {206, 228, 250, 272};
|
||||
private static final int[] UTILITY_DECKS = {28, 46, 64, 82, 100};
|
||||
|
||||
private static final BlockState CONCRETE_GRAY = Blocks.CONCRETE.pick(DyeColor.GRAY).defaultBlockState();
|
||||
private static final BlockState CONCRETE_LIGHT = Blocks.CONCRETE.pick(DyeColor.LIGHT_GRAY).defaultBlockState();
|
||||
private static final BlockState CONCRETE_WHITE = Blocks.CONCRETE.pick(DyeColor.WHITE).defaultBlockState();
|
||||
private static final BlockState GLASS_BLACK = Blocks.STAINED_GLASS.pick(DyeColor.BLACK).defaultBlockState();
|
||||
private static final BlockState GLASS_BROWN = Blocks.STAINED_GLASS.pick(DyeColor.BROWN).defaultBlockState();
|
||||
private static final BlockState GLASS_CYAN = Blocks.STAINED_GLASS.pick(DyeColor.CYAN).defaultBlockState();
|
||||
private static final BlockState CLAY_BROWN = Blocks.DYED_TERRACOTTA.pick(DyeColor.BROWN).defaultBlockState();
|
||||
private static final BlockState CLAY_ORANGE = Blocks.DYED_TERRACOTTA.pick(DyeColor.ORANGE).defaultBlockState();
|
||||
private static final BlockState CLAY_YELLOW = Blocks.DYED_TERRACOTTA.pick(DyeColor.YELLOW).defaultBlockState();
|
||||
private static final BlockState LEAVES = Blocks.OAK_LEAVES.defaultBlockState().setValue(LeavesBlock.PERSISTENT, true);
|
||||
private static final BlockState[] CORNER_MATERIALS = {
|
||||
Blocks.BRICKS.defaultBlockState(),
|
||||
Blocks.MUD_BRICKS.defaultBlockState(),
|
||||
Blocks.STONE_BRICKS.defaultBlockState(),
|
||||
CLAY_YELLOW,
|
||||
Blocks.SMOOTH_SANDSTONE.defaultBlockState()
|
||||
};
|
||||
private static final BlockState[] GLITCH_MATERIALS = {
|
||||
Blocks.BRICKS.defaultBlockState(),
|
||||
Blocks.MUD_BRICKS.defaultBlockState(),
|
||||
Blocks.STONE_BRICKS.defaultBlockState(),
|
||||
Blocks.CRACKED_STONE_BRICKS.defaultBlockState(),
|
||||
CLAY_BROWN
|
||||
};
|
||||
|
||||
public BackroomArchitectureFeature(Codec<NoneFeatureConfiguration> codec) {
|
||||
super(codec);
|
||||
@@ -47,17 +69,15 @@ public final class BackroomArchitectureFeature extends Feature<NoneFeatureConfig
|
||||
int startZ = chunkZ * CHUNK_SIZE;
|
||||
if (!withinPlayableArea(startX + 8, startZ + 8)) return false;
|
||||
|
||||
long seed = mix(level.getSeed(), chunkX, chunkZ);
|
||||
Random random = new Random(seed);
|
||||
Architecture architecture = architecture(level.getSeed(), chunkX, chunkZ);
|
||||
switch (architecture) {
|
||||
case CITY -> city(level, startX, startZ, random);
|
||||
case OFFICES -> offices(level, startX, startZ, random);
|
||||
case PLAINS -> plains(level, startX, startZ, random);
|
||||
case AGGREGATES -> aggregates(level, startX, startZ, random);
|
||||
case ROTATED -> rotated(level, startX, startZ, random);
|
||||
}
|
||||
placeLootLandmarks(level, startX, startZ, random, architecture);
|
||||
long worldSeed = level.getSeed();
|
||||
Random random = new Random(fingerprint(worldSeed, chunkX, chunkZ));
|
||||
buildLevelShells(level, worldSeed, startX, startZ);
|
||||
buildThreshold(level, worldSeed, startX, startZ);
|
||||
buildArchives(level, worldSeed, startX, startZ);
|
||||
buildCity(level, worldSeed, startX, startZ, chunkX, chunkZ);
|
||||
buildUtilities(level, worldSeed, startX, startZ);
|
||||
buildGlitchMaze(level, worldSeed, startX, startZ);
|
||||
placeLootLandmarks(level, worldSeed, startX, startZ, chunkX, chunkZ);
|
||||
placeRareAnomaly(level, startX, startZ, random);
|
||||
return true;
|
||||
}
|
||||
@@ -66,161 +86,398 @@ public final class BackroomArchitectureFeature extends Feature<NoneFeatureConfig
|
||||
return (long) x * x + (long) z * z <= (long) PLAYABLE_RADIUS * PLAYABLE_RADIUS;
|
||||
}
|
||||
|
||||
public static Architecture architecture(long worldSeed, int chunkX, int chunkZ) {
|
||||
int macroX = Math.floorDiv(chunkX, MACRO_CHUNKS);
|
||||
int macroZ = Math.floorDiv(chunkZ, MACRO_CHUNKS);
|
||||
int index = Math.floorMod((int) mix(worldSeed ^ 0x4f1bbcdcL, macroX, macroZ), Architecture.values().length);
|
||||
return Architecture.values()[index];
|
||||
}
|
||||
|
||||
/** Stable fingerprint used to prevent palette selection drift. */
|
||||
/** Stable fingerprint used to prevent authored layout drift. */
|
||||
public static long fingerprint(long worldSeed, int chunkX, int chunkZ) {
|
||||
return mix(worldSeed ^ architecture(worldSeed, chunkX, chunkZ).ordinal(), chunkX, chunkZ);
|
||||
return mix(worldSeed ^ 0x4f1bbcdcL, chunkX, chunkZ);
|
||||
}
|
||||
|
||||
private static void city(WorldGenLevel level, int x0, int z0, Random random) {
|
||||
int y = 64;
|
||||
for (int x = x0; x < x0 + 16; x++) for (int z = z0; z < z0 + 16; z++) {
|
||||
boolean road = Math.floorMod(x, 32) < 5 || Math.floorMod(z, 32) < 5;
|
||||
set(level, x, y, z, road ? CONCRETE_GRAY : CONCRETE_LIGHT);
|
||||
if (road && (Math.floorMod(x, 8) == 0 || Math.floorMod(z, 8) == 0)) set(level, x, y, z, Blocks.SMOOTH_STONE.defaultBlockState());
|
||||
}
|
||||
if (Math.floorMod(x0, 32) < 5 || Math.floorMod(z0, 32) < 5) return;
|
||||
int minX = x0 + 2, maxX = x0 + 13, minZ = z0 + 2, maxZ = z0 + 13;
|
||||
int height = 18 + random.nextInt(42);
|
||||
for (int floor = 0; floor <= height; floor++) {
|
||||
int py = y + 1 + floor;
|
||||
boolean slab = floor == 0 || floor == height || floor % 5 == 0;
|
||||
for (int x = minX; x <= maxX; x++) for (int z = minZ; z <= maxZ; z++) {
|
||||
boolean edge = x == minX || x == maxX || z == minZ || z == maxZ;
|
||||
if (slab) set(level, x, py, z, CONCRETE_LIGHT);
|
||||
else if (edge) set(level, x, py, z, floor % 5 == 1 ? CONCRETE_WHITE : GLASS_CYAN);
|
||||
private static void buildLevelShells(WorldGenLevel level, long seed, int x0, int z0) {
|
||||
for (BackroomLevel zone : BackroomLevel.values()) {
|
||||
for (int x = x0; x < x0 + CHUNK_SIZE; x++) for (int z = z0; z < z0 + CHUNK_SIZE; z++) {
|
||||
if (!floorHole(seed, zone, x, z)) set(level, x, zone.floorY(), z, floorMaterial(zone));
|
||||
if (!ceilingHole(seed, zone, x, z)) set(level, x, zone.ceilingY(), z, ceilingMaterial(zone));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void offices(WorldGenLevel level, int x0, int z0, Random random) {
|
||||
for (int baseY : new int[]{72, 84, 96}) {
|
||||
for (int x = x0; x < x0 + 16; x++) for (int z = z0; z < z0 + 16; z++) {
|
||||
set(level, x, baseY, z, Blocks.OAK_PLANKS.defaultBlockState());
|
||||
set(level, x, baseY + 9, z, CLAY_ORANGE);
|
||||
boolean wall = Math.floorMod(x, 12) == 0 || Math.floorMod(z, 12) == 0;
|
||||
if (wall) for (int y = baseY + 1; y < baseY + 9; y++) {
|
||||
BlockState material = y == baseY + 4 ? Blocks.STAINED_GLASS.pick(DyeColor.BROWN).defaultBlockState() : CLAY_BROWN;
|
||||
set(level, x, y, z, material);
|
||||
private static boolean floorHole(long seed, BackroomLevel zone, int x, int z) {
|
||||
return zone == BackroomLevel.LEVEL_4
|
||||
? BackroomLayout.voidHole(seed, x, z)
|
||||
: BackroomLayout.transitionHole(seed, zone.ordinal(), x, z);
|
||||
}
|
||||
|
||||
private static boolean ceilingHole(long seed, BackroomLevel zone, int x, int z) {
|
||||
return zone != BackroomLevel.LEVEL_0
|
||||
&& BackroomLayout.transitionHole(seed, zone.ordinal() - 1, x, z);
|
||||
}
|
||||
|
||||
private static BlockState floorMaterial(BackroomLevel zone) {
|
||||
return switch (zone) {
|
||||
case LEVEL_0 -> Blocks.BIRCH_PLANKS.defaultBlockState();
|
||||
case LEVEL_1 -> Blocks.DARK_OAK_PLANKS.defaultBlockState();
|
||||
case LEVEL_2 -> CONCRETE_GRAY;
|
||||
case LEVEL_3 -> Blocks.POLISHED_ANDESITE.defaultBlockState();
|
||||
case LEVEL_4 -> Blocks.STONE_BRICKS.defaultBlockState();
|
||||
};
|
||||
}
|
||||
|
||||
private static BlockState ceilingMaterial(BackroomLevel zone) {
|
||||
return switch (zone) {
|
||||
case LEVEL_0 -> CLAY_YELLOW;
|
||||
case LEVEL_1 -> CONCRETE_LIGHT;
|
||||
case LEVEL_2 -> CONCRETE_GRAY;
|
||||
case LEVEL_3 -> Blocks.BRICKS.defaultBlockState();
|
||||
case LEVEL_4 -> Blocks.MUD_BRICKS.defaultBlockState();
|
||||
};
|
||||
}
|
||||
|
||||
private static void buildThreshold(WorldGenLevel level, long seed, int x0, int z0) {
|
||||
int cellX = Math.floorDiv(x0, CHUNK_SIZE);
|
||||
int cellZ = Math.floorDiv(z0, CHUNK_SIZE);
|
||||
for (int deck = 0; deck < THRESHOLD_DECKS.length; deck++) {
|
||||
int floorY = THRESHOLD_DECKS[deck];
|
||||
int roofY = deck + 1 < THRESHOLD_DECKS.length ? THRESHOLD_DECKS[deck + 1]
|
||||
: BackroomLevel.LEVEL_0.ceilingY();
|
||||
if (deck > 0) fillDeck(level, seed, floorY, x0, z0, Blocks.BIRCH_PLANKS.defaultBlockState());
|
||||
int pattern = BackroomLayout.roomPattern(seed, BackroomLevel.LEVEL_0, cellX, cellZ, deck);
|
||||
BlockState wall = pattern % 2 == 0 ? CLAY_YELLOW : Blocks.SMOOTH_SANDSTONE.defaultBlockState();
|
||||
for (int x = x0; x < x0 + CHUNK_SIZE; x++) for (int z = z0; z < z0 + CHUNK_SIZE; z++) {
|
||||
int localX = Math.floorMod(x, CHUNK_SIZE), localZ = Math.floorMod(z, CHUNK_SIZE);
|
||||
boolean boundaryX = localX == 0, boundaryZ = localZ == 0;
|
||||
if (!boundaryX && !boundaryZ) continue;
|
||||
boolean doorway = boundaryX != boundaryZ && doorway(boundaryX ? localZ : localX, 7, 9);
|
||||
if (doorway) continue;
|
||||
for (int y = floorY + 1; y < roofY; y++) set(level, x, y, z, wall);
|
||||
}
|
||||
placeThresholdPattern(level, x0, floorY, z0, roofY, pattern, wall);
|
||||
for (RoomCorner corner : RoomCorner.values()) {
|
||||
placeCorner(level, seed, x0, floorY, z0, cellX, cellZ, deck, corner);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void placeThresholdPattern(WorldGenLevel level, int x0, int floorY, int z0,
|
||||
int roofY, int pattern, BlockState wall) {
|
||||
switch (pattern) {
|
||||
case 1 -> partitionX(level, x0 + 5, floorY, roofY, z0 + 2, z0 + 13, z0 + 8, wall, false);
|
||||
case 2 -> partitionZ(level, z0 + 10, floorY, roofY, x0 + 2, x0 + 13, x0 + 8, wall, false);
|
||||
case 3 -> {
|
||||
partitionX(level, x0 + 5, floorY, roofY, z0 + 2, z0 + 11, z0 + 7, wall, false);
|
||||
partitionZ(level, z0 + 10, floorY, roofY, x0 + 5, x0 + 13, x0 + 10, wall, false);
|
||||
}
|
||||
case 4 -> partitionX(level, x0 + 8, floorY, roofY, z0 + 2, z0 + 13, z0 + 8, wall, true);
|
||||
case 5 -> {
|
||||
for (int x : new int[]{x0 + 5, x0 + 10}) for (int z : new int[]{z0 + 5, z0 + 10}) {
|
||||
for (int y = floorY + 1; y <= Math.min(floorY + 6, roofY - 1); y++) {
|
||||
set(level, x, y, z, y % 2 == 0 ? CLAY_ORANGE : CLAY_BROWN);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (random.nextInt(3) == 0) {
|
||||
int deskX = x0 + 5 + random.nextInt(6), deskZ = z0 + 5 + random.nextInt(6);
|
||||
set(level, deskX, baseY + 1, deskZ, Blocks.OAK_SLAB.defaultBlockState());
|
||||
set(level, deskX + 1, baseY + 1, deskZ, Blocks.BOOKSHELF.defaultBlockState());
|
||||
set(level, deskX, baseY + 1, deskZ + 1, Blocks.CARPET.pick(DyeColor.BROWN).defaultBlockState());
|
||||
}
|
||||
default -> { }
|
||||
}
|
||||
}
|
||||
|
||||
private static void plains(WorldGenLevel level, int x0, int z0, Random random) {
|
||||
int baseY = 68 + random.nextInt(3);
|
||||
for (int x = x0; x < x0 + 16; x++) for (int z = z0; z < z0 + 16; z++) {
|
||||
set(level, x, baseY - 2, z, Blocks.STONE.defaultBlockState());
|
||||
set(level, x, baseY - 1, z, Blocks.DIRT.defaultBlockState());
|
||||
set(level, x, baseY, z, Blocks.GRASS_BLOCK.defaultBlockState());
|
||||
}
|
||||
if (random.nextInt(3) == 0) tree(level, x0 + 4 + random.nextInt(8), baseY + 1, z0 + 4 + random.nextInt(8));
|
||||
if (random.nextInt(7) == 0) {
|
||||
for (int x = x0 + 3; x <= x0 + 11; x++) for (int z = z0 + 3; z <= z0 + 11; z++) {
|
||||
boolean edge = x == x0 + 3 || x == x0 + 11 || z == z0 + 3 || z == z0 + 11;
|
||||
if (edge) for (int y = baseY + 1; y <= baseY + 4; y++) set(level, x, y, z, Blocks.MUD_BRICKS.defaultBlockState());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void aggregates(WorldGenLevel level, int x0, int z0, Random random) {
|
||||
int baseY = 74 + random.nextInt(10);
|
||||
platform(level, x0, z0, baseY, CONCRETE_LIGHT);
|
||||
boxRoom(level, x0 + 2, baseY + 1, z0 + 2, x0 + 12, baseY + 7, z0 + 12,
|
||||
Blocks.SMOOTH_STONE.defaultBlockState(), Blocks.OAK_PLANKS.defaultBlockState());
|
||||
if (random.nextBoolean()) {
|
||||
int upper = baseY + 20 + random.nextInt(12);
|
||||
platform(level, x0 + 4, z0 + 4, upper, CLAY_ORANGE);
|
||||
for (int y = baseY + 1; y <= upper; y++) set(level, x0 + 8, y, z0 + 8, Blocks.IRON_CHAIN.defaultBlockState());
|
||||
}
|
||||
}
|
||||
|
||||
private static void rotated(WorldGenLevel level, int x0, int z0, Random random) {
|
||||
int y0 = 70 + random.nextInt(12);
|
||||
for (int x = x0; x < x0 + 16; x++) for (int z = z0; z < z0 + 16; z++) set(level, x, y0, z, CONCRETE_LIGHT);
|
||||
boolean alongX = random.nextBoolean();
|
||||
for (int depth = 2; depth <= 13; depth++) for (int y = y0 + 1; y <= y0 + 13; y++) {
|
||||
int x = alongX ? x0 + depth : x0 + 3;
|
||||
int z = alongX ? z0 + 3 : z0 + depth;
|
||||
BlockState state = (y == y0 + 1 || y == y0 + 13 || depth == 2 || depth == 13)
|
||||
? CLAY_BROWN : Blocks.AIR.defaultBlockState();
|
||||
set(level, x, y, z, state);
|
||||
}
|
||||
// Furniture fixed to the vertical "floor" sells the rotated-room illusion without changing gravity.
|
||||
for (int offset = 4; offset <= 10; offset += 3) {
|
||||
int x = alongX ? x0 + offset : x0 + 4;
|
||||
int z = alongX ? z0 + 4 : z0 + offset;
|
||||
set(level, x, y0 + 4, z, Blocks.BOOKSHELF.defaultBlockState());
|
||||
}
|
||||
}
|
||||
|
||||
private static void placeLootLandmarks(WorldGenLevel level, int x0, int z0, Random random, Architecture architecture) {
|
||||
int y = switch (architecture) {
|
||||
case CITY -> 65;
|
||||
case OFFICES -> 73;
|
||||
case PLAINS -> 72;
|
||||
case AGGREGATES -> 83;
|
||||
case ROTATED -> 84;
|
||||
private static void placeCorner(WorldGenLevel level, long seed, int x0, int floorY, int z0,
|
||||
int cellX, int cellZ, int deck, RoomCorner corner) {
|
||||
int x = switch (corner) {
|
||||
case NORTH_WEST, SOUTH_WEST -> x0 + 2;
|
||||
case NORTH_EAST, SOUTH_EAST -> x0 + 13;
|
||||
};
|
||||
int x = x0 + 7, z = z0 + 7;
|
||||
while (!level.getBlockState(new BlockPos(x, y, z)).isAir() && y < 180) y++;
|
||||
while (!level.getBlockState(new BlockPos(x, y - 1, z)).isCollisionShapeFullBlock(level, new BlockPos(x, y - 1, z)) && y > 2) y--;
|
||||
if (random.nextInt(5) == 0) set(level, x, y, z, Blocks.CHEST.defaultBlockState());
|
||||
if (random.nextInt(17) == 0) placeBed(level, x0 + 9, y, z0 + 8);
|
||||
int z = switch (corner) {
|
||||
case NORTH_WEST, NORTH_EAST -> z0 + 2;
|
||||
case SOUTH_WEST, SOUTH_EAST -> z0 + 13;
|
||||
};
|
||||
BlockState material = CORNER_MATERIALS[BackroomLayout.cornerVariant(
|
||||
seed ^ (long) deck * 31L, BackroomLevel.LEVEL_0, cellX, cellZ, corner)];
|
||||
for (int y = floorY + 1; y <= floorY + 4; y++) set(level, x, y, z, material);
|
||||
int towardX = corner == RoomCorner.NORTH_WEST || corner == RoomCorner.SOUTH_WEST ? 1 : -1;
|
||||
int towardZ = corner == RoomCorner.NORTH_WEST || corner == RoomCorner.NORTH_EAST ? 1 : -1;
|
||||
for (int distance = 1; distance <= 2; distance++) {
|
||||
set(level, x + towardX * distance, floorY + 1, z, material);
|
||||
set(level, x, floorY + 1, z + towardZ * distance, material);
|
||||
}
|
||||
}
|
||||
|
||||
private static void buildArchives(WorldGenLevel level, long seed, int x0, int z0) {
|
||||
int macroX = Math.floorDiv(x0, 64), macroZ = Math.floorDiv(z0, 64);
|
||||
for (int deck = 0; deck < ARCHIVE_DECKS.length; deck++) {
|
||||
int floorY = ARCHIVE_DECKS[deck];
|
||||
int roofY = deck + 1 < ARCHIVE_DECKS.length ? ARCHIVE_DECKS[deck + 1]
|
||||
: BackroomLevel.LEVEL_1.ceilingY();
|
||||
if (deck > 0) fillDeck(level, seed, floorY, x0, z0, Blocks.DARK_OAK_PLANKS.defaultBlockState());
|
||||
boolean library = Math.floorMod((int) fingerprint(seed ^ deck, macroX, macroZ), 3) != 0;
|
||||
for (int x = x0; x < x0 + CHUNK_SIZE; x++) for (int z = z0; z < z0 + CHUNK_SIZE; z++) {
|
||||
int localX = Math.floorMod(x, 32), localZ = Math.floorMod(z, 32);
|
||||
boolean wallX = localX == 0, wallZ = localZ == 0;
|
||||
if (!wallX && !wallZ) continue;
|
||||
boolean open = wallX != wallZ && doorway(wallX ? localZ : localX, 14, 18);
|
||||
if (open) continue;
|
||||
for (int y = floorY + 1; y < roofY; y++) {
|
||||
BlockState state = y >= floorY + 5 && y <= floorY + 9 ? GLASS_BROWN : CONCRETE_LIGHT;
|
||||
set(level, x, y, z, state);
|
||||
}
|
||||
}
|
||||
if (library) buildLibraryRoom(level, x0, floorY, z0, roofY);
|
||||
else buildOfficeRoom(level, seed, x0, floorY, z0, roofY, deck);
|
||||
}
|
||||
}
|
||||
|
||||
private static void buildLibraryRoom(WorldGenLevel level, int x0, int floorY, int z0, int roofY) {
|
||||
for (int x = x0 + 1; x < x0 + 15; x++) for (int z = z0 + 1; z < z0 + 15; z++) {
|
||||
int roomX = Math.floorMod(x, 32), roomZ = Math.floorMod(z, 32);
|
||||
boolean shelf = (Math.floorMod(x, 8) == 3 || Math.floorMod(x, 8) == 4)
|
||||
&& roomX > 3 && roomX < 28 && (roomZ < 12 || roomZ > 20);
|
||||
if (!shelf) continue;
|
||||
for (int y = floorY + 1; y <= Math.min(floorY + 6, roofY - 1); y++) {
|
||||
set(level, x, y, z, Blocks.BOOKSHELF.defaultBlockState());
|
||||
}
|
||||
}
|
||||
if (Math.floorMod(x0 + z0 + floorY, 3) == 0) {
|
||||
for (int x = x0 + 6; x <= x0 + 9; x++) set(level, x, floorY + 1, z0 + 8,
|
||||
Blocks.OAK_SLAB.defaultBlockState());
|
||||
}
|
||||
}
|
||||
|
||||
private static void buildOfficeRoom(WorldGenLevel level, long seed, int x0, int floorY, int z0,
|
||||
int roofY, int deck) {
|
||||
boolean alongX = (fingerprint(seed, Math.floorDiv(x0, 16), Math.floorDiv(z0, 16)) + deck & 1L) == 0L;
|
||||
if (alongX) partitionX(level, x0 + 8, floorY, roofY, z0 + 1, z0 + 14, z0 + 8, CONCRETE_WHITE, true);
|
||||
else partitionZ(level, z0 + 8, floorY, roofY, x0 + 1, x0 + 14, x0 + 8, CONCRETE_WHITE, true);
|
||||
set(level, x0 + 4, floorY + 1, z0 + 4, Blocks.OAK_SLAB.defaultBlockState());
|
||||
set(level, x0 + 5, floorY + 1, z0 + 4, Blocks.BOOKSHELF.defaultBlockState());
|
||||
set(level, x0 + 11, floorY + 1, z0 + 11, Blocks.OAK_SLAB.defaultBlockState());
|
||||
}
|
||||
|
||||
private static void buildCity(WorldGenLevel level, long seed, int x0, int z0, int chunkX, int chunkZ) {
|
||||
boolean roadFree = true;
|
||||
for (int x = x0; x < x0 + CHUNK_SIZE; x++) for (int z = z0; z < z0 + CHUNK_SIZE; z++) {
|
||||
if (BackroomLayout.transitionHole(seed, BackroomLevel.LEVEL_2.ordinal(), x, z)) continue;
|
||||
boolean highway = BackroomLayout.cityHighway(x, z);
|
||||
if (highway) roadFree = false;
|
||||
BlockState surface = highway ? CONCRETE_GRAY : CONCRETE_LIGHT;
|
||||
if (highway && (Math.floorMod(x, 8) == 0 || Math.floorMod(z, 8) == 0)) surface = CONCRETE_WHITE;
|
||||
set(level, x, BackroomLevel.LEVEL_2.floorY(), z, surface);
|
||||
}
|
||||
boolean park = roadFree && BackroomLayout.cityPark(seed, chunkX, chunkZ);
|
||||
if (park) {
|
||||
buildPark(level, seed, x0, z0);
|
||||
return;
|
||||
}
|
||||
if (!roadFree) return;
|
||||
|
||||
Random random = new Random(fingerprint(seed ^ 0x514e7a2c39d8b60fL, chunkX, chunkZ));
|
||||
int floorY = BackroomLevel.LEVEL_2.floorY();
|
||||
int roofY = floorY + 24 + random.nextInt(56);
|
||||
int minX = x0 + 1, maxX = x0 + 14, minZ = z0 + 1, maxZ = z0 + 14;
|
||||
BlockState frame = random.nextBoolean() ? CONCRETE_LIGHT : Blocks.STONE_BRICKS.defaultBlockState();
|
||||
BlockState glass = random.nextBoolean() ? GLASS_CYAN : GLASS_BLACK;
|
||||
for (int y = floorY + 1; y <= roofY; y++) {
|
||||
boolean slab = y == roofY || Math.floorMod(y - floorY, 7) == 0;
|
||||
for (int x = minX; x <= maxX; x++) for (int z = minZ; z <= maxZ; z++) {
|
||||
boolean edge = x == minX || x == maxX || z == minZ || z == maxZ;
|
||||
boolean entrance = y <= floorY + 4 && (((x == minX || x == maxX) && doorway(z - z0, 7, 9))
|
||||
|| ((z == minZ || z == maxZ) && doorway(x - x0, 7, 9)));
|
||||
if (entrance) continue;
|
||||
if (slab) set(level, x, y, z, frame);
|
||||
else if (edge) set(level, x, y, z, Math.floorMod(y - floorY, 7) <= 2 ? frame : glass);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void buildPark(WorldGenLevel level, long seed, int x0, int z0) {
|
||||
int floorY = BackroomLevel.LEVEL_2.floorY();
|
||||
for (int x = x0 + 1; x < x0 + 15; x++) for (int z = z0 + 1; z < z0 + 15; z++) {
|
||||
if (BackroomLayout.transitionHole(seed, BackroomLevel.LEVEL_2.ordinal(), x, z)) continue;
|
||||
BlockState surface = x == x0 + 8 || z == z0 + 8
|
||||
? Blocks.SMOOTH_STONE.defaultBlockState() : Blocks.GRASS_BLOCK.defaultBlockState();
|
||||
set(level, x, floorY, z, surface);
|
||||
}
|
||||
Random random = new Random(fingerprint(seed ^ 0x1974acb65d203e8fL,
|
||||
Math.floorDiv(x0, 16), Math.floorDiv(z0, 16)));
|
||||
int trees = 1 + random.nextInt(2);
|
||||
for (int index = 0; index < trees; index++) {
|
||||
tree(level, x0 + 4 + random.nextInt(8), floorY + 1, z0 + 4 + random.nextInt(8));
|
||||
}
|
||||
}
|
||||
|
||||
private static void buildUtilities(WorldGenLevel level, long seed, int x0, int z0) {
|
||||
for (int deck = 0; deck < UTILITY_DECKS.length; deck++) {
|
||||
int floorY = UTILITY_DECKS[deck];
|
||||
int roofY = deck + 1 < UTILITY_DECKS.length ? UTILITY_DECKS[deck + 1]
|
||||
: BackroomLevel.LEVEL_3.ceilingY();
|
||||
if (deck > 0) fillDeck(level, seed, floorY, x0, z0, Blocks.POLISHED_ANDESITE.defaultBlockState());
|
||||
for (int x = x0; x < x0 + CHUNK_SIZE; x++) for (int z = z0; z < z0 + CHUNK_SIZE; z++) {
|
||||
int localX = Math.floorMod(x, 24), localZ = Math.floorMod(z, 24);
|
||||
boolean wallX = localX == 0, wallZ = localZ == 0;
|
||||
if (wallX || wallZ) {
|
||||
boolean open = wallX != wallZ && doorway(wallX ? localZ : localX, 10, 14);
|
||||
if (!open) for (int y = floorY + 1; y < roofY; y++) {
|
||||
BlockState state = y == floorY + 8 ? Blocks.IRON_BARS.defaultBlockState()
|
||||
: (deck + Math.floorDiv(x + z, 24)) % 2 == 0
|
||||
? Blocks.BRICKS.defaultBlockState() : Blocks.MUD_BRICKS.defaultBlockState();
|
||||
set(level, x, y, z, state);
|
||||
}
|
||||
}
|
||||
boolean overheadGrid = floorY + 12 < roofY
|
||||
&& ((localX == 4 && localZ >= 7 && localZ <= 17)
|
||||
|| (localZ == 4 && localX >= 7 && localX <= 17));
|
||||
if (overheadGrid) set(level, x, floorY + 12, z, Blocks.IRON_BARS.defaultBlockState());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void buildGlitchMaze(WorldGenLevel level, long seed, int x0, int z0) {
|
||||
for (int x = x0; x < x0 + CHUNK_SIZE; x++) for (int z = z0; z < z0 + CHUNK_SIZE; z++) {
|
||||
if (!BackroomLayout.deckHole(seed, BackroomLayout.MAZE_FLOOR_Y, x, z)) {
|
||||
set(level, x, BackroomLayout.MAZE_FLOOR_Y, z, Blocks.STONE_BRICKS.defaultBlockState());
|
||||
}
|
||||
if (!BackroomLayout.mazeRoofHole(seed, x, z)) {
|
||||
set(level, x, BackroomLayout.MAZE_ROOF_Y, z, Blocks.MUD_BRICKS.defaultBlockState());
|
||||
}
|
||||
for (int y = BackroomLevel.LEVEL_4.floorY() + 1; y < BackroomLevel.LEVEL_4.ceilingY(); y++) {
|
||||
if (y >= BackroomLayout.MAZE_FLOOR_Y && y <= BackroomLayout.MAZE_ROOF_Y) continue;
|
||||
boolean edgeX = Math.floorMod(x, 8) == 0;
|
||||
boolean edgeY = Math.floorMod(y - BackroomLevel.LEVEL_4.floorY(), 8) == 0;
|
||||
boolean edgeZ = Math.floorMod(z, 8) == 0;
|
||||
if ((edgeX && edgeY) || (edgeX && edgeZ) || (edgeY && edgeZ)) {
|
||||
set(level, x, y, z, glitchMaterial(seed, x, y, z));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (int x = x0; x < x0 + CHUNK_SIZE; x++) for (int z = z0; z < z0 + CHUNK_SIZE; z++) {
|
||||
boolean wallX = Math.floorMod(x, CHUNK_SIZE) == 0;
|
||||
boolean wallZ = Math.floorMod(z, CHUNK_SIZE) == 0;
|
||||
if (!wallX && !wallZ) continue;
|
||||
boolean open = false;
|
||||
if (wallX != wallZ) {
|
||||
if (wallX) {
|
||||
int rightX = Math.floorDiv(x, CHUNK_SIZE), cellZ = Math.floorDiv(z, CHUNK_SIZE);
|
||||
boolean connected = BackroomLayout.mazeConnected(seed, rightX - 1, cellZ, rightX, cellZ);
|
||||
boolean merged = BackroomLayout.mergedMazeRooms(seed, rightX - 1, cellZ, rightX, cellZ);
|
||||
open = merged || connected && doorway(Math.floorMod(z, CHUNK_SIZE), 7, 9);
|
||||
} else {
|
||||
int cellX = Math.floorDiv(x, CHUNK_SIZE), southZ = Math.floorDiv(z, CHUNK_SIZE);
|
||||
boolean connected = BackroomLayout.mazeConnected(seed, cellX, southZ - 1, cellX, southZ);
|
||||
boolean merged = BackroomLayout.mergedMazeRooms(seed, cellX, southZ - 1, cellX, southZ);
|
||||
open = merged || connected && doorway(Math.floorMod(x, CHUNK_SIZE), 7, 9);
|
||||
}
|
||||
}
|
||||
if (open) continue;
|
||||
for (int y = BackroomLayout.MAZE_FLOOR_Y + 1; y < BackroomLayout.MAZE_ROOF_Y; y++) {
|
||||
BlockState state = y >= BackroomLayout.MAZE_FLOOR_Y + 4
|
||||
&& y <= BackroomLayout.MAZE_FLOOR_Y + 6 ? GLASS_BLACK : glitchMaterial(seed, x, y, z);
|
||||
set(level, x, y, z, state);
|
||||
}
|
||||
}
|
||||
|
||||
int cellX = Math.floorDiv(x0, CHUNK_SIZE), cellZ = Math.floorDiv(z0, CHUNK_SIZE);
|
||||
int pattern = BackroomLayout.roomPattern(seed, BackroomLevel.LEVEL_4, cellX, cellZ, 0);
|
||||
BlockState wall = GLITCH_MATERIALS[Math.floorMod(pattern, GLITCH_MATERIALS.length)];
|
||||
placeThresholdPattern(level, x0, BackroomLayout.MAZE_FLOOR_Y, z0,
|
||||
BackroomLayout.MAZE_ROOF_Y, pattern, wall);
|
||||
}
|
||||
|
||||
private static BlockState glitchMaterial(long seed, int x, int y, int z) {
|
||||
int index = Math.floorMod((int) fingerprint(seed ^ y, Math.floorDiv(x, 8), Math.floorDiv(z, 8)),
|
||||
GLITCH_MATERIALS.length);
|
||||
return GLITCH_MATERIALS[index];
|
||||
}
|
||||
|
||||
private static void fillDeck(WorldGenLevel level, long seed, int floorY, int x0, int z0,
|
||||
BlockState state) {
|
||||
for (int x = x0; x < x0 + CHUNK_SIZE; x++) for (int z = z0; z < z0 + CHUNK_SIZE; z++) {
|
||||
if (!BackroomLayout.deckHole(seed, floorY, x, z)) set(level, x, floorY, z, state);
|
||||
}
|
||||
}
|
||||
|
||||
private static void partitionX(WorldGenLevel level, int x, int floorY, int roofY,
|
||||
int minZ, int maxZ, int doorZ, BlockState wall, boolean window) {
|
||||
for (int z = minZ; z <= maxZ; z++) {
|
||||
if (Math.abs(z - doorZ) <= 1) continue;
|
||||
for (int y = floorY + 1; y < roofY; y++) {
|
||||
set(level, x, y, z, window && y >= floorY + 5 && y <= floorY + 8 ? GLASS_BROWN : wall);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void partitionZ(WorldGenLevel level, int z, int floorY, int roofY,
|
||||
int minX, int maxX, int doorX, BlockState wall, boolean window) {
|
||||
for (int x = minX; x <= maxX; x++) {
|
||||
if (Math.abs(x - doorX) <= 1) continue;
|
||||
for (int y = floorY + 1; y < roofY; y++) {
|
||||
set(level, x, y, z, window && y >= floorY + 5 && y <= floorY + 8 ? GLASS_BROWN : wall);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean doorway(int coordinate, int minimum, int maximum) {
|
||||
return coordinate >= minimum && coordinate <= maximum;
|
||||
}
|
||||
|
||||
private static void placeLootLandmarks(WorldGenLevel level, long seed, int x0, int z0,
|
||||
int chunkX, int chunkZ) {
|
||||
for (BackroomLevel zone : BackroomLevel.values()) {
|
||||
if (BackroomLayout.lootChestChunk(seed, zone, chunkX, chunkZ)) {
|
||||
placeLootLandmark(level, zone, x0 + 7, z0 + 7);
|
||||
}
|
||||
}
|
||||
for (BackroomLevel zone : BackroomLevel.values()) {
|
||||
if (!BackroomLayout.exitBedChunk(seed, zone, chunkX, chunkZ)) continue;
|
||||
int floorY = BackroomLayout.exitBedFloor(zone);
|
||||
int x = x0 + 7, z = z0 + 8;
|
||||
set(level, x, floorY, z, floorMaterial(zone));
|
||||
set(level, x + 1, floorY, z, floorMaterial(zone));
|
||||
for (int dx = 0; dx <= 1; dx++) for (int dy = 1; dy <= 2; dy++) {
|
||||
set(level, x + dx, floorY + dy, z, Blocks.AIR.defaultBlockState());
|
||||
}
|
||||
placeBed(level, x, floorY + 1, z);
|
||||
}
|
||||
}
|
||||
|
||||
private static void placeLootLandmark(WorldGenLevel level, BackroomLevel zone, int x, int z) {
|
||||
int floorY = BackroomLayout.exitBedFloor(zone);
|
||||
for (int dx = -1; dx <= 1; dx++) for (int dz = -1; dz <= 1; dz++) {
|
||||
set(level, x + dx, floorY, z + dz, floorMaterial(zone));
|
||||
for (int dy = 1; dy <= 2; dy++) set(level, x + dx, floorY + dy, z + dz,
|
||||
Blocks.AIR.defaultBlockState());
|
||||
}
|
||||
set(level, x, floorY + 1, z, Blocks.CHEST.defaultBlockState());
|
||||
}
|
||||
|
||||
private static void placeRareAnomaly(WorldGenLevel level, int x0, int z0, Random random) {
|
||||
if (random.nextInt(96) == 0) {
|
||||
Block anomaly = BackroomMaterialApi.anomalyMaterial();
|
||||
for (int i = 0; i < 5; i++) set(level, x0 + 5 + random.nextInt(6), 66 + random.nextInt(20),
|
||||
z0 + 5 + random.nextInt(6), anomaly.defaultBlockState());
|
||||
BackroomLevel zone = BackroomLevel.values()[random.nextInt(BackroomLevel.values().length)];
|
||||
int floorY = BackroomLayout.exitBedFloor(zone);
|
||||
for (int index = 0; index < 5; index++) {
|
||||
set(level, x0 + 5 + random.nextInt(6), floorY + 1 + random.nextInt(4),
|
||||
z0 + 5 + random.nextInt(6), anomaly.defaultBlockState());
|
||||
}
|
||||
}
|
||||
if (random.nextInt(4_096) == 0
|
||||
&& withinPlayableArea(x0 + 7, z0 + 7) && withinPlayableArea(x0 + 9, z0 + 9)) {
|
||||
for (int dx = -1; dx <= 1; dx++) for (int dy = -1; dy <= 1; dy++) for (int dz = -1; dz <= 1; dz++) {
|
||||
set(level, x0 + 8 + dx, 40 + dy, z0 + 8 + dz, Blocks.SMOOTH_STONE.defaultBlockState());
|
||||
set(level, x0 + 8 + dx, -40 + dy, z0 + 8 + dz, Blocks.SMOOTH_STONE.defaultBlockState());
|
||||
}
|
||||
set(level, x0 + 8, 40, z0 + 8, Blocks.DIAMOND_BLOCK.defaultBlockState());
|
||||
set(level, x0 + 8, -40, z0 + 8, Blocks.DIAMOND_BLOCK.defaultBlockState());
|
||||
}
|
||||
}
|
||||
|
||||
private static void placeBed(WorldGenLevel level, int x, int y, int z) {
|
||||
if (!withinPlayableArea(x, z) || !withinPlayableArea(x + 1, z)) return;
|
||||
Block bed = Blocks.BED.pick(DyeColor.WHITE);
|
||||
BlockState foot = bed.defaultBlockState().setValue(BedBlock.FACING, Direction.EAST).setValue(BedBlock.PART, BedPart.FOOT);
|
||||
BlockState foot = bed.defaultBlockState().setValue(BedBlock.FACING, Direction.EAST)
|
||||
.setValue(BedBlock.PART, BedPart.FOOT);
|
||||
BlockState head = foot.setValue(BedBlock.PART, BedPart.HEAD);
|
||||
set(level, x, y, z, foot);
|
||||
set(level, x + 1, y, z, head);
|
||||
}
|
||||
|
||||
private static void platform(WorldGenLevel level, int x0, int z0, int y, BlockState state) {
|
||||
for (int x = x0; x < x0 + 16; x++) for (int z = z0; z < z0 + 16; z++) set(level, x, y, z, state);
|
||||
}
|
||||
|
||||
private static void boxRoom(WorldGenLevel level, int minX, int minY, int minZ,
|
||||
int maxX, int maxY, int maxZ, BlockState shell, BlockState floor) {
|
||||
for (int x = minX; x <= maxX; x++) for (int y = minY; y <= maxY; y++) for (int z = minZ; z <= maxZ; z++) {
|
||||
boolean boundary = x == minX || x == maxX || y == minY || y == maxY || z == minZ || z == maxZ;
|
||||
if (boundary) set(level, x, y, z, y == minY ? floor : shell);
|
||||
}
|
||||
}
|
||||
|
||||
private static void tree(WorldGenLevel level, int x, int y, int z) {
|
||||
for (int dy = 0; dy < 5; dy++) set(level, x, y + dy, z, Blocks.OAK_LOG.defaultBlockState());
|
||||
for (int dx = -2; dx <= 2; dx++) for (int dz = -2; dz <= 2; dz++) for (int dy = 3; dy <= 6; dy++) {
|
||||
if (Math.abs(dx) + Math.abs(dz) + Math.abs(dy - 5) <= 5) set(level, x + dx, y + dy, z + dz, LEAVES);
|
||||
for (int dy = 0; dy < 6; dy++) set(level, x, y + dy, z, Blocks.OAK_LOG.defaultBlockState());
|
||||
for (int dx = -2; dx <= 2; dx++) for (int dz = -2; dz <= 2; dz++) for (int dy = 4; dy <= 7; dy++) {
|
||||
if (Math.abs(dx) + Math.abs(dz) + Math.abs(dy - 6) <= 5) set(level, x + dx, y + dy, z + dz, LEAVES);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -238,6 +495,4 @@ public final class BackroomArchitectureFeature extends Feature<NoneFeatureConfig
|
||||
value *= 0x94D049BB133111EBL;
|
||||
return value ^ value >>> 31;
|
||||
}
|
||||
|
||||
public enum Architecture { CITY, OFFICES, PLAINS, AGGREGATES, ROTATED }
|
||||
}
|
||||
|
||||
+196
@@ -0,0 +1,196 @@
|
||||
package fr.koka99cab.sanctuary26.sanctuary.worldgen.feature;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Pure deterministic layout rules shared by Backroom generation, arrivals and executable tests.
|
||||
* Level numbers run from the top down so a fall always moves toward the next numbered level.
|
||||
*/
|
||||
public final class BackroomLayout {
|
||||
public static final int WORLD_MIN_Y = -64;
|
||||
public static final int WORLD_MAX_Y_EXCLUSIVE = 384;
|
||||
public static final int ENTRY_FLOOR_Y = 367;
|
||||
public static final int ROOM_SIZE = 16;
|
||||
public static final int MAZE_FLOOR_Y = 8;
|
||||
public static final int MAZE_ROOF_Y = 18;
|
||||
public static final int EXIT_BED_CELL_SIZE_CHUNKS = 3;
|
||||
public static final int LOOT_CHEST_CELL_SIZE_CHUNKS = 3;
|
||||
private static final int HOLE_CELL_SIZE = 64;
|
||||
|
||||
private BackroomLayout() {}
|
||||
|
||||
public static BackroomLevel levelAt(int y) {
|
||||
for (BackroomLevel level : BackroomLevel.values()) {
|
||||
if (y >= level.floorY && y <= level.ceilingY) return level;
|
||||
}
|
||||
throw new IllegalArgumentException("Y outside the Backroom: " + y);
|
||||
}
|
||||
|
||||
/** A matching opening through an upper floor and the lower level's ceiling. */
|
||||
public static boolean transitionHole(long seed, int transition, int x, int z) {
|
||||
if (transition < 0 || transition >= BackroomLevel.values().length - 1) return false;
|
||||
return authoredHole(seed ^ 0x2f53a71d4e6c890bL, transition, x, z, 1);
|
||||
}
|
||||
|
||||
/** Sparse holes between decks create vertical routes without turning every room into a shaft. */
|
||||
public static boolean deckHole(long seed, int floorY, int x, int z) {
|
||||
return authoredHole(seed ^ 0x74c8e6219b0d35afL, floorY, x, z, 1);
|
||||
}
|
||||
|
||||
/** The last floor deliberately has occasional exits into the actual void. */
|
||||
public static boolean voidHole(long seed, int x, int z) {
|
||||
return authoredHole(seed ^ 0x56de9c183af247b1L, 5, x, z, 2);
|
||||
}
|
||||
|
||||
/** Openings in the maze roof let a player who landed on it enter the generated rooms. */
|
||||
public static boolean mazeRoofHole(long seed, int x, int z) {
|
||||
return authoredHole(seed ^ 0x6b8f1de425a390c7L, 6, x, z, 1);
|
||||
}
|
||||
|
||||
public static int roomPattern(long seed, BackroomLevel level, int cellX, int cellZ, int deck) {
|
||||
return Math.floorMod((int) mix(seed ^ 0x3c79ac492ba7b653L,
|
||||
cellX + level.ordinal() * 1_009, cellZ + deck * 4_099), 6);
|
||||
}
|
||||
|
||||
/** Each of the four authored corner directions independently reaches five material variants. */
|
||||
public static int cornerVariant(long seed, BackroomLevel level, int cellX, int cellZ,
|
||||
RoomCorner corner) {
|
||||
Objects.requireNonNull(level, "level");
|
||||
Objects.requireNonNull(corner, "corner");
|
||||
return Math.floorMod((int) mix(seed ^ 0x1c69b3f74ac4ae35L,
|
||||
cellX + corner.ordinal() * 8_191, cellZ + level.ordinal() * 16_381), 5);
|
||||
}
|
||||
|
||||
/**
|
||||
* Symmetric room-edge rule. Every maze cell has a parent one step nearer the origin, which makes
|
||||
* the complete graph connected; extra deterministic edges introduce loops and glitches.
|
||||
*/
|
||||
public static boolean mazeConnected(long seed, int ax, int az, int bx, int bz) {
|
||||
if (Math.abs(ax - bx) + Math.abs(az - bz) != 1) return false;
|
||||
if (isParent(ax, az, bx, bz) || isParent(bx, bz, ax, az)) return true;
|
||||
int minX = Math.min(ax, bx), minZ = Math.min(az, bz);
|
||||
int edgeAxis = ax == bx ? 1 : 0;
|
||||
return Math.floorMod((int) mix(seed ^ 0x5d42c713e9a86bf0L,
|
||||
minX * 2 + edgeAxis, minZ * 2 + 1 - edgeAxis), 5) == 0;
|
||||
}
|
||||
|
||||
/** Some connected cells completely lose their partition and become rooms at a larger scale. */
|
||||
public static boolean mergedMazeRooms(long seed, int ax, int az, int bx, int bz) {
|
||||
if (!mazeConnected(seed, ax, az, bx, bz)) return false;
|
||||
int minX = Math.min(ax, bx), minZ = Math.min(az, bz);
|
||||
int edgeAxis = ax == bx ? 1 : 0;
|
||||
return Math.floorMod((int) mix(seed ^ 0x0b4e8f619c32d75aL,
|
||||
minX * 2 + edgeAxis, minZ * 2 + 1 - edgeAxis), 11) == 0;
|
||||
}
|
||||
|
||||
/** Infinite orthogonal highways, nine blocks wide and 96 blocks apart. */
|
||||
public static boolean cityHighway(int x, int z) {
|
||||
return centeredModulo(x - 8, 96) <= 4 || centeredModulo(z - 8, 96) <= 4;
|
||||
}
|
||||
|
||||
public static boolean cityPark(long seed, int chunkX, int chunkZ) {
|
||||
return Math.floorMod((int) mix(seed ^ 0x43c2a1769f0e85bdL, chunkX, chunkZ), 11) == 0;
|
||||
}
|
||||
|
||||
/** One easy-to-find loot landmark per three-by-three chunk cell and per vertical level. */
|
||||
public static boolean lootChestChunk(long seed, BackroomLevel level, int chunkX, int chunkZ) {
|
||||
Objects.requireNonNull(level, "level");
|
||||
int cellX = Math.floorDiv(chunkX, LOOT_CHEST_CELL_SIZE_CHUNKS);
|
||||
int cellZ = Math.floorDiv(chunkZ, LOOT_CHEST_CELL_SIZE_CHUNKS);
|
||||
long hash = mix(seed ^ 0x354c6f3a82d9e107L ^ (long) level.ordinal() * 0x9e3779b97f4a7c15L,
|
||||
cellX, cellZ);
|
||||
int targetX = cellX * LOOT_CHEST_CELL_SIZE_CHUNKS
|
||||
+ Math.floorMod((int) hash, LOOT_CHEST_CELL_SIZE_CHUNKS);
|
||||
int targetZ = cellZ * LOOT_CHEST_CELL_SIZE_CHUNKS
|
||||
+ Math.floorMod((int) (hash >>> 32), LOOT_CHEST_CELL_SIZE_CHUNKS);
|
||||
return chunkX == targetX && chunkZ == targetZ;
|
||||
}
|
||||
|
||||
/** One deterministic exit-bed chunk per three-by-three grid cell and per vertical level. */
|
||||
public static boolean exitBedChunk(long seed, BackroomLevel level, int chunkX, int chunkZ) {
|
||||
Objects.requireNonNull(level, "level");
|
||||
int cellX = Math.floorDiv(chunkX, EXIT_BED_CELL_SIZE_CHUNKS);
|
||||
int cellZ = Math.floorDiv(chunkZ, EXIT_BED_CELL_SIZE_CHUNKS);
|
||||
long hash = mix(seed ^ 0x6ac690c5d07f12e9L ^ (long) level.ordinal() * 0x9e3779b97f4a7c15L,
|
||||
cellX, cellZ);
|
||||
int targetX = cellX * EXIT_BED_CELL_SIZE_CHUNKS
|
||||
+ Math.floorMod((int) hash, EXIT_BED_CELL_SIZE_CHUNKS);
|
||||
int targetZ = cellZ * EXIT_BED_CELL_SIZE_CHUNKS
|
||||
+ Math.floorMod((int) (hash >>> 32), EXIT_BED_CELL_SIZE_CHUNKS);
|
||||
return chunkX == targetX && chunkZ == targetZ;
|
||||
}
|
||||
|
||||
/** A traversable authored floor used for exit beds in each of the five levels. */
|
||||
public static int exitBedFloor(BackroomLevel level) {
|
||||
return switch (Objects.requireNonNull(level, "level")) {
|
||||
case LEVEL_0 -> 349;
|
||||
case LEVEL_1 -> 250;
|
||||
case LEVEL_2 -> 117;
|
||||
case LEVEL_3 -> 64;
|
||||
case LEVEL_4 -> MAZE_FLOOR_Y;
|
||||
};
|
||||
}
|
||||
|
||||
private static boolean isParent(int childX, int childZ, int parentX, int parentZ) {
|
||||
if (childX == 0 && childZ == 0) return false;
|
||||
int expectedX = childX, expectedZ = childZ;
|
||||
if (Math.abs(childX) >= Math.abs(childZ) && childX != 0) expectedX -= Integer.signum(childX);
|
||||
else expectedZ -= Integer.signum(childZ);
|
||||
return expectedX == parentX && expectedZ == parentZ;
|
||||
}
|
||||
|
||||
private static boolean authoredHole(long seed, int salt, int x, int z, int radius) {
|
||||
int cellX = Math.floorDiv(x, HOLE_CELL_SIZE);
|
||||
int cellZ = Math.floorDiv(z, HOLE_CELL_SIZE);
|
||||
long hash = mix(seed ^ (long) salt * 0x9e3779b97f4a7c15L, cellX, cellZ);
|
||||
int centerX = cellX * HOLE_CELL_SIZE + 8 + Math.floorMod((int) hash, HOLE_CELL_SIZE - 16);
|
||||
int centerZ = cellZ * HOLE_CELL_SIZE + 8
|
||||
+ Math.floorMod((int) (hash >>> 32), HOLE_CELL_SIZE - 16);
|
||||
return Math.abs(x - centerX) <= radius && Math.abs(z - centerZ) <= radius;
|
||||
}
|
||||
|
||||
private static int centeredModulo(int value, int period) {
|
||||
int wrapped = Math.floorMod(value + period / 2, period) - period / 2;
|
||||
return Math.abs(wrapped);
|
||||
}
|
||||
|
||||
private static long mix(long seed, int x, int z) {
|
||||
long value = seed ^ (long) x * 0x9E3779B97F4A7C15L ^ (long) z * 0xC2B2AE3D27D4EB4FL;
|
||||
value ^= value >>> 30;
|
||||
value *= 0xBF58476D1CE4E5B9L;
|
||||
value ^= value >>> 27;
|
||||
value *= 0x94D049BB133111EBL;
|
||||
return value ^ value >>> 31;
|
||||
}
|
||||
|
||||
public enum BackroomLevel {
|
||||
LEVEL_0(295, 383),
|
||||
LEVEL_1(206, 294),
|
||||
LEVEL_2(117, 205),
|
||||
LEVEL_3(28, 116),
|
||||
LEVEL_4(-64, 27);
|
||||
|
||||
private final int floorY;
|
||||
private final int ceilingY;
|
||||
|
||||
BackroomLevel(int floorY, int ceilingY) {
|
||||
this.floorY = floorY;
|
||||
this.ceilingY = ceilingY;
|
||||
}
|
||||
|
||||
public int floorY() {
|
||||
return this.floorY;
|
||||
}
|
||||
|
||||
public int ceilingY() {
|
||||
return this.ceilingY;
|
||||
}
|
||||
}
|
||||
|
||||
public enum RoomCorner {
|
||||
NORTH_WEST,
|
||||
NORTH_EAST,
|
||||
SOUTH_WEST,
|
||||
SOUTH_EAST
|
||||
}
|
||||
}
|
||||
@@ -96,6 +96,7 @@
|
||||
"message.sanctuary.quest_request.details.reward_legacy": "Its author must edit this legacy request and deposit the reward.",
|
||||
"message.sanctuary.quest_request.details.progress": "Progress: %s/%s.",
|
||||
"message.sanctuary.quest_request.details.completed": "Request completed · reward dispatched.",
|
||||
"message.sanctuary.quest_request.details.completed_by": "Request completed by %s · reward dispatched.",
|
||||
"message.sanctuary.quest_request.accept": "[ACCEPT QUEST]",
|
||||
"message.sanctuary.quest_request.accept_hover": "Click to confirm after reading every detail.",
|
||||
"message.sanctuary.quest_request.accept_prompt": "Read the details, then confirm in the acceptance screen.",
|
||||
@@ -462,6 +463,7 @@
|
||||
"sign.sanctuary.quest_request.available": "AVAILABLE",
|
||||
"sign.sanctuary.quest_request.claimed": "TAKEN",
|
||||
"sign.sanctuary.quest_request.completed": "COMPLETED",
|
||||
"sign.sanctuary.quest_request.completed_by": "Completed by: %s",
|
||||
"sign.sanctuary.quest_request.details": "Click: details",
|
||||
"message.sanctuary.companion.active_locked": "Unlock the active companion power in Progression for %s XP levels.",
|
||||
"message.sanctuary.shop.delivery_box_registered": "Mailbox registered at %s, %s, %s.",
|
||||
@@ -515,6 +517,7 @@
|
||||
"item.sanctuary.mojang_classic_cape": "Mojang Classic Cape",
|
||||
"item.sanctuary.blue_scroll_cape": "Blue Scroll Cape",
|
||||
"item.sanctuary.white_cape": "White Cape",
|
||||
"tooltip.sanctuary.chicken_armor": "Can also equip and protect an adult chicken.",
|
||||
"dimension.sanctuary.alpha": "Alpha / Indev Island",
|
||||
"dimension.sanctuary.backroom": "Backroom",
|
||||
"message.sanctuary.backroom.unavailable": "The Backroom is unavailable in this world.",
|
||||
@@ -522,10 +525,21 @@
|
||||
"message.sanctuary.backroom.registry_busy": "The Backroom registry is settling too many interrupted transfers. Try again later.",
|
||||
"message.sanctuary.backroom.mailbox_missing": "Load and register your Mailbox before entering the Backroom.",
|
||||
"message.sanctuary.backroom.mailbox_full": "Your Mailbox has no room for all your gear, companion egg and cape.",
|
||||
"message.sanctuary.backroom.entered": "You slip through the bed with no equipment…",
|
||||
"message.sanctuary.backroom.entered": "You slip through the bed with no equipment… A torch marks your arrival point.",
|
||||
"message.sanctuary.backroom.bed_entry_only": "Only a bed can take you into the Backroom.",
|
||||
"message.sanctuary.backroom.exited": "The bed returns you to your spawn point with your findings.",
|
||||
"message.sanctuary.backroom.exited_with_losses": "The bed returns you with your findings and settles the recovered lost items.",
|
||||
"screen.sanctuary.atlas.landmark.boss": "Defeated boss",
|
||||
"message.sanctuary.notch_defeated": "Notch Titan has been defeated. The victory is now recorded in this world."
|
||||
"message.sanctuary.notch_defeated": "Notch Titan has been defeated. The victory is now recorded in this world.",
|
||||
"metric.sanctuary.ranking.play_time": "Play time",
|
||||
"metric.sanctuary.ranking.mined": "%s mined",
|
||||
"metric.sanctuary.ranking.crafted": "%s crafted",
|
||||
"metric.sanctuary.ranking.used": "%s used",
|
||||
"metric.sanctuary.ranking.broken": "%s broken",
|
||||
"metric.sanctuary.ranking.picked_up": "%s picked up",
|
||||
"metric.sanctuary.ranking.dropped": "%s dropped",
|
||||
"metric.sanctuary.ranking.killed": "Killed: %s",
|
||||
"metric.sanctuary.ranking.killed_by": "Killed by: %s",
|
||||
"metric.sanctuary.ranking.custom": "Custom statistic",
|
||||
"metric.sanctuary.ranking.unknown": "Unknown statistic"
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user