Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
21845bd736 | ||
|
|
4a6f63c52d | ||
|
|
6166d0aa9e | ||
|
|
064701f210 | ||
|
|
1eaa976ecf | ||
|
|
8c0020c774 | ||
|
|
289697becf | ||
|
|
e38f6cbdbe | ||
|
|
2090380712 |
@@ -20,9 +20,12 @@ La [vision complète](docs/vision.md) conserve les intentions ; le
|
||||
L’économie, les claims et les dimensions décrits dans la vision restent
|
||||
à implémenter.
|
||||
|
||||
La version **beta.110** réunit les développements jusqu’à l’atelier d’argile,
|
||||
aux œufs et à la neige saisonnière. Le [suivi de livraison](docs/clay-workshop-integration-beta110.md)
|
||||
précise les vérifications, la publication et la synchronisation de l’instance.
|
||||
La version **beta.114** ajoute une fuite chaotique aux animaux volants portés :
|
||||
un coup les fait décoller avec leur porteur, dans une direction aléatoire,
|
||||
pendant deux à trois secondes. Le [ticket](docs/carried-flight-panic-beta114.md)
|
||||
décrit le comportement et ses vérifications.
|
||||
La [livraison précédente](docs/clay-workshop-integration-beta110.md) réunit déjà
|
||||
l’atelier, les œufs et la neige saisonnière dans l’instance **Sanctuary Beta**.
|
||||
Les [synchronisations antérieures](docs/git-sync-beta092.md) restent documentées.
|
||||
|
||||
Les [archives pour le site](archives/web/README.md) conservent la maquette
|
||||
@@ -31,13 +34,45 @@ La [carte native beta.005](docs/atlas-beta005.md) prend le relais après
|
||||
l’étude de Xaero et des alternatives libres.
|
||||
|
||||
|
||||
## beta.114 — Animaux portés et fuite en vol
|
||||
|
||||
Un animal volant porté entraîne son porteur après un coup. La trajectoire
|
||||
aléatoire prend brièvement le dessus sur les commandes ; déposer l’animal
|
||||
interrompt le vol. [Contrat et vérifications](docs/carried-flight-panic-beta114.md).
|
||||
|
||||
## beta.113 — Sculptures d’argile et alignement
|
||||
|
||||
Pose contre le bord visé, orientation selon le regard et coordonnées entières
|
||||
pour chaque voxel, y compris les modèles de largeur impaire. L’accroche est
|
||||
conservée à la reconnexion. [Contrat et vérifications](docs/clay-sculpture-grid-beta113.md) ·
|
||||
[Pack normal](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.113/Sanctuary-beta.113.mrpack) ·
|
||||
[Pack de test](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.113/Sanctuary-Test-beta.113.mrpack).
|
||||
|
||||
## beta.112 — Atelier d’argile et hotbar
|
||||
|
||||
Neuf cases de hotbar et deux emplacements de fabrication, dans un cadre fixe.
|
||||
Les transferts restent dans la hotbar ; une barre pleine ne consomme pas d’argile.
|
||||
[Contrat et vérifications](docs/clay-workshop-hotbar-beta112.md) ·
|
||||
[Pack normal](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.112/Sanctuary-beta.112.mrpack) ·
|
||||
[Pack de test](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.112/Sanctuary-Test-beta.112.mrpack).
|
||||
|
||||
## beta.111 — Atelier d’argile et pose orientée
|
||||
|
||||
Cadre nine-slice, aperçu, fabrication et inventaire dans un menu commun.
|
||||
Les statuaires s’orientent selon le regard horizontal à chaque pose ; les
|
||||
anciens modèles gardent leur apparence. [Contrat et vérifications](docs/clay-workshop-ui-beta111.md).
|
||||
[Pack normal](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.111/Sanctuary-beta.111.mrpack) ·
|
||||
[Pack de test](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.111/Sanctuary-Test-beta.111.mrpack) ·
|
||||
[Release](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.111).
|
||||
|
||||
## beta.110 — Atelier d’argile et neige saisonnière réunis
|
||||
|
||||
Le Clay Workshop / atelier d’argile, les statuaires et les modèles 3D rejoignent
|
||||
les œufs, l’accumulation et la fonte saisonnière dans un seul pack.
|
||||
[Contrat et vérifications](docs/clay-workshop-integration-beta110.md).
|
||||
[Pack normal](build/Sanctuary-beta.110.mrpack) ·
|
||||
[Pack de test](build/Sanctuary-Test-beta.110.mrpack).
|
||||
[Pack normal](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.110/Sanctuary-beta.110.mrpack) ·
|
||||
[Pack de test](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.110/Sanctuary-Test-beta.110.mrpack) ·
|
||||
[Release et installation Prism](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.110).
|
||||
|
||||
## beta.109 — Fonte saisonnière
|
||||
|
||||
@@ -2051,7 +2086,7 @@ Depuis beta.090, la cible est **Minecraft 26.3 finale**, publiée le 15 septembr
|
||||
| Fabric API | 0.160.5+26.3 |
|
||||
| Fabric Loom | 1.17.20 |
|
||||
| Gradle Wrapper | 9.5.1, distribution vérifiée par SHA-256 |
|
||||
| Sanctuary / pack | beta.101 |
|
||||
| Sanctuary / pack | beta.112 |
|
||||
|
||||
Java 25 et Python 3.11 ou plus récent sont nécessaires. Le script pack utilise
|
||||
uniquement la bibliothèque standard et repère aussi une installation Python
|
||||
@@ -2080,7 +2115,7 @@ décrits dans [Validation](docs/testing.md).
|
||||
|
||||
Résultats :
|
||||
|
||||
- `mods/sanctuary/build/libs/sanctuary-beta.092.jar` : mod à installer avec
|
||||
- `mods/sanctuary/build/libs/sanctuary-beta.112.jar` : mod à installer avec
|
||||
Fabric API sur la version Minecraft indiquée.
|
||||
- `build/packwiz/` : pack de développement complet, avec le mod construit et
|
||||
l'index vérifié. Voir [Installation packwiz](packwiz/README.md).
|
||||
|
||||
+14
-1
@@ -1,5 +1,17 @@
|
||||
# Backlog Sanctuary
|
||||
|
||||
## STAT-03 — Atelier d’argile et hotbar — beta.112
|
||||
|
||||
Branche `codex/clay-workshop-hotbar-beta112`. [Contrat et vérifications](clay-workshop-hotbar-beta112.md).
|
||||
Onze cases : argile, résultat et hotbar active ; hauteur fixe et transferts
|
||||
limités aux neuf cases, même avec un inventaire agrandi.
|
||||
|
||||
## STAT-02 — Atelier d’argile et orientation — beta.111
|
||||
|
||||
Branche `codex/clay-workshop-ui-beta111`. [Contrat et vérifications](clay-workshop-ui-beta111.md).
|
||||
Cadre nine-slice fourni, inventaire intégré et pose selon le regard horizontal.
|
||||
Propriété `facing` additive ; les anciens statuaires gardent leur apparence.
|
||||
|
||||
## STAT-01 — Atelier d’argile et modèles 3D — beta.106
|
||||
|
||||
Branche `codex/statuary-import-beta106`. [Contrat et validation](statuary-beta106.md).
|
||||
@@ -1847,4 +1859,5 @@ Branche `codex/clay-workshop-integration-beta110`.
|
||||
[Ticket](clay-workshop-integration-beta110.md) : assemblage commun de
|
||||
beta.106 et beta.109, avec tous les systèmes intermédiaires. Essais natifs de
|
||||
l’atelier, des œufs et de la neige, build et archives réussis. Publication
|
||||
et synchronisation de l’instance demandées ensuite, en cours.
|
||||
et synchronisation de l’instance Sanctuary Beta terminées ; 923 fichiers
|
||||
personnels et réglages suivis conservés, aucun monde ouvert.
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
# CARRY-08 — Fuite des animaux volants portés — beta.114
|
||||
|
||||
Contrat du 17 septembre 2026, branche `codex/carried-flight-panic-beta114`.
|
||||
|
||||
Un coup reçu par un animal volant porté sur la tête déclenche une fuite de
|
||||
40 à 60 ticks : il entraîne son porteur vers le haut, dans une direction
|
||||
aléatoire, avec des embardées. Le serveur choisit le départ et la trajectoire,
|
||||
transmis au client pour la physique native. Les touches de déplacement et le
|
||||
regard ne pilotent pas cette brève fuite. La caméra reste libre. Un nouveau coup peut relancer la fuite avec un autre
|
||||
cap après dix ticks, sans cumuler les vitesses.
|
||||
|
||||
Le porteur peut viser et frapper son animal volant. Les animaux ordinaires
|
||||
reçoivent les dégâts habituels ; le familier conserve son coup amical sans
|
||||
perte de santé. Les attaques extérieures acceptées déclenchent aussi la fuite.
|
||||
Les autres protections entre passagers et porteurs restent en place.
|
||||
Les espèces reprennent le groupe déjà utilisé pour planer, poule comprise.
|
||||
|
||||
La vitesse reste bornée, les collisions sont natives et aucun bloc n’est
|
||||
traversé ni déplacé. Déposer l’animal (Maj + clic droit), le perdre, mourir,
|
||||
se déconnecter ou changer de dimension arrête la propulsion. Immersion,
|
||||
mode spectateur, vol créatif, élytres et porteur lui-même passager l’inhibent.
|
||||
La fin de fuite rend le contrôle au joueur et conserve le planeur existant
|
||||
si l’animal reste porté. Les montures colossales gardent leur pilotage actuel.
|
||||
|
||||
L’état de fuite est temporaire, synchronisé et non sauvegardé. Aucun format
|
||||
persistant, identifiant existant ou monde personnel n’est modifié. Les nouveaux
|
||||
retours d’interface sont traduits FR/EN. Client et serveur utilisent beta.114.
|
||||
|
||||
## Vérifications natives
|
||||
|
||||
`CarryPanic114ClientChecks` passe en **56 secondes** avec un serveur intégré
|
||||
Minecraft 26.3 et un monde plat jetable de graine 114 :
|
||||
|
||||
- Véritable ciblage du perroquet sur la tête et frappe avec la touche native,
|
||||
perte de santé de l’animal ordinaire, décollage et déplacement du joueur.
|
||||
- Trajectoire synchronisée et priorité sur les touches avant/droite/saut et
|
||||
le regard ; comparaison exacte avec la vitesse du tick de physique.
|
||||
- Expiration, suppression de l’état temporaire et retour au planeur normal.
|
||||
- 32 trajectoires tirées côté serveur couvrent les quatre quadrants, toutes
|
||||
dans les bornes de durée et de vitesse ; Maj + clic droit natif interrompt.
|
||||
- Sept espèces ordinaires : poule, chauve-souris, abeille, allay, perroquet,
|
||||
blaze et breeze. Une vache portée reste protégée et ne déclenche aucun vol.
|
||||
- Attaque extérieure, mort, activation du vol créatif et détachement arrêtent
|
||||
ou déclenchent la réaction selon le contrat.
|
||||
- Plafond bas : aucune traversée ; le passager bloqué est déposé par les
|
||||
contrôles de place existants et la propulsion s’interrompt.
|
||||
- Véritable coup amical sur un familier perroquet : lancement sans perte de
|
||||
santé, puis arrêt immédiat lors du détachement.
|
||||
|
||||
```sh
|
||||
./gradlew :sanctuary:runClientGameTest \
|
||||
-PsanctuaryClientTests=true -PsanctuaryCarryPanic114ClientTests=true \
|
||||
-PsanctuaryClientNoVsync=true -PsanctuaryQuickTests=true
|
||||
```
|
||||
|
||||
Marqueurs `CARRY114_NATIVE_FLIGHT_PASS` et `CARRY114_PASS` dans
|
||||
`build/carry-panic114-client.log`. Capture en jeu relue dans
|
||||
`build/carry-panic114-evidence/`. Le GameTest dédié reste exclu conformément
|
||||
au refus antérieur de son EULA. Pas de test Windows, de réseau distant ou de
|
||||
latence simulée ; aucun monde personnel n’a été ouvert.
|
||||
|
||||
## Livraison
|
||||
|
||||
`./gradlew check build assemblePack assembleTestPack -x :sanctuary:runGameTest`
|
||||
passe en **2 min 22 s**, 125 tâches. Les sources JAR correspondent exactement
|
||||
aux sources du dépôt. Les classes et ressources hors de ce ticket restent
|
||||
identiques à beta.113, notamment les sculptures d’argile. Les deux packs
|
||||
contiennent le même JAR, sans monde, GLB ni classes de test.
|
||||
|
||||
- Sanctuary-beta.114.mrpack : 10179428 octets, SHA-256
|
||||
`92aac61c2c00a2206f3f5ac3d170ab058f6765526cf5a2a64c5336261db3f5bf`.
|
||||
- Sanctuary-Test-beta.114.mrpack : 10198350 octets, SHA-256
|
||||
`5e8ec1f18e34ca51e09e2738f2b04f9a1bbac5d14c7a7c2d766218a4cff6021f`.
|
||||
- JAR Sanctuary : SHA-256
|
||||
`18105b76f63008210c62d5728f999c7abc083a861ecc1de2c7ee87262dde5a40`.
|
||||
|
||||
Reçu local : `build/carry-panic114-artifact.json`.
|
||||
|
||||
Publication et synchronisation de l’instance à effectuer après le commit source.
|
||||
@@ -38,4 +38,3 @@ Reçu : `build/beta104-artifact.json`.
|
||||
|
||||
Les GameTests dédiés restent exclus. Aucun monde, canal ou installation
|
||||
personnelle modifié ; archives locales uniquement.
|
||||
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
# STAT-04 — Sculptures d’argile et accroche au bord — beta.113
|
||||
|
||||
Contrat du 17 septembre 2026, branche `codex/clay-sculpture-grid-beta113`.
|
||||
|
||||
Les miniatures de l’atelier deviennent des « sculptures d’argile » / « Clay
|
||||
sculptures ». Le nom du modèle reste dans leur infobulle. Les statues en blocs
|
||||
du Métabli restent une catégorie distincte.
|
||||
|
||||
À la pose, le bord horizontal le plus proche du point visé accueille la
|
||||
sculpture. L’autre axe est centré au pixel entier le plus proche, sur la grille
|
||||
de 1/16 de bloc. Sur une face latérale, elle touche le bord du bloc support.
|
||||
À égalité exacte entre deux bords, l’axe Z départage ; au centre exact,
|
||||
le bord côté joueur est retenu. L’orientation suit toujours le regard.
|
||||
Le bouton Tourner prépare le modèle avant fabrication. Les marges horizontales vides du modèle ne créent pas de décalage au bord.
|
||||
Tous les sommets restent
|
||||
sur la grille entière, même avec des dimensions impaires et après rotation.
|
||||
|
||||
## Compatibilité préalable
|
||||
|
||||
La propriété native additive `anchor` du bloc `sanctuary:statuary` mémorise
|
||||
`center`, `north`, `south`, `east` ou `west`. Un ancien état sans cette propriété
|
||||
prend `center` : il reste centré, avec la correction visuelle du demi-voxel.
|
||||
Le schéma 1, les voxels, noms de modèles et identifiants ne changent pas.
|
||||
Il n’y a aucun parcours ni réécriture de chunks existants. La sauvegarde native
|
||||
conserve l’accroche ; les rotations et miroirs de structures la transforment.
|
||||
Casser et reposer choisit une nouvelle accroche sans modifier le modèle.
|
||||
Les nouveaux objets emploient le nom traduit de leur type ; les noms déjà
|
||||
enregistrés et les noms personnalisés restent conservés. La collision cubique
|
||||
et la limite d’une sculpture par bloc restent inchangées. Client et serveur
|
||||
doivent utiliser ensemble beta.113.
|
||||
|
||||
## Vérifications natives
|
||||
|
||||
Le parcours `Statuary106ClientChecks`, étendu par `Sculpture113ClientChecks`,
|
||||
passe en **1 min 25 s**, sur Minecraft 26.3 et un monde plat jetable de graine 106 :
|
||||
|
||||
- 5 120 combinaisons de largeur/profondeur, orientation et accroche : contrôle
|
||||
de la géométrie émise, grille exacte de 1/16 et absence de dépassement du bloc.
|
||||
- Modèles avec marges transparentes, repères colorés pour vérifier le sens du
|
||||
rendu et égalité de la géométrie centrée entre l’objet et le bloc.
|
||||
- Vrais clics de pose sur les quatre bords : orientation, bord côté serveur,
|
||||
réception côté client, butin/repose et conservation après reconnexion.
|
||||
- Contextes natifs de pose sur les faces latérales et coordonnées négatives.
|
||||
- Rotation/miroir de chaque combinaison d’accroche et d’orientation ; lecture
|
||||
des anciens états sans `anchor`, avec et sans `facing`.
|
||||
- Noms français/anglais des objets fabriqués, menu aux échelles 2 et 3,
|
||||
fabrication payée, Maj-clic limité à la hotbar et régression Métabli.
|
||||
|
||||
```sh
|
||||
./gradlew :sanctuary:runClientGameTest \
|
||||
-PsanctuaryClientTests=true -PsanctuaryStatuary106ClientTests=true \
|
||||
-PsanctuaryClientNoVsync=true -PsanctuaryQuickTests=true
|
||||
```
|
||||
|
||||
Marqueurs `STATUARY113_PASS` et `SCULPTURE113_GEOMETRY_PASS` dans
|
||||
`build/clay-grid113-client.log`. Captures relues dans
|
||||
`build/clay-grid113-evidence/`. Le GameTest dédié reste exclu conformément au
|
||||
refus antérieur de son EULA ; ces essais utilisent le serveur intégré macOS.
|
||||
Pas de validation Windows ni depuis deux ordinateurs. Aucun monde personnel
|
||||
n’a été ouvert.
|
||||
|
||||
## Livraison
|
||||
|
||||
`./gradlew check build assemblePack assembleTestPack -x :sanctuary:runGameTest`
|
||||
passe en **3 min 11 s**, 125 tâches. Les sources JAR correspondent exactement
|
||||
aux sources du dépôt. Les classes et ressources hors du ticket sont identiques
|
||||
à beta.112. Les deux packs contiennent le même JAR, sans monde, GLB ni test.
|
||||
|
||||
- Sanctuary-beta.113.mrpack : 10173807 octets, SHA-256
|
||||
`ed082ff29a0e9a70fd6786bc024241b276cb516bc09fb5151d6cb324604429bb`.
|
||||
- Sanctuary-Test-beta.113.mrpack : 10192733 octets, SHA-256
|
||||
`387f54dc77884a9c9bccdcc1904778ca7f0b2fbda126754fdc49b10a492e4602`.
|
||||
- JAR Sanctuary : SHA-256
|
||||
`e242d9fd2edeede922a72a10e988e29c2b49dea52105cfc9fc8573154849ead3`.
|
||||
|
||||
Reçu local : `build/clay-grid113-artifact.json`.
|
||||
|
||||
## Publication et installation
|
||||
|
||||
La [release beta.113](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.113)
|
||||
est publiée depuis `6166d0aa9eba3c3dd84b8b9f1501e3346231ac82`. Le tag exact
|
||||
et les artefacts sont immuables ; les téléchargements publics sont vérifiés.
|
||||
Le canal packwiz avance à `e4c16d517bfda4f2b688604cc61945a46a4513a9`.
|
||||
|
||||
Deux synchronisations isolées puis deux passages dans **Sanctuary Beta** ont
|
||||
réussi. Un seul JAR Sanctuary beta.113 est actif, et le second passage conserve
|
||||
les mêmes hashes. Les **923 fichiers personnels et réglages suivis** restent
|
||||
identiques. Aucun monde personnel n’a été ouvert. La sauvegarde ciblée est
|
||||
`sanctuary-backups/before-beta.113/` dans l’instance existante. Reçus locaux :
|
||||
`build/clay-grid113-isolated.json` et `build/clay-grid113-prism.json`.
|
||||
@@ -0,0 +1,79 @@
|
||||
# STAT-03 — Atelier d’argile limité à la hotbar — beta.112
|
||||
|
||||
Contrat du 17 septembre 2026, branche `codex/clay-workshop-hotbar-beta112`.
|
||||
|
||||
Le menu utilise uniquement les neuf cases de la hotbar active du joueur,
|
||||
avec les deux emplacements de fabrication. Les rangées de réserve, y compris
|
||||
les extensions Sanctuary, ne sont ni ajoutées au menu ni utilisées par ses
|
||||
transferts. La rangée choisie avant l’ouverture reste la hotbar courante.
|
||||
La touche Tab retrouve la navigation des boutons dans ce menu.
|
||||
|
||||
Le cadre nine-slice tient dans une hauteur fixe de 202 pixels d’interface.
|
||||
Il ne grandit plus avec la capacité de l’inventaire. Le Maj-clic du résultat
|
||||
cherche uniquement une place dans la hotbar ; sans place, il ne consomme pas
|
||||
d’argile. Les touches 1–9 restent utilisables, pas l’échange avec la seconde
|
||||
main. À la fermeture, la restitution native des objets restants est conservée.
|
||||
|
||||
Les identifiants, sauvegardes, données de sculpture et orientations beta.111
|
||||
restent inchangés. Aucun monde existant n’est ouvert ou réécrit. Client et
|
||||
serveur doivent utiliser ensemble beta.112 pour les nouveaux indices du menu
|
||||
temporaire (11 cases au total).
|
||||
|
||||
## Vérifications natives
|
||||
|
||||
Le parcours `Statuary106ClientChecks`, étendu à beta.112, passe en **1 min 13 s**
|
||||
sur Minecraft 26.3 avec serveur intégré et monde plat jetable de graine 106 :
|
||||
|
||||
- Menu FR/EN aux échelles 2 et 3 : hauteur fixe, neuf cases de hotbar native,
|
||||
deux cases de fabrication et absence de panneau d’inventaire évolutif.
|
||||
- Fabrication par Maj-clic depuis la hotbar, avec de l’argile aussi présente
|
||||
dans la réserve, l’extension et la seconde main : ces stocks restent intacts.
|
||||
- Rejet des indices de cases absents et de l’échange avec la seconde main.
|
||||
- Hotbar remplie de neuf piles : Maj-clic sans fabrication ni consommation.
|
||||
- Une seule place dans une pile de 63 statuaires : production d’un seul objet,
|
||||
coût d’une argile, aucune statue transférée dans une case cachée.
|
||||
- Restitution de l’argile restante à la fermeture, seize teintes, quatre
|
||||
orientations, butin/repose et sauvegarde/reconnexion sans fichier GLB.
|
||||
- Régression du Métabli et annulation d’import conservées.
|
||||
|
||||
```sh
|
||||
./gradlew :sanctuary:runClientGameTest \
|
||||
-PsanctuaryClientTests=true -PsanctuaryStatuary106ClientTests=true \
|
||||
-PsanctuaryClientNoVsync=true -PsanctuaryQuickTests=true
|
||||
```
|
||||
|
||||
Marqueur `STATUARY112_PASS` dans `build/clay-hotbar112-client.log` ; captures
|
||||
relues dans `build/clay-hotbar112-evidence/`. Le GameTest dédié reste exclu
|
||||
conformément au refus antérieur de son EULA. Pas de test Windows ni depuis
|
||||
deux ordinateurs ; aucun monde personnel n’a été ouvert.
|
||||
|
||||
## Livraison vérifiée
|
||||
|
||||
`./gradlew check build assemblePack assembleTestPack -x :sanctuary:runGameTest`
|
||||
passe en **3 min 6 s**, 125 tâches. Les sources JAR correspondent aux sources
|
||||
Java du dépôt. Les classes et ressources hors de ce ticket sont identiques
|
||||
à beta.111, notamment les données et le rendu orienté des statuaires.
|
||||
Les deux packs contiennent le même JAR, sans monde, GLB ni classes de tests.
|
||||
|
||||
- Pack normal : 10 169 337 octets, SHA-256
|
||||
`55723a5d7cb99e350ad8fe150e92e0567430b5261ca4685abd900be34068658f`.
|
||||
- Pack Test : 10 188 262 octets, SHA-256
|
||||
`987818bf817ad80fd055b97e84bf4775fca54820d7c3055d6fd4089dc7a5a3c0`.
|
||||
- JAR : SHA-256
|
||||
`9007a770bbe69db5846f93140d910514f10839cc34ec7c6315a9381d8850adaa`.
|
||||
|
||||
Reçu local : `build/clay-hotbar112-artifact.json`.
|
||||
|
||||
## Publication et installation
|
||||
|
||||
La [release beta.112](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.112)
|
||||
est publiée depuis `1eaa976ecf328c6c20afe5e4eb67c48118f87f95`. Le tag exact
|
||||
et les artefacts sont immuables ; les téléchargements publics sont vérifiés.
|
||||
Le canal packwiz avance à `573c5b0d84fcb3e9fec7cc8a83981cf35eaa1ead`.
|
||||
|
||||
Deux synchronisations isolées puis deux passages dans **Sanctuary Beta** ont
|
||||
réussi. Un seul JAR Sanctuary beta.112 est actif, et le second passage conserve
|
||||
les mêmes hashes. Les **923 fichiers personnels et réglages suivis** restent
|
||||
identiques. Aucun monde personnel n’a été ouvert. La sauvegarde ciblée est
|
||||
`sanctuary-backups/before-beta.112/` dans l’instance existante. Reçus locaux :
|
||||
`build/clay-hotbar112-isolated.json` et `build/clay-hotbar112-prism.json`.
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
Demande du 17 septembre 2026 : inclure le Clay Workshop dans la version avec
|
||||
la fonte saisonnière. Branche `codex/clay-workshop-integration-beta110`.
|
||||
État : assemblage commun vérifié, publication et synchronisation en cours.
|
||||
État : version commune vérifiée, publiée et synchronisée dans Sanctuary Beta.
|
||||
|
||||
## Périmètre et compatibilité
|
||||
|
||||
@@ -95,3 +95,34 @@ Reçu : `build/integration110-artifact.json`, manifeste des sources :
|
||||
Les essais natifs sont macOS avec serveur intégré ; pas de test Windows ni
|
||||
connexion depuis un second ordinateur. La neige antérieure non suivie garde
|
||||
la limite de fonte documentée dans beta.109.
|
||||
|
||||
Le script de publication a été corrigé pour autoriser uniquement `icon.png`
|
||||
en plus des manifestes TOML, après contrôle de l'index et comparaison à l'icône
|
||||
source. Les JAR restent exclusivement des pièces jointes de release. Cette
|
||||
correction de distribution ne change aucun binaire ni le tag source beta.110.
|
||||
|
||||
## Publication et installation terminées
|
||||
|
||||
[Release beta.110](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.110),
|
||||
tag source `468743bfe75087ecb0160b85a0864fa81bcdded5`. Les sources cumulatives
|
||||
depuis beta.092 sont enregistrées ; les anciens tags et artefacts sont conservés.
|
||||
Les corrections du script de distribution et ce reçu complètent la branche
|
||||
source sans déplacer ce tag ni remplacer les binaires.
|
||||
|
||||
Canal packwiz : `828353a6e2bd747f4f473ae9fb9e965d38032419`. Chaque manifeste et
|
||||
l'icône publiés correspondent aux fichiers validés localement. JAR, archives
|
||||
normale/Test et ZIP d'amorçage Prism sont disponibles dans la release.
|
||||
|
||||
Deux synchronisations de l'installateur packwiz dans un dossier neuf, puis deux
|
||||
dans la même instance **Sanctuary Beta**, ont réussi. Le second passage ne
|
||||
change aucun fichier géré. Un seul JAR Sanctuary beta.110 est actif, Fabric API
|
||||
0.160.5+26.3 est installé, Minecraft est réglé sur 26.3 et Loader sur 0.19.5.
|
||||
L'icône déclarée dans le pack est ajoutée. `instance.cfg` et les **923 fichiers
|
||||
personnels et réglages suivis** conservent leurs empreintes. Aucun monde ouvert,
|
||||
converti ou régénéré ; aucune autre instance ni serveur personnel modifié.
|
||||
|
||||
Sauvegarde ciblée dans l'instance : `sanctuary-backups/before-beta.110/`.
|
||||
Reçus ignorés : `build/integration110-publication.json`,
|
||||
`build/integration110-isolated.json`, `build/integration110-prism.json`.
|
||||
Les journaux de chaque synchronisation et les listes d'empreintes avant/après
|
||||
sont conservés dans le dossier de développement et dans la sauvegarde ciblée.
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
# STAT-02 — Atelier d’argile et orientation — beta.111
|
||||
|
||||
Contrat du 17 septembre 2026, branche `codex/clay-workshop-ui-beta111`.
|
||||
|
||||
## Résultat attendu
|
||||
|
||||
Le menu utilise le cadre `task_frame_unobtained.png` fourni par le créateur,
|
||||
sans retouche, comme sprite Sanctuary avec les métadonnées nine-slice natives
|
||||
de Minecraft. Les coins restent à leur taille d’origine. Catalogue, aperçu,
|
||||
fabrication, explications et inventaire évolutif tiennent dans ce cadre.
|
||||
La texture reste remplaçable par un pack de ressources.
|
||||
|
||||
La pose d’un statuaire suit le regard horizontal du joueur, dans les quatre
|
||||
directions. Le bouton Tourner règle toujours l’orientation du modèle importé.
|
||||
Le placement ajoute ensuite l’orientation du bloc sans réécrire ses voxels.
|
||||
|
||||
## Contrat de compatibilité préalable
|
||||
|
||||
Ajout de la propriété native `facing` au bloc `sanctuary:statuary`, avec `south`
|
||||
par défaut : cette orientation correspond au rendu historique, sans rotation.
|
||||
Les anciens états sans propriété prennent cette valeur à la lecture et gardent
|
||||
leur apparence. Aucun parcours ni réécriture des anciens chunks n’est effectué.
|
||||
Le schéma 1 de la sculpture, ses voxels, ses identifiants et les données des
|
||||
objets restent identiques. Casser puis reposer utilise le nouveau regard,
|
||||
sans cumuler les orientations précédentes. Rotation et miroir de structures
|
||||
transforment l’état du bloc. La collision cubique reste celle déjà livrée.
|
||||
|
||||
## Texture
|
||||
|
||||
Le fichier fourni, 26 × 26 pixels, est conservé octet pour octet sous
|
||||
`assets/sanctuary/textures/gui/sprites/container/clay_workshop/frame.png`.
|
||||
Le fichier adjacent `.png.mcmeta` utilise le moteur `nine_slice` de Minecraft
|
||||
avec une bordure fixe de quatre pixels et un centre extensible. L’identifiant
|
||||
du sprite est `sanctuary:container/clay_workshop/frame` ; aucun sprite global
|
||||
Minecraft n’est remplacé. Les emplacements utilisent `minecraft:container/slot`.
|
||||
|
||||
## Vérifications natives
|
||||
|
||||
`Statuary106ClientChecks`, étendu aux cas beta.111, passe en **1 min 3 s** sur
|
||||
Minecraft 26.3, avec serveur intégré et monde plat jetable de graine 106 :
|
||||
|
||||
- Fabrication payée, restitutions, Maj-clic et teintes des seize argiles.
|
||||
- Menu FR/EN aux échelles 2 et 3, cases et actions contenues dans le cadre.
|
||||
- Six rangées d’inventaire à l’échelle 3 : affichage borné et défilement.
|
||||
- Vrais clics de pose aux quatre points cardinaux : état côté serveur,
|
||||
transmission au client et rotation du rendu alignés sur le regard.
|
||||
- Rotation et miroir de structures ; lecture d’un ancien état sans `facing`.
|
||||
- Butin d’une statue orientée au nord puis repose vers l’ouest : nouvelle
|
||||
orientation, mêmes voxels. Sauvegarde/réouverture et transmission sans GLB.
|
||||
- Conversion au Métabli et annulation d’import conservées.
|
||||
|
||||
Commande :
|
||||
|
||||
```sh
|
||||
./gradlew :sanctuary:runClientGameTest \
|
||||
-PsanctuaryClientTests=true -PsanctuaryStatuary106ClientTests=true \
|
||||
-PsanctuaryClientNoVsync=true -PsanctuaryQuickTests=true
|
||||
```
|
||||
|
||||
Marqueurs `STATUARY106_PASS`, `STATUARY106_INTEROP_PASS` et `STATUARY111_PASS`
|
||||
dans `build/clay-ui111-client.log`. Captures dans `build/clay-ui111-evidence/`,
|
||||
dont les menus français et anglais relus visuellement.
|
||||
|
||||
Le GameTest dédié reste exclu conformément au refus antérieur de son EULA.
|
||||
Les essais tournent sur macOS avec serveur intégré ; pas de validation depuis
|
||||
deux ordinateurs ni sur Windows. Aucun monde personnel n’est ouvert.
|
||||
|
||||
## Livraison vérifiée
|
||||
|
||||
`./gradlew check build assemblePack assembleTestPack -x :sanctuary:runGameTest`
|
||||
passe en **2 min 33 s**, 125 tâches. Les sources JAR correspondent exactement
|
||||
aux fichiers Java du dépôt. Le contenu du mod hors classes et ressources de
|
||||
ce ticket est identique à beta.110 ; œufs, neige et fonte restent inclus.
|
||||
Les deux MRpack contiennent le même JAR vérifié, sans monde, GLB ni classe de test.
|
||||
|
||||
- Pack normal : `build/Sanctuary-beta.111.mrpack`, 10 169 372 octets,
|
||||
SHA-256 `f83098f80ac2edb8e32877bf8d5a362cd75f2fa9020593c2c7e4f9abe8b3e9f6`.
|
||||
- Pack Test : `build/Sanctuary-Test-beta.111.mrpack`, 10 188 298 octets,
|
||||
SHA-256 `1234b10eb4685d99aa7421de46bc54fd114b791f1b4d63803a09afc926d2c9d9`.
|
||||
- JAR Sanctuary : SHA-256
|
||||
`b3e967db7b336b970e4121981e3ef8140fe90f0ecc5850ce90a70caaf3772684`.
|
||||
|
||||
Reçu local : `build/clay-ui111-artifact.json`.
|
||||
|
||||
## Publication et installation
|
||||
|
||||
La [release beta.111](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.111)
|
||||
est publiée depuis le commit source `289697becf19db4acf50d224b1dd3d753eff1153`.
|
||||
Le tag exact et les artefacts sont immuables ; leurs téléchargements publics
|
||||
ont été vérifiés. Le canal packwiz avance au commit
|
||||
`87c9f81761309316c601ed32fa922c1a5893952c`.
|
||||
|
||||
Deux synchronisations isolées puis deux passages dans l’instance existante
|
||||
**Sanctuary Beta** ont réussi. Un seul JAR Sanctuary beta.111 est actif ; le
|
||||
second passage conserve les mêmes hashes. Les **923 fichiers personnels et
|
||||
réglages suivis** restent identiques. Aucun monde personnel n’a été ouvert.
|
||||
La sauvegarde ciblée est dans `sanctuary-backups/before-beta.111/` de cette
|
||||
instance. Reçus : `build/clay-ui111-isolated.json` et
|
||||
`build/clay-ui111-prism.json`.
|
||||
@@ -31,4 +31,3 @@ de débogage changent). Reçu : `build/beta103-artifact.json`.
|
||||
|
||||
Les GameTests du serveur dédié restent exclus. Aucun monde personnel,
|
||||
aucune installation ni aucun canal de distribution modifié.
|
||||
|
||||
|
||||
+8
-3
@@ -8,7 +8,8 @@ https://git.botsu.net/koka/sanctuary-beta/raw/branch/packwiz/pack.toml
|
||||
```
|
||||
|
||||
Les sources sont sur la branche de travail du ticket. La branche `packwiz`
|
||||
contient uniquement les manifestes de distribution produits par le build ; les
|
||||
contient les manifestes de distribution produits par le build et l’icône
|
||||
`icon.png` déclarée dans leur index ; les
|
||||
JAR Sanctuary sont des pièces jointes de releases Gitea. Aucun JAR de mod ou
|
||||
dépendance téléchargée n'entre dans l'historique Git. Le canal Beta est distinct
|
||||
de l'ancien pack 26.2.
|
||||
@@ -17,8 +18,12 @@ de l'ancien pack 26.2.
|
||||
|
||||
Clay Workshop, Statuaire et import GLB intégrés aux œufs et à la neige saisonnière.
|
||||
[Contrat, vérifications et suivi de mise à jour](clay-workshop-integration-beta110.md).
|
||||
Archives normal/Test vérifiées. Publication du canal et synchronisation de
|
||||
l’instance Sanctuary Beta demandées ; leur résultat est consigné dans le ticket.
|
||||
[Release beta.110](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.110)
|
||||
publiée ; canal `828353a6e2bd747f4f473ae9fb9e965d38032419`. Deux synchronisations
|
||||
isolées puis deux dans la même instance **Sanctuary Beta** réussies. Minecraft
|
||||
26.3 finale, Loader 0.19.5, Fabric API 0.160.5+26.3. Les 923 fichiers personnels
|
||||
et réglages suivis sont inchangés ; sauvegardes restées fermées. Copie préalable
|
||||
des fichiers remplacés : `sanctuary-backups/before-beta.110/`.
|
||||
|
||||
## beta.105 — archives locales
|
||||
|
||||
|
||||
+2
-2
@@ -9,8 +9,8 @@ loom_version=1.17.20
|
||||
fabric_api_version=0.160.5+26.3
|
||||
|
||||
# Release counter: beta.001, .002, .003, ... (see docs/versioning.md).
|
||||
mod_version=beta.110
|
||||
pack_version=beta.110
|
||||
mod_version=beta.114
|
||||
pack_version=beta.114
|
||||
resource_pack_version=beta.090
|
||||
maven_group=fr.koka.sanctuary
|
||||
jei_version=30.32.0-sanctuary.3
|
||||
|
||||
@@ -93,6 +93,10 @@ tasks.named('runGameTest') {
|
||||
}
|
||||
|
||||
tasks.named('processGametestResources') {
|
||||
def carryPanic114Client = providers.gradleProperty('sanctuaryCarryPanic114ClientTests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('sanctuaryCarryPanic114ClientTests', carryPanic114Client)
|
||||
if (carryPanic114Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.client.CarryPanic114ClientChecks') }
|
||||
|
||||
def statuary106Client = providers.gradleProperty('sanctuaryStatuary106ClientTests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('sanctuaryStatuary106ClientTests', statuary106Client)
|
||||
if (statuary106Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.client.Statuary106ClientChecks') }
|
||||
|
||||
+115
@@ -0,0 +1,115 @@
|
||||
package fr.koka.sanctuary.client;
|
||||
|
||||
import com.mojang.blaze3d.platform.InputConstants;
|
||||
import fr.koka.sanctuary.cosmetics.*;
|
||||
import fr.koka.sanctuary.gametest.Starter039TestSupport;
|
||||
import fr.koka.sanctuary.inventory.*;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.atomic.*;
|
||||
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.FabricClientGameTest;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.context.ClientGameTestContext;
|
||||
import net.minecraft.client.CameraType;
|
||||
import net.minecraft.client.gui.components.Button;
|
||||
import net.minecraft.client.gui.screens.worldselection.WorldCreationUiState;
|
||||
import net.minecraft.client.input.KeyEvent;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.component.DataComponents;
|
||||
import net.minecraft.nbt.CompoundTag;
|
||||
import net.minecraft.network.chat.contents.TranslatableContents;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.world.entity.*;
|
||||
import net.minecraft.world.item.*;
|
||||
import net.minecraft.world.item.component.CustomData;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.phys.*;
|
||||
|
||||
/** Real cursor/attack packets, synchronized player travel, interruption and collision regression. */
|
||||
public final class CarryPanic114ClientChecks implements FabricClientGameTest {
|
||||
public void runTest(ClientGameTestContext c){
|
||||
var intro=new AtomicBoolean(true);
|
||||
c.runOnClient(m->{m.options.renderDistance().set(6);m.options.simulationDistance().set(5);m.options.pauseOnLostFocus=false;IntroClient.setQuickTestIntro(true);});
|
||||
ClientTickEvents.END_CLIENT_TICK.register(m->{if(intro.get()&&m.gui.screen() instanceof HelloWorldScreen screen){
|
||||
Starter039TestSupport.selectFirst(screen);
|
||||
screen.children().stream().filter(Button.class::isInstance).map(Button.class::cast).filter(b->b.getClass().getSimpleName().equals("ColorButton")).findFirst().orElseThrow().onPress(enter());
|
||||
screen.children().stream().filter(Button.class::isInstance).map(Button.class::cast).filter(b->b.getMessage().getContents() instanceof TranslatableContents t&&t.getKey().equals("sanctuary.progression.enter")).findFirst().orElseThrow().onPress(enter());intro.set(false);
|
||||
}});
|
||||
try(var world=c.worldBuilder().adjustSettings(s->{s.setName("CarryPanic114 "+UUID.randomUUID());s.setSeed("114");s.setAllowCommands(true);s.setGameMode(WorldCreationUiState.SelectedGameMode.SURVIVAL);
|
||||
s.setWorldType(s.getNormalPresetList().stream().filter(e->e.preset()!=null&&e.preset().getRegisteredName().equals("sanctuary_test:flat")).findFirst().orElseThrow());}).create()){
|
||||
c.waitFor(m->ProgressionClient.active()&&m.player!=null&&m.gui.screen()==null&&!IntroSpawnFlash.active(),1200);var server=world.getServer();
|
||||
server.runOnServer(s->{var p=player(s);p.getInventory().clearContent();p.getAbilities().invulnerable=true;p.onUpdateAbilities();reset(p);
|
||||
for(int x=-24;x<=24;x++)for(int z=-24;z<=24;z++)p.level().setBlockAndUpdate(new BlockPos(x,0,z),Blocks.STONE.defaultBlockState());
|
||||
s.getCommands().performPrefixedCommand(s.createCommandSourceStack(),"sanctuary time vanilla");s.getCommands().performPrefixedCommand(s.createCommandSourceStack(),"time set noon");});
|
||||
int parrot=server.computeOnServer(s->carry(player(s),EntityTypes.PARROT).getId());
|
||||
c.waitFor(m->m.level.getEntity(parrot)!=null&&CarryService.carried(m.level.getEntity(parrot)),150);
|
||||
float health=server.computeOnServer(s->((LivingEntity)player(s).level().getEntity(parrot)).getHealth());
|
||||
attack(c,parrot);c.waitFor(m->CarryPanic.active(m.player),100);
|
||||
server.runOnServer(s->{var p=player(s);check(CarryPanic.active(p),"Real native attack triggers server panic");check(((LivingEntity)p.level().getEntity(parrot)).getHealth()<health,"Ordinary carried animal takes native damage");});
|
||||
c.getInput().holdKey(o->o.keyUp);c.getInput().holdKey(o->o.keyRight);c.getInput().holdKey(o->o.keyJump);
|
||||
c.runOnClient(m->{m.player.setYRot(170);m.player.setXRot(15);});c.waitTicks(12);
|
||||
c.runOnClient(m->{check(m.player.getY()>2.5&&m.player.position().subtract(new Vec3(.5,1,.5)).horizontalDistance()>1,"Actual client moves upward and sideways");
|
||||
check(CarryPanic.active(m.player),"Panic remains active through steering keys");
|
||||
var wanted=m.player.getAttached(CarryPanic.FLIGHT).velocity(m.level.getGameTime());
|
||||
// Level time can advance immediately after the player physics tick. Match either exact tick.
|
||||
var previous=m.player.getAttached(CarryPanic.FLIGHT).velocity(m.level.getGameTime()-1);
|
||||
check(Math.min(m.player.getDeltaMovement().distanceTo(wanted),m.player.getDeltaMovement().distanceTo(previous))<.00001,"Movement follows animal, not camera or held keys: actual="+m.player.getDeltaMovement()+", expected="+wanted+", flight="+m.player.getAttached(CarryPanic.FLIGHT)+", time="+m.level.getGameTime());check(m.player.fallDistance<.2,"Flight clears falling distance");
|
||||
m.options.setCameraType(CameraType.THIRD_PERSON_BACK);});c.waitTicks(2);c.takeScreenshot("carry114-chaotic-flight");
|
||||
c.getInput().releaseKey(o->o.keyUp);c.getInput().releaseKey(o->o.keyRight);c.getInput().releaseKey(o->o.keyJump);
|
||||
c.waitTicks(65);c.waitFor(m->!CarryPanic.active(m.player),100);
|
||||
server.runOnServer(s->{var p=player(s);check(!p.hasAttached(CarryPanic.FLIGHT)&&CarryGlide.supported(p),"Timeout removes panic and retains ordinary glider");check(p.connection.isAcceptingMessages(),"Flight is accepted by server");});
|
||||
c.runOnClient(m->{check(m.player.getDeltaMovement().y<=0&&m.player.getDeltaMovement().y>=-CarryGlide.DESCENT-.02,"Normal gentle descent resumes");m.options.setCameraType(CameraType.FIRST_PERSON);});
|
||||
System.out.println("CARRY114_NATIVE_FLIGHT_PASS cursor, real attack, damage, movement, input override, timeout, glide");
|
||||
|
||||
server.runOnServer(s->{var p=player(s);p.getRandom().setSeed(114);var bird=p.level().getEntity(parrot);var quadrants=new HashSet<Integer>();
|
||||
for(int i=0;i<32;i++){
|
||||
CarryPanic.clear(p);CarryPanic.start(bird);var flight=p.getAttached(CarryPanic.FLIGHT);check(flight.duration()>=40&&flight.duration()<=60,"Duration bounded at 2–3 seconds");quadrants.add((int)(flight.yaw()/(Math.PI/2)));
|
||||
for(int t=0;t<=flight.duration();t++){var v=flight.velocity(flight.start()+t);check(v.horizontalDistance()<=.331&&v.y>=0&&v.y<=.241,"Velocity bounded without cumulative impulses");}
|
||||
}
|
||||
check(quadrants.size()==4,"Server random headings cover every quadrant");
|
||||
});c.waitFor(m->CarryPanic.active(m.player),100);
|
||||
c.getInput().holdKey(o->o.keyShift);c.waitTicks(2);c.getInput().holdKey(o->o.keyUse);c.waitTicks(1);c.getInput().releaseKey(o->o.keyUse);c.getInput().releaseKey(o->o.keyShift);
|
||||
c.waitFor(m->!CarryService.carrying(m.player)&&!CarryPanic.active(m.player),100);
|
||||
server.runOnServer(s->{check(!player(s).hasAttached(CarryPanic.FLIGHT),"Native drop clears flight immediately");player(s).level().getEntity(parrot).discard();reset(player(s));});
|
||||
|
||||
int cow=server.computeOnServer(s->carry(player(s),EntityTypes.COW).getId());
|
||||
server.runOnServer(s->{var p=player(s);var animal=(LivingEntity)p.level().getEntity(cow);float before=animal.getHealth();p.attack(animal);
|
||||
check(CarryService.blocksAttack(p,animal)&&animal.getHealth()==before&&!animal.hurtServer(p.level(),p.damageSources().playerAttack(p),1)&&!CarryPanic.active(p),"Terrestrial carry pair stays protected");CarryPanic.start(animal);check(!CarryPanic.active(p),"Ground animal cannot launch player");CarryService.release(animal,true);animal.discard();reset(p);});
|
||||
|
||||
// Every supported appearance can trigger the shared flight, including the gliding chicken.
|
||||
server.runOnServer(s->{var p=player(s);for(var type:List.of(EntityTypes.CHICKEN,EntityTypes.BAT,EntityTypes.BEE,EntityTypes.ALLAY,EntityTypes.PARROT,EntityTypes.BLAZE,EntityTypes.BREEZE)){
|
||||
var animal=carry(p,type);check(CarryGlide.flyer(animal)&&CarryPanic.canStrike(p,animal),"Carried flying species can be struck: "+type);
|
||||
check(animal.hurtServer(p.level(),p.damageSources().playerAttack(p),1)&&CarryPanic.active(p),"Accepted damage launches carrier: "+type);CarryService.release(animal,true);animal.discard();check(!p.hasAttached(CarryPanic.FLIGHT),"Release clears each flight");
|
||||
}});
|
||||
int external=server.computeOnServer(s->carry(player(s),EntityTypes.PARROT).getId());
|
||||
server.runOnServer(s->{var p=player(s);var bird=(LivingEntity)p.level().getEntity(external);var zombie=EntityTypes.ZOMBIE.create(p.level(),EntitySpawnReason.COMMAND);
|
||||
check(bird.hurtServer(p.level(),p.damageSources().mobAttack(zombie),1)&&CarryPanic.active(p),"External accepted hit also triggers flight");
|
||||
p.getAbilities().flying=true;CarryGlide.tick(p);check(!p.hasAttached(CarryPanic.FLIGHT),"Creative flying invalidates propulsion");p.getAbilities().flying=false;
|
||||
CarryPanic.start(bird);bird.hurtServer(p.level(),p.damageSources().mobAttack(zombie),100);CarryGlide.tick(p);check(!CarryPanic.active(p),"Death stops propulsion");CarryService.detach(p);bird.discard();reset(p);});
|
||||
|
||||
int ceiling=server.computeOnServer(s->{var p=player(s);var bird=carry(p,EntityTypes.PARROT);for(int x=-3;x<=3;x++)for(int z=-3;z<=3;z++)p.level().setBlockAndUpdate(new BlockPos(x,4,z),Blocks.STONE.defaultBlockState());return bird.getId();});
|
||||
c.waitFor(m->m.level.getEntity(ceiling)!=null&&CarryService.carried(m.level.getEntity(ceiling)),100);
|
||||
server.runOnServer(s->{var p=player(s);((LivingEntity)p.level().getEntity(ceiling)).hurtServer(p.level(),p.damageSources().playerAttack(p),1);});c.waitTicks(12);
|
||||
server.runOnServer(s->{var p=player(s);check(p.getY()<2&&p.level().noBlockCollision(p,p.getBoundingBox().deflate(.02)),"Player cannot fly through low ceiling");check(!CarryService.carrying(p)&&!CarryPanic.active(p),"Blocked carried animal is safely released");
|
||||
p.level().getEntity(ceiling).discard();for(int x=-3;x<=3;x++)for(int z=-3;z<=3;z++)p.level().removeBlock(new BlockPos(x,4,z),false);reset(p);
|
||||
var egg=new ItemStack(Items.PARROT_SPAWN_EGG);var size=new CompoundTag();size.putInt("schema",1);size.putInt("per_mille",1000);CustomData.update(DataComponents.CUSTOM_DATA,egg,t->t.put(FamiliarSize.KEY,size));AccessoryAccess.of(p).setItem(AccessoryContainer.FAMILIAR,egg);p.inventoryMenu.broadcastChanges();});
|
||||
c.waitTicks(25);
|
||||
int familiar=server.computeOnServer(s->{var p=player(s);var pet=FamiliarService.active(p);check(pet!=null,"Familiar spawned");pet.setNoAi(true);pet.setPos(p.position().add(0,0,2));check(CarryService.interact(p,pet),"Flying familiar picked up");return pet.getId();});
|
||||
c.waitFor(m->m.level.getEntity(familiar)!=null&&CarryService.carried(m.level.getEntity(familiar)),100);
|
||||
float petHealth=server.computeOnServer(s->FamiliarService.active(player(s)).getHealth());attack(c,familiar);c.waitFor(m->CarryPanic.active(m.player),100);
|
||||
server.runOnServer(s->{var p=player(s);check(FamiliarService.active(p).getHealth()==petHealth&&CarryPanic.active(p),"Friendly familiar tap launches without hurting it");CarryService.detach(p);check(!p.hasAttached(CarryPanic.FLIGHT),"Lifecycle detach clears synchronized flight");});
|
||||
System.out.println("CARRY114_PASS random flight, native ordinary/familiar strikes, seven species, outside hits, ground protection, drop/death/creative cancellation, ceiling collisions, timeout and normal glide");
|
||||
}finally{intro.set(false);c.getInput().releaseKey(o->o.keyUp);c.getInput().releaseKey(o->o.keyRight);c.getInput().releaseKey(o->o.keyJump);c.getInput().releaseKey(o->o.keyShift);c.getInput().releaseKey(o->o.keyAttack);c.getInput().releaseKey(o->o.keyUse);}
|
||||
}
|
||||
private static void attack(ClientGameTestContext c,int id){
|
||||
c.waitFor(m->m.level.getEntity(id).position().distanceTo(CarryService.head(m.player,m.level.getEntity(id)))<.01,100);
|
||||
c.runOnClient(m->{m.options.setCameraType(CameraType.FIRST_PERSON);m.player.lookAt(net.minecraft.commands.arguments.EntityAnchorArgument.Anchor.EYES,m.level.getEntity(id).getBoundingBox().getCenter());});c.waitTicks(3);
|
||||
c.runOnClient(m->check(m.hitResult instanceof EntityHitResult hit&&hit.getEntity().getId()==id,"Native cursor can pick own carried flyer: hit="+m.hitResult+", target="+m.level.getEntity(id).getBoundingBox()+", eye="+m.player.getEyePosition()+", view="+m.player.getLookAngle()+", eligible="+CarryPanic.canStrike(m.player,m.level.getEntity(id))));
|
||||
c.getInput().holdKey(o->o.keyAttack);c.waitTicks(1);c.getInput().releaseKey(o->o.keyAttack);
|
||||
}
|
||||
private static Mob carry(ServerPlayer p,EntityType<? extends Mob> type){var mob=type.create(p.level(),EntitySpawnReason.COMMAND);mob.setNoAi(true);mob.setPos(p.position().add(0,0,2));p.level().addFreshEntity(mob);check(CarryService.interact(p,mob),"Animal picked up: "+type);return mob;}
|
||||
private static void reset(ServerPlayer p){CarryService.detach(p);p.teleportTo(.5,1,.5);p.setDeltaMovement(Vec3.ZERO);p.resetFallDistance();p.setShiftKeyDown(false);}
|
||||
private static ServerPlayer player(MinecraftServer s){return s.getPlayerList().getPlayers().getFirst();}
|
||||
private static KeyEvent enter(){return new KeyEvent(InputConstants.KEY_RETURN,0,0);}
|
||||
private static void check(boolean ok,String message){if(!ok)throw new AssertionError(message);}
|
||||
}
|
||||
+90
@@ -0,0 +1,90 @@
|
||||
package fr.koka.sanctuary.client;
|
||||
|
||||
import fr.koka.sanctuary.plans.*;
|
||||
import java.util.*;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.core.*;
|
||||
import net.minecraft.nbt.CompoundTag;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.world.InteractionHand;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.item.context.BlockPlaceContext;
|
||||
import net.minecraft.world.level.block.*;
|
||||
import net.minecraft.world.level.storage.TagValueInput;
|
||||
import net.minecraft.util.ProblemReporter;
|
||||
import net.minecraft.world.phys.*;
|
||||
|
||||
/** Checks emitted render geometry, not a duplicate of the placement implementation. */
|
||||
final class Sculpture113ClientChecks {
|
||||
static void placementContexts(ServerPlayer player){
|
||||
for(var pos:List.of(new BlockPos(2,0,-2),new BlockPos(-2,0,2))){
|
||||
for(var edge:Direction.Plane.HORIZONTAL){
|
||||
var top=new Vec3(pos.getX()+.5+edge.getStepX()*.4,pos.getY()+1,pos.getZ()+.5+edge.getStepZ()*.4);
|
||||
var context=new BlockPlaceContext(player,InteractionHand.MAIN_HAND,new ItemStack(Statuary.ITEM),new BlockHitResult(top,Direction.UP,pos,false));
|
||||
var state=Statuary.BLOCK.getStateForPlacement(context);
|
||||
check(context.getClickedPos().equals(pos.above())&&state.getValue(Statuary.ANCHOR)==SculptureAnchor.of(edge),"Top hit selects aimed edge at positive/negative coordinates");
|
||||
var side=Vec3.atCenterOf(pos).add(edge.getStepX()*.5,0,edge.getStepZ()*.5);
|
||||
context=new BlockPlaceContext(player,InteractionHand.MAIN_HAND,new ItemStack(Statuary.ITEM),new BlockHitResult(side,edge,pos,false));
|
||||
check(Statuary.BLOCK.getStateForPlacement(context).getValue(Statuary.ANCHOR)==SculptureAnchor.of(edge.getOpposite()),"Side hit anchors against supporting block");
|
||||
}
|
||||
}
|
||||
for(var facing:Direction.Plane.HORIZONTAL)for(var anchor:SculptureAnchor.values()){
|
||||
var state=Statuary.BLOCK.defaultBlockState().setValue(Statuary.FACING,facing).setValue(Statuary.ANCHOR,anchor);
|
||||
for(var rotation:Rotation.values()){
|
||||
var actual=state.rotate(rotation);
|
||||
check(actual.getValue(Statuary.ANCHOR)==(anchor==SculptureAnchor.CENTER?anchor:SculptureAnchor.of(rotation.rotate(Direction.valueOf(anchor.name())))),"Structure rotates anchor independently of facing");
|
||||
}
|
||||
for(var mirror:Mirror.values()){
|
||||
var actual=state.mirror(mirror);
|
||||
check(actual.getValue(Statuary.ANCHOR)==(anchor==SculptureAnchor.CENTER?anchor:SculptureAnchor.of(mirror.mirror(Direction.valueOf(anchor.name())))),"Structure mirrors anchor independently of facing");
|
||||
}
|
||||
var legacy=net.minecraft.nbt.NbtUtils.writeBlockState(state);legacy.getCompoundOrEmpty("properties").remove("anchor");
|
||||
var loaded=net.minecraft.nbt.NbtUtils.readBlockState(net.minecraft.core.registries.BuiltInRegistries.BLOCK,legacy);
|
||||
check(loaded.getValue(Statuary.FACING)==facing&&loaded.getValue(Statuary.ANCHOR)==SculptureAnchor.CENTER,"Beta.111/112 state keeps facing and defaults anchor");
|
||||
}
|
||||
}
|
||||
static void geometry(Minecraft client,String model){
|
||||
// Every footprint, odd and even, through all orientations and saved anchor states.
|
||||
for(int w=1;w<=16;w++)for(int d=1;d<=16;d++){
|
||||
var sculpture=new VoxelSculpture(model,"Grid fixture",w,2,d,List.of(new VoxelSculpture.Cell(0,0,0,0xFFFF0000),new VoxelSculpture.Cell(w-1,1,d-1,0xFF0000FF)));
|
||||
for(var facing:Direction.Plane.HORIZONTAL)for(var anchor:SculptureAnchor.values())rendered(entity(client,sculpture,facing,anchor));
|
||||
}
|
||||
// Transparent margins in imported models must not leave a gap at the aimed edge.
|
||||
var padded=new VoxelSculpture(model,"Padded fixture",13,2,15,List.of(new VoxelSculpture.Cell(2,0,3,-1),new VoxelSculpture.Cell(6,1,10,-1)));
|
||||
for(var facing:Direction.Plane.HORIZONTAL)for(var anchor:SculptureAnchor.values())rendered(entity(client,padded,facing,anchor));
|
||||
// Colored markers identify forward motion after rotation, independent of the origin shift.
|
||||
var sculpture=new VoxelSculpture(model,"Direction fixture",3,2,7,List.of(new VoxelSculpture.Cell(0,0,0,0xFFFF0000),new VoxelSculpture.Cell(0,0,6,0xFF0000FF),new VoxelSculpture.Cell(2,1,0,0xFF00FF00)));
|
||||
for(var facing:Direction.Plane.HORIZONTAL){
|
||||
var mesh=mesh(entity(client,sculpture,facing,SculptureAnchor.CENTER));
|
||||
var red=mesh.stream().filter(q->q.color()==0xFFFF0000).findFirst().orElseThrow();var blue=mesh.stream().filter(q->q.color()==0xFF0000FF).findFirst().orElseThrow();
|
||||
check(blue.x()-red.x()==facing.getStepX()*6/16f&&blue.z()-red.z()==facing.getStepZ()*6/16f,"Colored front faces gaze");
|
||||
}
|
||||
var itemMesh=new StatuaryRenderer.ItemRenderer().extractArgument(StatuaryEntity.item(sculpture));
|
||||
check(itemMesh.equals(mesh(entity(client,sculpture,Direction.SOUTH,SculptureAnchor.CENTER))),"Item preview shares exact centered grid geometry");
|
||||
System.out.println("SCULPTURE113_GEOMETRY_PASS 5120 footprints/orientations/anchors and colored direction markers");
|
||||
}
|
||||
private static StatuaryEntity entity(Minecraft client,VoxelSculpture sculpture,Direction facing,SculptureAnchor anchor){
|
||||
var entity=new StatuaryEntity(BlockPos.ZERO,Statuary.BLOCK.defaultBlockState().setValue(Statuary.FACING,facing).setValue(Statuary.ANCHOR,anchor));entity.setLevel(client.level);
|
||||
var data=new CompoundTag();data.put("sculpture",StatuaryEntity.encode(sculpture));entity.loadWithComponents(TagValueInput.create(ProblemReporter.DISCARDING,client.level.registryAccess(),data));return entity;
|
||||
}
|
||||
private static List<StatuaryRenderer.Quad> mesh(StatuaryEntity entity){
|
||||
var renderer=new StatuaryRenderer();var state=renderer.createRenderState();renderer.extractRenderState(entity,state,0,Vec3.ZERO,null);return state.mesh;
|
||||
}
|
||||
static void rendered(StatuaryEntity entity){
|
||||
var mesh=mesh(entity);check(!mesh.isEmpty(),"Sculpture has visible geometry");
|
||||
float minX=1,minZ=1,maxX=0,maxZ=0;
|
||||
for(var q:mesh){
|
||||
check(q.x()*16==Math.rint(q.x()*16)&&q.y()*16==Math.rint(q.y()*16)&&q.z()*16==Math.rint(q.z()*16),"Every rendered voxel is on the exact 1/16 grid");
|
||||
check(q.x()>=0&&q.y()>=0&&q.z()>=0&&q.x()+1/16f<=1&&q.y()+1/16f<=1&&q.z()+1/16f<=1,"No voxel spills out of its block");
|
||||
minX=Math.min(minX,q.x());maxX=Math.max(maxX,q.x()+1/16f);minZ=Math.min(minZ,q.z());maxZ=Math.max(maxZ,q.z()+1/16f);
|
||||
}
|
||||
var anchor=entity.getBlockState().getValue(Statuary.ANCHOR);
|
||||
switch(anchor){
|
||||
case NORTH -> check(minZ==0,"North edge flush");case SOUTH -> check(maxZ==1,"South edge flush");
|
||||
case WEST -> check(minX==0,"West edge flush");case EAST -> check(maxX==1,"East edge flush");default -> {}
|
||||
}
|
||||
if(anchor!=SculptureAnchor.WEST&&anchor!=SculptureAnchor.EAST)check(Math.abs(minX-(1-maxX))<=1/16f,"Other axis centered to nearest whole voxel");
|
||||
if(anchor!=SculptureAnchor.NORTH&&anchor!=SculptureAnchor.SOUTH)check(Math.abs(minZ-(1-maxZ))<=1/16f,"Other axis centered to nearest whole voxel");
|
||||
}
|
||||
private static void check(boolean ok,String message){if(!ok)throw new AssertionError(message);}
|
||||
}
|
||||
+94
-7
@@ -23,6 +23,7 @@ import net.minecraft.network.chat.contents.TranslatableContents;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.world.InteractionHand;
|
||||
import net.minecraft.world.entity.item.ItemEntity;
|
||||
import net.minecraft.world.entity.player.Inventory;
|
||||
import net.minecraft.world.inventory.ContainerInput;
|
||||
import net.minecraft.world.item.*;
|
||||
import net.minecraft.world.level.GameType;
|
||||
@@ -31,6 +32,7 @@ import net.minecraft.world.phys.*;
|
||||
|
||||
/** Actual GLB texture/scene import, paid native crafting, placement, drops, reconnect and plan coexistence. */
|
||||
public final class Statuary106ClientChecks implements FabricClientGameTest {
|
||||
private static final Map<Direction,BlockPos> DIRECTIONS=Map.of(Direction.NORTH,new BlockPos(0,1,4),Direction.SOUTH,new BlockPos(4,1,4),Direction.EAST,new BlockPos(-4,1,4),Direction.WEST,new BlockPos(4,1,-2));
|
||||
private static final BlockPos WORKSHOP=new BlockPos(2,1,2),SCULPTURE=new BlockPos(2,1,0),TABLE=new BlockPos(-3,1,2);
|
||||
private static final AtomicReference<VoxelSculpture> ORIGINAL=new AtomicReference<>();
|
||||
private static final AtomicReference<String> MODEL=new AtomicReference<>();
|
||||
@@ -61,16 +63,40 @@ public final class Statuary106ClientChecks implements FabricClientGameTest {
|
||||
s.setWorldType(s.getNormalPresetList().stream().filter(e->e.preset()!=null&&e.preset().getRegisteredName().equals("sanctuary_test:flat")).findFirst().orElseThrow());}).create()){
|
||||
saved=world.getWorldSave();c.waitFor(m->ProgressionClient.active()&&m.player!=null&&m.gui.screen()==null&&!IntroSpawnFlash.active(),1200);
|
||||
var server=world.getServer();
|
||||
server.runOnServer(s->{var p=player(s);p.teleportTo(.5,1,.5);p.getInventory().clearContent();p.setGameMode(GameType.SURVIVAL);for(int x=-4;x<=6;x++)for(int z=-2;z<=6;z++)p.level().setBlockAndUpdate(new BlockPos(x,0,z),Blocks.STONE.defaultBlockState());p.level().setBlockAndUpdate(WORKSHOP,Statuary.WORKSHOP.defaultBlockState());
|
||||
server.runOnServer(s->{var p=player(s);p.teleportTo(.5,1,.5);p.getInventory().clearContent();p.setGameMode(GameType.SURVIVAL);for(int x=-7;x<=9;x++)for(int z=-5;z<=9;z++)p.level().setBlockAndUpdate(new BlockPos(x,0,z),Blocks.STONE.defaultBlockState());p.level().setBlockAndUpdate(WORKSHOP,Statuary.WORKSHOP.defaultBlockState());
|
||||
var legacy=net.minecraft.nbt.NbtUtils.writeBlockState(Statuary.BLOCK.defaultBlockState());legacy.remove("properties");
|
||||
check(net.minecraft.nbt.NbtUtils.readBlockState(net.minecraft.core.registries.BuiltInRegistries.BLOCK,legacy).getValue(Statuary.FACING)==Direction.SOUTH,"Legacy state defaults to unchanged model orientation");
|
||||
check(net.minecraft.nbt.NbtUtils.readBlockState(net.minecraft.core.registries.BuiltInRegistries.BLOCK,legacy).getValue(Statuary.ANCHOR)==SculptureAnchor.CENTER,"Legacy state keeps centered anchor");
|
||||
Sculpture113ClientChecks.placementContexts(p);
|
||||
for(var q:Metatable.members(TABLE))p.level().setBlockAndUpdate(q,Blocks.CRAFTING_TABLE.defaultBlockState());check(Metatable.assemble(p,TABLE),"Metabli");
|
||||
s.getCommands().performPrefixedCommand(s.createCommandSourceStack(),"sanctuary time vanilla");s.getCommands().performPrefixedCommand(s.createCommandSourceStack(),"time set noon");});
|
||||
c.waitFor(m->m.level.getBlockState(WORKSHOP).is(Statuary.WORKSHOP)&&!m.player.isCreative(),200);
|
||||
c.runOnClient(m->{m.options.guiScale().set(2);m.getWindow().setWindowed(1280,800);m.resizeGui();});
|
||||
c.runOnClient(m->{m.options.guiScale().set(2);m.getWindow().setWindowed(1280,800);m.resizeGui();Sculpture113ClientChecks.geometry(m,source.modelId());});
|
||||
open(c);c.runOnClient(m->((ClayWorkshopScreen)m.gui.screen()).onFilesDrop(List.of(file)));
|
||||
c.waitFor(m->!((ClayWorkshopScreen)m.gui.screen()).busy()&&((ClayWorkshopScreen)m.gui.screen()).selected()!=null,200);c.waitTicks(10);
|
||||
server.runOnServer(s->{var p=player(s);var menu=(ClayWorkshopMenu)p.containerMenu;check(menu.getSlot(1).getItem().isEmpty(),"No clay, no result");menu.getSlot(0).set(new ItemStack(Items.CLAY,3));menu.broadcastChanges();});
|
||||
server.runOnServer(s->{var p=player(s);var menu=(ClayWorkshopMenu)p.containerMenu;check(menu.getSlot(1).getItem().isEmpty(),"No clay, no result");
|
||||
menu.getSlot(ClayWorkshopMenu.HOTBAR_START).set(new ItemStack(Items.CLAY,3));
|
||||
p.getInventory().setItem(18,new ItemStack(Items.CLAY,64));p.getInventory().setItem(Inventory.SLOT_OFFHAND,new ItemStack(Items.CLAY,12));
|
||||
fr.koka.sanctuary.inventory.SanctuaryOverflowAccess.of(p).setItem(17,new ItemStack(Items.CLAY,64));menu.broadcastChanges();});
|
||||
c.waitFor(m->m.player.containerMenu.getSlot(ClayWorkshopMenu.HOTBAR_START).getItem().getCount()==3,100);
|
||||
c.runOnClient(m->m.gameMode.handleContainerInput(m.player.containerMenu.containerId,ClayWorkshopMenu.HOTBAR_START,0,ContainerInput.QUICK_MOVE,m.player));
|
||||
c.waitFor(m->m.player.containerMenu.getSlot(1).getItem().is(Statuary.ITEM),100);
|
||||
c.runOnClient(m->check(m.player.containerMenu.getSlot(1).getItem().getHoverName().getString().equals("Clay Sculpture"),"Crafted sculpture has translated type name EN"));
|
||||
c.takeScreenshot("statuary106-workshop-en");
|
||||
checkWorkshopLayout(c);
|
||||
c.runOnClient(m->{m.options.guiScale().set(3);m.resizeGui();});c.waitTicks(3);checkWorkshopLayout(c);c.takeScreenshot("statuary111-workshop-en-scale3");
|
||||
// Hidden inventory, overflow and offhand must never join this menu or feed its input.
|
||||
c.runOnClient(m->check(!((SanctuaryContainerScreenGeometry)(Object)m.gui.screen()).sanctuary$hasIntegratedInventory(),"No expanding player-inventory panel"));
|
||||
server.runOnServer(s->{var p=player(s);var menu=(ClayWorkshopMenu)p.containerMenu;
|
||||
check(menu.slots.size()==11&&menu.getSlot(0).getItem().getCount()==3,"Two crafting slots plus exactly nine hotbar slots");
|
||||
check(!((fr.koka.sanctuary.inventory.SanctuaryExtendedInventoryMenu)(Object)menu).sanctuary$hasExtendedInventory(),"No hidden overflow destination");
|
||||
menu.clicked(0,Inventory.SLOT_OFFHAND,ContainerInput.SWAP,p);menu.clicked(11,0,ContainerInput.PICKUP,p);menu.clicked(-2,0,ContainerInput.PICKUP,p);
|
||||
check(menu.quickMoveStack(p,11).isEmpty()&&menu.getCarried().isEmpty(),"Out-of-menu slot rejected");
|
||||
check(menu.getSlot(0).getItem().getCount()==3&&p.getOffhandItem().getCount()==12,"Offhand swap refused");
|
||||
check(p.getInventory().getItem(18).getCount()==64&&fr.koka.sanctuary.inventory.SanctuaryOverflowAccess.of(p).getItem(17).getCount()==64,"Reserve clay untouched");
|
||||
p.getInventory().setItem(18,ItemStack.EMPTY);p.getInventory().setItem(Inventory.SLOT_OFFHAND,ItemStack.EMPTY);fr.koka.sanctuary.inventory.SanctuaryOverflowAccess.of(p).setItem(17,ItemStack.EMPTY);
|
||||
});
|
||||
c.runOnClient(m->{m.options.guiScale().set(2);m.resizeGui();});
|
||||
server.runOnServer(s->{var menu=(ClayWorkshopMenu)player(s).containerMenu;ORIGINAL.set(StatuaryEntity.sculpture(menu.getSlot(1).getItem()));check(ORIGINAL.get().equals(source),"Plain clay keeps exact colors and model ID");
|
||||
for(var family:ColoredBricks.families()){var colored=ClayWorkshopMenu.material(source,new ItemStack(family.clay()));check(colored.modelId().equals(source.modelId())&&colored.cells().size()==source.cells().size()&&colored.cells().stream().map(VoxelSculpture.Cell::color).distinct().count()==1,"All 16 clays keep geometry and ID, recolor uniformly");}});
|
||||
c.runOnClient(m->m.gameMode.handleContainerInput(m.player.containerMenu.containerId,1,0,ContainerInput.PICKUP,m.player));
|
||||
@@ -87,11 +113,39 @@ public final class Statuary106ClientChecks implements FabricClientGameTest {
|
||||
server.runOnServer(s->{var p=player(s);p.level().destroyBlock(SCULPTURE,true);var dropped=p.level().getEntitiesOfClass(ItemEntity.class,new AABB(SCULPTURE).inflate(1)).stream().filter(e->e.getItem().is(Statuary.ITEM)).findFirst().orElseThrow();
|
||||
check(StatuaryEntity.sculpture(dropped.getItem()).equals(source),"Native drop keeps name, ID and voxels");p.getInventory().setItem(p.getInventory().getSelectedSlot(),dropped.getItem().copy());dropped.discard();p.inventoryMenu.broadcastFullState();});
|
||||
c.waitFor(m->m.level.getBlockState(SCULPTURE).isAir()&&m.player.getMainHandItem().is(Statuary.ITEM),100);place(c,SCULPTURE);server.waitFor(s->player(s).level().getBlockEntity(SCULPTURE) instanceof StatuaryEntity e&&source.equals(e.sculpture()),100);c.waitFor(m->m.level.getBlockEntity(SCULPTURE) instanceof StatuaryEntity e&&source.equals(e.sculpture()),100);
|
||||
// Real use-key placements must follow the gaze on both sides of the network.
|
||||
for(var direction:Direction.Plane.HORIZONTAL){
|
||||
var pos=DIRECTIONS.get(direction);double px=pos.getX()+.5-direction.getStepX()*2,pz=pos.getZ()+.5-direction.getStepZ()*2;
|
||||
server.runOnServer(s->{var p=player(s);p.teleportTo(px,1,pz);p.getInventory().setItem(p.getInventory().getSelectedSlot(),StatuaryEntity.item(source));p.inventoryMenu.broadcastFullState();});
|
||||
c.waitFor(m->m.player.distanceToSqr(px,1,pz)<.05&&m.player.getMainHandItem().is(Statuary.ITEM),150);place(c,pos,direction.getOpposite());
|
||||
server.waitFor(s->player(s).level().getBlockState(pos).is(Statuary.BLOCK),100);
|
||||
server.runOnServer(s->{var state=player(s).level().getBlockState(pos);check(state.getValue(Statuary.FACING)==direction,"Native placement faces gaze: "+direction);
|
||||
check(state.getValue(Statuary.ANCHOR)==SculptureAnchor.of(direction.getOpposite()),"Aimed edge stored on server");
|
||||
check(state.rotate(net.minecraft.world.level.block.Rotation.CLOCKWISE_90).getValue(Statuary.FACING)==direction.getClockWise(),"Structure rotation follows native directions");
|
||||
check(state.mirror(net.minecraft.world.level.block.Mirror.LEFT_RIGHT).getValue(Statuary.FACING)==(direction.getAxis()==Direction.Axis.Z?direction.getOpposite():direction),"Structure mirror follows native directions");
|
||||
check(((StatuaryEntity)player(s).level().getBlockEntity(pos)).sculpture().equals(source),"Placement never rewrites model voxels");});
|
||||
c.waitFor(m->m.level.getBlockEntity(pos) instanceof StatuaryEntity e&&source.equals(e.sculpture()),100);
|
||||
c.runOnClient(m->{var entity=(StatuaryEntity)m.level.getBlockEntity(pos);check(entity.getBlockState().getValue(Statuary.FACING)==direction,"Client receives direction");
|
||||
check(entity.getBlockState().getValue(Statuary.ANCHOR)==SculptureAnchor.of(direction.getOpposite()),"Client receives aimed edge");
|
||||
Sculpture113ClientChecks.rendered(entity);});
|
||||
c.takeScreenshot("statuary111-facing-"+direction.getSerializedName());
|
||||
}
|
||||
// Re-place a dropped north-facing model while looking west: no baked or cumulative turn.
|
||||
var north=DIRECTIONS.get(Direction.NORTH);var west=DIRECTIONS.get(Direction.WEST);
|
||||
server.runOnServer(s->{var p=player(s);p.level().destroyBlock(north,true);p.level().removeBlock(west,false);
|
||||
var drop=p.level().getEntitiesOfClass(ItemEntity.class,new AABB(north).inflate(1)).stream().filter(e->e.getItem().is(Statuary.ITEM)).findFirst().orElseThrow();
|
||||
check(source.equals(StatuaryEntity.sculpture(drop.getItem())),"Direction does not alter dropped item");
|
||||
p.getInventory().setItem(p.getInventory().getSelectedSlot(),drop.getItem().copy());drop.discard();p.teleportTo(west.getX()+2.5,1,west.getZ()+.5);p.inventoryMenu.broadcastFullState();});
|
||||
c.waitFor(m->m.player.distanceToSqr(west.getX()+2.5,1,west.getZ()+.5)<.05&&m.player.getMainHandItem().is(Statuary.ITEM)&&m.level.getBlockState(west).isAir(),100);place(c,west,Direction.EAST);
|
||||
server.waitFor(s->player(s).level().getBlockState(west).is(Statuary.BLOCK),100);
|
||||
server.runOnServer(s->check(player(s).level().getBlockState(west).getValue(Statuary.FACING)==Direction.WEST,"Dropped statue takes new gaze"));
|
||||
server.runOnServer(s->player(s).teleportTo(.5,1,.5));c.waitFor(m->m.player.distanceToSqr(.5,1,.5)<.05,100);
|
||||
// Reuse the same file with colored clay; server, rather than the client, chooses the tint.
|
||||
server.runOnServer(s->player(s).getInventory().setItem(player(s).getInventory().getSelectedSlot(),ItemStack.EMPTY));c.waitTicks(4);open(c);
|
||||
c.runOnClient(m->((ClayWorkshopScreen)m.gui.screen()).load(file));c.waitFor(m->!((ClayWorkshopScreen)m.gui.screen()).busy()&&((ClayWorkshopScreen)m.gui.screen()).selected()!=null,200);c.waitTicks(10);
|
||||
server.runOnServer(s->{var menu=(ClayWorkshopMenu)player(s).containerMenu;var blue=ColoredBricks.families().stream().filter(f->f.color()==DyeColor.BLUE).findFirst().orElseThrow();menu.getSlot(0).set(new ItemStack(blue.clay(),2));menu.broadcastChanges();});
|
||||
c.waitFor(m->m.player.containerMenu.getSlot(1).getItem().is(Statuary.ITEM),100);language(c,"fr_fr");c.takeScreenshot("statuary106-workshop-fr-blue");
|
||||
c.waitFor(m->m.player.containerMenu.getSlot(1).getItem().is(Statuary.ITEM),100);language(c,"fr_fr");c.runOnClient(m->check(m.player.containerMenu.getSlot(1).getItem().getHoverName().getString().equals("Sculpture d’argile"),"Crafted sculpture has translated type name FR"));c.takeScreenshot("statuary106-workshop-fr-blue");
|
||||
checkWorkshopLayout(c);
|
||||
server.runOnServer(s->{var menu=(ClayWorkshopMenu)player(s).containerMenu;var blue=StatuaryEntity.sculpture(menu.getSlot(1).getItem());check(blue.modelId().equals(MODEL.get())&&blue.cells().stream().allMatch(v->v.color()==0xFFA0A7BC),"Blue input overrides uploaded model colors");});
|
||||
c.runOnClient(m->m.gameMode.handleContainerInput(m.player.containerMenu.containerId,1,0,ContainerInput.QUICK_MOVE,m.player));c.waitTicks(10);
|
||||
server.runOnServer(s->{var p=player(s);check(p.containerMenu.getSlot(0).getItem().isEmpty(),"Shift crafting consumes stock");check(count(p,Statuary.ITEM)==2,"Shift crafting produces exactly paid quantity");});close(c);
|
||||
@@ -101,8 +155,24 @@ public final class Statuary106ClientChecks implements FabricClientGameTest {
|
||||
c.runOnClient(m->button(m.gui.screen(),"sanctuary.plans.generate").onPress(enter()));c.waitFor(m->!PlanClient.busy&&PlanClient.plan!=null&&m.gui.screen()==null,400);
|
||||
c.runOnClient(m->check(PlanClient.plan.height()==32&&PlanClient.plan.cells().size()>source.cells().size(),"Same source produces real-block statue at selected height"));
|
||||
server.runOnServer(s->check(((StatuaryEntity)player(s).level().getBlockEntity(SCULPTURE)).sculpture().equals(source),"Plan generation leaves existing miniature intact"));
|
||||
// Saturated hotbar must not spill crafted statues into hidden inventory or consume unpaid clay.
|
||||
c.runOnClient(m->PlanClient.clear());open(c);c.runOnClient(m->((ClayWorkshopScreen)m.gui.screen()).load(file));
|
||||
c.waitFor(m->!((ClayWorkshopScreen)m.gui.screen()).busy()&&((ClayWorkshopScreen)m.gui.screen()).selected()!=null,200);c.waitTicks(10);
|
||||
var savedHotbar=new AtomicReference<List<ItemStack>>();
|
||||
server.runOnServer(s->{var p=player(s);var menu=(ClayWorkshopMenu)p.containerMenu;savedHotbar.set(java.util.stream.IntStream.range(0,9).mapToObj(i->p.getInventory().getItem(i).copy()).toList());
|
||||
for(int i=0;i<9;i++)p.getInventory().setItem(i,new ItemStack(Items.STONE,64));menu.getSlot(0).set(new ItemStack(Items.CLAY,2));menu.broadcastChanges();});
|
||||
c.waitFor(m->m.player.containerMenu.getSlot(1).getItem().is(Statuary.ITEM)&&m.player.containerMenu.getSlot(2).getItem().is(Items.STONE),100);
|
||||
c.runOnClient(m->m.gameMode.handleContainerInput(m.player.containerMenu.containerId,1,0,ContainerInput.QUICK_MOVE,m.player));c.waitTicks(6);
|
||||
server.runOnServer(s->{var p=player(s);var menu=(ClayWorkshopMenu)p.containerMenu;check(menu.getSlot(0).getItem().getCount()==2&&count(p,Statuary.ITEM)==0,"Full hotbar consumes nothing and does not fill reserve");
|
||||
var nearlyFull=StatuaryEntity.item(source);nearlyFull.setCount(63);p.getInventory().setItem(8,nearlyFull);menu.broadcastChanges();});
|
||||
c.waitFor(m->m.player.containerMenu.getSlot(10).getItem().getCount()==63,100);
|
||||
c.runOnClient(m->m.gameMode.handleContainerInput(m.player.containerMenu.containerId,1,0,ContainerInput.QUICK_MOVE,m.player));c.waitTicks(6);
|
||||
server.runOnServer(s->{var p=player(s);var menu=(ClayWorkshopMenu)p.containerMenu;check(p.getInventory().getItem(8).getCount()==64&&count(p,Statuary.ITEM)==64&&menu.getSlot(0).getItem().getCount()==1,"Partial merge crafts only the one statue that fits");
|
||||
p.getInventory().setItem(0,ItemStack.EMPTY);menu.broadcastChanges();});
|
||||
close(c);server.waitFor(s->player(s).containerMenu==player(s).inventoryMenu,100);
|
||||
server.runOnServer(s->{var p=player(s);check(p.getInventory().getItem(0).is(Items.CLAY)&&p.getInventory().getItem(0).getCount()==1,"Closing returns the unused clay");for(int i=0;i<9;i++)p.getInventory().setItem(i,savedHotbar.get().get(i));p.inventoryMenu.broadcastFullState();});
|
||||
// Cancel an import by closing the native menu; late worker completion cannot craft in the next session.
|
||||
c.runOnClient(m->PlanClient.clear());open(c);c.runOnClient(m->{((ClayWorkshopScreen)m.gui.screen()).load(file);m.gui.screen().onClose();});c.waitTicks(12);
|
||||
open(c);c.runOnClient(m->{((ClayWorkshopScreen)m.gui.screen()).load(file);m.gui.screen().onClose();});c.waitTicks(12);
|
||||
server.runOnServer(s->check(player(s).containerMenu==player(s).inventoryMenu,"Closed workshop releases crafting session"));
|
||||
check(Arrays.equals(Files.readAllBytes(file),fixture),"Original GLB never rewritten");
|
||||
}
|
||||
@@ -110,17 +180,34 @@ public final class Statuary106ClientChecks implements FabricClientGameTest {
|
||||
try(var world=saved.open()){
|
||||
c.waitFor(m->m.player!=null&&m.gui.screen()==null,1200);var server=world.getServer();
|
||||
server.runOnServer(s->{var entity=(StatuaryEntity)player(s).level().getBlockEntity(SCULPTURE);check(entity.sculpture().equals(ORIGINAL.get()),"Server save and reload preserve source-independent voxels");check(count(player(s),Statuary.ITEM)==2,"Colored inventory statues survive reconnect");});
|
||||
for(var direction:List.of(Direction.SOUTH,Direction.EAST,Direction.WEST)){
|
||||
var pos=DIRECTIONS.get(direction);
|
||||
server.runOnServer(s->check(player(s).level().getBlockState(pos).getValue(Statuary.FACING)==direction&&((StatuaryEntity)player(s).level().getBlockEntity(pos)).sculpture().equals(ORIGINAL.get()),"Direction and model survive server save/reload"));
|
||||
server.runOnServer(s->check(player(s).level().getBlockState(pos).getValue(Statuary.ANCHOR)==SculptureAnchor.of(direction.getOpposite()),"Anchor survives server save/reload"));
|
||||
c.waitFor(m->m.level.getBlockEntity(pos) instanceof StatuaryEntity e&&ORIGINAL.get().equals(e.sculpture())&&e.getBlockState().getValue(Statuary.FACING)==direction,150);
|
||||
}
|
||||
c.waitFor(m->m.level.getBlockEntity(SCULPTURE) instanceof StatuaryEntity e&&e.sculpture()!=null,150);c.runOnClient(m->check(((StatuaryEntity)m.level.getBlockEntity(SCULPTURE)).sculpture().equals(ORIGINAL.get()),"Fresh client receives server model without file"));
|
||||
c.getInput().lookAt(SCULPTURE);c.takeScreenshot("statuary106-reload-no-source");
|
||||
}
|
||||
System.out.println("STATUARY106_PASS GLB textured scene, bounded import, shared folder filters, native paid crafting, 16 clay colors, model ID, shift-click, place/drop/re-place, Metabli plan, cancellation, server save/reconnect without source");
|
||||
System.out.println("STATUARY111_PASS nine-slice GUI FR/EN scales 2/3, four native gaze placements, renderer directions, structure rotation/mirror, legacy state default, drop/re-place and persisted orientation");
|
||||
System.out.println("STATUARY113_PASS clay sculpture FR/EN, native aimed edges, voxel grid across every footprint/orientation/anchor, side placement, legacy states, rotated/mirrored anchor, save/reconnect");
|
||||
System.out.println("STATUARY112_PASS fixed compact hotbar-only menu, native input transfer, hidden slots/offhand rejected, full hotbar no consumption, partial merge exact cost, closing restitution");
|
||||
}catch(IOException e){throw new AssertionError(e);}finally{try{Files.deleteIfExists(file);}catch(IOException ignored){}}
|
||||
}
|
||||
private static ServerPlayer player(net.minecraft.server.MinecraftServer s){return s.getPlayerList().getPlayers().getFirst();}
|
||||
private static void checkWorkshopLayout(ClientGameTestContext c){c.runOnClient(m->{var screen=(ClayWorkshopScreen)m.gui.screen();var refresh=button(screen,"sanctuary.statuary.refresh");int left=refresh.getX()-10,top=refresh.getY()-127;
|
||||
check(top>=0&&top+screen.panelHeight()<=screen.height,"Workshop frame fits GUI height");
|
||||
check(screen.panelHeight()==202&&screen.getMenu().slots.size()==11,"Compact height and nine player slots regardless of reserve capacity");
|
||||
for(int i=0;i<9;i++){var slot=screen.getMenu().getSlot(i+2);check(slot.container==m.player.getInventory()&&slot.getContainerSlot()==i,"Displayed player slot belongs to active native hotbar");}
|
||||
for(var slot:screen.getMenu().slots)if(slot.x>=0&&slot.y>=0)check(slot.x>3&&slot.x+17<310&&slot.y>3&&slot.y+17<screen.panelHeight(),"Every visible slot fits inside nine-slice frame");
|
||||
for(var widget:screen.children())if(widget instanceof Button b)check(b.getX()>=left&&b.getRight()<=left+310&&b.getY()>=top&&b.getBottom()<=top+145,"Every action stays clear of explanations and inventory");
|
||||
});}
|
||||
private static void close(ClientGameTestContext c){c.runOnClient(m->m.gui.screen().onClose());c.waitFor(m->m.gui.screen()==null,100);}
|
||||
private static void open(ClientGameTestContext c){c.runOnClient(m->m.gameMode.useItemOn(m.player,InteractionHand.MAIN_HAND,new BlockHitResult(Vec3.atCenterOf(WORKSHOP),Direction.UP,WORKSHOP,false)));c.waitForScreen(ClayWorkshopScreen.class);}
|
||||
private static void place(ClientGameTestContext c,BlockPos pos){
|
||||
c.runOnClient(m->m.player.lookAt(net.minecraft.commands.arguments.EntityAnchorArgument.Anchor.EYES,new Vec3(pos.getX()+.5,pos.getY()-.001,pos.getZ()+.5)));c.waitTicks(3);
|
||||
private static void place(ClientGameTestContext c,BlockPos pos){place(c,pos,null);}
|
||||
private static void place(ClientGameTestContext c,BlockPos pos,Direction edge){
|
||||
c.runOnClient(m->m.player.lookAt(net.minecraft.commands.arguments.EntityAnchorArgument.Anchor.EYES,new Vec3(pos.getX()+.5+(edge==null?0:edge.getStepX()*.4),pos.getY()-.001,pos.getZ()+.5+(edge==null?0:edge.getStepZ()*.4))));c.waitTicks(3);
|
||||
c.runOnClient(m->check(m.hitResult instanceof BlockHitResult hit&&hit.getBlockPos().equals(pos.below())&&hit.getDirection()==Direction.UP,"Native cursor targets support top face"));
|
||||
c.getInput().holdKey(o->o.keyUse);c.waitTicks(1);c.getInput().releaseKey(o->o.keyUse);
|
||||
}
|
||||
|
||||
@@ -17,16 +17,21 @@ import net.minecraft.world.item.ItemStack;
|
||||
|
||||
/** File selection is local; the two ordinary inventory slots and every crafted item are server-owned. */
|
||||
public final class ClayWorkshopScreen extends AbstractContainerScreen<ClayWorkshopMenu> {
|
||||
private static final Identifier FRAME=fr.koka.sanctuary.SanctuaryMod.id("container/clay_workshop/frame");
|
||||
private static final Identifier SLOT=Identifier.withDefaultNamespace("container/slot");
|
||||
private static final Identifier SELECTION=fr.koka.sanctuary.SanctuaryMod.id("hud/hotbar_selection");
|
||||
private static final ExecutorService WORKER=Executors.newSingleThreadExecutor(r->{var t=new Thread(r,"Sanctuary clay models");t.setDaemon(true);return t;});
|
||||
private List<Path> files=List.of();private int page,generation,ticks;private boolean busy,pending;
|
||||
private VoxelSculpture source,previewSource;private ItemStack preview=ItemStack.EMPTY,lastClay=ItemStack.EMPTY;
|
||||
private Component status=text("choose_model");private Button rotate;
|
||||
public ClayWorkshopScreen(ClayWorkshopMenu menu,Inventory inv,Component title){super(menu,inv,title,310,242);inventoryLabelX=138;inventoryLabelY=130;}
|
||||
public ClayWorkshopScreen(ClayWorkshopMenu menu,Inventory inv,Component title){super(menu,inv,title,310,202);}
|
||||
public static Component text(String key,Object...args){return Component.translatable("sanctuary.statuary."+key,args);}
|
||||
public static void register(){
|
||||
net.minecraft.client.gui.screens.MenuScreens.register(Statuary.MENU,ClayWorkshopScreen::new);StatuaryRenderer.register();
|
||||
net.fabricmc.fabric.api.client.item.v1.ItemTooltipCallback.EVENT.register((stack,context,flags,lines)->{
|
||||
if(!stack.is(Statuary.ITEM))return;var sculpture=StatuaryEntity.sculpture(stack);if(sculpture!=null){lines.add(text("model_name",sculpture.name()));if(flags.isAdvanced())lines.add(Component.literal(sculpture.modelId()));}
|
||||
if(!stack.is(Statuary.ITEM))return;var sculpture=StatuaryEntity.sculpture(stack);if(sculpture!=null){
|
||||
if(!stack.getHoverName().getString().equals(Statuary.BLOCK.getName().getString()))lines.add(Statuary.BLOCK.getName());
|
||||
lines.add(text("model_name",sculpture.name()));if(flags.isAdvanced())lines.add(Component.literal(sculpture.modelId()));}
|
||||
});
|
||||
}
|
||||
@Override protected void init(){
|
||||
@@ -34,12 +39,13 @@ public final class ClayWorkshopScreen extends AbstractContainerScreen<ClayWorksh
|
||||
try{files=CreationLibrary.files(CreationLibrary.Kind.MODEL);}catch(Exception e){status=ModelImportErrors.message(e);}
|
||||
page=Math.min(page,Math.max(0,(files.size()-1)/4));
|
||||
for(int i=0;i<4;i++){int at=page*4+i;if(at>=files.size())break;var file=files.get(at);
|
||||
var label=Component.literal(file.getFileName().toString());var b=addRenderableWidget(Button.builder(label,p->load(file)).bounds(leftPos+10,topPos+29+i*18,128,17).build());b.setTooltip(Tooltip.create(label));}
|
||||
addRenderableWidget(Button.builder(Component.literal("←"),b->{page--;rebuildWidgets();}).bounds(leftPos+10,topPos+103,22,18).build()).active=page>0;
|
||||
addRenderableWidget(Button.builder(text("folder"),b->folder()).bounds(leftPos+35,topPos+103,78,18).build());
|
||||
addRenderableWidget(Button.builder(Component.literal("→"),b->{page++;rebuildWidgets();}).bounds(leftPos+116,topPos+103,22,18).build()).active=(page+1)*4<files.size();
|
||||
addRenderableWidget(Button.builder(text("refresh"),b->rebuildWidgets()).bounds(leftPos+10,topPos+123,128,18).build());
|
||||
rotate=addRenderableWidget(Button.builder(text("rotate"),b->{if(source!=null){source=source.rotate();pending=true;}}).bounds(leftPos+215,topPos+112,92,18).build());rotate.active=source!=null&&!busy;
|
||||
var label=Component.literal(file.getFileName().toString());var b=addRenderableWidget(Button.builder(label,p->load(file)).bounds(leftPos+10,topPos+32+i*18,128,17).build());b.setTooltip(Tooltip.create(label));}
|
||||
addRenderableWidget(Button.builder(Component.literal("←"),b->{page--;rebuildWidgets();}).bounds(leftPos+10,topPos+106,22,18).build()).active=page>0;
|
||||
addRenderableWidget(Button.builder(text("folder"),b->folder()).bounds(leftPos+35,topPos+106,78,18).build());
|
||||
addRenderableWidget(Button.builder(Component.literal("→"),b->{page++;rebuildWidgets();}).bounds(leftPos+116,topPos+106,22,18).build()).active=(page+1)*4<files.size();
|
||||
addRenderableWidget(Button.builder(text("refresh"),b->rebuildWidgets()).bounds(leftPos+10,topPos+127,128,18).build());
|
||||
rotate=addRenderableWidget(Button.builder(text("rotate"),b->{if(source!=null){source=source.rotate();pending=true;}}).bounds(leftPos+148,topPos+127,152,18).build());rotate.active=source!=null&&!busy;
|
||||
rotate.setTooltip(Tooltip.create(text("placement")));
|
||||
}
|
||||
private void folder(){try{Files.createDirectories(CreationLibrary.directory());Blaze3D.openPath(CreationLibrary.directory());}catch(Exception e){status=ModelImportErrors.message(e);}}
|
||||
public void load(Path file){
|
||||
@@ -62,19 +68,25 @@ public final class ClayWorkshopScreen extends AbstractContainerScreen<ClayWorksh
|
||||
}
|
||||
@Override public void onFilesDrop(List<Path> files){if(!files.isEmpty())load(files.getFirst());}
|
||||
@Override public void removed(){generation++;super.removed();}
|
||||
int panelHeight(){return imageHeight;}
|
||||
@Override public void extractBackground(GuiGraphicsExtractor g,int mx,int my,float partial){
|
||||
g.fill(leftPos,topPos,leftPos+imageWidth,topPos+142,0xFFE2DDD3);g.fill(leftPos+2,topPos+2,leftPos+imageWidth-2,topPos+140,0xFFC6C6C6);
|
||||
g.fill(leftPos+147,topPos+24,leftPos+imageWidth-10,topPos+87,0xFF92958E);
|
||||
for(var slot:menu.slots)if(slot.x> -1000)g.blitSprite(RenderPipelines.GUI_TEXTURED,Identifier.withDefaultNamespace("container/slot"),leftPos+slot.x-1,topPos+slot.y-1,18,18);
|
||||
g.blitSprite(RenderPipelines.GUI_TEXTURED,FRAME,leftPos,topPos,imageWidth,panelHeight());
|
||||
g.fill(leftPos+148,topPos+23,leftPos+300,topPos+92,0xFF8B8B8B);
|
||||
g.fill(leftPos+149,topPos+24,leftPos+299,topPos+91,0xFF373737);
|
||||
for(var slot:menu.slots)if(slot.x>=0&&slot.y>=0)g.blitSprite(RenderPipelines.GUI_TEXTURED,SLOT,leftPos+slot.x-1,topPos+slot.y-1,18,18);
|
||||
if(minecraft.player!=null)g.blitSprite(RenderPipelines.GUI_TEXTURED,SELECTION,
|
||||
leftPos+ClayWorkshopMenu.HOTBAR_X-3+minecraft.player.getInventory().getSelectedSlot()*18,topPos+ClayWorkshopMenu.HOTBAR_Y-3,22,21);
|
||||
if(!preview.isEmpty()){
|
||||
g.pose().pushMatrix();g.pose().translate(leftPos+195,topPos+28);g.pose().scale(3.5f,3.5f);g.item(preview,0,0);g.pose().popMatrix();
|
||||
g.pose().pushMatrix();g.pose().translate(leftPos+194,topPos+27);g.pose().scale(3.75f,3.75f);g.item(preview,0,0);g.pose().popMatrix();
|
||||
}
|
||||
g.text(font,Component.literal("→"),leftPos+254,topPos+97,0xFF404040,false);
|
||||
var detail=font.split(status,128);for(int i=0;i<Math.min(4,detail.size());i++)g.text(font,detail.get(i),leftPos+10,topPos+150+i*font.lineHeight,0xFFE7E7E7);
|
||||
g.text(font,Component.literal("→"),leftPos+254,topPos+111,0xFF404040,false);
|
||||
var detail=font.split(status,290);for(int i=0;i<Math.min(2,detail.size());i++)g.text(font,detail.get(i),leftPos+10,topPos+150+i*font.lineHeight,0xFF404040,false);
|
||||
if(detail.size()>2&&mx>=leftPos+10&&mx<leftPos+300&&my>=topPos+148&&my<topPos+174)g.setTooltipForNextFrame(font,status,mx,my);
|
||||
if(files.isEmpty())g.text(font,text("empty"),leftPos+10,topPos+32,0xFF404040,false);
|
||||
}
|
||||
@Override protected void extractLabels(GuiGraphicsExtractor g,int mx,int my){
|
||||
g.text(font,title,10,8,0xFF303030,false);g.text(font,text("input"),203,80,0xFF303030,false);g.text(font,text("output"),imageWidth-10-font.width(text("output")),80,0xFF303030,false);
|
||||
g.text(font,title,10,8,0xFF303030,false);g.text(font,text("models"),10,22,0xFF404040,false);
|
||||
g.text(font,text("input"),230-font.width(text("input"))/2,95,0xFF303030,false);g.text(font,text("output"),imageWidth-10-font.width(text("output")),95,0xFF303030,false);
|
||||
}
|
||||
public boolean busy(){return busy;}
|
||||
public VoxelSculpture selected(){return source;}
|
||||
|
||||
@@ -90,6 +90,8 @@ public final class HotbarClient {
|
||||
}
|
||||
return true;
|
||||
});
|
||||
// This workshop exposes only the current hotbar; Tab remains native focus navigation.
|
||||
if (screen instanceof ClayWorkshopScreen) return;
|
||||
ScreenKeyboardEvents.afterKeyRelease(screen).register((s,event) -> {if (key.matches(event)) held=false;});
|
||||
ScreenKeyboardEvents.allowKeyPress(screen).register((s,event) -> {
|
||||
if (!ProgressionClient.active() || !key.matches(event) || event.isEscape() || editing(s.getFocused())) return true;
|
||||
|
||||
@@ -14,6 +14,7 @@ import net.minecraft.client.renderer.special.SpecialModelRenderer;
|
||||
import net.minecraft.client.renderer.state.level.CameraRenderState;
|
||||
import net.minecraft.client.renderer.texture.OverlayTexture;
|
||||
import net.minecraft.core.component.DataComponents;
|
||||
import net.minecraft.core.Direction;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.item.component.TypedEntityData;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
@@ -22,8 +23,9 @@ import org.joml.Vector3fc;
|
||||
|
||||
/** Shared block/item render geometry: opaque visible faces, generated once per cached sculpture. */
|
||||
public final class StatuaryRenderer implements BlockEntityRenderer<StatuaryEntity,StatuaryRenderer.State> {
|
||||
private record Quad(float x,float y,float z,int side,int color){}
|
||||
private static final Map<VoxelSculpture,List<Quad>> MESHES=new LinkedHashMap<>(32,.75f,true);
|
||||
record Quad(float x,float y,float z,int side,int color){}
|
||||
private record Placement(VoxelSculpture sculpture,Direction facing,SculptureAnchor anchor){}
|
||||
private static final Map<Placement,List<Quad>> MESHES=new LinkedHashMap<>(32,.75f,true);
|
||||
private static final Map<TypedEntityData<?>,VoxelSculpture> ITEMS=new LinkedHashMap<>(32,.75f,true);
|
||||
private static final float[][] FACES={
|
||||
{0,0,0, 0,1,0, 1,1,0, 1,0,0, 0,0,-1},
|
||||
@@ -37,19 +39,30 @@ public final class StatuaryRenderer implements BlockEntityRenderer<StatuaryEntit
|
||||
public static final class State extends BlockEntityRenderState {List<Quad> mesh=List.of();}
|
||||
public State createRenderState(){return new State();}
|
||||
@Override public void extractRenderState(StatuaryEntity entity,State state,float delta,Vec3 camera,ModelFeatureRenderer.CrumblingOverlay breakProgress){
|
||||
BlockEntityRenderer.super.extractRenderState(entity,state,delta,camera,breakProgress);state.mesh=mesh(entity.sculpture());
|
||||
BlockEntityRenderer.super.extractRenderState(entity,state,delta,camera,breakProgress);
|
||||
state.mesh=mesh(entity.sculpture(),entity.getBlockState().getValue(Statuary.FACING),entity.getBlockState().getValue(Statuary.ANCHOR));
|
||||
}
|
||||
public void submit(State state,PoseStack pose,SubmitNodeCollector collector,CameraRenderState camera){draw(state.mesh,pose,collector,state.lightCoords,OverlayTexture.NO_OVERLAY);}
|
||||
private static List<Quad> mesh(VoxelSculpture sculpture){
|
||||
if(sculpture==null)return List.of();var cached=MESHES.get(sculpture);if(cached!=null)return cached;
|
||||
public void submit(State state,PoseStack pose,SubmitNodeCollector collector,CameraRenderState camera){
|
||||
draw(state.mesh,pose,collector,state.lightCoords,OverlayTexture.NO_OVERLAY);
|
||||
}
|
||||
private static List<Quad> mesh(VoxelSculpture sculpture,Direction facing,SculptureAnchor anchor){
|
||||
if(sculpture==null)return List.of();var key=new Placement(sculpture,facing,anchor);var cached=MESHES.get(key);if(cached!=null)return cached;
|
||||
// Rotate integer cells before anchoring: odd dimensions never introduce half-pixel offsets.
|
||||
int turns=switch(facing){case WEST -> 1;case NORTH -> 2;case EAST -> 3;default -> 0;};
|
||||
for(int i=0;i<turns;i++)sculpture=sculpture.rotate();
|
||||
var occupied=new BitSet(4096);for(var cell:sculpture.cells())occupied.set(cell.key());var quads=new ArrayList<Quad>();
|
||||
float ox=(16-sculpture.width())/32f,oz=(16-sculpture.depth())/32f;
|
||||
int minX=16,minZ=16,maxX=0,maxZ=0;
|
||||
for(var cell:sculpture.cells()){
|
||||
minX=Math.min(minX,cell.x());minZ=Math.min(minZ,cell.z());
|
||||
maxX=Math.max(maxX,cell.x()+1);maxZ=Math.max(maxZ,cell.z()+1);
|
||||
}
|
||||
float ox=(anchor.originX(maxX-minX)-minX)/16f,oz=(anchor.originZ(maxZ-minZ)-minZ)/16f;
|
||||
for(var cell:sculpture.cells())for(int side=0;side<6;side++){
|
||||
var s=STEP[side];int x=cell.x()+s[0],y=cell.y()+s[1],z=cell.z()+s[2];
|
||||
if(x>=0&&x<16&&y>=0&&y<16&&z>=0&&z<16&&occupied.get(x|y<<4|z<<8))continue;
|
||||
quads.add(new Quad(ox+cell.x()/16f,cell.y()/16f,oz+cell.z()/16f,side,cell.color()));
|
||||
}
|
||||
cached=List.copyOf(quads);MESHES.put(sculpture,cached);if(MESHES.size()>32)MESHES.remove(MESHES.keySet().iterator().next());return cached;
|
||||
cached=List.copyOf(quads);MESHES.put(key,cached);if(MESHES.size()>32)MESHES.remove(MESHES.keySet().iterator().next());return cached;
|
||||
}
|
||||
private static void draw(List<Quad> mesh,PoseStack pose,SubmitNodeCollector collector,int light,int overlay){
|
||||
if(mesh.isEmpty())return;
|
||||
@@ -63,7 +76,7 @@ public final class StatuaryRenderer implements BlockEntityRenderer<StatuaryEntit
|
||||
var data=stack.get(DataComponents.BLOCK_ENTITY_DATA);if(data==null)return List.of();
|
||||
var sculpture=ITEMS.get(data);
|
||||
if(sculpture==null){sculpture=StatuaryEntity.sculpture(stack);if(sculpture==null)return List.of();ITEMS.put(data,sculpture);if(ITEMS.size()>64)ITEMS.remove(ITEMS.keySet().iterator().next());}
|
||||
return mesh(sculpture);
|
||||
return mesh(sculpture,Direction.SOUTH,SculptureAnchor.CENTER);
|
||||
}
|
||||
public void submit(List<Quad> mesh,PoseStack pose,SubmitNodeCollector collector,int light,int overlay,boolean foil,int outline){draw(mesh,pose,collector,light,overlay);}
|
||||
public void getExtents(java.util.function.Consumer<Vector3fc> out){out.accept(new Vector3f(0,0,0));out.accept(new Vector3f(1,1,1));}
|
||||
|
||||
@@ -31,6 +31,7 @@ public final class CarryGlide {
|
||||
}
|
||||
public static boolean active(Player p){return supported(p)&&!p.onGround()&&(p.getDeltaMovement().y<=0||lifting(p));}
|
||||
public static void tick(Player p){
|
||||
if(CarryPanic.apply(p))return;
|
||||
boolean lift=lifting(p);
|
||||
if(!lift&&!active(p))return;
|
||||
var velocity=p.getDeltaMovement();
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
package fr.koka.sanctuary.cosmetics;
|
||||
|
||||
import fr.koka.sanctuary.SanctuaryMod;
|
||||
import net.fabricmc.fabric.api.attachment.v1.*;
|
||||
import net.fabricmc.fabric.api.entity.event.v1.ServerLivingEntityEvents;
|
||||
import net.minecraft.network.RegistryFriendlyByteBuf;
|
||||
import net.minecraft.network.codec.StreamCodec;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.world.entity.*;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
|
||||
/** Server-chosen, temporary flight; both sides use the same bounded native movement. */
|
||||
public final class CarryPanic {
|
||||
public record Flight(int animal,long start,int duration,float yaw,float phase){
|
||||
static final StreamCodec<RegistryFriendlyByteBuf,Flight> CODEC=StreamCodec.of(
|
||||
(b,f)->{b.writeVarInt(f.animal);b.writeLong(f.start);b.writeVarInt(f.duration);b.writeFloat(f.yaw);b.writeFloat(f.phase);},
|
||||
b->new Flight(b.readVarInt(),b.readLong(),b.readVarInt(),b.readFloat(),b.readFloat()));
|
||||
public Vec3 velocity(long now){
|
||||
double t=Math.clamp(now-start,0,duration),angle=yaw+Math.sin(t*.27+phase)*.65+Math.sin(t*.61)*.18;
|
||||
double speed=.28+.05*Math.sin(t*.23+phase);
|
||||
double rise=t<5?.24:.12+.045*Math.sin(t*.32+phase);
|
||||
double fade=Math.min(1,(duration-t)/8.0);
|
||||
return new Vec3(-Math.sin(angle)*speed*fade,rise*fade,Math.cos(angle)*speed*fade);
|
||||
}
|
||||
}
|
||||
public static final AttachmentType<Flight> FLIGHT=AttachmentRegistry.create(SanctuaryMod.id("carry_panic"),
|
||||
b->b.syncWith(Flight.CODEC,AttachmentSyncPredicate.all()));
|
||||
private CarryPanic(){}
|
||||
public static void register(){
|
||||
ServerLivingEntityEvents.AFTER_DAMAGE.register((entity,source,base,taken,blocked)->{
|
||||
if(taken>0&&!blocked)start(entity);
|
||||
});
|
||||
}
|
||||
public static boolean canStrike(Entity actor,Entity target){
|
||||
return actor instanceof Player p&&!p.isSpectator()&&p.isAlive()&&target instanceof Mob
|
||||
&&target.getVehicle()==p&&CarryService.carried(target)&&CarryGlide.flyer(target);
|
||||
}
|
||||
public static void start(Entity animal){
|
||||
if(!(animal.getVehicle() instanceof ServerPlayer p)||!CarryService.carried(animal)||!CarryGlide.flyer(animal)||!CarryGlide.supported(p))return;
|
||||
long now=p.level().getGameTime();var previous=p.getAttached(FLIGHT);
|
||||
if(previous!=null&&now-previous.start()<10)return;
|
||||
var random=p.getRandom();
|
||||
var flight=new Flight(animal.getId(),now,40+random.nextInt(21),random.nextFloat()*(float)(Math.PI*2),random.nextFloat()*(float)(Math.PI*2));
|
||||
p.setAttached(FLIGHT,flight);apply(p);p.syncVelocity=true;
|
||||
p.sendOverlayMessage(Component.translatable("sanctuary.carry.panic"));
|
||||
}
|
||||
public static boolean active(Player p){
|
||||
var flight=p.getAttached(FLIGHT);if(flight==null||!CarryGlide.supported(p))return false;
|
||||
long elapsed=p.level().getGameTime()-flight.start();
|
||||
return elapsed>=0&&elapsed<flight.duration()&&p.getPassengers().stream().anyMatch(e->e.getId()==flight.animal()&&CarryService.carried(e)&&CarryGlide.flyer(e));
|
||||
}
|
||||
public static boolean apply(Player p){
|
||||
if(!active(p)){
|
||||
if(!p.level().isClientSide()&&p.hasAttached(FLIGHT))p.removeAttached(FLIGHT);
|
||||
return false;
|
||||
}
|
||||
p.setDeltaMovement(p.getAttached(FLIGHT).velocity(p.level().getGameTime()));p.resetFallDistance();return true;
|
||||
}
|
||||
public static void clear(Player p){if(p.hasAttached(FLIGHT))p.removeAttached(FLIGHT);}
|
||||
}
|
||||
@@ -46,6 +46,7 @@ public final class CarryService {
|
||||
&&!fr.koka.sanctuary.boats.BoatPassengerInteractions.aboardTogether(a,b)
|
||||
&&!fr.koka.sanctuary.boats.BoatPassengerInteractions.aboardTogether(b,a));}
|
||||
private static Entity root(Entity e){while(carried(e))e=e.getVehicle();return e;}
|
||||
public static boolean blocksAttack(Entity attacker,Entity target){return pair(attacker,target)&&!CarryPanic.canStrike(attacker,target);}
|
||||
/** One seat per head; the chain includes babies even though they add no carrying penalty. */
|
||||
private static Entity passenger(Entity e){
|
||||
return e instanceof Player&&e.getPassengers().size()==1&&carried(e.getFirstPassenger())?e.getFirstPassenger():null;
|
||||
@@ -60,6 +61,7 @@ public final class CarryService {
|
||||
};
|
||||
}
|
||||
public static void register(){
|
||||
CarryPanic.register();
|
||||
net.fabricmc.fabric.api.networking.v1.PayloadTypeRegistry.serverboundPlay().register(Drop.TYPE,Drop.CODEC);
|
||||
net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking.registerGlobalReceiver(Drop.TYPE,(request,context)->context.server().execute(()->{
|
||||
if(active(context.player()))for(var passenger:List.copyOf(context.player().getPassengers()))if(carried(passenger))release(passenger,true);
|
||||
@@ -72,7 +74,7 @@ public final class CarryService {
|
||||
if(!success)player.sendOverlayMessage(Component.translatable("sanctuary.carry.unavailable"));
|
||||
return InteractionResult.SUCCESS;
|
||||
});
|
||||
AttackEntityCallback.EVENT.register((p,level,hand,target,hit)->pair(p,target)?InteractionResult.FAIL:InteractionResult.PASS);
|
||||
AttackEntityCallback.EVENT.register((p,level,hand,target,hit)->blocksAttack(p,target)?InteractionResult.FAIL:InteractionResult.PASS);
|
||||
ServerTickEvents.END_SERVER_TICK.register(s->{
|
||||
for(var rider:new ArrayList<>(RIDES.keySet())){
|
||||
var ride=RIDES.get(rider);if(ride==null||ride.carrier.level().getServer()!=s)continue;
|
||||
@@ -187,7 +189,7 @@ public final class CarryService {
|
||||
public static boolean mayDismount(Player p){var ride=RIDES.get(p);return ride==null||ride.releasedShift;}
|
||||
public static void removed(Entity rider){
|
||||
var ride=RIDES.remove(rider);rider.removeAttached(CARRIED);
|
||||
if(ride!=null){refreshBelow(ride.carrier);ride.carrier.connection.send(new net.minecraft.network.protocol.game.ClientboundSetPassengersPacket(ride.carrier));}
|
||||
if(ride!=null){CarryPanic.clear(ride.carrier);refreshBelow(ride.carrier);ride.carrier.connection.send(new net.minecraft.network.protocol.game.ClientboundSetPassengersPacket(ride.carrier));}
|
||||
}
|
||||
public static boolean release(Entity rider,boolean place){
|
||||
var ride=RIDES.get(rider);if(ride==null){rider.removeAttached(CARRIED);return false;}
|
||||
@@ -209,6 +211,7 @@ public final class CarryService {
|
||||
return true;
|
||||
}
|
||||
public static void detach(ServerPlayer p){
|
||||
CarryPanic.clear(p);
|
||||
// Put down the upper group before removing its support, without orphaning its riders.
|
||||
for(var e:List.copyOf(p.getPassengers()))if(carried(e))release(e,true);
|
||||
if(RIDES.containsKey(p))release(p,true);
|
||||
|
||||
@@ -245,7 +245,7 @@ public final class FamiliarEntity extends PathfinderMob {
|
||||
return super.causeFallDamage(distance,multiplier,source);
|
||||
}
|
||||
@Override public boolean hurtServer(ServerLevel level,DamageSource source,float amount) {
|
||||
boolean hurt=fr.koka.sanctuary.familiar.FamiliarBattle.hurt(this,source,amount);if(hurt)playTapSound(source);return hurt;
|
||||
boolean hurt=fr.koka.sanctuary.familiar.FamiliarBattle.hurt(this,source,amount);if(hurt){playTapSound(source);CarryPanic.start(this);}return hurt;
|
||||
}
|
||||
@Override public boolean isIgnoringBlockTriggers() {return true;}
|
||||
@Override public boolean canBeHitByProjectile() {return true;}
|
||||
@@ -316,11 +316,12 @@ public final class FamiliarEntity extends PathfinderMob {
|
||||
}
|
||||
/** Native feedback without damage callbacks, enchantments, aggro or durability. */
|
||||
public void tap(ServerPlayer player) {
|
||||
if(hurtTime>0||!isAlive()||player.level()!=level()||player.isSpectator()||CarryService.pair(player,this))return;
|
||||
if(hurtTime>0||!isAlive()||player.level()!=level()||player.isSpectator()||CarryService.blocksAttack(player,this))return;
|
||||
var source=damageSources().playerAttack(player);
|
||||
if(!HeadEquipment.get(this).isEmpty()){MobHeadActions.activate(this,source,true);}
|
||||
animateHurt(player.getYRot());
|
||||
((ServerLevel)level()).broadcastDamageEvent(this,source);
|
||||
if(CarryPanic.canStrike(player,this)){CarryPanic.start(this);playTapSound(source);return;}
|
||||
double dx=player.getX()-getX(),dz=player.getZ()-getZ();
|
||||
if(dx*dx+dz*dz<1.0E-5){var look=player.getLookAngle();dx=-look.x;dz=-look.z;}
|
||||
knockback(.4,dx,dz,source,0);
|
||||
|
||||
@@ -29,7 +29,7 @@ public final class FamiliarService {
|
||||
net.fabricmc.fabric.api.event.player.AttackEntityCallback.EVENT.register((player,level,hand,target,hit)->{
|
||||
if(!(target instanceof FamiliarEntity pet)||player.isSpectator())return net.minecraft.world.InteractionResult.PASS;
|
||||
if(pet.owner()!=null&&fr.koka.sanctuary.familiar.FamiliarDuels.fighting(pet.owner()))return net.minecraft.world.InteractionResult.FAIL;
|
||||
if(CarryService.pair(player,pet))return net.minecraft.world.InteractionResult.FAIL;
|
||||
if(CarryService.blocksAttack(player,pet))return net.minecraft.world.InteractionResult.FAIL;
|
||||
if(player instanceof ServerPlayer attacker&&fr.koka.sanctuary.familiar.FamiliarBattle.enemy(attacker,pet))return net.minecraft.world.InteractionResult.PASS;
|
||||
if(player instanceof ServerPlayer p&&p.isWithinEntityInteractionRange(pet,0)&&p.hasLineOfSight(pet))pet.tap(p);
|
||||
return net.minecraft.world.InteractionResult.SUCCESS;
|
||||
|
||||
+2
-2
@@ -7,13 +7,13 @@ import org.spongepowered.asm.mixin.*;
|
||||
import org.spongepowered.asm.mixin.injection.*;
|
||||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
|
||||
|
||||
/** Sustained lift is legitimate only while the server sees the equipped, carried large dragon. */
|
||||
/** Flight exemption is limited to server-validated dragon lift or a carried flyer's brief panic. */
|
||||
@Mixin(ServerGamePacketListenerImpl.class)
|
||||
public abstract class CarryFlightConnectionMixin {
|
||||
@Shadow public ServerPlayer player;
|
||||
@Shadow private boolean clientIsFloating;
|
||||
@Inject(method="tick",at=@At("HEAD"))
|
||||
private void sanctuary$carriedFlight(CallbackInfo ci){
|
||||
if(CarryGlide.lifting(player))clientIsFloating=false;
|
||||
if(CarryGlide.lifting(player)||fr.koka.sanctuary.cosmetics.CarryPanic.active(player))clientIsFloating=false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,8 @@ import org.spongepowered.asm.mixin.injection.*;
|
||||
import org.spongepowered.asm.mixin.injection.callback.*;
|
||||
@Mixin(LivingEntity.class)
|
||||
public abstract class CarryGlideMixin {
|
||||
@ModifyVariable(method="travelInAir",at=@At("HEAD"),argsOnly=true)
|
||||
private Vec3 sanctuary$panicSteering(Vec3 input){return (Object)this instanceof Player p&&fr.koka.sanctuary.cosmetics.CarryPanic.active(p)?Vec3.ZERO:input;}
|
||||
@Inject(method="travelInAir",at={@At("HEAD"),@At("TAIL")})
|
||||
private void sanctuary$glide(Vec3 input,CallbackInfo ci){if((Object)this instanceof Player p)CarryGlide.tick(p);}
|
||||
@Inject(method="getEffectiveGravity",at=@At("RETURN"),cancellable=true)
|
||||
|
||||
@@ -16,6 +16,6 @@ public abstract class CarryLivingMixin {
|
||||
}
|
||||
@Inject(method="hurtServer",at=@At("HEAD"),cancellable=true)
|
||||
private void sanctuary$protectPair(ServerLevel level,DamageSource source,float damage,CallbackInfoReturnable<Boolean> ci){
|
||||
if(CarryService.pair((Entity)(Object)this,source.getEntity()))ci.setReturnValue(false);
|
||||
if(CarryService.blocksAttack(source.getEntity(),(Entity)(Object)this))ci.setReturnValue(false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,5 +16,5 @@ public abstract class CarryPlayerMixin {
|
||||
if(p.getVehicle() instanceof fr.koka.sanctuary.cosmetics.FamiliarEntity pet&&!pet.mayDismount())ci.setReturnValue(false);
|
||||
}
|
||||
@Inject(method="attack",at=@At("HEAD"),cancellable=true)
|
||||
private void sanctuary$noPassengerAttack(Entity target,CallbackInfo ci){if(CarryService.pair((Entity)(Object)this,target))ci.cancel();}
|
||||
private void sanctuary$noPassengerAttack(Entity target,CallbackInfo ci){if(CarryService.blocksAttack((Entity)(Object)this,target))ci.cancel();}
|
||||
}
|
||||
|
||||
-1
@@ -331,7 +331,6 @@ abstract class AbstractContainerScreenMixin implements SanctuaryContainerScreenG
|
||||
// These screens paint previews or controls below their lowest machine
|
||||
// slot. Their explicit anchors keep the common panel from covering them.
|
||||
if (screen instanceof StonecutterScreen) return 70;
|
||||
if (screen instanceof fr.koka.sanctuary.client.ClayWorkshopScreen) return 142;
|
||||
if (screen instanceof CartographyTableScreen) return 81;
|
||||
if (screen instanceof EnchantmentScreen) return 73;
|
||||
if (screen instanceof SmithingScreen) return 82;
|
||||
|
||||
+2
-1
@@ -15,7 +15,8 @@ public abstract class BoatPassengerPickMixin {
|
||||
at=@At(value="INVOKE",target="Lnet/minecraft/world/entity/Entity;getRootVehicle()Lnet/minecraft/world/entity/Entity;"))
|
||||
private static Entity sanctuary$pickSeatedMob(Entity candidate, Operation<Entity> original, Entity source) {
|
||||
// Only the player's cursor: projectile collisions and native range/occlusion stay unchanged.
|
||||
return source instanceof LocalPlayer && BoatPassengerInteractions.aboardTogether(source,candidate)
|
||||
return source instanceof LocalPlayer && (BoatPassengerInteractions.aboardTogether(source,candidate)
|
||||
||fr.koka.sanctuary.cosmetics.CarryPanic.canStrike(source,candidate))
|
||||
? candidate : original.call(candidate);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ import net.minecraft.world.phys.*;
|
||||
|
||||
/** Native input/result slots. Server recomputes the color from the actual clay and charges each take. */
|
||||
public final class ClayWorkshopMenu extends AbstractContainerMenu {
|
||||
public static final int HOTBAR_START=2,HOTBAR_END=HOTBAR_START+Inventory.SELECTION_SIZE,HOTBAR_X=75,HOTBAR_Y=176;
|
||||
public record View(BlockPos pos,UUID token){
|
||||
public static final StreamCodec<RegistryFriendlyByteBuf,View> CODEC=StreamCodec.of((b,p)->{b.writeBlockPos(p.pos);b.writeUUID(p.token);},b->new View(b.readBlockPos(),b.readUUID()));
|
||||
}
|
||||
@@ -45,13 +46,14 @@ public final class ClayWorkshopMenu extends AbstractContainerMenu {
|
||||
public ClayWorkshopMenu(int id,Inventory inv,View view){this(id,inv,view,null);}
|
||||
public ClayWorkshopMenu(int id,Inventory inv,View view,Level level){
|
||||
super(Statuary.MENU,id);this.view=view;this.level=level;
|
||||
addSlot(new Slot(input,0,222,92){@Override public boolean mayPlace(ItemStack stack){return clay(stack);}});
|
||||
addSlot(new Slot(result,0,282,92){
|
||||
addSlot(new Slot(input,0,222,106){@Override public boolean mayPlace(ItemStack stack){return clay(stack);}});
|
||||
addSlot(new Slot(result,0,282,106){
|
||||
@Override public boolean mayPlace(ItemStack stack){return false;}
|
||||
@Override public boolean mayPickup(Player p){return mayCraft(p)&&clay(input.getItem(0))&&!getItem().isEmpty();}
|
||||
@Override public void onTake(Player p,ItemStack stack){input.removeItem(0,1);input.setChanged();super.onTake(p,stack);}
|
||||
});
|
||||
addStandardInventorySlots(inv,138,142);
|
||||
// Use the nine carried slots without registering a full inventory/overflow range.
|
||||
addInventoryHotbarSlots(inv,HOTBAR_X,HOTBAR_Y);
|
||||
}
|
||||
// Colors match the published clay palette (tools/colored-bricks-palette.json), in native dye order.
|
||||
private static final int[] CLAY_COLORS={0xC6C4C8,0xD3AA8D,0xD39FD0,0x91B0CA,0xC8BF98,0xA2C69A,0xDAC4DD,0x74767D,0xA9A8AC,0xA8C5C1,0xBE91E3,0xA0A7BC,0xA49A91,0xA0B499,0xB49A99,0x444541};
|
||||
@@ -81,13 +83,17 @@ public final class ClayWorkshopMenu extends AbstractContainerMenu {
|
||||
var hit=new BlockHitResult(Vec3.atCenterOf(view.pos),Direction.UP,view.pos,false);
|
||||
return UseBlockCallback.EVENT.invoker().interact(p,level,InteractionHand.MAIN_HAND,hit)==InteractionResult.PASS&&stillValid(p);
|
||||
}
|
||||
@Override public void clicked(int slot,int button,ContainerInput input,Player p){if(stillValid(p))super.clicked(slot,button,input,p);}
|
||||
@Override public void clicked(int slot,int button,ContainerInput input,Player p){
|
||||
if(!stillValid(p)||slot>=slots.size()||(slot< -1&&slot!=-999))return;
|
||||
if(input==ContainerInput.SWAP&&(button<0||button>=Inventory.SELECTION_SIZE))return;
|
||||
super.clicked(slot,button,input,p);
|
||||
}
|
||||
@Override public boolean canTakeItemForPickAll(ItemStack stack,Slot slot){return slot.container!=result&&super.canTakeItemForPickAll(stack,slot);}
|
||||
@Override public void removed(Player p){super.removed(p);if(level!=null)clearContainer(p,input);result.clearContent();selected=null;}
|
||||
@Override public ItemStack quickMoveStack(Player p,int index){
|
||||
if(index<0||index>=slots.size()||!stillValid(p))return ItemStack.EMPTY;var slot=slots.get(index);if(!slot.mayPickup(p)||!slot.hasItem())return ItemStack.EMPTY;
|
||||
var stack=slot.getItem();var copy=stack.copy();
|
||||
if(index<2){if(!moveItemStackTo(stack,2,38,true))return ItemStack.EMPTY;}
|
||||
if(index<HOTBAR_START){if(!moveItemStackTo(stack,HOTBAR_START,HOTBAR_END,true))return ItemStack.EMPTY;}
|
||||
else if(clay(stack)){if(!moveItemStackTo(stack,0,1,false))return ItemStack.EMPTY;}
|
||||
else return ItemStack.EMPTY;
|
||||
if(stack.isEmpty())slot.set(ItemStack.EMPTY);else slot.setChanged();
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
package fr.koka.sanctuary.plans;
|
||||
|
||||
import java.util.Locale;
|
||||
import net.minecraft.core.Direction;
|
||||
import net.minecraft.util.StringRepresentable;
|
||||
import net.minecraft.world.item.context.BlockPlaceContext;
|
||||
import net.minecraft.world.level.block.Mirror;
|
||||
import net.minecraft.world.level.block.Rotation;
|
||||
|
||||
/** World-space edge of the occupied block; CENTER is the legacy placement. */
|
||||
public enum SculptureAnchor implements StringRepresentable {
|
||||
CENTER(null), NORTH(Direction.NORTH), SOUTH(Direction.SOUTH), EAST(Direction.EAST), WEST(Direction.WEST);
|
||||
|
||||
private final Direction direction;
|
||||
SculptureAnchor(Direction direction){this.direction=direction;}
|
||||
public String getSerializedName(){return name().toLowerCase(Locale.ROOT);}
|
||||
public SculptureAnchor rotate(Rotation rotation){return direction==null?this:of(rotation.rotate(direction));}
|
||||
public SculptureAnchor mirror(Mirror mirror){return direction==null?this:of(mirror.mirror(direction));}
|
||||
public static SculptureAnchor of(Direction direction){return switch(direction){
|
||||
case NORTH -> NORTH;case SOUTH -> SOUTH;case EAST -> EAST;case WEST -> WEST;
|
||||
default -> throw new IllegalArgumentException("Horizontal anchor required");
|
||||
};}
|
||||
public static SculptureAnchor placement(BlockPlaceContext context){
|
||||
var pos=context.getClickedPos();var hit=context.getClickLocation();
|
||||
double x=hit.x-pos.getX()-.5,z=hit.z-pos.getZ()-.5;
|
||||
if(Math.abs(x)<1e-7&&Math.abs(z)<1e-7)return of(context.getHorizontalDirection().getOpposite());
|
||||
if(Math.abs(x)>Math.abs(z))return x<0?WEST:EAST;
|
||||
return z<0?NORTH:SOUTH;
|
||||
}
|
||||
public int originX(int width){return this==WEST?0:this==EAST?VoxelSculpture.EDGE-width:(VoxelSculpture.EDGE-width)/2;}
|
||||
public int originZ(int depth){return this==NORTH?0:this==SOUTH?VoxelSculpture.EDGE-depth:(VoxelSculpture.EDGE-depth)/2;}
|
||||
}
|
||||
@@ -13,10 +13,12 @@ import net.minecraft.world.*;
|
||||
import net.minecraft.world.entity.player.*;
|
||||
import net.minecraft.world.inventory.*;
|
||||
import net.minecraft.world.item.*;
|
||||
import net.minecraft.world.item.context.BlockPlaceContext;
|
||||
import net.minecraft.world.level.*;
|
||||
import net.minecraft.world.level.block.*;
|
||||
import net.minecraft.world.level.block.entity.*;
|
||||
import net.minecraft.world.level.block.state.*;
|
||||
import net.minecraft.world.level.block.state.properties.*;
|
||||
import net.minecraft.world.level.material.PushReaction;
|
||||
import net.minecraft.world.level.storage.loot.LootParams;
|
||||
import net.minecraft.world.level.storage.loot.parameters.LootContextParams;
|
||||
@@ -25,6 +27,8 @@ import net.minecraft.network.chat.Component;
|
||||
|
||||
/** The clay workshop crafts portable sculptures; a placed sculpture is only a decoration. */
|
||||
public final class Statuary {
|
||||
public static final EnumProperty<Direction> FACING=BlockStateProperties.HORIZONTAL_FACING;
|
||||
public static final EnumProperty<SculptureAnchor> ANCHOR=EnumProperty.create("anchor",SculptureAnchor.class);
|
||||
public static final Block BLOCK=block("statuary",new SculptureBlock());
|
||||
public static final Item ITEM=blockItem("statuary",BLOCK);
|
||||
public static final Block WORKSHOP=block("clay_workshop",new WorkshopBlock());
|
||||
@@ -56,7 +60,11 @@ public final class Statuary {
|
||||
private static Item blockItem(String name,Block block){return Registry.register(BuiltInRegistries.ITEM,SanctuaryMod.id(name),new BlockItem(block,new Item.Properties().setId(ResourceKey.create(Registries.ITEM,SanctuaryMod.id(name))).useBlockDescriptionPrefix()));}
|
||||
private static BlockBehaviour.Properties properties(String id){return BlockBehaviour.Properties.of().strength(1.5f).noOcclusion().noLootTable().pushReaction(PushReaction.IMMOVEABLE).setId(ResourceKey.create(Registries.BLOCK,SanctuaryMod.id(id)));}
|
||||
private static final class SculptureBlock extends Block implements EntityBlock {
|
||||
SculptureBlock(){super(Statuary.properties("statuary"));}
|
||||
SculptureBlock(){super(Statuary.properties("statuary"));registerDefaultState(stateDefinition.any().setValue(FACING,Direction.SOUTH).setValue(ANCHOR,SculptureAnchor.CENTER));}
|
||||
@Override protected void createBlockStateDefinition(StateDefinition.Builder<Block,BlockState> builder){builder.add(FACING,ANCHOR);}
|
||||
@Override public BlockState getStateForPlacement(BlockPlaceContext context){return defaultBlockState().setValue(FACING,context.getHorizontalDirection()).setValue(ANCHOR,SculptureAnchor.placement(context));}
|
||||
@Override protected BlockState rotate(BlockState state,Rotation rotation){return state.setValue(FACING,rotation.rotate(state.getValue(FACING))).setValue(ANCHOR,state.getValue(ANCHOR).rotate(rotation));}
|
||||
@Override protected BlockState mirror(BlockState state,Mirror mirror){return state.setValue(FACING,mirror.mirror(state.getValue(FACING))).setValue(ANCHOR,state.getValue(ANCHOR).mirror(mirror));}
|
||||
public BlockEntity newBlockEntity(BlockPos pos,BlockState state){return new StatuaryEntity(pos,state);}
|
||||
@Override protected List<ItemStack> getDrops(BlockState s,LootParams.Builder context){return List.of(context.getOptionalParameter(LootContextParams.BLOCK_ENTITY) instanceof StatuaryEntity entity?entity.item():new ItemStack(ITEM));}
|
||||
@Override protected ItemStack getCloneItemStack(LevelReader l,BlockPos pos,BlockState s,boolean data){return l.getBlockEntity(pos) instanceof StatuaryEntity entity?entity.item():new ItemStack(ITEM);}
|
||||
|
||||
@@ -49,7 +49,7 @@ public final class StatuaryEntity extends BlockEntity {
|
||||
public static ItemStack item(VoxelSculpture sculpture){
|
||||
var item=new ItemStack(Statuary.ITEM);var tag=new CompoundTag();tag.put("sculpture",encode(sculpture));
|
||||
item.set(DataComponents.BLOCK_ENTITY_DATA,TypedEntityData.of(Statuary.TYPE,tag));
|
||||
item.set(DataComponents.ITEM_NAME,net.minecraft.network.chat.Component.literal(sculpture.name()));return item;
|
||||
return item;
|
||||
}
|
||||
public static VoxelSculpture sculpture(ItemStack item){
|
||||
var data=item.get(DataComponents.BLOCK_ENTITY_DATA);if(!item.is(Statuary.ITEM)||data==null||data.type()!=Statuary.TYPE)return null;
|
||||
|
||||
@@ -1013,6 +1013,7 @@
|
||||
"sanctuary.companion.rename_hint": "Rename its egg at an anvil, or use a name tag on your familiar.",
|
||||
"sanctuary.companion.interactions_version_required": "Sanctuary beta.020 is required on both client and server.",
|
||||
"sanctuary.carry.unavailable": "No free space to carry or put down here.",
|
||||
"sanctuary.carry.panic": "The animal panics and carries you away!",
|
||||
"sanctuary.companion.tooltip_size": "Size: %s",
|
||||
"sanctuary.companion.size.tiny": "Tiny",
|
||||
"sanctuary.companion.size.small": "Small",
|
||||
@@ -2150,22 +2151,24 @@
|
||||
"sanctuary.weather.season.summer": "Summer",
|
||||
"sanctuary.weather.season.autumn": "Autumn",
|
||||
"sanctuary.weather.season.winter": "Winter",
|
||||
"block.sanctuary.statuary": "Statuary",
|
||||
"block.sanctuary.statuary": "Clay Sculpture",
|
||||
"block.sanctuary.clay_workshop": "Clay workshop",
|
||||
"sanctuary.plans.imported_models": "My 3D models",
|
||||
"sanctuary.plans.native_models": "Discovered creatures",
|
||||
"sanctuary.plans.imported_model": "3D model → block statue",
|
||||
"sanctuary.plans.model_files_help": "GLB models in schematics/sanctuary/, alongside your schematics. Create a full-block statue here. Craft 16³ miniatures at the clay workshop.",
|
||||
"sanctuary.plans.empty_models": "Place a .glb file in the shared folder, then refresh.",
|
||||
"sanctuary.statuary.choose_model": "Choose a GLB model. 1 clay block = 1 statue. Plain clay keeps model colors; colored clay gives a solid color.",
|
||||
"sanctuary.statuary.choose_model": "Choose a GLB model, then add clay from your hotbar.",
|
||||
"sanctuary.statuary.folder": "Shared folder",
|
||||
"sanctuary.statuary.refresh": "Refresh",
|
||||
"sanctuary.statuary.models": "GLB models",
|
||||
"sanctuary.statuary.placement": "The sculpture faces your gaze and snaps to the aimed block edge, on the pixel grid. Rotate adjusts the model before crafting.",
|
||||
"sanctuary.statuary.rotate": "Rotate",
|
||||
"sanctuary.statuary.working": "Voxelizing to 16 × 16 × 16…",
|
||||
"sanctuary.statuary.ready": "1 clay = 1 statue. Plain clay keeps model colors. Colored clay gives a solid color.",
|
||||
"sanctuary.statuary.ready": "1 clay = 1 sculpture. Plain: model colors. Colored: solid tint.",
|
||||
"sanctuary.statuary.empty": "No .glb files",
|
||||
"sanctuary.statuary.input": "Clay",
|
||||
"sanctuary.statuary.output": "Statue",
|
||||
"sanctuary.statuary.output": "Sculpture",
|
||||
"sanctuary.statuary.model_name": "Model: %s",
|
||||
"sanctuary.statuary.error.file": "Cannot read this local file.",
|
||||
"sanctuary.statuary.error.format": "Invalid or unsupported GLB 2.0. Export a self-contained static model.",
|
||||
|
||||
@@ -1013,6 +1013,7 @@
|
||||
"sanctuary.companion.rename_hint": "Renomme son œuf à l’enclume, ou utilise une étiquette sur lui.",
|
||||
"sanctuary.companion.interactions_version_required": "Sanctuary beta.020 est requis côté client et serveur.",
|
||||
"sanctuary.carry.unavailable": "Pas de place libre pour porter ou déposer ici.",
|
||||
"sanctuary.carry.panic": "L’animal s’affole et vous entraîne !",
|
||||
"sanctuary.companion.tooltip_size": "Taille : %s",
|
||||
"sanctuary.companion.size.tiny": "Minuscule",
|
||||
"sanctuary.companion.size.small": "Petit",
|
||||
@@ -2150,22 +2151,24 @@
|
||||
"sanctuary.weather.season.summer": "Été",
|
||||
"sanctuary.weather.season.autumn": "Automne",
|
||||
"sanctuary.weather.season.winter": "Hiver",
|
||||
"block.sanctuary.statuary": "Statuaire",
|
||||
"block.sanctuary.statuary": "Sculpture d’argile",
|
||||
"block.sanctuary.clay_workshop": "Atelier d’argile",
|
||||
"sanctuary.plans.imported_models": "Mes modèles 3D",
|
||||
"sanctuary.plans.native_models": "Créatures découvertes",
|
||||
"sanctuary.plans.imported_model": "Modèle 3D → statue en blocs",
|
||||
"sanctuary.plans.model_files_help": "Modèles GLB dans schematics/sanctuary/, avec vos schémas. Ici, créez une grande statue en blocs. Les miniatures 16³ se fabriquent à l’atelier d’argile.",
|
||||
"sanctuary.plans.empty_models": "Déposez un fichier .glb dans le dossier commun, puis actualisez.",
|
||||
"sanctuary.statuary.choose_model": "Choisissez un modèle GLB. 1 bloc d’argile = 1 statue. Argile normale : couleurs du modèle ; argile colorée : teinte unie.",
|
||||
"sanctuary.statuary.choose_model": "Choisissez un modèle GLB, puis déposez de l’argile depuis votre barre rapide.",
|
||||
"sanctuary.statuary.folder": "Dossier commun",
|
||||
"sanctuary.statuary.refresh": "Actualiser",
|
||||
"sanctuary.statuary.models": "Modèles GLB",
|
||||
"sanctuary.statuary.placement": "La sculpture suit votre regard et se cale contre le bord visé du bloc, sur la grille de pixels. Tourner ajuste le modèle avant fabrication.",
|
||||
"sanctuary.statuary.rotate": "Tourner",
|
||||
"sanctuary.statuary.working": "Voxelisation en 16 × 16 × 16…",
|
||||
"sanctuary.statuary.ready": "1 argile = 1 statue. Normale : couleurs du modèle. Colorée : teinte unie.",
|
||||
"sanctuary.statuary.ready": "1 argile = 1 sculpture. Normale : couleurs du modèle. Colorée : teinte unie.",
|
||||
"sanctuary.statuary.empty": "Aucun fichier .glb",
|
||||
"sanctuary.statuary.input": "Argile",
|
||||
"sanctuary.statuary.output": "Statue",
|
||||
"sanctuary.statuary.output": "Sculpture",
|
||||
"sanctuary.statuary.model_name": "Modèle : %s",
|
||||
"sanctuary.statuary.error.file": "Impossible de lire ce fichier local.",
|
||||
"sanctuary.statuary.error.format": "GLB 2.0 invalide ou non pris en charge. Exportez un modèle statique autonome.",
|
||||
|
||||
BIN
Binary file not shown.
|
After Width: | Height: | Size: 106 B |
+11
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"gui": {
|
||||
"scaling": {
|
||||
"type": "nine_slice",
|
||||
"width": 26,
|
||||
"height": 26,
|
||||
"border": 4,
|
||||
"stretch_inner": true
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
name = "Sanctuary"
|
||||
author = "KOKA99CAB"
|
||||
version = "beta.110"
|
||||
version = "beta.114"
|
||||
pack-format = "packwiz:1.1.0"
|
||||
|
||||
[index]
|
||||
|
||||
+16
-5
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Publish a verified build to Gitea and advance the metadata-only packwiz branch.
|
||||
"""Publish a verified build to Gitea and advance the packwiz manifests and indexed icon.
|
||||
|
||||
This command publishes externally. Run check/build/assemblePack, commit, and push
|
||||
the source branch first. Git's configured credential helper supplies credentials
|
||||
@@ -115,12 +115,21 @@ def remote_tag_commit(tag):
|
||||
return found.get('refs/tags/' + tag + '^{}') or found.get('refs/tags/' + tag)
|
||||
|
||||
|
||||
def channel_file(relative):
|
||||
"""The indexed pack icon is the only non-TOML file allowed on the channel."""
|
||||
path = Path(relative)
|
||||
return path.suffix == '.toml' or path.as_posix() == 'icon.png'
|
||||
|
||||
|
||||
def publish_channel(origin, version, source_head):
|
||||
staged = ROOT / 'build/packwiz-release'
|
||||
pack.check_index(staged)
|
||||
files = [path for path in staged.rglob('*') if path.is_file()]
|
||||
pack.require(all(path.suffix == '.toml' for path in files),
|
||||
'Distribution branch must contain only generated TOML metadata')
|
||||
pack.require(all(channel_file(path.relative_to(staged)) for path in files),
|
||||
'Distribution branch must contain only generated TOML metadata and the pack icon')
|
||||
if (staged / 'icon.png').exists():
|
||||
pack.require((staged / 'icon.png').read_bytes() == (ROOT / 'packwiz/icon.png').read_bytes(),
|
||||
'Distribution icon differs from the verified source icon')
|
||||
remote = git('ls-remote', '--heads', 'origin', 'refs/heads/' + CHANNEL)
|
||||
with tempfile.TemporaryDirectory(prefix='sanctuary-pack-channel-') as temp:
|
||||
directory = Path(temp)
|
||||
@@ -134,7 +143,7 @@ def publish_channel(origin, version, source_head):
|
||||
git('fetch', '--quiet', 'origin', CHANNEL, cwd=directory)
|
||||
git('checkout', '--quiet', '-b', CHANNEL, 'FETCH_HEAD', cwd=directory)
|
||||
tracked = git('ls-files', cwd=directory).splitlines()
|
||||
pack.require(all(name.endswith('.toml') for name in tracked),
|
||||
pack.require(all(channel_file(name) for name in tracked),
|
||||
'Existing distribution branch contains unexpected files')
|
||||
for name in tracked:
|
||||
(directory / name).unlink()
|
||||
@@ -204,7 +213,9 @@ def main():
|
||||
remote_manifest = response.read()
|
||||
pack.require(remote_manifest == (ROOT / 'build/packwiz-release/pack.toml').read_bytes(),
|
||||
'Published manifest differs from this build; inspect the distribution channel')
|
||||
for path in (ROOT / 'build/packwiz-release').rglob('*.toml'):
|
||||
for path in (ROOT / 'build/packwiz-release').rglob('*'):
|
||||
if not path.is_file():
|
||||
continue
|
||||
relative = path.relative_to(ROOT / 'build/packwiz-release').as_posix()
|
||||
with urlopen(stable_url.removesuffix('pack.toml') + quote(relative), timeout=60) as response:
|
||||
published = response.read()
|
||||
|
||||
Reference in New Issue
Block a user