Compare commits
24
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
14980c993f | ||
|
|
e17433f28f | ||
|
|
0833d4e44f | ||
|
|
07b2eba5c5 | ||
|
|
0f95a5cfc7 | ||
|
|
028d63b64a | ||
|
|
61ba842206 | ||
|
|
0476a74d43 | ||
|
|
d5fe55ddcc | ||
|
|
4a9f50e488 | ||
|
|
b9aabae914 | ||
|
|
b121606b4e | ||
|
|
fa40548db7 | ||
|
|
7ee1e5f094 | ||
|
|
91b4def490 | ||
|
|
122ff939aa | ||
|
|
3fef2d5437 | ||
|
|
75ddc5b682 | ||
|
|
bf09bdfa78 | ||
|
|
2df4f35e48 | ||
|
|
4b4881d922 | ||
|
|
66cd185983 | ||
|
|
bd93dc1cd6 | ||
|
|
2838bf0767 |
+5
-3
@@ -32,9 +32,11 @@ Ambiance possède les trois payloads historiques canaplia:hemp_visual_sync, cana
|
||||
|
||||
## Codex
|
||||
|
||||
Les monstres tués par un joueur peuvent laisser une `canaplia:codex_page` et, très rarement, le `canaplia:codex` complet. Quatre pages récupérées permettent aussi de relier le Codex autour d'un livre. L'objet utilise la texture du livre Minecraft et son écran repose sur les pages vanilla. Il distingue les découvertes personnelles des silhouettes connues par la communauté ; les origines génériques et les conditions détaillées se révèlent progressivement sans envoyer de payload de découverte client-vers-serveur.
|
||||
Les monstres tués par un joueur peuvent laisser une `canaplia:codex_page` et, très rarement, le `canaplia:codex` complet. Quatre pages récupérées permettent aussi d'assembler le Livre interdit autour d'un livre vanilla. Un clic droit sur une page découvre au hasard une entrée encore inconnue et ne consomme la page qu'en cas de nouvelle découverte.
|
||||
|
||||
Les données canaplia:codex sont versionnées. Les clés futures syntaxiquement valides restent conservées lors d'un aller-retour afin qu'une migration ne perde pas les découvertes ajoutées par une version ultérieure.
|
||||
L'ID historique `canaplia:codex` est conservé, mais l'objet s'affiche désormais comme le « Livre interdit » : il utilise la couleur du livre enchanté, ouvre directement les seize entrées et les présente sur les pages claires du livre vanilla. En main, son contenu est strictement personnel. Posé sur un pupitre, il est temporairement inscrit au nom du joueur qui le pose et partage ses découvertes avec tous les lecteurs ; le récupérer en s'accroupissant ou casser le pupitre efface cette inscription.
|
||||
|
||||
Les données `canaplia:codex` restent en version 1. Les clés futures syntaxiquement valides et l'ancien champ partagé restent conservés lors d'un aller-retour afin qu'une migration ne perde aucune découverte, mais les nouvelles découvertes ne sont plus publiées globalement. Aucun payload de découverte n'est accepté du client vers le serveur et le protocole It's Alive reste en version 4.
|
||||
|
||||
## Tickets livrés
|
||||
|
||||
@@ -55,4 +57,4 @@ Les IDs historiques, les clés et le namespace sont inchangés. L'ancien format
|
||||
|
||||
La source historique 2.0.0 demeure **All Rights Reserved**, copyright KOKA99CAB. À la demande explicite du détenteur des droits, ses 224 textures 16×16 ont été restaurées dans l'alpha.209 ; leur provenance est documentée avec les ressources. Aucun fichier Java, modèle, son ou JSON historique n'est copié. Le code d'intégration reste sous GPL-3.0-or-later.
|
||||
|
||||
Les inventaires machine sont dans `pack/migrations/canaplia-foundation-alpha32.json` et `pack/migrations/canaplia-gameplay-alpha33.json`. La release `26.2.0-alpha.209` décrit la correction de gameplay et l'import autorisé dans `pack/migrations/26.2.0-alpha.208-canaplia-fixes-to-alpha.209.json`.
|
||||
Les inventaires machine sont dans `pack/migrations/canaplia-foundation-alpha32.json` et `pack/migrations/canaplia-gameplay-alpha33.json`. La release `26.2.0-alpha.209` décrit la correction de gameplay et l'import autorisé dans `pack/migrations/26.2.0-alpha.208-canaplia-fixes-to-alpha.209.json`; l’alpha.210 décrit le Livre interdit et les autres changements distribués dans `pack/migrations/26.2.0-alpha.209-forbidden-book-and-polish-to-alpha.210.json`.
|
||||
|
||||
@@ -6,6 +6,44 @@ Ce journal suit les **versions du modpack**. Lorsqu'un seul module change, sa ve
|
||||
- Depuis `26.2.0-alpha.6`, le pack et les modules ont des versions indépendantes. I Like To Move It et Only Fun deviennent les cinquième et sixième modules actifs dans l'alpha 46.
|
||||
- Les versions ci-dessous ont été reconstituées à partir des JAR archivés, des packs locaux et du code source actuel. Les dates n'étant pas enregistrées de manière fiable, elles ne sont pas inventées.
|
||||
|
||||
## Non distribué
|
||||
|
||||
Modules modifiés : **It's Alive `0.0.0-alpha.36`**, **Sanctuary `0.0.0-alpha.119`**, **Ambiance `0.0.0-alpha.15`**, **Only Fun `0.0.0-alpha.24`** et **Ouch `0.0.0-alpha.10`**. Le pack distribué reste en `26.2.0-alpha.210` jusqu’à la prochaine release.
|
||||
|
||||
- remplace les pages noires du Livre interdit par l’affichage clair du livre vanilla ;
|
||||
- conserve les seize entrées, les découvertes personnelles, le partage sur pupitre, les IDs, les données v3 et le protocole réseau 4.
|
||||
- rétablit l'accès au Black Market par clic molette sur un panneau, avec une intention vide et une nouvelle validation complète de la cible côté serveur ;
|
||||
- conserve les données Sanctuary v11 et porte son protocole réseau de 21 à 22, sans nouvel ID persistant.
|
||||
- ajoute le disque blanc, à fabriquer puis renommer avec une URL YouTube HTTPS avant de l’insérer dans un jukebox ;
|
||||
- valide strictement l’hôte et l’identifiant vidéo côté serveur, ne transmet qu’un identifiant borné de onze caractères et limite chaque client à quatre lectures de trente minutes maximum ;
|
||||
- diffuse le son en mono positionnel dans le canal des disques, sans nouvelle donnée persistante ; Ambiance conserve ses données v2 et passe son protocole réseau de 5 à 6.
|
||||
- importe le cœur GPL historique d’Abjection dans Only Fun, sans onzième JAR : `/piss`, `/vomit`, `/shit` et `/smoke` retrouvent leurs durées, jets, impacts et messages ;
|
||||
- restaure `abjection:shit_ball` ainsi que le bloc boueux `abjection:shit_cube`, sa recette, son butin et ses ressources originales en FR/EN/RU ;
|
||||
- expose une API serveur unique pour les commandes et le futur raccourci, nettoie les actions temporaires à la mort, en spectateur ou à la déconnexion, et conserve les données v2 ainsi que le protocole réseau 2 d’Only Fun.
|
||||
- rend `abjection:shit_ball` lançable à la main et par dispenser grâce à une fabrique publique installée par Ouch, sans réenregistrer l’objet possédé par Only Fun ;
|
||||
- restaure le projectile historique de 0,25 bloc, sa consommation unique hors créatif, son rendu d’objet, son impact boueux et ses sept secondes de nausée ajoutées à la durée existante ;
|
||||
- déclare la dépendance sans cycle `ouch -> onlyfun` ; les données et protocoles réseau d’Ouch et d’Only Fun restent inchangés.
|
||||
- ajoute la touche configurable « Chier » dans les contrôles Only Fun, assignée à `J` par défaut et sans répétition lorsque la touche reste maintenue ;
|
||||
- transmet une demande C2S vide, déduplique le spam d’un même tick et revalide côté serveur la connexion, la vie et le mode spectateur avant de réutiliser exactement l’action `/shit` ;
|
||||
- conserve les données Only Fun v2 et porte son protocole réseau de 2 à 3, sans nouveau cooldown, besoin ni donnée persistante.
|
||||
|
||||
## `26.2.0-alpha.210`
|
||||
|
||||
Modules modifiés : **Sanctuary `0.0.0-alpha.118`**, **Only Fun `0.0.0-alpha.21`**, **I Like To Move It `0.0.0-alpha.15`** et **It's Alive `0.0.0-alpha.35`**. Les six autres modules conservent leur version issue de l’alpha.209.
|
||||
|
||||
- sépare le compteur quotidien et le statut de déblocage du Shop pour empêcher leur chevauchement ;
|
||||
- retire du Shop Only Fun les raccourcis qui contournaient la progression normale ;
|
||||
- attribue à tous les Zombies vanilla des gamertags déterministes inspirés de la Xbox 360 ;
|
||||
- resynchronise les capes après une reconnexion avec des tentatives retardées et bornées ;
|
||||
- réserve les requêtes communautaires aux panneaux dédiés au lieu d’intercepter les panneaux ordinaires ;
|
||||
- double la vitesse de croisière du Happy Ghast lorsqu’un joueur le pilote, sans accélérer ses déplacements autonomes ;
|
||||
- conserve les données v3 et le protocole réseau 5, sans migration de sauvegarde ni nouvel ID.
|
||||
- transforme le Codex Canaplia en « Livre interdit » aux pages noires, sans page d'accueil, et fait découvrir une entrée aléatoire encore inconnue à chaque page consommée ;
|
||||
- garde les découvertes du Livre interdit personnelles en main et partage temporairement celles du propriétaire uniquement lorsque le livre est posé sur un pupitre ;
|
||||
- conserve les IDs et les anciennes données partagées, ainsi que les données v3 et le protocole réseau 4 d'It's Alive, sans migration de sauvegarde.
|
||||
- actualise JEI de `30.28.0.191` à la bêta compatible `30.28.0.193` dans Packwiz et le manifeste Prism/Modrinth ;
|
||||
- distribue les quatre JAR mis à jour dans Packwiz, Prism et le `.mrpack` partagé client/serveur, sans exiger de nouveau monde.
|
||||
|
||||
## `26.2.0-alpha.209`
|
||||
|
||||
Module modifié : **It's Alive `0.0.0-alpha.34`**. Les neuf autres modules conservent leur version issue de l’alpha.208.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Sanctuary 26.2
|
||||
|
||||
Workspace Fabric multi-projet de Sanctuary 2.0. Il construit dix JAR indépendants sur Minecraft 26.2, Fabric Loader 0.19.3, Fabric API 0.155.0+26.2, Loom 1.17, Gradle 9.5.1 et Java 25.
|
||||
Workspace Fabric multi-projet de Sanctuary 2.0. Il construit dix JAR indépendants sur Minecraft 26.2, Fabric Loader 0.19.3, Fabric API 0.158.0+26.2, Loom 1.17, Gradle 9.5.1 et Java 25.
|
||||
|
||||
Auteur : **KOKA99CAB**
|
||||
Licence : **GNU GPL 3.0 or Later** (`GPL-3.0-or-later`)
|
||||
@@ -11,7 +11,7 @@ Licence : **GNU GPL 3.0 or Later** (`GPL-3.0-or-later`)
|
||||
|---|---|---|---|
|
||||
| `anotherworld` | Another World | choses matérielles | matière, photographie, instruments météo et 108 biomes natifs |
|
||||
| `itsalive` | It's Alive ! | vivant | Creepers, créatures, agriculture et gameplay serveur complet de Canaplia |
|
||||
| `ambiance` | Ambiance | perception | atmosphère, météo, lumières dynamiques, calendrier et composition visuelle accessible |
|
||||
| `ambiance` | Ambiance | perception | atmosphère, météo, lumières dynamiques, audio positionnel, calendrier et composition visuelle accessible |
|
||||
| `redstoner` | Red-Stoner | calcul et signaux | ordinateurs, identité serveur, Drawer et Particuleur |
|
||||
| `iliketomoveit` | I Like To Move It | déplacement | portails, véhicules, grappin, poulies et waystones locaux |
|
||||
| `ouch` | Ouch | combat et destruction | arsenal actif (6 armes) et TNT météo |
|
||||
@@ -22,11 +22,11 @@ Licence : **GNU GPL 3.0 or Later** (`GPL-3.0-or-later`)
|
||||
|
||||
Les descriptions FR/EN/RU sont déclarées dans chaque `fabric.mod.json` et dans les fichiers de langue. Les images fournies servent d'icônes Fabric de leur module.
|
||||
|
||||
Version courante du pack distribué : **`26.2.0-alpha.208`**. Cette release distribue Canaplia complet avec It's Alive **`0.0.0-alpha.33`**, Ambiance **`0.0.0-alpha.14`** et Only Fun **`0.0.0-alpha.20`**.
|
||||
Version courante du pack distribué : **`26.2.0-alpha.210`**. Cette release distribue le Livre interdit Canaplia avec It's Alive **`0.0.0-alpha.35`**, ainsi que Sanctuary **`0.0.0-alpha.118`**, Only Fun **`0.0.0-alpha.21`** et I Like To Move It **`0.0.0-alpha.15`**.
|
||||
|
||||
La migration [CANA-01 à CANA-09](CANAPLIA.md) porte Canaplia vers Minecraft 26.2. It's Alive **`0.0.0-alpha.33`** enregistre les seize familles, les découvertes, hybridations, résines, effets serveur et le Codex. Ambiance **`0.0.0-alpha.14`** compose les profils visuels bornés avec ses réglages d'accessibilité, et Only Fun **`0.0.0-alpha.20`** exclut explicitement Canaplia du Shop et des récompenses ordinaires. Ces trois modules sont inclus dans l'alpha.208 pour les clients et les serveurs.
|
||||
La migration [CANA-01 à CANA-09](CANAPLIA.md) porte Canaplia vers Minecraft 26.2. It's Alive **`0.0.0-alpha.35`** enregistre les seize familles, les découvertes, hybridations, résines, effets serveur et le Livre interdit personnel, partageable temporairement sur un pupitre. Ambiance **`0.0.0-alpha.14`** compose les profils visuels bornés avec ses réglages d'accessibilité, et Only Fun **`0.0.0-alpha.21`** exclut explicitement Canaplia et les raccourcis de progression du Shop.
|
||||
|
||||
L’alpha.208 ajoute Canaplia aux packs Modrinth, Prism et Packwiz sans exiger de nouveau monde. Les sources sauvages fonctionnent dans les chunks existants, tandis que les nouvelles données de compostage et de Codex sont créées ou migrées automatiquement à leur première utilisation.
|
||||
L’alpha.210 ajoute le Livre interdit, accélère le Happy Ghast piloté, regroupe les corrections du Shop, des requêtes, des capes et des noms de Zombies, puis actualise JEI en **`30.28.0.193`**. Elle met à jour les packs Modrinth, Prism et Packwiz sans exiger de nouveau monde ni réinitialiser les sauvegardes.
|
||||
|
||||
L’alpha.207 rend le stockage du coffre-fort pratiquement illimité : rubis, saphirs et émeraudes utilisent chacun un solde 64 bits. Les 27 anciennes piles sont additionnées automatiquement lors du chargement, le Shop continue de consommer directement ces soldes et les 20 % pillés sont crédités dans le coffre-fort de l’attaquant sans créer des millions d’entités ou de piles d’objets.
|
||||
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
# Disque blanc YouTube
|
||||
|
||||
Le disque blanc appartient à **Ambiance**, propriétaire de l’audio positionnel. Il conserve l’ID
|
||||
`ambiance:white_disc` et se fabrique avec un disque 13 entouré de huit colorants blancs. Pour le
|
||||
lire, il faut le renommer dans une enclume avec une URL YouTube HTTPS puis l’insérer dans un
|
||||
jukebox vanilla.
|
||||
|
||||
## Contrat réseau et sécurité
|
||||
|
||||
- le serveur reste l’autorité de l’insertion et refuse les noms qui ne correspondent pas à une URL
|
||||
HTTPS exacte de `youtube.com`, `www.youtube.com`, `m.youtube.com`, `music.youtube.com` ou
|
||||
`youtu.be` ;
|
||||
- les formats `watch`, `shorts`, `embed`, `live` et le lien court sont acceptés, mais l’identifiant
|
||||
doit contenir exactement onze caractères ASCII autorisés ;
|
||||
- le payload S2C ne contient que la position du jukebox et cet identifiant borné, jamais une URL
|
||||
libre ; il est envoyé uniquement aux clients qui suivent le chunk et annoncent le canal ;
|
||||
- chaque client ouvre lui-même la source YouTube, refuse les directs et les vidéos de plus de
|
||||
trente minutes, et limite les lectures simultanées à quatre ;
|
||||
- la lecture utilise le canal `RECORDS`, un flux PCM mono positionnel et une atténuation à
|
||||
64 blocs. Elle s’arrête lorsque le disque est éjecté, le chunk disparaît ou la session se ferme.
|
||||
|
||||
Le disque ne crée aucune donnée persistante et ne demande aucune migration de sauvegarde.
|
||||
Ambiance conserve ses données v2 ; son protocole passe de 5 à 6.
|
||||
|
||||
## Dépendances clientes embarquées
|
||||
|
||||
Le décodage repose sur Lavaplayer `2.2.7` (Apache-2.0) et youtube-source `1.18.2` (MIT), ainsi que
|
||||
leurs dépendances transitives placées en JAR imbriqués par Loom. Leurs avis de licence restent dans
|
||||
les archives d’origine. L’accès YouTube est effectué uniquement par le client et peut cesser de
|
||||
fonctionner si YouTube modifie ses mécanismes de lecture ; aucune clé ou URL arbitraire n’est
|
||||
acceptée par le serveur.
|
||||
+86
-3
@@ -1,8 +1,43 @@
|
||||
import groovy.json.JsonSlurper
|
||||
|
||||
configurations {
|
||||
whiteDiscLibraries
|
||||
implementation.extendsFrom whiteDiscLibraries
|
||||
include.extendsFrom whiteDiscLibraries
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation project(':sanctuary')
|
||||
compileOnly "maven.modrinth:YL57xq9U:oaD6KQls"
|
||||
whiteDiscLibraries("dev.arbjerg:lavaplayer:2.2.7") {
|
||||
exclude group: "org.slf4j", module: "slf4j-api"
|
||||
exclude group: "org.jetbrains", module: "annotations"
|
||||
}
|
||||
whiteDiscLibraries("dev.lavalink.youtube:v2:1.18.2") {
|
||||
exclude group: "org.slf4j", module: "slf4j-api"
|
||||
exclude group: "org.jetbrains", module: "annotations"
|
||||
}
|
||||
[
|
||||
"dev.arbjerg:lavaplayer-natives:2.2.7",
|
||||
"dev.arbjerg:lava-common:2.2.7",
|
||||
"dev.lavalink.youtube:common:1.18.2",
|
||||
"org.mozilla:rhino-engine:1.7.15",
|
||||
"org.mozilla:rhino:1.7.15",
|
||||
"commons-io:commons-io:2.13.0",
|
||||
"org.jsoup:jsoup:1.16.1",
|
||||
"net.iharder:base64:2.3.9",
|
||||
"org.json:json:20240303",
|
||||
"org.apache.httpcomponents:httpclient:4.5.14",
|
||||
"org.apache.httpcomponents:httpcore:4.4.16",
|
||||
"commons-logging:commons-logging:1.2",
|
||||
"commons-codec:commons-codec:1.11",
|
||||
"com.fasterxml.jackson.core:jackson-core:2.15.2",
|
||||
"com.fasterxml.jackson.core:jackson-databind:2.15.2",
|
||||
"com.fasterxml.jackson.core:jackson-annotations:2.15.2",
|
||||
"com.grack:nanojson:1.7"
|
||||
].each { coordinates ->
|
||||
whiteDiscLibraries(coordinates) { transitive = false }
|
||||
}
|
||||
}
|
||||
|
||||
tasks.register("ambianceVisualApiSmoke", JavaExec) {
|
||||
@@ -21,13 +56,21 @@ tasks.register("ambianceScheduleSmoke", JavaExec) {
|
||||
mainClass = "fr.koka99cab.sanctuary26.ambiance.environment.AmbianceScheduleSmoke"
|
||||
}
|
||||
|
||||
tasks.register("whiteDiscUrlSmoke", JavaExec) {
|
||||
group = "verification"
|
||||
description = "Checks strict and bounded YouTube URL parsing for the white disc."
|
||||
dependsOn tasks.named("testClasses")
|
||||
classpath = sourceSets.test.runtimeClasspath
|
||||
mainClass = "fr.koka99cab.sanctuary26.ambiance.white_disc.WhiteDiscUrlSmoke"
|
||||
}
|
||||
|
||||
tasks.named("test") {
|
||||
failOnNoDiscoveredTests = false
|
||||
}
|
||||
|
||||
tasks.register("verifyAmbiance") {
|
||||
group = "verification"
|
||||
description = "Checks Ambiance alpha.12, including timed weather overrides, playable night fog and weather/lunar controls."
|
||||
description = "Checks Ambiance alpha.15, including the bounded YouTube white-disc playback contract."
|
||||
inputs.files(fileTree("src/main/java"))
|
||||
inputs.files(fileTree("src/main/resources"))
|
||||
inputs.file(rootProject.file("pack/migrations/26.2.0-alpha.125-realtime-calendar-to-alpha.126.json"))
|
||||
@@ -68,8 +111,17 @@ tasks.register("verifyAmbiance") {
|
||||
def languages = ["fr_fr", "en_us", "ru_ru"].collectEntries { language ->
|
||||
[(language): new JsonSlurper().parse(file("src/main/resources/assets/realtime/lang/${language}.json"))]
|
||||
}
|
||||
def ambianceLanguages = ["fr_fr", "en_us", "ru_ru"].collectEntries { language ->
|
||||
[(language): new JsonSlurper().parse(file("src/main/resources/assets/ambiance/lang/${language}.json"))]
|
||||
}
|
||||
def whiteDiscTexture = javax.imageio.ImageIO.read(
|
||||
file("src/main/resources/assets/ambiance/textures/item/white_disc.png"))
|
||||
def whiteDiscRecipe = new JsonSlurper().parse(
|
||||
file("src/main/resources/data/ambiance/recipe/white_disc.json"))
|
||||
def whiteDiscSong = new JsonSlurper().parse(
|
||||
file("src/main/resources/data/ambiance/jukebox_song/white_disc.json"))
|
||||
|
||||
if (project.version.toString() != "0.0.0-alpha.14"
|
||||
if (project.version.toString() != "0.0.0-alpha.15"
|
||||
|| migration.source?.pack_version != "26.2.0-alpha.125"
|
||||
|| migration.target?.pack_version != "26.2.0-alpha.126"
|
||||
|| migration.target?.modules?.ambiance != "0.0.0-alpha.1"
|
||||
@@ -84,7 +136,7 @@ tasks.register("verifyAmbiance") {
|
||||
|| migration.compatibility?.ambiance_data_version != 1
|
||||
|| migration.compatibility?.ambiance_network_protocol != 2
|
||||
|| manifest.custom?.sanctuary26?.data_version != 2
|
||||
|| manifest.custom?.sanctuary26?.network_protocol != 5
|
||||
|| manifest.custom?.sanctuary26?.network_protocol != 6
|
||||
|| dailyMigration.target?.pack_version != "26.2.0-alpha.128"
|
||||
|| dailyMigration.target?.modules?.ambiance != "0.0.0-alpha.2"
|
||||
|| dailyMigration.lunar_schedule?.cycle != "minecraft_native_eight_days"
|
||||
@@ -262,6 +314,36 @@ tasks.register("verifyAmbiance") {
|
||||
|| !javaText.contains("Mth.lerp(night, 1.0F, 0.98F)")) {
|
||||
throw new GradleException("Ambiance alpha.11 playable-night fog contract is incomplete")
|
||||
}
|
||||
if (!javaText.contains('id("white_disc_playback")')
|
||||
|| !javaText.contains('readUtf(WhiteDiscUrl.VIDEO_ID_LENGTH)')
|
||||
|| !javaText.contains('PlayerLookup.tracking(level, pos)')
|
||||
|| !javaText.contains('ServerPlayNetworking.canSend(receiver, WhiteDiscPlaybackPayload.ID)')
|
||||
|| !javaText.contains('MAXIMUM_SIMULTANEOUS_DISCS = 4')
|
||||
|| !javaText.contains('REQUESTS.size() + PLAYBACKS.size() >= MAXIMUM_SIMULTANEOUS_DISCS')
|
||||
|| !javaText.contains('client.level.dimension().equals(playback.dimension())')
|
||||
|| !javaText.contains('MAXIMUM_TRACK_MILLIS = 30L * 60L * 1_000L')
|
||||
|| !javaText.contains('new Pcm16AudioDataFormat(1, LavaplayerAudioStream.SAMPLE_RATE')
|
||||
|| !javaText.contains('SoundSource.RECORDS')
|
||||
|| !javaText.contains('channel.linearAttenuation(ATTENUATION_DISTANCE)')
|
||||
|| !javaText.contains('LONG_HOSTS = Set.of(')
|
||||
|| !javaText.contains('"https".equalsIgnoreCase(uri.getScheme())')
|
||||
|| !file("src/main/resources/assets/ambiance/items/white_disc.json").isFile()
|
||||
|| !file("src/main/resources/assets/ambiance/models/item/white_disc.json").isFile()
|
||||
|| whiteDiscTexture == null || whiteDiscTexture.width != 16 || whiteDiscTexture.height != 16
|
||||
|| whiteDiscRecipe.result?.id != "ambiance:white_disc"
|
||||
|| whiteDiscRecipe.key?.W != "minecraft:white_dye"
|
||||
|| whiteDiscSong.sound_event != "minecraft:intentionally_empty"
|
||||
|| whiteDiscSong.length_in_seconds != 1800
|
||||
|| ambianceLanguages.any { locale, translations ->
|
||||
!["item.ambiance.white_disc", "tooltip.ambiance.white_disc",
|
||||
"message.ambiance.white_disc.invalid_url", "message.ambiance.white_disc.failed"].every {
|
||||
translations.containsKey(it)
|
||||
}
|
||||
}
|
||||
|| !file("build.gradle").text.contains('dev.arbjerg:lavaplayer:2.2.7')
|
||||
|| !file("build.gradle").text.contains('dev.lavalink.youtube:v2:1.18.2')) {
|
||||
throw new GradleException("Ambiance alpha.15 white-disc playback contract is incomplete")
|
||||
}
|
||||
|
||||
[
|
||||
'class RealtimeMod', 'class RealtimeConfig', 'class MinecraftChronology',
|
||||
@@ -313,4 +395,5 @@ tasks.named("check") {
|
||||
dependsOn tasks.named("verifyAmbiance")
|
||||
dependsOn tasks.named("ambianceScheduleSmoke")
|
||||
dependsOn tasks.named("ambianceVisualApiSmoke")
|
||||
dependsOn tasks.named("whiteDiscUrlSmoke")
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import fr.koka99cab.sanctuary26.ambiance.environment.AmbianceBloodNightAssault;
|
||||
import fr.koka99cab.sanctuary26.ambiance.environment.AmbianceWeatherCommands;
|
||||
import fr.koka99cab.sanctuary26.ambiance.network.AmbianceNetworking;
|
||||
import fr.koka99cab.sanctuary26.ambiance.registry.AmbianceRegistries;
|
||||
import fr.koka99cab.sanctuary26.ambiance.white_disc.WhiteDiscService;
|
||||
import net.fabricmc.api.ModInitializer;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import org.slf4j.Logger;
|
||||
@@ -29,6 +30,7 @@ public final class AmbianceMod implements ModInitializer {
|
||||
public void onInitialize() {
|
||||
AmbianceRegistries.initialize();
|
||||
AmbianceNetworking.initialize();
|
||||
WhiteDiscService.initialize();
|
||||
RealtimeMod.initialize();
|
||||
AmbianceEnvironmentController.initialize();
|
||||
AmbianceFirstSpawnVision.initialize();
|
||||
|
||||
@@ -4,7 +4,10 @@ import fr.koka99cab.realtime.client.RealtimeClient;
|
||||
import fr.koka99cab.sanctuary26.ambiance.AmbianceMod;
|
||||
import fr.koka99cab.sanctuary26.ambiance.network.AmbianceEnvironmentPayload;
|
||||
import fr.koka99cab.sanctuary26.ambiance.network.AmbianceCanapliaVisualPayloads;
|
||||
import fr.koka99cab.sanctuary26.ambiance.network.WhiteDiscPlaybackPayload;
|
||||
import fr.koka99cab.sanctuary26.ambiance.client.audio.WhiteDiscAudioPlayer;
|
||||
import net.fabricmc.api.ClientModInitializer;
|
||||
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientLifecycleEvents;
|
||||
import net.fabricmc.fabric.api.client.networking.v1.ClientPlayConnectionEvents;
|
||||
import net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking;
|
||||
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
|
||||
@@ -41,11 +44,15 @@ public final class AmbianceClient implements ClientModInitializer {
|
||||
context.client().execute(() -> AmbianceClientVisualEffects.apply(payload)));
|
||||
ClientPlayNetworking.registerGlobalReceiver(AmbianceCanapliaVisualPayloads.Clear.ID, (payload, context) ->
|
||||
context.client().execute(AmbianceClientVisualEffects::clear));
|
||||
ClientPlayNetworking.registerGlobalReceiver(WhiteDiscPlaybackPayload.ID, (payload, context) ->
|
||||
context.client().execute(() -> WhiteDiscAudioPlayer.play(
|
||||
context.client(), payload.pos(), payload.videoId())));
|
||||
visualSettingsKey = KeyMappingHelper.registerKeyMapping(new KeyMapping(
|
||||
"key.ambiance.visual_settings", InputConstants.Type.KEYSYM,
|
||||
GLFW.GLFW_KEY_UNKNOWN, AMBIANCE_CATEGORY));
|
||||
ClientTickEvents.END_CLIENT_TICK.register(client -> {
|
||||
AmbianceClientVisualEffects.tick();
|
||||
WhiteDiscAudioPlayer.tick(client);
|
||||
while (visualSettingsKey.consumeClick()) {
|
||||
if (client.gui.screen() == null) client.gui.setScreen(new AmbianceVisualSettingsScreen(null));
|
||||
}
|
||||
@@ -54,7 +61,9 @@ public final class AmbianceClient implements ClientModInitializer {
|
||||
AmbianceClientEnvironment.reset();
|
||||
DynamicTorchLights.reset();
|
||||
AmbianceClientVisualEffects.clear();
|
||||
WhiteDiscAudioPlayer.clear();
|
||||
});
|
||||
ClientLifecycleEvents.CLIENT_STOPPING.register(client -> WhiteDiscAudioPlayer.shutdown());
|
||||
AmbianceVisualOverlay.register();
|
||||
AmbianceBiomeTints.registerColorResolvers();
|
||||
registerWaterFluidTint();
|
||||
|
||||
+87
@@ -0,0 +1,87 @@
|
||||
package fr.koka99cab.sanctuary26.ambiance.client.audio;
|
||||
|
||||
import com.sedmelluq.discord.lavaplayer.player.AudioPlayer;
|
||||
import com.sedmelluq.discord.lavaplayer.track.playback.AudioFrame;
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
import javax.sound.sampled.AudioFormat;
|
||||
import net.minecraft.client.sounds.AudioStream;
|
||||
|
||||
/** Adapts bounded mono PCM frames from Lavaplayer to Minecraft's streaming OpenAL channel. */
|
||||
final class LavaplayerAudioStream implements AudioStream {
|
||||
static final int SAMPLE_RATE = 48_000;
|
||||
private static final int FRAME_SAMPLES = 960;
|
||||
private static final int BYTES_PER_SAMPLE = 2;
|
||||
private static final AudioFormat FORMAT =
|
||||
new AudioFormat(SAMPLE_RATE, 16, 1, true, false);
|
||||
private final AudioPlayer player;
|
||||
private byte[] pending = new byte[0];
|
||||
private int pendingOffset;
|
||||
private volatile boolean closed;
|
||||
private volatile boolean ended;
|
||||
|
||||
LavaplayerAudioStream(AudioPlayer player) {
|
||||
this.player = player;
|
||||
}
|
||||
|
||||
@Override
|
||||
public AudioFormat getFormat() {
|
||||
return FORMAT;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ByteBuffer read(int maximumBytes) throws IOException {
|
||||
if (closed || maximumBytes <= 0) return null;
|
||||
ByteBuffer output = ByteBuffer.allocateDirect(maximumBytes);
|
||||
while (output.hasRemaining() && !closed) {
|
||||
if (pendingOffset < pending.length) {
|
||||
int length = Math.min(output.remaining(), pending.length - pendingOffset);
|
||||
output.put(pending, pendingOffset, length);
|
||||
pendingOffset += length;
|
||||
continue;
|
||||
}
|
||||
AudioFrame frame = nextFrame();
|
||||
if (frame == null) {
|
||||
if (ended) break;
|
||||
int silence = Math.min(output.remaining(), FRAME_SAMPLES * BYTES_PER_SAMPLE);
|
||||
for (int index = 0; index < silence; index++) output.put((byte) 0);
|
||||
continue;
|
||||
}
|
||||
if (frame.isTerminator()) {
|
||||
ended = true;
|
||||
break;
|
||||
}
|
||||
pending = frame.getData();
|
||||
pendingOffset = 0;
|
||||
}
|
||||
if (output.position() == 0 && ended) return null;
|
||||
output.flip();
|
||||
return output;
|
||||
}
|
||||
|
||||
private AudioFrame nextFrame() throws IOException {
|
||||
try {
|
||||
AudioFrame frame = player.provide(2, TimeUnit.SECONDS);
|
||||
if (frame == null && player.getPlayingTrack() == null) ended = true;
|
||||
return frame;
|
||||
} catch (TimeoutException ignored) {
|
||||
return null;
|
||||
} catch (InterruptedException interrupted) {
|
||||
Thread.currentThread().interrupt();
|
||||
throw new IOException("Interrupted while buffering the white disc", interrupted);
|
||||
}
|
||||
}
|
||||
|
||||
boolean ended() {
|
||||
return ended;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
if (closed) return;
|
||||
closed = true;
|
||||
player.destroy();
|
||||
}
|
||||
}
|
||||
+213
@@ -0,0 +1,213 @@
|
||||
package fr.koka99cab.sanctuary26.ambiance.client.audio;
|
||||
|
||||
import com.mojang.blaze3d.audio.Library;
|
||||
import com.sedmelluq.discord.lavaplayer.format.Pcm16AudioDataFormat;
|
||||
import com.sedmelluq.discord.lavaplayer.player.AudioLoadResultHandler;
|
||||
import com.sedmelluq.discord.lavaplayer.player.AudioPlayer;
|
||||
import com.sedmelluq.discord.lavaplayer.player.DefaultAudioPlayerManager;
|
||||
import com.sedmelluq.discord.lavaplayer.tools.FriendlyException;
|
||||
import com.sedmelluq.discord.lavaplayer.track.AudioPlaylist;
|
||||
import com.sedmelluq.discord.lavaplayer.track.AudioTrack;
|
||||
import dev.lavalink.youtube.YoutubeAudioSourceManager;
|
||||
import fr.koka99cab.sanctuary26.ambiance.AmbianceMod;
|
||||
import fr.koka99cab.sanctuary26.ambiance.mixin.client.SoundEngineAccessor;
|
||||
import fr.koka99cab.sanctuary26.ambiance.mixin.client.SoundManagerAccessor;
|
||||
import fr.koka99cab.sanctuary26.ambiance.white_disc.WhiteDiscUrl;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.sounds.ChannelAccess;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.sounds.SoundSource;
|
||||
import net.minecraft.world.level.Level;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.block.JukeboxBlock;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
|
||||
/** Client-only, positional and resource-bounded YouTube playback for white discs. */
|
||||
public final class WhiteDiscAudioPlayer {
|
||||
private static final int MAXIMUM_SIMULTANEOUS_DISCS = 4;
|
||||
private static final long MAXIMUM_TRACK_MILLIS = 30L * 60L * 1_000L;
|
||||
private static final long FINISHED_GRACE_NANOS = 5_000_000_000L;
|
||||
private static final float ATTENUATION_DISTANCE = 64.0F;
|
||||
private static final DefaultAudioPlayerManager MANAGER = createManager();
|
||||
private static final Map<Long, Long> REQUESTS = new HashMap<>();
|
||||
private static final LinkedHashMap<Long, Playback> PLAYBACKS = new LinkedHashMap<>();
|
||||
private static long requestSequence;
|
||||
private static boolean shutdown;
|
||||
|
||||
private WhiteDiscAudioPlayer() {
|
||||
}
|
||||
|
||||
public static void play(Minecraft client, BlockPos pos, String videoId) {
|
||||
if (shutdown || !WhiteDiscUrl.validVideoId(videoId) || !validTarget(client, pos)) return;
|
||||
long key = pos.asLong();
|
||||
if (REQUESTS.containsKey(key)) return;
|
||||
boolean replacingPlayback = PLAYBACKS.containsKey(key);
|
||||
if (!replacingPlayback
|
||||
&& REQUESTS.size() + PLAYBACKS.size() >= MAXIMUM_SIMULTANEOUS_DISCS) return;
|
||||
stop(key);
|
||||
long request = ++requestSequence;
|
||||
REQUESTS.put(key, request);
|
||||
client.player.sendOverlayMessage(Component.translatable("message.ambiance.white_disc.loading"));
|
||||
MANAGER.loadItemOrdered(key, WhiteDiscUrl.canonicalUrl(videoId), new AudioLoadResultHandler() {
|
||||
@Override
|
||||
public void trackLoaded(AudioTrack track) {
|
||||
client.execute(() -> acceptTrack(client, pos, request, track));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void playlistLoaded(AudioPlaylist playlist) {
|
||||
AudioTrack selected = playlist.getSelectedTrack();
|
||||
if (selected == null && !playlist.getTracks().isEmpty()) selected = playlist.getTracks().getFirst();
|
||||
if (selected == null) noMatches();
|
||||
else trackLoaded(selected);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void noMatches() {
|
||||
client.execute(() -> fail(client, key, request));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void loadFailed(FriendlyException exception) {
|
||||
AmbianceMod.LOGGER.warn("Unable to load white-disc YouTube video {}: {}", videoId,
|
||||
exception.getMessage());
|
||||
client.execute(() -> fail(client, key, request));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static void acceptTrack(Minecraft client, BlockPos pos, long request, AudioTrack track) {
|
||||
long key = pos.asLong();
|
||||
if (REQUESTS.getOrDefault(key, -1L) != request) return;
|
||||
if (!validTarget(client, pos)) {
|
||||
REQUESTS.remove(key);
|
||||
return;
|
||||
}
|
||||
if (track.getInfo().isStream || track.getDuration() <= 0L
|
||||
|| track.getDuration() > MAXIMUM_TRACK_MILLIS) {
|
||||
REQUESTS.remove(key);
|
||||
if (client.player != null) client.player.sendOverlayMessage(
|
||||
Component.translatable("message.ambiance.white_disc.too_long"));
|
||||
return;
|
||||
}
|
||||
while (PLAYBACKS.size() >= MAXIMUM_SIMULTANEOUS_DISCS) {
|
||||
stop(PLAYBACKS.keySet().iterator().next());
|
||||
}
|
||||
ChannelAccess channels = ((SoundEngineAccessor) ((SoundManagerAccessor) client.getSoundManager())
|
||||
.ambiance$getSoundEngine()).ambiance$getChannelAccess();
|
||||
channels.createHandle(Library.Pool.STREAMING).thenAccept(handle -> client.execute(() -> {
|
||||
if (handle == null || REQUESTS.getOrDefault(key, -1L) != request) {
|
||||
if (handle != null) handle.release();
|
||||
return;
|
||||
}
|
||||
AudioPlayer player = MANAGER.createPlayer();
|
||||
player.playTrack(track);
|
||||
LavaplayerAudioStream stream = new LavaplayerAudioStream(player);
|
||||
Playback playback = new Playback(pos.immutable(), client.level.dimension(), handle, stream);
|
||||
PLAYBACKS.put(key, playback);
|
||||
REQUESTS.remove(key);
|
||||
float volume = client.options.getFinalSoundSourceVolume(SoundSource.RECORDS);
|
||||
handle.execute(channel -> {
|
||||
channel.setSelfPosition(Vec3.atCenterOf(pos));
|
||||
channel.setRelative(false);
|
||||
channel.linearAttenuation(ATTENUATION_DISTANCE);
|
||||
channel.setVolume(volume);
|
||||
channel.attachBufferStream(stream);
|
||||
channel.play();
|
||||
});
|
||||
if (client.player != null) client.player.sendOverlayMessage(
|
||||
Component.translatable("message.ambiance.white_disc.playing", track.getInfo().title));
|
||||
}));
|
||||
}
|
||||
|
||||
private static void fail(Minecraft client, long key, long request) {
|
||||
if (REQUESTS.getOrDefault(key, -1L) != request) return;
|
||||
REQUESTS.remove(key);
|
||||
if (client.player != null) client.player.sendOverlayMessage(
|
||||
Component.translatable("message.ambiance.white_disc.failed"));
|
||||
}
|
||||
|
||||
public static void stop(BlockPos pos) {
|
||||
stop(pos.asLong());
|
||||
}
|
||||
|
||||
private static void stop(long key) {
|
||||
REQUESTS.remove(key);
|
||||
Playback playback = PLAYBACKS.remove(key);
|
||||
if (playback == null) return;
|
||||
playback.stream().close();
|
||||
playback.handle().execute(channel -> channel.stop());
|
||||
playback.handle().release();
|
||||
}
|
||||
|
||||
public static void tick(Minecraft client) {
|
||||
if (client.level == null) {
|
||||
clear();
|
||||
return;
|
||||
}
|
||||
long now = System.nanoTime();
|
||||
for (Map.Entry<Long, Playback> entry : Map.copyOf(PLAYBACKS).entrySet()) {
|
||||
Playback playback = entry.getValue();
|
||||
boolean missing = !client.level.dimension().equals(playback.dimension())
|
||||
|| !client.level.hasChunkAt(playback.pos())
|
||||
|| !client.level.getBlockState(playback.pos()).is(Blocks.JUKEBOX)
|
||||
|| !client.level.getBlockState(playback.pos()).getValue(JukeboxBlock.HAS_RECORD);
|
||||
if (missing || playback.finishedFor(now) >= FINISHED_GRACE_NANOS) {
|
||||
stop(entry.getKey());
|
||||
continue;
|
||||
}
|
||||
float volume = client.options.getFinalSoundSourceVolume(SoundSource.RECORDS);
|
||||
playback.handle().execute(channel -> channel.setVolume(volume));
|
||||
}
|
||||
}
|
||||
|
||||
public static void clear() {
|
||||
REQUESTS.clear();
|
||||
for (long key : PLAYBACKS.keySet().stream().mapToLong(Long::longValue).toArray()) stop(key);
|
||||
}
|
||||
|
||||
public static void shutdown() {
|
||||
if (shutdown) return;
|
||||
shutdown = true;
|
||||
clear();
|
||||
MANAGER.shutdown();
|
||||
}
|
||||
|
||||
private static DefaultAudioPlayerManager createManager() {
|
||||
DefaultAudioPlayerManager manager = new DefaultAudioPlayerManager();
|
||||
manager.getConfiguration().setOutputFormat(
|
||||
new Pcm16AudioDataFormat(1, LavaplayerAudioStream.SAMPLE_RATE, 960, false));
|
||||
manager.setFrameBufferDuration(5_000);
|
||||
manager.setItemLoaderThreadPoolSize(2);
|
||||
manager.registerSourceManager(new YoutubeAudioSourceManager(false));
|
||||
return manager;
|
||||
}
|
||||
|
||||
private static boolean validTarget(Minecraft client, BlockPos pos) {
|
||||
return client.player != null && client.level != null
|
||||
&& client.player.distanceToSqr(Vec3.atCenterOf(pos))
|
||||
<= ATTENUATION_DISTANCE * ATTENUATION_DISTANCE * 4.0D
|
||||
&& client.level.getBlockState(pos).is(Blocks.JUKEBOX)
|
||||
&& client.level.getBlockState(pos).getValue(JukeboxBlock.HAS_RECORD);
|
||||
}
|
||||
|
||||
private record Playback(BlockPos pos, ResourceKey<Level> dimension,
|
||||
ChannelAccess.ChannelHandle handle,
|
||||
LavaplayerAudioStream stream, long[] finishedAt) {
|
||||
private Playback(BlockPos pos, ResourceKey<Level> dimension,
|
||||
ChannelAccess.ChannelHandle handle, LavaplayerAudioStream stream) {
|
||||
this(pos, dimension, handle, stream, new long[1]);
|
||||
}
|
||||
|
||||
private long finishedFor(long now) {
|
||||
if (!stream.ended()) return 0L;
|
||||
if (finishedAt[0] == 0L) finishedAt[0] = now;
|
||||
return now - finishedAt[0];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package fr.koka99cab.sanctuary26.ambiance.item;
|
||||
|
||||
import java.util.function.Consumer;
|
||||
import net.minecraft.ChatFormatting;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.world.item.Item;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.item.TooltipFlag;
|
||||
import net.minecraft.world.item.component.TooltipDisplay;
|
||||
|
||||
/** Blank disc whose custom name may hold a validated YouTube URL. */
|
||||
public final class WhiteDiscItem extends Item {
|
||||
public WhiteDiscItem(Properties properties) {
|
||||
super(properties);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void appendHoverText(ItemStack stack, TooltipContext context, TooltipDisplay display,
|
||||
Consumer<Component> tooltip, TooltipFlag flag) {
|
||||
tooltip.accept(Component.translatable("tooltip.ambiance.white_disc")
|
||||
.withStyle(ChatFormatting.GRAY));
|
||||
}
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
package fr.koka99cab.sanctuary26.ambiance.mixin.client;
|
||||
|
||||
import fr.koka99cab.sanctuary26.ambiance.client.audio.WhiteDiscAudioPlayer;
|
||||
import net.minecraft.client.renderer.LevelRenderer;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.world.entity.Entity;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.injection.At;
|
||||
import org.spongepowered.asm.mixin.injection.Inject;
|
||||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
|
||||
|
||||
@Mixin(LevelRenderer.class)
|
||||
public abstract class LevelRendererMixin {
|
||||
@Inject(method = "levelEvent", at = @At("HEAD"))
|
||||
private void ambiance$stopWhiteDisc(Entity source, int type, BlockPos pos, int data,
|
||||
CallbackInfo callback) {
|
||||
if (type == 1011) WhiteDiscAudioPlayer.stop(pos);
|
||||
}
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
package fr.koka99cab.sanctuary26.ambiance.mixin.client;
|
||||
|
||||
import net.minecraft.client.sounds.ChannelAccess;
|
||||
import net.minecraft.client.sounds.SoundEngine;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.gen.Accessor;
|
||||
|
||||
@Mixin(SoundEngine.class)
|
||||
public interface SoundEngineAccessor {
|
||||
@Accessor("channelAccess")
|
||||
ChannelAccess ambiance$getChannelAccess();
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
package fr.koka99cab.sanctuary26.ambiance.mixin.client;
|
||||
|
||||
import net.minecraft.client.sounds.SoundEngine;
|
||||
import net.minecraft.client.sounds.SoundManager;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.gen.Accessor;
|
||||
|
||||
@Mixin(SoundManager.class)
|
||||
public interface SoundManagerAccessor {
|
||||
@Accessor("soundEngine")
|
||||
SoundEngine ambiance$getSoundEngine();
|
||||
}
|
||||
+3
-1
@@ -13,7 +13,7 @@ import net.minecraft.server.level.ServerPlayer;
|
||||
* Networking contract for Ambiance. Payload IDs must use the module namespace.
|
||||
*/
|
||||
public final class AmbianceNetworking {
|
||||
public static final int PROTOCOL_VERSION = 5;
|
||||
public static final int PROTOCOL_VERSION = 6;
|
||||
private static boolean initialized;
|
||||
|
||||
private AmbianceNetworking() {
|
||||
@@ -34,6 +34,8 @@ public final class AmbianceNetworking {
|
||||
AmbianceCanapliaVisualPayloads.BadTrip.ID, AmbianceCanapliaVisualPayloads.BadTrip.CODEC);
|
||||
PayloadTypeRegistry.clientboundPlay().register(
|
||||
AmbianceCanapliaVisualPayloads.Clear.ID, AmbianceCanapliaVisualPayloads.Clear.CODEC);
|
||||
PayloadTypeRegistry.clientboundPlay().register(
|
||||
WhiteDiscPlaybackPayload.ID, WhiteDiscPlaybackPayload.CODEC);
|
||||
ServerPlayConnectionEvents.JOIN.register((handler, sender, server) ->
|
||||
send(handler.player, AmbianceEnvironmentController.snapshot(server))
|
||||
);
|
||||
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
package fr.koka99cab.sanctuary26.ambiance.network;
|
||||
|
||||
import fr.koka99cab.sanctuary26.ambiance.AmbianceMod;
|
||||
import fr.koka99cab.sanctuary26.ambiance.white_disc.WhiteDiscUrl;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.network.RegistryFriendlyByteBuf;
|
||||
import net.minecraft.network.codec.StreamCodec;
|
||||
import net.minecraft.network.protocol.common.custom.CustomPacketPayload;
|
||||
|
||||
/** Bounded clientbound intent: a server-resolved jukebox position and an 11-character video ID. */
|
||||
public record WhiteDiscPlaybackPayload(BlockPos pos, String videoId) implements CustomPacketPayload {
|
||||
public static final Type<WhiteDiscPlaybackPayload> ID =
|
||||
new Type<>(AmbianceMod.id("white_disc_playback"));
|
||||
public static final StreamCodec<RegistryFriendlyByteBuf, WhiteDiscPlaybackPayload> CODEC =
|
||||
CustomPacketPayload.codec(WhiteDiscPlaybackPayload::write, WhiteDiscPlaybackPayload::new);
|
||||
|
||||
public WhiteDiscPlaybackPayload {
|
||||
if (!WhiteDiscUrl.validVideoId(videoId)) videoId = "";
|
||||
}
|
||||
|
||||
private WhiteDiscPlaybackPayload(RegistryFriendlyByteBuf buffer) {
|
||||
this(buffer.readBlockPos(), buffer.readUtf(WhiteDiscUrl.VIDEO_ID_LENGTH));
|
||||
}
|
||||
|
||||
private void write(RegistryFriendlyByteBuf buffer) {
|
||||
buffer.writeBlockPos(pos);
|
||||
buffer.writeUtf(videoId, WhiteDiscUrl.VIDEO_ID_LENGTH);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Type<? extends CustomPacketPayload> type() {
|
||||
return ID;
|
||||
}
|
||||
}
|
||||
+23
-2
@@ -1,12 +1,32 @@
|
||||
package fr.koka99cab.sanctuary26.ambiance.registry;
|
||||
|
||||
import fr.koka99cab.sanctuary26.ambiance.AmbianceMod;
|
||||
import fr.koka99cab.sanctuary26.ambiance.item.WhiteDiscItem;
|
||||
import net.fabricmc.fabric.api.creativetab.v1.CreativeModeTabEvents;
|
||||
import net.minecraft.core.Registry;
|
||||
import net.minecraft.core.registries.BuiltInRegistries;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.world.item.CreativeModeTab;
|
||||
import net.minecraft.world.item.Item;
|
||||
import net.minecraft.world.item.JukeboxSong;
|
||||
import net.minecraft.world.item.Rarity;
|
||||
|
||||
/**
|
||||
* Single registration entry point for the Ambiance namespace.
|
||||
* Feature registries must be called from here to keep initialization ordering explicit.
|
||||
*/
|
||||
public final class AmbianceRegistries {
|
||||
public static final ResourceKey<JukeboxSong> WHITE_DISC_SONG = ResourceKey.create(
|
||||
Registries.JUKEBOX_SONG, AmbianceMod.id("white_disc"));
|
||||
private static final ResourceKey<Item> WHITE_DISC_KEY = ResourceKey.create(
|
||||
Registries.ITEM, AmbianceMod.id("white_disc"));
|
||||
private static final ResourceKey<CreativeModeTab> TOOLS_TAB = ResourceKey.create(
|
||||
Registries.CREATIVE_MODE_TAB, Identifier.withDefaultNamespace("tools_and_utilities"));
|
||||
public static final Item WHITE_DISC = Registry.register(BuiltInRegistries.ITEM, WHITE_DISC_KEY,
|
||||
new WhiteDiscItem(new Item.Properties().stacksTo(1).rarity(Rarity.UNCOMMON)
|
||||
.jukeboxPlayable(WHITE_DISC_SONG).setId(WHITE_DISC_KEY)));
|
||||
private static boolean initialized;
|
||||
|
||||
private AmbianceRegistries() {
|
||||
@@ -17,7 +37,8 @@ public final class AmbianceRegistries {
|
||||
return;
|
||||
}
|
||||
initialized = true;
|
||||
AmbianceMod.LOGGER.debug("[{}] Empty registry hub initialized.", AmbianceMod.DISPLAY_NAME);
|
||||
CreativeModeTabEvents.modifyOutputEvent(TOOLS_TAB).register(entries -> entries.accept(WHITE_DISC));
|
||||
AmbianceMod.LOGGER.debug("[{}] Registry hub initialized with the white disc.",
|
||||
AmbianceMod.DISPLAY_NAME);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+70
@@ -0,0 +1,70 @@
|
||||
package fr.koka99cab.sanctuary26.ambiance.white_disc;
|
||||
|
||||
import fr.koka99cab.sanctuary26.ambiance.network.WhiteDiscPlaybackPayload;
|
||||
import fr.koka99cab.sanctuary26.ambiance.registry.AmbianceRegistries;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import net.fabricmc.fabric.api.event.player.UseBlockCallback;
|
||||
import net.fabricmc.fabric.api.networking.v1.PlayerLookup;
|
||||
import net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.component.DataComponents;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.world.InteractionResult;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.item.JukeboxPlayable;
|
||||
import net.minecraft.world.level.block.JukeboxBlock;
|
||||
import net.minecraft.world.level.block.entity.JukeboxBlockEntity;
|
||||
|
||||
/** Server-authoritative insertion and bounded playback dispatch for the white disc. */
|
||||
public final class WhiteDiscService {
|
||||
private static boolean initialized;
|
||||
|
||||
private WhiteDiscService() {
|
||||
}
|
||||
|
||||
public static void initialize() {
|
||||
if (initialized) return;
|
||||
initialized = true;
|
||||
UseBlockCallback.EVENT.register((player, level, hand, hit) -> {
|
||||
BlockPos pos = hit.getBlockPos();
|
||||
if (!(level.getBlockEntity(pos) instanceof JukeboxBlockEntity jukebox)
|
||||
|| jukebox.getBlockState().getValue(JukeboxBlock.HAS_RECORD)) return InteractionResult.PASS;
|
||||
ItemStack held = player.getItemInHand(hand);
|
||||
if (!held.is(AmbianceRegistries.WHITE_DISC)) return InteractionResult.PASS;
|
||||
|
||||
Optional<String> videoId = videoId(held);
|
||||
if (videoId.isEmpty()) {
|
||||
if (!level.isClientSide()) player.sendOverlayMessage(
|
||||
Component.translatable("message.ambiance.white_disc.invalid_url"));
|
||||
return InteractionResult.FAIL;
|
||||
}
|
||||
if (level.isClientSide()) return InteractionResult.SUCCESS;
|
||||
InteractionResult result = JukeboxPlayable.tryInsertIntoJukebox(level, pos, held, player);
|
||||
if (result.consumesAction() && level instanceof ServerLevel serverLevel) {
|
||||
broadcast(serverLevel, pos, videoId.get(), player instanceof ServerPlayer serverPlayer
|
||||
? serverPlayer : null);
|
||||
}
|
||||
return result;
|
||||
});
|
||||
}
|
||||
|
||||
public static Optional<String> videoId(ItemStack stack) {
|
||||
Component name = stack.get(DataComponents.CUSTOM_NAME);
|
||||
return name == null ? Optional.empty() : WhiteDiscUrl.videoId(name.getString());
|
||||
}
|
||||
|
||||
private static void broadcast(ServerLevel level, BlockPos pos, String videoId, ServerPlayer inserter) {
|
||||
WhiteDiscPlaybackPayload payload = new WhiteDiscPlaybackPayload(pos, videoId);
|
||||
Set<ServerPlayer> receivers = new LinkedHashSet<>(PlayerLookup.tracking(level, pos));
|
||||
if (inserter != null) receivers.add(inserter);
|
||||
for (ServerPlayer receiver : receivers) {
|
||||
if (ServerPlayNetworking.canSend(receiver, WhiteDiscPlaybackPayload.ID)) {
|
||||
ServerPlayNetworking.send(receiver, payload);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
package fr.koka99cab.sanctuary26.ambiance.white_disc;
|
||||
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.util.Locale;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/** Strict, side-effect-free YouTube URL parser used before any client network request. */
|
||||
public final class WhiteDiscUrl {
|
||||
public static final int MAXIMUM_URL_LENGTH = 256;
|
||||
public static final int VIDEO_ID_LENGTH = 11;
|
||||
private static final Pattern VIDEO_ID = Pattern.compile("[A-Za-z0-9_-]{11}");
|
||||
private static final Set<String> LONG_HOSTS = Set.of(
|
||||
"youtube.com", "www.youtube.com", "m.youtube.com", "music.youtube.com");
|
||||
|
||||
private WhiteDiscUrl() {
|
||||
}
|
||||
|
||||
public static Optional<String> videoId(String value) {
|
||||
if (value == null || value.isBlank() || value.length() > MAXIMUM_URL_LENGTH) return Optional.empty();
|
||||
try {
|
||||
URI uri = new URI(value.strip());
|
||||
if (!"https".equalsIgnoreCase(uri.getScheme()) || uri.getUserInfo() != null
|
||||
|| (uri.getPort() != -1 && uri.getPort() != 443)) return Optional.empty();
|
||||
String host = uri.getHost();
|
||||
if (host == null) return Optional.empty();
|
||||
host = host.toLowerCase(Locale.ROOT);
|
||||
if ("youtu.be".equals(host)) return valid(singlePathSegment(uri.getRawPath()));
|
||||
if (!LONG_HOSTS.contains(host)) return Optional.empty();
|
||||
|
||||
String path = uri.getRawPath();
|
||||
if ("/watch".equals(path)) return valid(queryParameter(uri.getRawQuery(), "v"));
|
||||
for (String prefix : new String[] {"/shorts/", "/embed/", "/live/"}) {
|
||||
if (path != null && path.startsWith(prefix)) return valid(singlePathSegment(path.substring(prefix.length())));
|
||||
}
|
||||
} catch (URISyntaxException ignored) {
|
||||
// Invalid user text is rejected without network access.
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
public static boolean validVideoId(String value) {
|
||||
return value != null && VIDEO_ID.matcher(value).matches();
|
||||
}
|
||||
|
||||
public static String canonicalUrl(String videoId) {
|
||||
if (!validVideoId(videoId)) throw new IllegalArgumentException("Invalid YouTube video ID");
|
||||
return "https://www.youtube.com/watch?v=" + videoId;
|
||||
}
|
||||
|
||||
private static String singlePathSegment(String path) {
|
||||
if (path == null || path.isEmpty()) return "";
|
||||
String segment = path.startsWith("/") ? path.substring(1) : path;
|
||||
return segment.contains("/") ? "" : segment;
|
||||
}
|
||||
|
||||
private static String queryParameter(String query, String name) {
|
||||
if (query == null) return "";
|
||||
String found = null;
|
||||
for (String parameter : query.split("&", -1)) {
|
||||
int equals = parameter.indexOf('=');
|
||||
if (equals <= 0 || !name.equals(parameter.substring(0, equals))) continue;
|
||||
if (found != null) return "";
|
||||
found = parameter.substring(equals + 1);
|
||||
}
|
||||
return found == null ? "" : found;
|
||||
}
|
||||
|
||||
private static Optional<String> valid(String candidate) {
|
||||
return validVideoId(candidate) ? Optional.of(candidate) : Optional.empty();
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,9 @@
|
||||
"GameRendererMixin",
|
||||
"LightCoordsUtilMixin",
|
||||
"LightmapRenderStateExtractorMixin",
|
||||
"LevelRendererMixin",
|
||||
"SoundEngineAccessor",
|
||||
"SoundManagerAccessor",
|
||||
"SkyRendererMixin"
|
||||
],
|
||||
"injectors": {
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"model": {
|
||||
"type": "minecraft:model",
|
||||
"model": "ambiance:item/white_disc"
|
||||
}
|
||||
}
|
||||
@@ -24,5 +24,13 @@
|
||||
"options.ambiance.visual.wobble": "Camera wobble",
|
||||
"options.ambiance.visual.ghosting": "Edge ghosting",
|
||||
"key.categories.ambiance.ambiance": "Ambiance",
|
||||
"key.ambiance.visual_settings": "Visual accessibility settings"
|
||||
"key.ambiance.visual_settings": "Visual accessibility settings",
|
||||
"item.ambiance.white_disc": "White Disc",
|
||||
"jukebox_song.ambiance.white_disc": "White Disc",
|
||||
"tooltip.ambiance.white_disc": "Rename it to an HTTPS YouTube URL, then insert it into a jukebox.",
|
||||
"message.ambiance.white_disc.invalid_url": "Rename the white disc to a valid HTTPS YouTube URL.",
|
||||
"message.ambiance.white_disc.loading": "Loading YouTube video…",
|
||||
"message.ambiance.white_disc.playing": "Now playing: %s",
|
||||
"message.ambiance.white_disc.failed": "This YouTube video could not be played.",
|
||||
"message.ambiance.white_disc.too_long": "This video is live or longer than 30 minutes."
|
||||
}
|
||||
|
||||
@@ -24,5 +24,13 @@
|
||||
"options.ambiance.visual.wobble": "Oscillation de la caméra",
|
||||
"options.ambiance.visual.ghosting": "Rémanence sur les bords",
|
||||
"key.categories.ambiance.ambiance": "Ambiance",
|
||||
"key.ambiance.visual_settings": "Réglages d’accessibilité visuelle"
|
||||
"key.ambiance.visual_settings": "Réglages d’accessibilité visuelle",
|
||||
"item.ambiance.white_disc": "Disque blanc",
|
||||
"jukebox_song.ambiance.white_disc": "Disque blanc",
|
||||
"tooltip.ambiance.white_disc": "Renommez-le avec une URL YouTube HTTPS, puis insérez-le dans un jukebox.",
|
||||
"message.ambiance.white_disc.invalid_url": "Renommez le disque blanc avec une URL YouTube HTTPS valide.",
|
||||
"message.ambiance.white_disc.loading": "Chargement de la vidéo YouTube…",
|
||||
"message.ambiance.white_disc.playing": "Lecture : %s",
|
||||
"message.ambiance.white_disc.failed": "Impossible de lire cette vidéo YouTube.",
|
||||
"message.ambiance.white_disc.too_long": "Cette vidéo est en direct ou dépasse 30 minutes."
|
||||
}
|
||||
|
||||
@@ -24,5 +24,13 @@
|
||||
"options.ambiance.visual.wobble": "Покачивание камеры",
|
||||
"options.ambiance.visual.ghosting": "Шлейф по краям",
|
||||
"key.categories.ambiance.ambiance": "Ambiance",
|
||||
"key.ambiance.visual_settings": "Настройки визуальной доступности"
|
||||
"key.ambiance.visual_settings": "Настройки визуальной доступности",
|
||||
"item.ambiance.white_disc": "Белая пластинка",
|
||||
"jukebox_song.ambiance.white_disc": "Белая пластинка",
|
||||
"tooltip.ambiance.white_disc": "Переименуйте её в HTTPS-ссылку YouTube и вставьте в проигрыватель.",
|
||||
"message.ambiance.white_disc.invalid_url": "Переименуйте белую пластинку в допустимую HTTPS-ссылку YouTube.",
|
||||
"message.ambiance.white_disc.loading": "Загрузка видео YouTube…",
|
||||
"message.ambiance.white_disc.playing": "Сейчас играет: %s",
|
||||
"message.ambiance.white_disc.failed": "Не удалось воспроизвести это видео YouTube.",
|
||||
"message.ambiance.white_disc.too_long": "Это прямая трансляция или видео длиннее 30 минут."
|
||||
}
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"parent": "minecraft:item/template_music_disc",
|
||||
"textures": {
|
||||
"layer0": "ambiance:item/white_disc"
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 393 B |
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"comparator_output": 15,
|
||||
"description": {
|
||||
"translate": "jukebox_song.ambiance.white_disc"
|
||||
},
|
||||
"length_in_seconds": 1800.0,
|
||||
"sound_event": "minecraft:intentionally_empty"
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"type": "minecraft:crafting_shaped",
|
||||
"category": "misc",
|
||||
"pattern": [
|
||||
"WWW",
|
||||
"WDW",
|
||||
"WWW"
|
||||
],
|
||||
"key": {
|
||||
"D": "minecraft:music_disc_13",
|
||||
"W": "minecraft:white_dye"
|
||||
},
|
||||
"result": {
|
||||
"id": "ambiance:white_disc"
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,7 @@
|
||||
"id": "ambiance",
|
||||
"version": "${version}",
|
||||
"name": "Ambiance",
|
||||
"description": "Owns world atmosphere, dynamic lights, the calendar and composable accessible visual effects, including Canaplia.",
|
||||
"description": "Owns world atmosphere, dynamic lights, positional audio, the calendar and composable accessible visual effects, including Canaplia.",
|
||||
"authors": [
|
||||
"KOKA99CAB"
|
||||
],
|
||||
@@ -36,13 +36,13 @@
|
||||
"system_type": "gameplay-system",
|
||||
"namespace": "ambiance",
|
||||
"data_version": 2,
|
||||
"network_protocol": 5,
|
||||
"network_protocol": 6,
|
||||
"lifecycle": "${lifecycle}",
|
||||
"pack_version": "${pack_version}",
|
||||
"descriptions": {
|
||||
"fr_fr": "Gère l'atmosphère, les lumières dynamiques, le calendrier et les effets visuels accessibles et composables, dont Canaplia.",
|
||||
"en_us": "Owns world atmosphere, dynamic lights, the calendar and composable accessible visual effects, including Canaplia.",
|
||||
"ru_ru": "Управляет атмосферой, динамическим освещением, календарём и доступными составными визуальными эффектами, включая Canaplia."
|
||||
"fr_fr": "Gère l'atmosphère, les lumières dynamiques, l'audio positionnel, le calendrier et les effets visuels accessibles et composables, dont Canaplia.",
|
||||
"en_us": "Owns world atmosphere, dynamic lights, positional audio, the calendar and composable accessible visual effects, including Canaplia.",
|
||||
"ru_ru": "Управляет атмосферой, динамическим освещением, позиционным звуком, календарём и доступными составными визуальными эффектами, включая Canaplia."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
package fr.koka99cab.sanctuary26.ambiance.white_disc;
|
||||
|
||||
/** Executable URL-policy regression test, intentionally independent from a running game. */
|
||||
public final class WhiteDiscUrlSmoke {
|
||||
private static final String VIDEO_ID = "dQw4w9WgXcQ";
|
||||
|
||||
private WhiteDiscUrlSmoke() {
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
require(VIDEO_ID.equals(id("https://www.youtube.com/watch?v=" + VIDEO_ID)), "watch URL rejected");
|
||||
require(VIDEO_ID.equals(id("https://youtu.be/" + VIDEO_ID + "?si=abc")), "short URL rejected");
|
||||
require(VIDEO_ID.equals(id("https://music.youtube.com/watch?v=" + VIDEO_ID)), "music URL rejected");
|
||||
require(VIDEO_ID.equals(id("https://www.youtube.com/shorts/" + VIDEO_ID)), "shorts URL rejected");
|
||||
require(VIDEO_ID.equals(id("https://www.youtube.com/embed/" + VIDEO_ID)), "embed URL rejected");
|
||||
|
||||
reject("http://www.youtube.com/watch?v=" + VIDEO_ID);
|
||||
reject("https://youtube.example/watch?v=" + VIDEO_ID);
|
||||
reject("https://youtube.com.evil.example/watch?v=" + VIDEO_ID);
|
||||
reject("https://user@www.youtube.com/watch?v=" + VIDEO_ID);
|
||||
reject("https://www.youtube.com:8443/watch?v=" + VIDEO_ID);
|
||||
reject("https://www.youtube.com/watch?v=too_short");
|
||||
reject("https://www.youtube.com/watch?v=" + VIDEO_ID + "&v=" + VIDEO_ID);
|
||||
reject("https://youtu.be/" + VIDEO_ID + "/extra");
|
||||
reject("https://www.youtube.com/watch?v=dQw4w9WgX%63Q");
|
||||
reject("x".repeat(WhiteDiscUrl.MAXIMUM_URL_LENGTH + 1));
|
||||
}
|
||||
|
||||
private static String id(String url) {
|
||||
return WhiteDiscUrl.videoId(url).orElseThrow();
|
||||
}
|
||||
|
||||
private static void reject(String url) {
|
||||
require(WhiteDiscUrl.videoId(url).isEmpty(), "Unsafe URL accepted: " + url);
|
||||
}
|
||||
|
||||
private static void require(boolean condition, String message) {
|
||||
if (!condition) throw new AssertionError(message);
|
||||
}
|
||||
}
|
||||
+314
-126
@@ -33,6 +33,15 @@ allprojects {
|
||||
}
|
||||
filter { includeGroup "maven.modrinth" }
|
||||
}
|
||||
exclusiveContent {
|
||||
forRepository {
|
||||
maven {
|
||||
name = "Lavalink"
|
||||
url = "https://maven.lavalink.dev/releases"
|
||||
}
|
||||
}
|
||||
filter { includeGroup "dev.lavalink.youtube" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -330,7 +339,7 @@ tasks.register("verifyWorkspace") {
|
||||
}
|
||||
def progressionJeiMigration = new JsonSlurper().parse(file(
|
||||
"pack/migrations/26.2.0-alpha.87-progression-jei-audio-to-alpha.88.json"))
|
||||
def jeiFiles = prismIndex.files.findAll { it.path == "mods/jei-26.2-fabric-30.28.0.191.jar" }
|
||||
def jeiFiles = prismIndex.files.findAll { it.path == "mods/jei-26.2-fabric-30.28.0.193.jar" }
|
||||
if (progressionJeiMigration.source?.pack_version != "26.2.0-alpha.87"
|
||||
|| progressionJeiMigration.target?.pack_version != "26.2.0-alpha.88"
|
||||
|| progressionJeiMigration.target?.modules?.sanctuary != "0.0.0-alpha.38"
|
||||
@@ -345,8 +354,8 @@ tasks.register("verifyWorkspace") {
|
||||
|| progressionJeiMigration.compatibility?.network_protocol != 10
|
||||
|| progressionJeiMigration.compatibility?.network_protocol_changed != true
|
||||
|| jeiFiles.size() != 1
|
||||
|| jeiFiles[0].hashes?.sha1 != "35353e18b1cf2a61339463447fce1f3e2f716b80"
|
||||
|| jeiFiles[0].hashes?.sha512 != "aa7dca89026fea2293a2e25fa4a84faf0eeaa6407b7e84991e69626ff1ee0f9df9ba3a5e096cbf631d08842693b115653c5c8373e5ef3953318a892dbdc0a33d"
|
||||
|| jeiFiles[0].hashes?.sha1 != "494f9dc787d6c9cd9d32793da2784ab16834faf3"
|
||||
|| jeiFiles[0].hashes?.sha512 != "0869f4d5840a732a04bedc7684b40140f087f3d759066c206d9d1ec710007f7c9585e4f5ebb04f10e56bef1cf2eddc6978bb2b2c93541cff5fcf7e1609859468"
|
||||
|| jeiFiles[0].env?.client != "required" || jeiFiles[0].env?.server != "required") {
|
||||
throw new GradleException("The alpha.88 progression, JEI and Minecrafter audio contract is incomplete")
|
||||
}
|
||||
@@ -3428,9 +3437,9 @@ tasks.register("verifyProgressivePauseMenuRelease") {
|
||||
[(locale): new JsonSlurper().parse(file(
|
||||
"sanctuary/src/main/resources/assets/sanctuary/lang/${locale}.json"))]
|
||||
}
|
||||
if (rootProject.pack_version != "26.2.0-alpha.209"
|
||||
|| rootProject.sanctuary_version != "0.0.0-alpha.117"
|
||||
|| rootProject.onlyfun_version != "0.0.0-alpha.20"
|
||||
if (rootProject.pack_version != "26.2.0-alpha.210"
|
||||
|| rootProject.sanctuary_version != "0.0.0-alpha.119"
|
||||
|| rootProject.onlyfun_version != "0.0.0-alpha.24"
|
||||
|| release.pack_version != rootProject.pack_version || index.versionId != rootProject.pack_version
|
||||
|| migration.source?.modules != [sanctuary: "0.0.0-alpha.108", onlyfun: "0.0.0-alpha.17"]
|
||||
|| migration.target?.modules != [sanctuary: "0.0.0-alpha.109", onlyfun: "0.0.0-alpha.17"]
|
||||
@@ -3478,7 +3487,7 @@ tasks.register("verifyMirrorLavenderDrawerRelease") {
|
||||
def redStonerRegistry = file(
|
||||
"redstoner/src/main/java/fr/koka99cab/sanctuary26/redstoner/registry/RedStonerRegistries.java").text
|
||||
def drawerRecipe = file("redstoner/src/main/resources/data/redstoner/recipe/drawer.json")
|
||||
if (rootProject.pack_version != "26.2.0-alpha.209"
|
||||
if (rootProject.pack_version != "26.2.0-alpha.210"
|
||||
|| rootProject.anotherworld_version != "0.0.0-alpha.55"
|
||||
|| rootProject.redstoner_version != "0.0.0-alpha.10"
|
||||
|| release.pack_version != rootProject.pack_version || index.versionId != rootProject.pack_version
|
||||
@@ -3597,6 +3606,7 @@ project(":ouch") {
|
||||
dependencies {
|
||||
implementation project(":anotherworld")
|
||||
implementation project(":ambiance")
|
||||
implementation project(":onlyfun")
|
||||
implementation project(":sanctuary")
|
||||
}
|
||||
}
|
||||
@@ -3627,9 +3637,25 @@ project(":onlyfun") {
|
||||
mainClass = "fr.koka99cab.sanctuary26.onlyfun.loot.OnlyFunGeneratorSmoke"
|
||||
}
|
||||
|
||||
tasks.register("abjectionActionContractSmoke", JavaExec) {
|
||||
group = "verification"
|
||||
description = "Checks the bounded historical Abjection action contract."
|
||||
dependsOn tasks.named("testClasses")
|
||||
classpath = sourceSets.test.runtimeClasspath
|
||||
mainClass = "fr.koka99cab.sanctuary26.onlyfun.api.abjection.AbjectionActionContractSmoke"
|
||||
}
|
||||
|
||||
tasks.register("shitActionRequestSmoke", JavaExec) {
|
||||
group = "verification"
|
||||
description = "Checks one-shot and same-tick deduplication for the Abjection key request."
|
||||
dependsOn tasks.named("testClasses")
|
||||
classpath = sourceSets.test.runtimeClasspath
|
||||
mainClass = "fr.koka99cab.sanctuary26.onlyfun.abjection.ShitActionRequestSmoke"
|
||||
}
|
||||
|
||||
tasks.register("verifyOnlyFun") {
|
||||
group = "verification"
|
||||
description = "Checks Only Fun alpha.19 lootboxes, CSV Shop catalog and anchored Cobble Dungeons."
|
||||
description = "Checks Only Fun alpha.24, including the bounded configurable Abjection key."
|
||||
inputs.files(fileTree("src/main/java"))
|
||||
inputs.files(fileTree("src/main/resources"))
|
||||
inputs.file(file("art/shop/shop_catalog_editable.csv"))
|
||||
@@ -3712,9 +3738,61 @@ project(":onlyfun") {
|
||||
def shopCsv = file("art/shop/shop_catalog_editable.csv")
|
||||
def shopCsvText = shopCsv.getText("UTF-8")
|
||||
def shopCsvLines = shopCsv.readLines("UTF-8")
|
||||
def abjectionLanguages = ["fr_fr", "en_us", "ru_ru"].collectEntries { locale ->
|
||||
[(locale): new JsonSlurper().parse(file(
|
||||
"src/main/resources/assets/abjection/lang/${locale}.json"))]
|
||||
}
|
||||
def shitCubeTexture = javax.imageio.ImageIO.read(file(
|
||||
"src/main/resources/assets/abjection/textures/block/shit_cube.png"))
|
||||
def shitBallTexture = javax.imageio.ImageIO.read(file(
|
||||
"src/main/resources/assets/abjection/textures/item/shit_ball.png"))
|
||||
def shitCubeRecipe = new JsonSlurper().parse(file(
|
||||
"src/main/resources/data/abjection/recipe/shit_cube.json"))
|
||||
if (manifest.custom.sanctuary26.data_version != 2
|
||||
|| project.version.toString() != "0.0.0-alpha.20"
|
||||
|| project.version.toString() != "0.0.0-alpha.24"
|
||||
|| manifest.depends?.sanctuary != "*"
|
||||
|| manifest.provides != ["abjection"]
|
||||
|| manifest.custom.sanctuary26.network_protocol != 3
|
||||
|| !javaText.contains('NAMESPACE = "abjection"')
|
||||
|| !javaText.contains('public static StartResult start(ServerPlayer player, Action action)')
|
||||
|| !javaText.contains('PISS(15 * 20, "piss")')
|
||||
|| !javaText.contains('VOMIT(8 * 20, "vomit")')
|
||||
|| !javaText.contains('SHIT(6 * 20, "shit")')
|
||||
|| !javaText.contains('SMOKE(10 * 20, "smoke")')
|
||||
|| !javaText.contains('ServerPlayConnectionEvents.DISCONNECT')
|
||||
|| !javaText.contains('action == Action.SHIT && !restarted')
|
||||
|| !javaText.contains('new ItemStack(AbjectionRegistries.SHIT_BALL)')
|
||||
|| !javaText.contains('BlockBehaviour.Properties.ofFullCopy(Blocks.MUD)')
|
||||
|| !javaText.contains('class ShitBallItem extends Item implements ProjectileItem')
|
||||
|| !javaText.contains('public static void registerFactory(Factory implementation)')
|
||||
|| !javaText.contains('return AbjectionProjectileApi.create(level, position, stack, direction)')
|
||||
|| !javaText.contains('id("c2s/shit_action")')
|
||||
|| !javaText.contains('CustomPacketPayload.codec((payload, buffer) -> {},')
|
||||
|| !javaText.contains('GLFW.GLFW_KEY_J')
|
||||
|| !javaText.contains('while (shitKey.consumeClick())')
|
||||
|| !javaText.contains('server.getPlayerList().getPlayer(player.getUUID()) != player')
|
||||
|| !javaText.contains('acceptTick(player.getUUID(), server.getTickCount())')
|
||||
|| !javaText.contains('AbjectionActions.start(player, AbjectionActions.Action.SHIT)')
|
||||
|| shitCubeTexture == null || shitCubeTexture.width != 16 || shitCubeTexture.height != 16
|
||||
|| shitBallTexture == null || shitBallTexture.width != 16 || shitBallTexture.height != 16
|
||||
|| shitCubeRecipe.key?.get("#") != "abjection:shit_ball"
|
||||
|| shitCubeRecipe.result?.id != "abjection:shit_cube"
|
||||
|| !file("src/main/resources/data/abjection/loot_table/blocks/shit_cube.json").isFile()
|
||||
|| !file("src/main/resources/data/minecraft/tags/block/mineable/shovel.json").text
|
||||
.contains('"abjection:shit_cube"')
|
||||
|| abjectionLanguages.any { locale, translations ->
|
||||
!["item.abjection.shit_ball", "block.abjection.shit_cube",
|
||||
"command.abjection.piss.started", "command.abjection.vomit.started",
|
||||
"command.abjection.shit.started", "command.abjection.smoke.started"].every {
|
||||
translations.containsKey(it)
|
||||
}
|
||||
}
|
||||
|| ["fr_fr", "en_us", "ru_ru"].any { locale ->
|
||||
def translations = new JsonSlurper().parse(file(
|
||||
"src/main/resources/assets/onlyfun/lang/${locale}.json"))
|
||||
!translations.containsKey("key.categories.onlyfun.onlyfun")
|
||||
|| !translations.containsKey("key.onlyfun.shit")
|
||||
}
|
||||
|| !javaText.contains("ANOMALY_DENOMINATOR = 256")
|
||||
|| !javaText.contains("PITY_START = 24")
|
||||
|| !javaText.contains("class LootboxItem")
|
||||
@@ -3861,6 +3939,8 @@ project(":onlyfun") {
|
||||
}
|
||||
|
||||
tasks.named("check") {
|
||||
dependsOn tasks.named("abjectionActionContractSmoke")
|
||||
dependsOn tasks.named("shitActionRequestSmoke")
|
||||
dependsOn tasks.named("onlyFunGeneratorSmoke")
|
||||
dependsOn tasks.named("verifyOnlyFun")
|
||||
}
|
||||
@@ -4126,9 +4206,9 @@ tasks.register("verifyShopPricingLayoutRelease") {
|
||||
"pack/migrations/26.2.0-alpha.192-shop-pricing-layout-to-alpha.193.json"))
|
||||
def release = new JsonSlurper().parse(file("pack/release.json"))
|
||||
def index = new JsonSlurper().parse(file("pack/prism/modrinth.index.json"))
|
||||
if (rootProject.pack_version != "26.2.0-alpha.209"
|
||||
|| rootProject.onlyfun_version != "0.0.0-alpha.20"
|
||||
|| rootProject.sanctuary_version != "0.0.0-alpha.117"
|
||||
if (rootProject.pack_version != "26.2.0-alpha.210"
|
||||
|| rootProject.onlyfun_version != "0.0.0-alpha.24"
|
||||
|| rootProject.sanctuary_version != "0.0.0-alpha.119"
|
||||
|| release.pack_version != rootProject.pack_version || index.versionId != rootProject.pack_version
|
||||
|| migration.source?.pack_version != "26.2.0-alpha.192"
|
||||
|| migration.target?.pack_version != "26.2.0-alpha.193"
|
||||
@@ -4171,8 +4251,8 @@ tasks.register("verifyCuisineCropsRelease") {
|
||||
def climate = file("itsalive/src/main/java/fr/koka99cab/sanctuary26/itsalive/agriculture/ClimateCropForaging.java").text
|
||||
def rice = file("itsalive/src/main/java/fr/koka99cab/sanctuary26/itsalive/agriculture/RiceCropBlock.java").text
|
||||
def bean = file("itsalive/src/main/java/fr/koka99cab/sanctuary26/itsalive/agriculture/BeanCropBlock.java").text
|
||||
if (rootProject.pack_version != "26.2.0-alpha.209"
|
||||
|| rootProject.itsalive_version != "0.0.0-alpha.34"
|
||||
if (rootProject.pack_version != "26.2.0-alpha.210"
|
||||
|| rootProject.itsalive_version != "0.0.0-alpha.36"
|
||||
|| release.pack_version != rootProject.pack_version || index.versionId != rootProject.pack_version
|
||||
|| migration.source?.pack_version != "26.2.0-alpha.193"
|
||||
|| migration.source?.modules != [itsalive: "0.0.0-alpha.26"]
|
||||
@@ -4220,9 +4300,9 @@ tasks.register("verifyShopCsvBossGatesRelease") {
|
||||
def bossGate = file("onlyfun/src/main/java/fr/koka99cab/sanctuary26/onlyfun/shop/OnlyFunBossShopGate.java").text
|
||||
def catalog = file("onlyfun/src/main/java/fr/koka99cab/sanctuary26/onlyfun/shop/OnlyFunShopCatalog.java").text
|
||||
def offerCatalog = file("sanctuary/src/main/java/fr/koka99cab/sanctuary26/sanctuary/shop/ShopOfferCatalog.java").text
|
||||
if (rootProject.pack_version != "26.2.0-alpha.209"
|
||||
|| rootProject.onlyfun_version != "0.0.0-alpha.20"
|
||||
|| rootProject.sanctuary_version != "0.0.0-alpha.117"
|
||||
if (rootProject.pack_version != "26.2.0-alpha.210"
|
||||
|| rootProject.onlyfun_version != "0.0.0-alpha.24"
|
||||
|| rootProject.sanctuary_version != "0.0.0-alpha.119"
|
||||
|| release.pack_version != rootProject.pack_version || index.versionId != rootProject.pack_version
|
||||
|| migration.source?.pack_version != "26.2.0-alpha.194"
|
||||
|| migration.source?.modules != [onlyfun: "0.0.0-alpha.18", sanctuary: "0.0.0-alpha.110"]
|
||||
@@ -4266,7 +4346,7 @@ tasks.register("verifyCulinaryMailboxGrappleFixesRelease") {
|
||||
def release = new JsonSlurper().parse(file("pack/release.json"))
|
||||
def prism = new JsonSlurper().parse(file("pack/prism/modrinth.index.json"))
|
||||
def packwiz = file("pack/packwiz/pack.toml").text
|
||||
if (rootProject.pack_version != "26.2.0-alpha.209"
|
||||
if (rootProject.pack_version != "26.2.0-alpha.210"
|
||||
|| migration.source?.pack_version != "26.2.0-alpha.195"
|
||||
|| migration.target?.pack_version != "26.2.0-alpha.196"
|
||||
|| migration.target?.modules != [
|
||||
@@ -4284,10 +4364,10 @@ tasks.register("verifyCulinaryMailboxGrappleFixesRelease") {
|
||||
|| migration.distribution?.jei?.target != "30.26.0.182"
|
||||
|| migration.compatibility?.save_migration_required != false
|
||||
|| migration.compatibility?.registry_ids_changed != false
|
||||
|| release.pack_version != "26.2.0-alpha.209"
|
||||
|| release.modrinth?.primary_file != "sanctuary-26.2.0-alpha.209.mrpack"
|
||||
|| prism.versionId != "26.2.0-alpha.209"
|
||||
|| !packwiz.contains('version = "26.2.0-alpha.209"')) {
|
||||
|| release.pack_version != "26.2.0-alpha.210"
|
||||
|| release.modrinth?.primary_file != "sanctuary-26.2.0-alpha.210.mrpack"
|
||||
|| prism.versionId != "26.2.0-alpha.210"
|
||||
|| !packwiz.contains('version = "26.2.0-alpha.210"')) {
|
||||
throw new GradleException("The alpha.196 culinary, mailbox and grapple release contract is incomplete")
|
||||
}
|
||||
}
|
||||
@@ -4312,9 +4392,9 @@ tasks.register("verifyQuestRequestsCauldronBatchesRelease") {
|
||||
def prism = new JsonSlurper().parse(file("pack/prism/modrinth.index.json"))
|
||||
def packwiz = file("pack/packwiz/pack.toml").text
|
||||
def sanctuaryManifest = new JsonSlurper().parse(file("sanctuary/src/main/resources/fabric.mod.json"))
|
||||
if (rootProject.pack_version != "26.2.0-alpha.209"
|
||||
|| rootProject.itsalive_version != "0.0.0-alpha.34"
|
||||
|| rootProject.sanctuary_version != "0.0.0-alpha.117"
|
||||
if (rootProject.pack_version != "26.2.0-alpha.210"
|
||||
|| rootProject.itsalive_version != "0.0.0-alpha.36"
|
||||
|| rootProject.sanctuary_version != "0.0.0-alpha.119"
|
||||
|| migration.source?.pack_version != "26.2.0-alpha.196"
|
||||
|| migration.target?.pack_version != "26.2.0-alpha.197"
|
||||
|| migration.target?.modules != [itsalive: "0.0.0-alpha.29", sanctuary: "0.0.0-alpha.113"]
|
||||
@@ -4331,11 +4411,11 @@ tasks.register("verifyQuestRequestsCauldronBatchesRelease") {
|
||||
|| migration.distribution?.jei?.prism_source != "30.24.0.176"
|
||||
|| migration.distribution?.jei?.target != "30.26.0.186"
|
||||
|| migration.compatibility?.sanctuary_target_network_protocol != 19
|
||||
|| sanctuaryManifest.custom?.sanctuary26?.network_protocol != 21
|
||||
|| release.pack_version != "26.2.0-alpha.209"
|
||||
|| release.modrinth?.primary_file != "sanctuary-26.2.0-alpha.209.mrpack"
|
||||
|| prism.versionId != "26.2.0-alpha.209"
|
||||
|| !packwiz.contains('version = "26.2.0-alpha.209"')) {
|
||||
|| sanctuaryManifest.custom?.sanctuary26?.network_protocol != 22
|
||||
|| release.pack_version != "26.2.0-alpha.210"
|
||||
|| release.modrinth?.primary_file != "sanctuary-26.2.0-alpha.210.mrpack"
|
||||
|| prism.versionId != "26.2.0-alpha.210"
|
||||
|| !packwiz.contains('version = "26.2.0-alpha.210"')) {
|
||||
throw new GradleException("The alpha.197 quest-request and cauldron-batch release contract is incomplete")
|
||||
}
|
||||
}
|
||||
@@ -4362,8 +4442,8 @@ tasks.register("verifyFermentationBarrelAutomationRelease") {
|
||||
def packwiz = file("pack/packwiz/pack.toml").text
|
||||
def packwizIndex = file("pack/packwiz/index.toml").text
|
||||
def itsAliveManifest = new JsonSlurper().parse(file("itsalive/src/main/resources/fabric.mod.json"))
|
||||
if (rootProject.pack_version != "26.2.0-alpha.209"
|
||||
|| rootProject.itsalive_version != "0.0.0-alpha.34"
|
||||
if (rootProject.pack_version != "26.2.0-alpha.210"
|
||||
|| rootProject.itsalive_version != "0.0.0-alpha.36"
|
||||
|| migration.source?.pack_version != "26.2.0-alpha.197"
|
||||
|| migration.target?.pack_version != "26.2.0-alpha.198"
|
||||
|| migration.target?.modules != [itsalive: "0.0.0-alpha.30", sanctuary: "0.0.0-alpha.114"]
|
||||
@@ -4381,11 +4461,11 @@ tasks.register("verifyFermentationBarrelAutomationRelease") {
|
||||
|| migration.distribution?.packwiz_index_refreshed != true
|
||||
|| itsAliveManifest.custom?.sanctuary26?.data_version != 3
|
||||
|| itsAliveManifest.custom?.sanctuary26?.network_protocol != 4
|
||||
|| release.pack_version != "26.2.0-alpha.209"
|
||||
|| release.modrinth?.primary_file != "sanctuary-26.2.0-alpha.209.mrpack"
|
||||
|| prism.versionId != "26.2.0-alpha.209"
|
||||
|| !packwiz.contains('version = "26.2.0-alpha.209"')
|
||||
|| !packwizIndex.contains('file = "mods/itsalive-0.0.0-alpha.34.jar"')) {
|
||||
|| release.pack_version != "26.2.0-alpha.210"
|
||||
|| release.modrinth?.primary_file != "sanctuary-26.2.0-alpha.210.mrpack"
|
||||
|| prism.versionId != "26.2.0-alpha.210"
|
||||
|| !packwiz.contains('version = "26.2.0-alpha.210"')
|
||||
|| !packwizIndex.contains('file = "mods/itsalive-0.0.0-alpha.35.jar"')) {
|
||||
throw new GradleException("The alpha.198 fermentation barrel release contract is incomplete")
|
||||
}
|
||||
}
|
||||
@@ -4412,9 +4492,9 @@ tasks.register("verifyQuestRequestContractsRelease") {
|
||||
def packwiz = file("pack/packwiz/pack.toml").text
|
||||
def packwizIndex = file("pack/packwiz/index.toml").text
|
||||
def sanctuaryManifest = new JsonSlurper().parse(file("sanctuary/src/main/resources/fabric.mod.json"))
|
||||
if (rootProject.pack_version != "26.2.0-alpha.209"
|
||||
|| rootProject.itsalive_version != "0.0.0-alpha.34"
|
||||
|| rootProject.sanctuary_version != "0.0.0-alpha.117"
|
||||
if (rootProject.pack_version != "26.2.0-alpha.210"
|
||||
|| rootProject.itsalive_version != "0.0.0-alpha.36"
|
||||
|| rootProject.sanctuary_version != "0.0.0-alpha.119"
|
||||
|| migration.source?.pack_version != "26.2.0-alpha.198"
|
||||
|| migration.source?.modules?.sanctuary != "0.0.0-alpha.114"
|
||||
|| migration.target?.pack_version != "26.2.0-alpha.199"
|
||||
@@ -4425,13 +4505,13 @@ tasks.register("verifyQuestRequestContractsRelease") {
|
||||
|| migration.quest_request_contracts?.right_click_shows_details_before_claim != true
|
||||
|| migration.quest_request_contracts?.chat_accept_revalidates_distance_owner_sign_and_claim != true
|
||||
|| migration.compatibility?.sanctuary_target_network_protocol != 20
|
||||
|| sanctuaryManifest.custom?.sanctuary26?.network_protocol != 21
|
||||
|| release.pack_version != "26.2.0-alpha.209"
|
||||
|| release.modrinth?.primary_file != "sanctuary-26.2.0-alpha.209.mrpack"
|
||||
|| prism.versionId != "26.2.0-alpha.209"
|
||||
|| !packwiz.contains('version = "26.2.0-alpha.209"')
|
||||
|| !packwizIndex.contains('file = "mods/itsalive-0.0.0-alpha.34.jar"')
|
||||
|| !packwizIndex.contains('file = "mods/sanctuary-0.0.0-alpha.117.jar"')) {
|
||||
|| sanctuaryManifest.custom?.sanctuary26?.network_protocol != 22
|
||||
|| release.pack_version != "26.2.0-alpha.210"
|
||||
|| release.modrinth?.primary_file != "sanctuary-26.2.0-alpha.210.mrpack"
|
||||
|| prism.versionId != "26.2.0-alpha.210"
|
||||
|| !packwiz.contains('version = "26.2.0-alpha.210"')
|
||||
|| !packwizIndex.contains('file = "mods/itsalive-0.0.0-alpha.35.jar"')
|
||||
|| !packwizIndex.contains('file = "mods/sanctuary-0.0.0-alpha.118.jar"')) {
|
||||
throw new GradleException("The alpha.199 quest-request contract release is incomplete")
|
||||
}
|
||||
}
|
||||
@@ -4467,9 +4547,9 @@ tasks.register("verifyWeatherTntAmbianceRelease") {
|
||||
def ambianceManifest = new JsonSlurper().parse(file("ambiance/src/main/resources/fabric.mod.json"))
|
||||
def ouchManifest = new JsonSlurper().parse(file("ouch/src/main/resources/fabric.mod.json"))
|
||||
def rootBuild = file("build.gradle").text
|
||||
if (rootProject.pack_version != "26.2.0-alpha.209"
|
||||
|| rootProject.ambiance_version != "0.0.0-alpha.14"
|
||||
|| rootProject.ouch_version != "0.0.0-alpha.9"
|
||||
if (rootProject.pack_version != "26.2.0-alpha.210"
|
||||
|| rootProject.ambiance_version != "0.0.0-alpha.15"
|
||||
|| rootProject.ouch_version != "0.0.0-alpha.10"
|
||||
|| migration.source?.pack_version != "26.2.0-alpha.199"
|
||||
|| migration.source?.modules != [ambiance: "0.0.0-alpha.11", ouch: "0.0.0-alpha.8"]
|
||||
|| migration.target?.pack_version != "26.2.0-alpha.200"
|
||||
@@ -4488,7 +4568,7 @@ tasks.register("verifyWeatherTntAmbianceRelease") {
|
||||
|| migration.compatibility?.ouch_network_protocol != 1
|
||||
|| migration.compatibility?.save_migration_required != false
|
||||
|| ambianceManifest.custom?.sanctuary26?.data_version != 2
|
||||
|| ambianceManifest.custom?.sanctuary26?.network_protocol != 5
|
||||
|| ambianceManifest.custom?.sanctuary26?.network_protocol != 6
|
||||
|| ouchManifest.custom?.sanctuary26?.data_version != 1
|
||||
|| ouchManifest.custom?.sanctuary26?.network_protocol != 1
|
||||
|| ouchManifest.depends?.ambiance != ">=0.0.0-alpha.12"
|
||||
@@ -4501,10 +4581,10 @@ tasks.register("verifyWeatherTntAmbianceRelease") {
|
||||
|| !weatherTnt.contains("AmbianceDailyWeather.CLEAR")
|
||||
|| !weatherTnt.contains("CLEAR_WEATHER_TICKS = 864_000")
|
||||
|| weatherTnt.contains("setWeatherParameters")
|
||||
|| release.pack_version != "26.2.0-alpha.209"
|
||||
|| release.modrinth?.primary_file != "sanctuary-26.2.0-alpha.209.mrpack"
|
||||
|| prism.versionId != "26.2.0-alpha.209"
|
||||
|| !packwiz.contains('version = "26.2.0-alpha.209"')
|
||||
|| release.pack_version != "26.2.0-alpha.210"
|
||||
|| release.modrinth?.primary_file != "sanctuary-26.2.0-alpha.210.mrpack"
|
||||
|| prism.versionId != "26.2.0-alpha.210"
|
||||
|| !packwiz.contains('version = "26.2.0-alpha.210"')
|
||||
|| !packwizIndex.contains('file = "mods/ambiance-0.0.0-alpha.14.jar"')
|
||||
|| !packwizIndex.contains('file = "mods/ouch-0.0.0-alpha.9.jar"')) {
|
||||
throw new GradleException("The alpha.200 Weather TNT and Ambiance release is incomplete")
|
||||
@@ -4533,8 +4613,8 @@ tasks.register("verifyQuestRequestEscrowRelease") {
|
||||
def packwiz = file("pack/packwiz/pack.toml").text
|
||||
def packwizIndex = file("pack/packwiz/index.toml").text
|
||||
def sanctuaryManifest = new JsonSlurper().parse(file("sanctuary/src/main/resources/fabric.mod.json"))
|
||||
if (rootProject.pack_version != "26.2.0-alpha.209"
|
||||
|| rootProject.sanctuary_version != "0.0.0-alpha.117"
|
||||
if (rootProject.pack_version != "26.2.0-alpha.210"
|
||||
|| rootProject.sanctuary_version != "0.0.0-alpha.119"
|
||||
|| migration.source?.pack_version != "26.2.0-alpha.200"
|
||||
|| migration.source?.modules?.sanctuary != "0.0.0-alpha.115"
|
||||
|| migration.target?.pack_version != "26.2.0-alpha.201"
|
||||
@@ -4547,12 +4627,12 @@ tasks.register("verifyQuestRequestEscrowRelease") {
|
||||
|| migration.quest_request_escrow?.reward_delivery_to_claimant_mailbox != true
|
||||
|| migration.compatibility?.sanctuary_target_network_protocol != 21
|
||||
|| sanctuaryManifest.custom?.sanctuary26?.data_version != 11
|
||||
|| sanctuaryManifest.custom?.sanctuary26?.network_protocol != 21
|
||||
|| release.pack_version != "26.2.0-alpha.209"
|
||||
|| release.modrinth?.primary_file != "sanctuary-26.2.0-alpha.209.mrpack"
|
||||
|| prism.versionId != "26.2.0-alpha.209"
|
||||
|| !packwiz.contains('version = "26.2.0-alpha.209"')
|
||||
|| !packwizIndex.contains('file = "mods/sanctuary-0.0.0-alpha.117.jar"')) {
|
||||
|| sanctuaryManifest.custom?.sanctuary26?.network_protocol != 22
|
||||
|| release.pack_version != "26.2.0-alpha.210"
|
||||
|| release.modrinth?.primary_file != "sanctuary-26.2.0-alpha.210.mrpack"
|
||||
|| prism.versionId != "26.2.0-alpha.210"
|
||||
|| !packwiz.contains('version = "26.2.0-alpha.210"')
|
||||
|| !packwizIndex.contains('file = "mods/sanctuary-0.0.0-alpha.118.jar"')) {
|
||||
throw new GradleException("The alpha.201 quest-request escrow release is incomplete")
|
||||
}
|
||||
}
|
||||
@@ -4587,11 +4667,11 @@ tasks.register("verifyWaystoneShopCulinaryAlpha202Release") {
|
||||
"iliketomoveit/src/main/resources/fabric.mod.json"))
|
||||
def sanctuaryManifest = new JsonSlurper().parse(file(
|
||||
"sanctuary/src/main/resources/fabric.mod.json"))
|
||||
if (rootProject.pack_version != "26.2.0-alpha.209"
|
||||
if (rootProject.pack_version != "26.2.0-alpha.210"
|
||||
|| rootProject.anotherworld_version != "0.0.0-alpha.55"
|
||||
|| rootProject.itsalive_version != "0.0.0-alpha.34"
|
||||
|| rootProject.iliketomoveit_version != "0.0.0-alpha.14"
|
||||
|| rootProject.sanctuary_version != "0.0.0-alpha.117"
|
||||
|| rootProject.itsalive_version != "0.0.0-alpha.36"
|
||||
|| rootProject.iliketomoveit_version != "0.0.0-alpha.15"
|
||||
|| rootProject.sanctuary_version != "0.0.0-alpha.119"
|
||||
|| migration.source?.pack_version != "26.2.0-alpha.201"
|
||||
|| migration.source?.modules != [
|
||||
itsalive: "0.0.0-alpha.30",
|
||||
@@ -4626,15 +4706,15 @@ tasks.register("verifyWaystoneShopCulinaryAlpha202Release") {
|
||||
|| moveItManifest.custom?.sanctuary26?.data_version != 3
|
||||
|| moveItManifest.custom?.sanctuary26?.network_protocol != 5
|
||||
|| sanctuaryManifest.custom?.sanctuary26?.data_version != 11
|
||||
|| sanctuaryManifest.custom?.sanctuary26?.network_protocol != 21
|
||||
|| release.pack_version != "26.2.0-alpha.209"
|
||||
|| release.modrinth?.primary_file != "sanctuary-26.2.0-alpha.209.mrpack"
|
||||
|| prism.versionId != "26.2.0-alpha.209"
|
||||
|| !packwiz.contains('version = "26.2.0-alpha.209"')
|
||||
|| sanctuaryManifest.custom?.sanctuary26?.network_protocol != 22
|
||||
|| release.pack_version != "26.2.0-alpha.210"
|
||||
|| release.modrinth?.primary_file != "sanctuary-26.2.0-alpha.210.mrpack"
|
||||
|| prism.versionId != "26.2.0-alpha.210"
|
||||
|| !packwiz.contains('version = "26.2.0-alpha.210"')
|
||||
|| !packwizIndex.contains('file = "mods/anotherworld-0.0.0-alpha.55.jar"')
|
||||
|| !packwizIndex.contains('file = "mods/itsalive-0.0.0-alpha.34.jar"')
|
||||
|| !packwizIndex.contains('file = "mods/iliketomoveit-0.0.0-alpha.14.jar"')
|
||||
|| !packwizIndex.contains('file = "mods/sanctuary-0.0.0-alpha.117.jar"')) {
|
||||
|| !packwizIndex.contains('file = "mods/itsalive-0.0.0-alpha.35.jar"')
|
||||
|| !packwizIndex.contains('file = "mods/iliketomoveit-0.0.0-alpha.15.jar"')
|
||||
|| !packwizIndex.contains('file = "mods/sanctuary-0.0.0-alpha.118.jar"')) {
|
||||
throw new GradleException("The alpha.202 Waystone, Shop and culinary lectern release is incomplete")
|
||||
}
|
||||
}
|
||||
@@ -4747,9 +4827,9 @@ tasks.register("verifySafeVaultFeature") {
|
||||
def image = javax.imageio.ImageIO.read(file("anotherworld/src/main/resources/assets/anotherworld/textures/${path}"))
|
||||
image == null || image.width != size[0] || image.height != size[1]
|
||||
}.keySet()
|
||||
if (rootProject.pack_version != "26.2.0-alpha.209"
|
||||
if (rootProject.pack_version != "26.2.0-alpha.210"
|
||||
|| rootProject.anotherworld_version != "0.0.0-alpha.55"
|
||||
|| rootProject.sanctuary_version != "0.0.0-alpha.117"
|
||||
|| rootProject.sanctuary_version != "0.0.0-alpha.119"
|
||||
|| migration.schema_version != 1
|
||||
|| migration.source?.pack_version != "26.2.0-alpha.201"
|
||||
|| migration.target?.pack_version != "26.2.0-alpha.202"
|
||||
@@ -4938,8 +5018,8 @@ tasks.register("verifySafeVaultInterfaceAlpha203Release") {
|
||||
def prism = new JsonSlurper().parse(file("pack/prism/modrinth.index.json"))
|
||||
def packwiz = file("pack/packwiz/pack.toml").text
|
||||
def packwizIndex = file("pack/packwiz/index.toml").text
|
||||
def jeiFiles = prism.files.findAll { it.path == "mods/jei-26.2-fabric-30.28.0.191.jar" }
|
||||
if (rootProject.pack_version != "26.2.0-alpha.209"
|
||||
def jeiFiles = prism.files.findAll { it.path == "mods/jei-26.2-fabric-30.28.0.193.jar" }
|
||||
if (rootProject.pack_version != "26.2.0-alpha.210"
|
||||
|| rootProject.anotherworld_version != "0.0.0-alpha.55"
|
||||
|| migration.source?.pack_version != "26.2.0-alpha.202"
|
||||
|| migration.source?.modules != [anotherworld: "0.0.0-alpha.50"]
|
||||
@@ -4957,14 +5037,14 @@ tasks.register("verifySafeVaultInterfaceAlpha203Release") {
|
||||
|| migration.distribution?.jei?.modrinth_version_id != "XXTFqqwG"
|
||||
|| migration.distribution?.jei?.prism_manifest_updated != true
|
||||
|| release.pack_version != rootProject.pack_version
|
||||
|| release.modrinth?.primary_file != "sanctuary-26.2.0-alpha.209.mrpack"
|
||||
|| release.modrinth?.primary_file != "sanctuary-26.2.0-alpha.210.mrpack"
|
||||
|| prism.versionId != rootProject.pack_version
|
||||
|| !packwiz.contains('version = "26.2.0-alpha.209"')
|
||||
|| !packwiz.contains('version = "26.2.0-alpha.210"')
|
||||
|| !packwizIndex.contains('file = "mods/anotherworld-0.0.0-alpha.55.jar"')
|
||||
|| packwizIndex.contains('file = "mods/anotherworld-0.0.0-alpha.50.jar"')
|
||||
|| jeiFiles.size() != 1
|
||||
|| jeiFiles[0].hashes?.sha1 != "35353e18b1cf2a61339463447fce1f3e2f716b80"
|
||||
|| jeiFiles[0].hashes?.sha512 != "aa7dca89026fea2293a2e25fa4a84faf0eeaa6407b7e84991e69626ff1ee0f9df9ba3a5e096cbf631d08842693b115653c5c8373e5ef3953318a892dbdc0a33d") {
|
||||
|| jeiFiles[0].hashes?.sha1 != "494f9dc787d6c9cd9d32793da2784ab16834faf3"
|
||||
|| jeiFiles[0].hashes?.sha512 != "0869f4d5840a732a04bedc7684b40140f087f3d759066c206d9d1ec710007f7c9585e4f5ebb04f10e56bef1cf2eddc6978bb2b2c93541cff5fcf7e1609859468") {
|
||||
throw new GradleException("The alpha.203 upright safe-vault interface release is incomplete")
|
||||
}
|
||||
}
|
||||
@@ -4984,8 +5064,8 @@ tasks.register("verifySafeVaultFixAlpha204Release") {
|
||||
def prism = new JsonSlurper().parse(file("pack/prism/modrinth.index.json"))
|
||||
def packwiz = file("pack/packwiz/pack.toml").text
|
||||
def packwizIndex = file("pack/packwiz/index.toml").text
|
||||
def jeiFiles = prism.files.findAll { it.path == "mods/jei-26.2-fabric-30.28.0.191.jar" }
|
||||
if (rootProject.pack_version != "26.2.0-alpha.209"
|
||||
def jeiFiles = prism.files.findAll { it.path == "mods/jei-26.2-fabric-30.28.0.193.jar" }
|
||||
if (rootProject.pack_version != "26.2.0-alpha.210"
|
||||
|| rootProject.anotherworld_version != "0.0.0-alpha.55"
|
||||
|| migration.source?.pack_version != "26.2.0-alpha.203"
|
||||
|| migration.source?.modules != [anotherworld: "0.0.0-alpha.51"]
|
||||
@@ -5011,14 +5091,14 @@ tasks.register("verifySafeVaultFixAlpha204Release") {
|
||||
|| migration.distribution?.jei?.modrinth_version_id != "OcMayhUH"
|
||||
|| migration.distribution?.jei?.prism_manifest_updated != true
|
||||
|| release.pack_version != rootProject.pack_version
|
||||
|| release.modrinth?.primary_file != "sanctuary-26.2.0-alpha.209.mrpack"
|
||||
|| release.modrinth?.primary_file != "sanctuary-26.2.0-alpha.210.mrpack"
|
||||
|| prism.versionId != rootProject.pack_version
|
||||
|| !packwiz.contains('version = "26.2.0-alpha.209"')
|
||||
|| !packwiz.contains('version = "26.2.0-alpha.210"')
|
||||
|| !packwizIndex.contains('file = "mods/anotherworld-0.0.0-alpha.55.jar"')
|
||||
|| packwizIndex.contains('file = "mods/anotherworld-0.0.0-alpha.51.jar"')
|
||||
|| jeiFiles.size() != 1
|
||||
|| jeiFiles[0].hashes?.sha1 != "35353e18b1cf2a61339463447fce1f3e2f716b80"
|
||||
|| jeiFiles[0].hashes?.sha512 != "aa7dca89026fea2293a2e25fa4a84faf0eeaa6407b7e84991e69626ff1ee0f9df9ba3a5e096cbf631d08842693b115653c5c8373e5ef3953318a892dbdc0a33d") {
|
||||
|| jeiFiles[0].hashes?.sha1 != "494f9dc787d6c9cd9d32793da2784ab16834faf3"
|
||||
|| jeiFiles[0].hashes?.sha512 != "0869f4d5840a732a04bedc7684b40140f087f3d759066c206d9d1ec710007f7c9585e4f5ebb04f10e56bef1cf2eddc6978bb2b2c93541cff5fcf7e1609859468") {
|
||||
throw new GradleException("The alpha.204 Safe Block finalization release is incomplete")
|
||||
}
|
||||
}
|
||||
@@ -5038,8 +5118,8 @@ tasks.register("verifySafeVaultCubeAlpha205Release") {
|
||||
def prism = new JsonSlurper().parse(file("pack/prism/modrinth.index.json"))
|
||||
def packwiz = file("pack/packwiz/pack.toml").text
|
||||
def packwizIndex = file("pack/packwiz/index.toml").text
|
||||
def jeiFiles = prism.files.findAll { it.path == "mods/jei-26.2-fabric-30.28.0.191.jar" }
|
||||
if (rootProject.pack_version != "26.2.0-alpha.209"
|
||||
def jeiFiles = prism.files.findAll { it.path == "mods/jei-26.2-fabric-30.28.0.193.jar" }
|
||||
if (rootProject.pack_version != "26.2.0-alpha.210"
|
||||
|| rootProject.anotherworld_version != "0.0.0-alpha.55"
|
||||
|| migration.source?.pack_version != "26.2.0-alpha.204"
|
||||
|| migration.source?.modules != [anotherworld: "0.0.0-alpha.52"]
|
||||
@@ -5068,14 +5148,14 @@ tasks.register("verifySafeVaultCubeAlpha205Release") {
|
||||
|| migration.distribution?.jei?.modrinth_version_id != "NtfJDMhk"
|
||||
|| migration.distribution?.jei?.prism_manifest_updated != true
|
||||
|| release.pack_version != rootProject.pack_version
|
||||
|| release.modrinth?.primary_file != "sanctuary-26.2.0-alpha.209.mrpack"
|
||||
|| release.modrinth?.primary_file != "sanctuary-26.2.0-alpha.210.mrpack"
|
||||
|| prism.versionId != rootProject.pack_version
|
||||
|| !packwiz.contains('version = "26.2.0-alpha.209"')
|
||||
|| !packwiz.contains('version = "26.2.0-alpha.210"')
|
||||
|| !packwizIndex.contains('file = "mods/anotherworld-0.0.0-alpha.55.jar"')
|
||||
|| packwizIndex.contains('file = "mods/anotherworld-0.0.0-alpha.52.jar"')
|
||||
|| jeiFiles.size() != 1
|
||||
|| jeiFiles[0].hashes?.sha1 != "35353e18b1cf2a61339463447fce1f3e2f716b80"
|
||||
|| jeiFiles[0].hashes?.sha512 != "aa7dca89026fea2293a2e25fa4a84faf0eeaa6407b7e84991e69626ff1ee0f9df9ba3a5e096cbf631d08842693b115653c5c8373e5ef3953318a892dbdc0a33d") {
|
||||
|| jeiFiles[0].hashes?.sha1 != "494f9dc787d6c9cd9d32793da2784ab16834faf3"
|
||||
|| jeiFiles[0].hashes?.sha512 != "0869f4d5840a732a04bedc7684b40140f087f3d759066c206d9d1ec710007f7c9585e4f5ebb04f10e56bef1cf2eddc6978bb2b2c93541cff5fcf7e1609859468") {
|
||||
throw new GradleException("The alpha.205 hollow safe-vault cube release is incomplete")
|
||||
}
|
||||
}
|
||||
@@ -5096,8 +5176,8 @@ tasks.register("verifySafeVaultCtmAlpha206Release") {
|
||||
def prism = new JsonSlurper().parse(file("pack/prism/modrinth.index.json"))
|
||||
def packwiz = file("pack/packwiz/pack.toml").text
|
||||
def packwizIndex = file("pack/packwiz/index.toml").text
|
||||
def jeiFiles = prism.files.findAll { it.path == "mods/jei-26.2-fabric-30.28.0.191.jar" }
|
||||
if (rootProject.pack_version != "26.2.0-alpha.209"
|
||||
def jeiFiles = prism.files.findAll { it.path == "mods/jei-26.2-fabric-30.28.0.193.jar" }
|
||||
if (rootProject.pack_version != "26.2.0-alpha.210"
|
||||
|| rootProject.anotherworld_version != "0.0.0-alpha.55"
|
||||
|| migration.source?.pack_version != "26.2.0-alpha.205"
|
||||
|| migration.source?.modules != [anotherworld: "0.0.0-alpha.53"]
|
||||
@@ -5120,14 +5200,14 @@ tasks.register("verifySafeVaultCtmAlpha206Release") {
|
||||
|| migration.compatibility?.pack_release_updated != true
|
||||
|| migration.distribution?.packwiz_index_refreshed != true
|
||||
|| release.pack_version != rootProject.pack_version
|
||||
|| release.modrinth?.primary_file != "sanctuary-26.2.0-alpha.209.mrpack"
|
||||
|| release.modrinth?.primary_file != "sanctuary-26.2.0-alpha.210.mrpack"
|
||||
|| prism.versionId != rootProject.pack_version
|
||||
|| !packwiz.contains('version = "26.2.0-alpha.209"')
|
||||
|| !packwiz.contains('version = "26.2.0-alpha.210"')
|
||||
|| !packwizIndex.contains('file = "mods/anotherworld-0.0.0-alpha.55.jar"')
|
||||
|| packwizIndex.contains('file = "mods/anotherworld-0.0.0-alpha.53.jar"')
|
||||
|| jeiFiles.size() != 1
|
||||
|| jeiFiles[0].hashes?.sha1 != "35353e18b1cf2a61339463447fce1f3e2f716b80"
|
||||
|| jeiFiles[0].hashes?.sha512 != "aa7dca89026fea2293a2e25fa4a84faf0eeaa6407b7e84991e69626ff1ee0f9df9ba3a5e096cbf631d08842693b115653c5c8373e5ef3953318a892dbdc0a33d") {
|
||||
|| jeiFiles[0].hashes?.sha1 != "494f9dc787d6c9cd9d32793da2784ab16834faf3"
|
||||
|| jeiFiles[0].hashes?.sha512 != "0869f4d5840a732a04bedc7684b40140f087f3d759066c206d9d1ec710007f7c9585e4f5ebb04f10e56bef1cf2eddc6978bb2b2c93541cff5fcf7e1609859468") {
|
||||
throw new GradleException("The alpha.206 Safe Block CTM column release is incomplete")
|
||||
}
|
||||
}
|
||||
@@ -5148,8 +5228,8 @@ tasks.register("verifySafeVaultUnlimitedAlpha207Release") {
|
||||
def prism = new JsonSlurper().parse(file("pack/prism/modrinth.index.json"))
|
||||
def packwiz = file("pack/packwiz/pack.toml").text
|
||||
def packwizIndex = file("pack/packwiz/index.toml").text
|
||||
def jeiFiles = prism.files.findAll { it.path == "mods/jei-26.2-fabric-30.28.0.191.jar" }
|
||||
if (rootProject.pack_version != "26.2.0-alpha.209"
|
||||
def jeiFiles = prism.files.findAll { it.path == "mods/jei-26.2-fabric-30.28.0.193.jar" }
|
||||
if (rootProject.pack_version != "26.2.0-alpha.210"
|
||||
|| rootProject.anotherworld_version != "0.0.0-alpha.55"
|
||||
|| migration.source?.pack_version != "26.2.0-alpha.206"
|
||||
|| migration.source?.modules != [anotherworld: "0.0.0-alpha.54"]
|
||||
@@ -5179,14 +5259,14 @@ tasks.register("verifySafeVaultUnlimitedAlpha207Release") {
|
||||
|| migration.compatibility?.pack_release_updated != true
|
||||
|| migration.distribution?.packwiz_index_refreshed != true
|
||||
|| release.pack_version != rootProject.pack_version
|
||||
|| release.modrinth?.primary_file != "sanctuary-26.2.0-alpha.209.mrpack"
|
||||
|| release.modrinth?.primary_file != "sanctuary-26.2.0-alpha.210.mrpack"
|
||||
|| prism.versionId != rootProject.pack_version
|
||||
|| !packwiz.contains('version = "26.2.0-alpha.209"')
|
||||
|| !packwiz.contains('version = "26.2.0-alpha.210"')
|
||||
|| !packwizIndex.contains('file = "mods/anotherworld-0.0.0-alpha.55.jar"')
|
||||
|| packwizIndex.contains('file = "mods/anotherworld-0.0.0-alpha.54.jar"')
|
||||
|| jeiFiles.size() != 1
|
||||
|| jeiFiles[0].hashes?.sha1 != "35353e18b1cf2a61339463447fce1f3e2f716b80"
|
||||
|| jeiFiles[0].hashes?.sha512 != "aa7dca89026fea2293a2e25fa4a84faf0eeaa6407b7e84991e69626ff1ee0f9df9ba3a5e096cbf631d08842693b115653c5c8373e5ef3953318a892dbdc0a33d") {
|
||||
|| jeiFiles[0].hashes?.sha1 != "494f9dc787d6c9cd9d32793da2784ab16834faf3"
|
||||
|| jeiFiles[0].hashes?.sha512 != "0869f4d5840a732a04bedc7684b40140f087f3d759066c206d9d1ec710007f7c9585e4f5ebb04f10e56bef1cf2eddc6978bb2b2c93541cff5fcf7e1609859468") {
|
||||
throw new GradleException("The alpha.207 unlimited safe-vault balance release is incomplete")
|
||||
}
|
||||
}
|
||||
@@ -5237,9 +5317,9 @@ tasks.register("verifyCanapliaFoundation") {
|
||||
"itsalive/src/main/java/fr/koka99cab/sanctuary26/itsalive/ItsAliveMod.java").text
|
||||
def visualApiSource = file(
|
||||
"ambiance/src/main/java/fr/koka99cab/sanctuary26/ambiance/api/visual/AmbianceVisualEffectApi.java").text
|
||||
if (rootProject.pack_version != "26.2.0-alpha.209"
|
||||
|| rootProject.itsalive_version != "0.0.0-alpha.34"
|
||||
|| rootProject.ambiance_version != "0.0.0-alpha.14"
|
||||
if (rootProject.pack_version != "26.2.0-alpha.210"
|
||||
|| rootProject.itsalive_version != "0.0.0-alpha.36"
|
||||
|| rootProject.ambiance_version != "0.0.0-alpha.15"
|
||||
|| migration.issue != 35
|
||||
|| migration.source?.fabric_id != "canaplia"
|
||||
|| migration.source?.version != "2.0.0"
|
||||
@@ -5285,7 +5365,7 @@ tasks.register("verifyCanapliaFoundation") {
|
||||
|| itsAliveManifest.custom?.sanctuary26?.data_version != 3
|
||||
|| itsAliveManifest.custom?.sanctuary26?.network_protocol != 4
|
||||
|| ambianceManifest.custom?.sanctuary26?.data_version != 2
|
||||
|| ambianceManifest.custom?.sanctuary26?.network_protocol != 5
|
||||
|| ambianceManifest.custom?.sanctuary26?.network_protocol != 6
|
||||
|| !itsAliveDependencies.contains('implementation project(":ambiance")')
|
||||
|| !itsAliveDependencies.contains('implementation project(":sanctuary")')
|
||||
|| settingsText.contains('"canaplia"')
|
||||
@@ -5303,6 +5383,7 @@ tasks.register("verifyCanapliaFoundation") {
|
||||
tasks.named("verifyWorkspace") {
|
||||
dependsOn "verifyCanapliaGameplay"
|
||||
dependsOn "verifyCanapliaAlpha209Release"
|
||||
dependsOn "verifyAlpha210Release"
|
||||
dependsOn tasks.named("verifyWaystoneShopCulinaryAlpha202Release")
|
||||
dependsOn tasks.named("verifySafeVaultFeature")
|
||||
dependsOn tasks.named("verifySafeVaultInterfaceAlpha203Release")
|
||||
@@ -5349,8 +5430,10 @@ tasks.register("verifyCanapliaGameplay") {
|
||||
def resinState = new JsonSlurper().parse(new File(generated, "assets/canaplia/blockstates/resin_block.json"))
|
||||
def codexRecipe = new JsonSlurper().parse(new File(generated, "data/canaplia/recipe/codex.json"))
|
||||
def requiredCodexKeys = [
|
||||
"screen.canaplia.codex.introduction", "screen.canaplia.codex.entry_number",
|
||||
"screen.canaplia.codex.origin", "item.canaplia.codex_page"
|
||||
"screen.canaplia.codex.title", "screen.canaplia.codex.page",
|
||||
"screen.canaplia.codex.entry_number", "screen.canaplia.codex.origin",
|
||||
"screen.canaplia.codex.hidden", "message.canaplia.codex_page.complete",
|
||||
"tooltip.canaplia.codex.personal", "item.canaplia.codex", "item.canaplia.codex_page"
|
||||
]
|
||||
def languageKeysMissing = ["fr_fr", "en_us", "ru_ru"].collectMany { locale ->
|
||||
def lang = new JsonSlurper().parse(file(
|
||||
@@ -5414,7 +5497,7 @@ tasks.register("verifyCanapliaGameplay") {
|
||||
|| migration.compatibility?.pack_release_updated != false
|
||||
|| itsAliveManifest.custom?.sanctuary26?.data_version != 3
|
||||
|| itsAliveManifest.custom?.sanctuary26?.network_protocol != 4
|
||||
|| ambianceManifest.custom?.sanctuary26?.network_protocol != 5
|
||||
|| ambianceManifest.custom?.sanctuary26?.network_protocol != 6
|
||||
|| discovery.schema_version != 1 || discovery.sources.size() > 128
|
||||
|| discovery.sources.any {
|
||||
it.chance_numerator < 1 || it.chance_numerator >= it.chance_denominator
|
||||
@@ -5446,7 +5529,7 @@ tasks.register("verifyCanapliaGameplay") {
|
||||
|| !codexScreen.contains("extends BookViewScreen")
|
||||
|| !harvesting.contains("NEIGHBOR_RADIUS = 1")
|
||||
|| !effects.contains("ServerPlayConnectionEvents.DISCONNECT")
|
||||
|| !knowledge.contains("unknown future keys survive round-trips")
|
||||
|| !knowledge.contains("the legacy shared field is preserved")
|
||||
|| !policy.contains('namespace.equals("canaplia")')) {
|
||||
throw new GradleException(
|
||||
"The complete Canaplia gameplay contract is incomplete; missing languages="
|
||||
@@ -5474,7 +5557,7 @@ tasks.register("verifyCanapliaAlpha209Release") {
|
||||
def packwiz = file("pack/packwiz/pack.toml").text
|
||||
def packwizIndex = file("pack/packwiz/index.toml").text
|
||||
def jeiFiles = prism.files.findAll {
|
||||
it.path == "mods/jei-26.2-fabric-30.28.0.191.jar"
|
||||
it.path == "mods/jei-26.2-fabric-30.28.0.193.jar"
|
||||
}
|
||||
def expectedSource = [
|
||||
ambiance: "0.0.0-alpha.12",
|
||||
@@ -5484,10 +5567,10 @@ tasks.register("verifyCanapliaAlpha209Release") {
|
||||
ambiance: "0.0.0-alpha.14",
|
||||
itsalive: "0.0.0-alpha.33",
|
||||
onlyfun: "0.0.0-alpha.20"]
|
||||
if (rootProject.pack_version != "26.2.0-alpha.209"
|
||||
|| rootProject.ambiance_version != expectedTarget.ambiance
|
||||
|| rootProject.itsalive_version != "0.0.0-alpha.34"
|
||||
|| rootProject.onlyfun_version != expectedTarget.onlyfun
|
||||
if (rootProject.pack_version != "26.2.0-alpha.210"
|
||||
|| rootProject.ambiance_version != "0.0.0-alpha.15"
|
||||
|| rootProject.itsalive_version != "0.0.0-alpha.36"
|
||||
|| rootProject.onlyfun_version != "0.0.0-alpha.24"
|
||||
|| migration.source?.pack_version != "26.2.0-alpha.207"
|
||||
|| migration.source?.modules != expectedSource
|
||||
|| migration.target?.pack_version != "26.2.0-alpha.208"
|
||||
@@ -5532,23 +5615,128 @@ tasks.register("verifyCanapliaAlpha209Release") {
|
||||
|| fixMigration.compatibility?.network_protocol_changed != false
|
||||
|| fixMigration.distribution?.pack_release_updated != true
|
||||
|| release.pack_version != rootProject.pack_version
|
||||
|| release.modrinth?.primary_file != "sanctuary-26.2.0-alpha.209.mrpack"
|
||||
|| release.modrinth?.primary_file != "sanctuary-26.2.0-alpha.210.mrpack"
|
||||
|| prism.versionId != rootProject.pack_version
|
||||
|| !packwiz.contains('version = "26.2.0-alpha.209"')
|
||||
|| !packwiz.contains('version = "26.2.0-alpha.210"')
|
||||
|| !packwizIndex.contains('file = "mods/ambiance-0.0.0-alpha.14.jar"')
|
||||
|| !packwizIndex.contains('file = "mods/itsalive-0.0.0-alpha.34.jar"')
|
||||
|| !packwizIndex.contains('file = "mods/onlyfun-0.0.0-alpha.20.jar"')
|
||||
|| !packwizIndex.contains('file = "mods/itsalive-0.0.0-alpha.35.jar"')
|
||||
|| !packwizIndex.contains('file = "mods/onlyfun-0.0.0-alpha.21.jar"')
|
||||
|| packwizIndex.contains('file = "mods/ambiance-0.0.0-alpha.12.jar"')
|
||||
|| packwizIndex.contains('file = "mods/itsalive-0.0.0-alpha.33.jar"')
|
||||
|| packwizIndex.contains('file = "mods/onlyfun-0.0.0-alpha.19.jar"')
|
||||
|| jeiFiles.size() != 1
|
||||
|| jeiFiles[0].hashes?.sha1 != "35353e18b1cf2a61339463447fce1f3e2f716b80"
|
||||
|| jeiFiles[0].hashes?.sha512 != "aa7dca89026fea2293a2e25fa4a84faf0eeaa6407b7e84991e69626ff1ee0f9df9ba3a5e096cbf631d08842693b115653c5c8373e5ef3953318a892dbdc0a33d") {
|
||||
|| jeiFiles[0].hashes?.sha1 != "494f9dc787d6c9cd9d32793da2784ab16834faf3"
|
||||
|| jeiFiles[0].hashes?.sha512 != "0869f4d5840a732a04bedc7684b40140f087f3d759066c206d9d1ec710007f7c9585e4f5ebb04f10e56bef1cf2eddc6978bb2b2c93541cff5fcf7e1609859468") {
|
||||
throw new GradleException("The alpha.209 Canaplia correction release is incomplete")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tasks.register("verifyAlpha210Release") {
|
||||
group = "verification"
|
||||
description = "Checks the Sanctuary 26.2.0-alpha.210 Forbidden Book and gameplay-polish release."
|
||||
inputs.file(file("pack/migrations/26.2.0-alpha.209-forbidden-book-and-polish-to-alpha.210.json"))
|
||||
inputs.file(file("pack/release.json"))
|
||||
inputs.file(file("pack/prism/modrinth.index.json"))
|
||||
inputs.file(file("pack/packwiz/pack.toml"))
|
||||
inputs.file(file("pack/packwiz/index.toml"))
|
||||
inputs.file(file("CHANGELOG.md"))
|
||||
inputs.files([
|
||||
file("sanctuary/src/main/resources/fabric.mod.json"),
|
||||
file("onlyfun/src/main/resources/fabric.mod.json"),
|
||||
file("iliketomoveit/src/main/resources/fabric.mod.json"),
|
||||
file("itsalive/src/main/resources/fabric.mod.json")])
|
||||
doLast {
|
||||
def migration = new JsonSlurper().parse(file(
|
||||
"pack/migrations/26.2.0-alpha.209-forbidden-book-and-polish-to-alpha.210.json"))
|
||||
def release = new JsonSlurper().parse(file("pack/release.json"))
|
||||
def prism = new JsonSlurper().parse(file("pack/prism/modrinth.index.json"))
|
||||
def packwiz = file("pack/packwiz/pack.toml").text
|
||||
def packwizIndex = file("pack/packwiz/index.toml").text
|
||||
def changelog = file("CHANGELOG.md").text
|
||||
def manifests = ["sanctuary", "onlyfun", "iliketomoveit", "itsalive"].collectEntries { id ->
|
||||
[(id): new JsonSlurper().parse(file("${id}/src/main/resources/fabric.mod.json"))]
|
||||
}
|
||||
def expectedSource = [
|
||||
sanctuary: "0.0.0-alpha.117",
|
||||
onlyfun: "0.0.0-alpha.20",
|
||||
iliketomoveit: "0.0.0-alpha.14",
|
||||
itsalive: "0.0.0-alpha.34"]
|
||||
def expectedTarget = [
|
||||
sanctuary: "0.0.0-alpha.118",
|
||||
onlyfun: "0.0.0-alpha.21",
|
||||
iliketomoveit: "0.0.0-alpha.15",
|
||||
itsalive: "0.0.0-alpha.35"]
|
||||
def requiredJars = expectedTarget.collect { id, version ->
|
||||
"file = \"mods/${id}-${version}.jar\""
|
||||
}
|
||||
def obsoleteJars = expectedSource.collect { id, version ->
|
||||
"file = \"mods/${id}-${version}.jar\""
|
||||
}
|
||||
def jeiFiles = prism.files.findAll {
|
||||
it.path == "mods/jei-26.2-fabric-30.28.0.193.jar"
|
||||
}
|
||||
if (rootProject.pack_version != "26.2.0-alpha.210"
|
||||
|| rootProject.sanctuary_version != "0.0.0-alpha.119"
|
||||
|| rootProject.onlyfun_version != "0.0.0-alpha.24"
|
||||
|| rootProject.iliketomoveit_version != expectedTarget.iliketomoveit
|
||||
|| rootProject.itsalive_version != "0.0.0-alpha.36"
|
||||
|| migration.source?.pack_version != "26.2.0-alpha.209"
|
||||
|| migration.source?.modules != expectedSource
|
||||
|| migration.target?.pack_version != "26.2.0-alpha.210"
|
||||
|| migration.target?.modules != expectedTarget
|
||||
|| migration.features?.shop_status_layout_separated != true
|
||||
|| migration.features?.shop_progression_bypasses_removed != true
|
||||
|| migration.features?.zombie_xbox_360_names != true
|
||||
|| migration.features?.cape_reconnect_resynchronization != true
|
||||
|| migration.features?.quest_requests_require_dedicated_signs != true
|
||||
|| migration.features?.ridden_happy_ghast_cruise_speed_multiplier != 2.0
|
||||
|| migration.features?.canaplia_codex_name != "Forbidden Book"
|
||||
|| migration.features?.canaplia_page_learns_random_unknown_entry != true
|
||||
|| migration.features?.canaplia_handheld_knowledge_personal != true
|
||||
|| migration.features?.canaplia_lectern_shares_owner_knowledge != true
|
||||
|| migration.compatibility?.new_world_required != false
|
||||
|| migration.compatibility?.save_reset_required != false
|
||||
|| migration.compatibility?.registry_ids_changed != false
|
||||
|| migration.compatibility?.sanctuary_data_version != 11
|
||||
|| migration.compatibility?.sanctuary_network_protocol != 21
|
||||
|| migration.compatibility?.onlyfun_data_version != 2
|
||||
|| migration.compatibility?.onlyfun_network_protocol != 2
|
||||
|| migration.compatibility?.iliketomoveit_data_version != 3
|
||||
|| migration.compatibility?.iliketomoveit_network_protocol != 5
|
||||
|| migration.compatibility?.itsalive_data_version != 3
|
||||
|| migration.compatibility?.itsalive_network_protocol != 4
|
||||
|| migration.distribution?.pack_release_updated != true
|
||||
|| migration.distribution?.packwiz_index_refreshed != true
|
||||
|| migration.distribution?.prism_modrinth_index_updated != true
|
||||
|| migration.distribution?.shared_client_server_mrpack != true
|
||||
|| migration.distribution?.jei?.source != "30.28.0.191"
|
||||
|| migration.distribution?.jei?.target != "30.28.0.193"
|
||||
|| migration.distribution?.jei?.modrinth_version_id != "qaCQ7LGO"
|
||||
|| migration.distribution?.jei?.packwiz_and_prism_synchronized != true
|
||||
|| manifests.sanctuary.custom?.sanctuary26?.data_version != 11
|
||||
|| manifests.sanctuary.custom?.sanctuary26?.network_protocol != 22
|
||||
|| manifests.onlyfun.custom?.sanctuary26?.data_version != 2
|
||||
|| manifests.onlyfun.custom?.sanctuary26?.network_protocol != 3
|
||||
|| manifests.iliketomoveit.custom?.sanctuary26?.data_version != 3
|
||||
|| manifests.iliketomoveit.custom?.sanctuary26?.network_protocol != 5
|
||||
|| manifests.itsalive.custom?.sanctuary26?.data_version != 3
|
||||
|| manifests.itsalive.custom?.sanctuary26?.network_protocol != 4
|
||||
|| release.pack_version != "26.2.0-alpha.210"
|
||||
|| release.modrinth?.primary_file != "sanctuary-26.2.0-alpha.210.mrpack"
|
||||
|| prism.versionId != "26.2.0-alpha.210"
|
||||
|| !packwiz.contains('version = "26.2.0-alpha.210"')
|
||||
|| requiredJars.any { !packwizIndex.contains(it) }
|
||||
|| obsoleteJars.any { packwizIndex.contains(it) }
|
||||
|| jeiFiles.size() != 1
|
||||
|| jeiFiles[0].hashes?.sha1 != "494f9dc787d6c9cd9d32793da2784ab16834faf3"
|
||||
|| jeiFiles[0].hashes?.sha512 != "0869f4d5840a732a04bedc7684b40140f087f3d759066c206d9d1ec710007f7c9585e4f5ebb04f10e56bef1cf2eddc6978bb2b2c93541cff5fcf7e1609859468"
|
||||
|| !changelog.contains("## `26.2.0-alpha.210`")) {
|
||||
throw new GradleException("The alpha.210 Forbidden Book and gameplay-polish release is incomplete")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tasks.named("check") {
|
||||
dependsOn tasks.named("verifyWorkspace")
|
||||
dependsOn tasks.named("verifyTransport")
|
||||
|
||||
+7
-7
@@ -12,23 +12,23 @@ fabric_api_version=0.158.0+26.2
|
||||
# Standby modules stay at zero until their migration really begins.
|
||||
mod_version=0.0.0-alpha.0
|
||||
module_lifecycle=standby
|
||||
ambiance_version=0.0.0-alpha.14
|
||||
ambiance_version=0.0.0-alpha.15
|
||||
ambiance_lifecycle=active
|
||||
redstoner_version=0.0.0-alpha.10
|
||||
redstoner_lifecycle=active
|
||||
anotherworld_version=0.0.0-alpha.55
|
||||
anotherworld_lifecycle=active
|
||||
itsalive_version=0.0.0-alpha.34
|
||||
itsalive_version=0.0.0-alpha.36
|
||||
itsalive_lifecycle=active
|
||||
ouch_version=0.0.0-alpha.9
|
||||
ouch_version=0.0.0-alpha.10
|
||||
ouch_lifecycle=active
|
||||
masterkey_version=0.0.0-alpha.8
|
||||
masterkey_lifecycle=active
|
||||
iliketomoveit_version=0.0.0-alpha.14
|
||||
iliketomoveit_version=0.0.0-alpha.15
|
||||
iliketomoveit_lifecycle=active
|
||||
onlyfun_version=0.0.0-alpha.20
|
||||
onlyfun_version=0.0.0-alpha.24
|
||||
onlyfun_lifecycle=active
|
||||
sanctuary_version=0.0.0-alpha.117
|
||||
sanctuary_version=0.0.0-alpha.119
|
||||
sanctuary_lifecycle=active
|
||||
pack_version=26.2.0-alpha.209
|
||||
pack_version=26.2.0-alpha.210
|
||||
maven_group=fr.koka99cab.sanctuary26
|
||||
|
||||
@@ -16,6 +16,14 @@ tasks.register("waystoneElevatorRulesSmoke", JavaExec) {
|
||||
dependsOn tasks.named("testClasses")
|
||||
}
|
||||
|
||||
tasks.register("happyGhastCruiseSpeedSmoke", JavaExec) {
|
||||
group = "verification"
|
||||
description = "Checks the rider-only Happy Ghast cruise-speed multiplier."
|
||||
classpath = sourceSets.test.runtimeClasspath
|
||||
mainClass = "fr.koka99cab.sanctuary26.iliketomoveit.transport.HappyGhastCruiseSpeedSmoke"
|
||||
dependsOn tasks.named("testClasses")
|
||||
}
|
||||
|
||||
tasks.named("test") {
|
||||
// Runtime contracts are executable smoke tasks, not JUnit engine tests.
|
||||
failOnNoDiscoveredTests = false
|
||||
@@ -168,6 +176,7 @@ tasks.register("verifyGrappling") {
|
||||
"data/iliketomoveit/loot_table/blocks/pulley_head.json",
|
||||
"data/spiderman/loot_table/blocks/pulley_head.json",
|
||||
"data/minecraft/tags/block/needs_stone_tool.json",
|
||||
"iliketomoveit.mixins.json",
|
||||
"iliketomoveit.client.mixins.json"
|
||||
]
|
||||
def missingResources = requiredResources.findAll { !file("src/main/resources/${it}").isFile() }
|
||||
@@ -184,10 +193,11 @@ tasks.register("verifyGrappling") {
|
||||
}
|
||||
|
||||
def manifest = new JsonSlurper().parse(file("src/main/resources/fabric.mod.json"))
|
||||
if (!manifest.mixins?.any { it.config == "iliketomoveit.client.mixins.json" && it.environment == "client" }
|
||||
if (!manifest.mixins?.any { it.config == "iliketomoveit.mixins.json" }
|
||||
|| !manifest.mixins?.any { it.config == "iliketomoveit.client.mixins.json" && it.environment == "client" }
|
||||
|| manifest.custom?.sanctuary26?.data_version != 3
|
||||
|| manifest.custom?.sanctuary26?.network_protocol != 5) {
|
||||
throw new GradleException("Grappling must keep I Like To Move It aggregate data v3/protocol 5 and register its client mixin")
|
||||
throw new GradleException("I Like To Move It must keep aggregate data v3/protocol 5 and register its common/client mixins")
|
||||
}
|
||||
["fr_fr", "en_us", "ru_ru"].each { language ->
|
||||
def translations = new JsonSlurper().parse(file("src/main/resources/assets/iliketomoveit/lang/${language}.json"))
|
||||
@@ -295,7 +305,7 @@ tasks.register("verifyPortals") {
|
||||
}
|
||||
def atlasCavernsMigration = new JsonSlurper().parse(rootProject.file(
|
||||
"pack/migrations/26.2.0-alpha.149-atlas-caverns-to-alpha.150.json"))
|
||||
if (project.version.toString() != "0.0.0-alpha.14"
|
||||
if (project.version.toString() != "0.0.0-alpha.15"
|
||||
|| atlasCavernsMigration.target?.pack_version != "26.2.0-alpha.150"
|
||||
|| atlasCavernsMigration.target?.modules?.iliketomoveit != "0.0.0-alpha.9"
|
||||
|| atlasCavernsMigration.portals?.active_routes != [
|
||||
@@ -331,7 +341,7 @@ tasks.register("verifySilverAircraft") {
|
||||
def manifest = new JsonSlurper().parse(file("src/main/resources/fabric.mod.json"))
|
||||
def migration = new JsonSlurper().parse(rootProject.file(
|
||||
"pack/migrations/26.2.0-alpha.184-silver-engineering-to-alpha.185.json"))
|
||||
if (project.version.toString() != "0.0.0-alpha.14"
|
||||
if (project.version.toString() != "0.0.0-alpha.15"
|
||||
|| recipe.pattern != ["PGP", "GCG", "PBP"]
|
||||
|| recipe.key != [B: "anotherworld:silver_block", C: "anotherworld:aircraft_cockpit",
|
||||
G: "anotherworld:silver_gear", P: "anotherworld:silver_plate"]
|
||||
@@ -360,7 +370,7 @@ tasks.register("verifyGrapplePrediction") {
|
||||
def payload = file("src/main/java/fr/koka99cab/sanctuary26/iliketomoveit/network/GrappleStatePayload.java").text
|
||||
def controller = file("src/main/java/fr/koka99cab/sanctuary26/iliketomoveit/grapple/GrappleController.java").text
|
||||
def prediction = file("src/main/java/fr/koka99cab/sanctuary26/iliketomoveit/client/grapple/ClientGrapplePrediction.java").text
|
||||
if (project.version.toString() != "0.0.0-alpha.14"
|
||||
if (project.version.toString() != "0.0.0-alpha.15"
|
||||
|| migration.target?.modules?.iliketomoveit != "0.0.0-alpha.13"
|
||||
|| migration.grappling?.owner_prediction_payload != "iliketomoveit:s2c/grapple_state"
|
||||
|| migration.compatibility?.iliketomoveit_source_network_protocol != 4
|
||||
@@ -392,7 +402,7 @@ tasks.register("verifyWaystoneDescendingRelease") {
|
||||
"src/main/java/fr/koka99cab/sanctuary26/iliketomoveit/waystone/WaystoneElevatorRules.java").text
|
||||
def teleportation = file(
|
||||
"src/main/java/fr/koka99cab/sanctuary26/iliketomoveit/network/WaystoneTeleportation.java").text
|
||||
if (project.version.toString() != "0.0.0-alpha.14"
|
||||
if (project.version.toString() != "0.0.0-alpha.15"
|
||||
|| migration.target?.modules?.iliketomoveit != "0.0.0-alpha.14"
|
||||
|| migration.waystone_elevator?.undiscovered_downward_destination_allowed != true
|
||||
|| migration.waystone_elevator?.destination_discovered_after_arrival != true
|
||||
@@ -411,6 +421,7 @@ tasks.register("verifyWaystoneDescendingRelease") {
|
||||
tasks.named("check") {
|
||||
dependsOn tasks.named("grapplePhysicsSmoke")
|
||||
dependsOn tasks.named("waystoneElevatorRulesSmoke")
|
||||
dependsOn tasks.named("happyGhastCruiseSpeedSmoke")
|
||||
dependsOn tasks.named("verifyWaystones")
|
||||
dependsOn tasks.named("verifyGrappling")
|
||||
dependsOn tasks.named("verifyGrapplePrediction")
|
||||
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
package fr.koka99cab.sanctuary26.iliketomoveit.mixin;
|
||||
|
||||
import fr.koka99cab.sanctuary26.iliketomoveit.transport.HappyGhastCruiseSpeed;
|
||||
import net.minecraft.world.entity.animal.happyghast.HappyGhast;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.injection.At;
|
||||
import org.spongepowered.asm.mixin.injection.Inject;
|
||||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
|
||||
|
||||
@Mixin(HappyGhast.class)
|
||||
public abstract class HappyGhastMixin {
|
||||
@Inject(method = "getRiddenInput", at = @At("RETURN"), cancellable = true)
|
||||
private void iliketomoveit$boostCruiseSpeed(Player rider, Vec3 input,
|
||||
CallbackInfoReturnable<Vec3> callback) {
|
||||
callback.setReturnValue(HappyGhastCruiseSpeed.boost(callback.getReturnValue()));
|
||||
}
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
package fr.koka99cab.sanctuary26.iliketomoveit.transport;
|
||||
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
|
||||
/** Keeps the Happy Ghast ride practical without changing its autonomous movement. */
|
||||
public final class HappyGhastCruiseSpeed {
|
||||
public static final double RIDER_INPUT_MULTIPLIER = 2.0D;
|
||||
|
||||
private HappyGhastCruiseSpeed() {
|
||||
}
|
||||
|
||||
public static Vec3 boost(Vec3 vanillaInput) {
|
||||
return vanillaInput.scale(RIDER_INPUT_MULTIPLIER);
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,9 @@
|
||||
]
|
||||
},
|
||||
"mixins": [
|
||||
{
|
||||
"config": "iliketomoveit.mixins.json"
|
||||
},
|
||||
{
|
||||
"config": "iliketomoveit.client.mixins.json",
|
||||
"environment": "client"
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"required": true,
|
||||
"package": "fr.koka99cab.sanctuary26.iliketomoveit.mixin",
|
||||
"compatibilityLevel": "JAVA_25",
|
||||
"mixins": [
|
||||
"HappyGhastMixin"
|
||||
],
|
||||
"injectors": {
|
||||
"defaultRequire": 1
|
||||
}
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
package fr.koka99cab.sanctuary26.iliketomoveit.transport;
|
||||
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
|
||||
/** Regression checks for the rider-only Happy Ghast cruise multiplier. */
|
||||
public final class HappyGhastCruiseSpeedSmoke {
|
||||
private HappyGhastCruiseSpeedSmoke() {
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
Vec3 vanillaInput = new Vec3(-0.195D, 0.0975D, 0.195D);
|
||||
Vec3 boosted = HappyGhastCruiseSpeed.boost(vanillaInput);
|
||||
|
||||
assertNear(boosted.x, -0.39D, "sideways input");
|
||||
assertNear(boosted.y, 0.195D, "vertical input");
|
||||
assertNear(boosted.z, 0.39D, "forward input");
|
||||
if (!HappyGhastCruiseSpeed.boost(Vec3.ZERO).equals(Vec3.ZERO)) {
|
||||
throw new IllegalStateException("Idle Happy Ghasts must remain idle");
|
||||
}
|
||||
}
|
||||
|
||||
private static void assertNear(double actual, double expected, String label) {
|
||||
if (Math.abs(actual - expected) > 1.0E-12D) {
|
||||
throw new IllegalStateException(label + ": expected " + expected + ", got " + actual);
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
|
||||
It's Alive possède le vivant de Sanctuary. Il enrichit directement le Creeper vanilla et accueille désormais les papillons de Pipallon, les Mooblooms fleuries et les créatures de Real Ghost — baleine volante comprise — sous un référencement unifié `itsalive:*`.
|
||||
|
||||
## Version courante — `0.0.0-alpha.20`
|
||||
## Version courante — `0.0.0-alpha.36`
|
||||
|
||||
## Creepers
|
||||
|
||||
|
||||
+76
-6
@@ -153,7 +153,7 @@ tasks.register("generateCanapliaResources") {
|
||||
writeJson("data/c/tags/item/seeds.json", [replace: false, values: seedIds])
|
||||
writeJson("data/canaplia/tags/item/hemp_buds.json", [replace: false, values: budIds])
|
||||
|
||||
itemDefinition("codex", "minecraft:item/book")
|
||||
itemDefinition("codex", "minecraft:item/enchanted_book")
|
||||
itemDefinition("codex_page", "minecraft:item/paper")
|
||||
writeJson("data/canaplia/recipe/codex.json", [
|
||||
type: "minecraft:crafting_shaped", category: "misc",
|
||||
@@ -556,7 +556,7 @@ tasks.register("verifyItsAlive") {
|
||||
def climateSource = file("src/main/java/fr/koka99cab/sanctuary26/itsalive/agriculture/ClimateCropForaging.java").text
|
||||
def pineappleSource = file("src/main/java/fr/koka99cab/sanctuary26/itsalive/agriculture/PineappleItem.java").text
|
||||
def worldgenSource = file("src/main/java/fr/koka99cab/sanctuary26/itsalive/agriculture/ItsAliveAgricultureWorldGeneration.java").text
|
||||
if (project.version.toString() != "0.0.0-alpha.34"
|
||||
if (project.version.toString() != "0.0.0-alpha.36"
|
||||
|| climateMigration.target?.pack_version != "26.2.0-alpha.157"
|
||||
|| climateMigration.target?.modules?.itsalive != "0.0.0-alpha.24"
|
||||
|| !climateSource.contains("PlayerBlockBreakEvents.AFTER.register")
|
||||
@@ -1313,7 +1313,7 @@ tasks.register("verifyCulinaryReliability") {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (project.version.toString() != "0.0.0-alpha.34"
|
||||
if (project.version.toString() != "0.0.0-alpha.36"
|
||||
|| migration.target?.modules?.itsalive != "0.0.0-alpha.28"
|
||||
|| migration.culinary_fixes?.food_press_contents_destroyed_on_break != true
|
||||
|| migration.culinary_fixes?.comparator_contract?.blocks?.size() != 6
|
||||
@@ -1351,7 +1351,7 @@ tasks.register("verifyCauldronBatching") {
|
||||
def recipe = file("src/main/java/fr/koka99cab/sanctuary26/itsalive/culinary/recipe/CulinaryProcessRecipe.java").text
|
||||
def cauldron = file("src/main/java/fr/koka99cab/sanctuary26/itsalive/culinary/block/entity/CookingCauldronBlockEntity.java").text
|
||||
def timed = file("src/main/java/fr/koka99cab/sanctuary26/itsalive/culinary/block/entity/AbstractTimedCulinaryBlockEntity.java").text
|
||||
if (project.version.toString() != "0.0.0-alpha.34"
|
||||
if (project.version.toString() != "0.0.0-alpha.36"
|
||||
|| migration.target?.modules?.itsalive != "0.0.0-alpha.29"
|
||||
|| migration.cauldron_batches?.logical_stack_limit != 64
|
||||
|| migration.cauldron_batches?.identity != "same_item_same_components"
|
||||
@@ -1389,7 +1389,7 @@ tasks.register("verifyFermentationBarrelAutomation") {
|
||||
def partialMatching = file("src/main/java/fr/koka99cab/sanctuary26/itsalive/culinary/recipe/CulinaryPartialMatching.java").text
|
||||
def blockstate = new JsonSlurper().parse(
|
||||
file("src/main/resources/assets/itsalive/blockstates/fermentation_barrel.json"))
|
||||
if (project.version.toString() != "0.0.0-alpha.34"
|
||||
if (project.version.toString() != "0.0.0-alpha.36"
|
||||
|| migration.source?.modules?.itsalive != "0.0.0-alpha.29"
|
||||
|| migration.target?.modules?.itsalive != "0.0.0-alpha.30"
|
||||
|| migration.fermentation_barrel?.maximum_units_per_input_slot != 1
|
||||
@@ -1437,7 +1437,7 @@ tasks.register("verifyCulinaryLecternRelease") {
|
||||
[(locale): new JsonSlurper().parse(file(
|
||||
"src/main/resources/assets/itsalive/lang/${locale}.json"))]
|
||||
}
|
||||
if (project.version.toString() != "0.0.0-alpha.34"
|
||||
if (project.version.toString() != "0.0.0-alpha.36"
|
||||
|| migration.target?.modules?.itsalive != "0.0.0-alpha.31"
|
||||
|| migration.culinary_lectern?.book_tag != "minecraft:lectern_books"
|
||||
|| migration.culinary_lectern?.owner_catalog_shared_while_placed != true
|
||||
@@ -1461,9 +1461,79 @@ tasks.register("verifyCulinaryLecternRelease") {
|
||||
}
|
||||
}
|
||||
|
||||
tasks.register("verifyCanapliaForbiddenBook") {
|
||||
group = "verification"
|
||||
description = "Checks personal Forbidden Books, random page learning and lectern sharing."
|
||||
dependsOn tasks.named("generateCanapliaResources")
|
||||
inputs.file(file("src/main/java/fr/koka99cab/sanctuary26/itsalive/canaplia/CanapliaKnowledge.java"))
|
||||
inputs.file(file("src/main/java/fr/koka99cab/sanctuary26/itsalive/canaplia/CanapliaKnowledgeData.java"))
|
||||
inputs.file(file("src/main/java/fr/koka99cab/sanctuary26/itsalive/canaplia/CanapliaLectern.java"))
|
||||
inputs.file(file("src/main/java/fr/koka99cab/sanctuary26/itsalive/canaplia/item/CanapliaCodexPageItem.java"))
|
||||
inputs.file(file("src/main/java/fr/koka99cab/sanctuary26/itsalive/canaplia/client/CanapliaCodexScreen.java"))
|
||||
inputs.file(file("src/main/java/fr/koka99cab/sanctuary26/itsalive/registry/ItsAliveDataComponents.java"))
|
||||
inputs.file(file("src/main/resources/data/minecraft/tags/item/lectern_books.json"))
|
||||
inputs.files(fileTree("src/main/resources/assets/canaplia/lang") { include "*.json" })
|
||||
inputs.file(rootProject.file("CANAPLIA.md"))
|
||||
|
||||
doLast {
|
||||
def knowledge = file(
|
||||
"src/main/java/fr/koka99cab/sanctuary26/itsalive/canaplia/CanapliaKnowledge.java").text
|
||||
def knowledgeData = file(
|
||||
"src/main/java/fr/koka99cab/sanctuary26/itsalive/canaplia/CanapliaKnowledgeData.java").text
|
||||
def lectern = file(
|
||||
"src/main/java/fr/koka99cab/sanctuary26/itsalive/canaplia/CanapliaLectern.java").text
|
||||
def pageItem = file(
|
||||
"src/main/java/fr/koka99cab/sanctuary26/itsalive/canaplia/item/CanapliaCodexPageItem.java").text
|
||||
def screen = file(
|
||||
"src/main/java/fr/koka99cab/sanctuary26/itsalive/canaplia/client/CanapliaCodexScreen.java").text
|
||||
def components = file(
|
||||
"src/main/java/fr/koka99cab/sanctuary26/itsalive/registry/ItsAliveDataComponents.java").text
|
||||
def lecternBooks = new JsonSlurper().parse(file(
|
||||
"src/main/resources/data/minecraft/tags/item/lectern_books.json"))
|
||||
def generatedCodex = new JsonSlurper().parse(new File(
|
||||
generatedCanapliaResources.get().asFile, "assets/canaplia/items/codex.json"))
|
||||
def locales = ["fr_fr", "en_us", "ru_ru"].collectEntries { locale ->
|
||||
[(locale): new JsonSlurper().parse(file(
|
||||
"src/main/resources/assets/canaplia/lang/${locale}.json"))]
|
||||
}
|
||||
def expectedNames = [
|
||||
fr_fr: "Livre interdit",
|
||||
en_us: "Forbidden Book",
|
||||
ru_ru: "Запретная книга"]
|
||||
if (project.version.toString() != "0.0.0-alpha.36"
|
||||
|| !pageItem.contains("CanapliaKnowledge.learnRandomUnknown(serverPlayer)")
|
||||
|| !knowledge.contains("unknownVariants(data.personal(player.getUUID()))")
|
||||
|| !knowledge.contains("data.personal(inscription.ownerId())")
|
||||
|| knowledge.contains("data.shared()")
|
||||
|| !knowledgeData.contains("the legacy shared field is preserved")
|
||||
|| !lectern.contains("UseBlockCallback.EVENT.register")
|
||||
|| !lectern.contains("PlayerBlockBreakEvents.BEFORE.register")
|
||||
|| !lectern.contains("CanapliaKnowledge.open(serverPlayer, book)")
|
||||
|| !lectern.contains("CanapliaKnowledge.forgetInscription(taken)")
|
||||
|| screen.contains("PAGE_BACKGROUND")
|
||||
|| screen.contains("extractBackground(GuiGraphicsExtractor")
|
||||
|| screen.contains("extractRenderState(GuiGraphicsExtractor")
|
||||
|| !screen.contains("ChatFormatting.DARK_GRAY")
|
||||
|| !screen.contains("ChatFormatting.DARK_GREEN")
|
||||
|| screen.contains("screen.canaplia.codex.introduction")
|
||||
|| !components.contains('ItsAliveMod.id("canaplia_codex")')
|
||||
|| lecternBooks["values"] != ["itsalive:culinary_recipe_book", "canaplia:codex"]
|
||||
|| generatedCodex.model?.model != "minecraft:item/enchanted_book"
|
||||
|| locales.any { locale, values ->
|
||||
values["item.canaplia.codex"] != expectedNames[locale]
|
||||
|| values["screen.canaplia.codex.title"] != expectedNames[locale]
|
||||
|| !values.containsKey("message.canaplia.codex_page.complete")
|
||||
|| !values.containsKey("tooltip.canaplia.codex.personal")
|
||||
}) {
|
||||
throw new GradleException("The Canaplia Forbidden Book contract is incomplete")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tasks.named("check") {
|
||||
dependsOn tasks.named("verifyItsAlive")
|
||||
dependsOn tasks.named("canapliaFoundationSmoke")
|
||||
dependsOn tasks.named("verifyCanapliaForbiddenBook")
|
||||
dependsOn tasks.named("verifyCulinaryReliability")
|
||||
dependsOn tasks.named("verifyCauldronBatching")
|
||||
dependsOn tasks.named("culinaryBatchingSmoke")
|
||||
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
package fr.koka99cab.sanctuary26.itsalive.canaplia;
|
||||
|
||||
import com.mojang.serialization.Codec;
|
||||
import com.mojang.serialization.codecs.RecordCodecBuilder;
|
||||
import java.util.UUID;
|
||||
import net.minecraft.core.UUIDUtil;
|
||||
import net.minecraft.network.RegistryFriendlyByteBuf;
|
||||
import net.minecraft.network.codec.ByteBufCodecs;
|
||||
import net.minecraft.network.codec.StreamCodec;
|
||||
|
||||
/** Binds a placed Forbidden Book to the player whose knowledge it shares. */
|
||||
public record CanapliaCodexInscription(UUID ownerId, String ownerName) {
|
||||
private static final int MAX_NAME_LENGTH = 32;
|
||||
public static final Codec<CanapliaCodexInscription> CODEC = RecordCodecBuilder.create(instance ->
|
||||
instance.group(
|
||||
UUIDUtil.CODEC.fieldOf("owner_id").forGetter(CanapliaCodexInscription::ownerId),
|
||||
Codec.STRING.fieldOf("owner_name").forGetter(CanapliaCodexInscription::ownerName)
|
||||
).apply(instance, CanapliaCodexInscription::new)
|
||||
);
|
||||
public static final StreamCodec<RegistryFriendlyByteBuf, CanapliaCodexInscription> STREAM_CODEC =
|
||||
StreamCodec.composite(
|
||||
UUIDUtil.STREAM_CODEC,
|
||||
CanapliaCodexInscription::ownerId,
|
||||
ByteBufCodecs.STRING_UTF8,
|
||||
CanapliaCodexInscription::ownerName,
|
||||
CanapliaCodexInscription::new
|
||||
);
|
||||
|
||||
public CanapliaCodexInscription {
|
||||
if (ownerId == null) throw new IllegalArgumentException("Forbidden Book owner is required");
|
||||
ownerName = sanitizeName(ownerName);
|
||||
}
|
||||
|
||||
public static CanapliaCodexInscription of(UUID ownerId, String ownerName) {
|
||||
return new CanapliaCodexInscription(ownerId, ownerName);
|
||||
}
|
||||
|
||||
private static String sanitizeName(String ownerName) {
|
||||
String cleaned = ownerName == null ? "" : ownerName.replaceAll("\\p{Cntrl}", "").trim();
|
||||
if (cleaned.length() > MAX_NAME_LENGTH) cleaned = cleaned.substring(0, MAX_NAME_LENGTH);
|
||||
return cleaned.isEmpty() ? "?" : cleaned;
|
||||
}
|
||||
}
|
||||
+2
-1
@@ -6,6 +6,7 @@ import fr.koka99cab.sanctuary26.itsalive.canaplia.block.CanapliaHempTopBlock;
|
||||
import fr.koka99cab.sanctuary26.itsalive.canaplia.block.CanapliaResinBlock;
|
||||
import fr.koka99cab.sanctuary26.itsalive.canaplia.block.CanapliaResinBlockEntity;
|
||||
import fr.koka99cab.sanctuary26.itsalive.canaplia.item.CanapliaCodexItem;
|
||||
import fr.koka99cab.sanctuary26.itsalive.canaplia.item.CanapliaCodexPageItem;
|
||||
import fr.koka99cab.sanctuary26.itsalive.canaplia.item.CanapliaJointItem;
|
||||
import fr.koka99cab.sanctuary26.itsalive.canaplia.item.CanapliaVariantBlockItem;
|
||||
import java.util.ArrayList;
|
||||
@@ -37,7 +38,7 @@ public final class CanapliaContent {
|
||||
public static final BlockEntityType<CanapliaResinBlockEntity> RESIN_BLOCK_ENTITY_TYPE = registerResinBlockEntity();
|
||||
private static final Map<CanapliaVariantKey, Family> FAMILIES = registerFamilies();
|
||||
public static final Item CODEX_PAGE = registerItem("codex_page", key ->
|
||||
new Item(new Item.Properties().setId(key)));
|
||||
new CanapliaCodexPageItem(new Item.Properties().setId(key)));
|
||||
public static final Item CODEX = registerItem("codex", key ->
|
||||
new CanapliaCodexItem(new Item.Properties().stacksTo(1).setId(key)));
|
||||
private static final List<Item> CREATIVE_ITEMS = createCreativeItems();
|
||||
|
||||
+1
@@ -21,6 +21,7 @@ public final class CanapliaFoundation {
|
||||
throw new IllegalStateException("Unsupported Ambiance visual contract");
|
||||
}
|
||||
CanapliaContent.register();
|
||||
CanapliaLectern.initialize();
|
||||
CanapliaCodexDrops.initialize();
|
||||
CanapliaDiscoverySources.register();
|
||||
CanapliaDiscovery.initialize();
|
||||
|
||||
+47
-2
@@ -1,11 +1,16 @@
|
||||
package fr.koka99cab.sanctuary26.itsalive.canaplia;
|
||||
|
||||
import fr.koka99cab.sanctuary26.itsalive.network.CanapliaCodexPayload;
|
||||
import fr.koka99cab.sanctuary26.itsalive.registry.ItsAliveDataComponents;
|
||||
import fr.koka99cab.sanctuary26.sanctuary.progression.SanctuaryProgressionService;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
|
||||
/** Trusted discovery service and compact Codex snapshot builder. */
|
||||
public final class CanapliaKnowledge {
|
||||
@@ -22,10 +27,50 @@ public final class CanapliaKnowledge {
|
||||
}
|
||||
|
||||
public static void open(ServerPlayer player) {
|
||||
open(player, player.getUUID(), Set.of());
|
||||
}
|
||||
|
||||
public static void open(ServerPlayer player, ItemStack book) {
|
||||
CanapliaCodexInscription inscription = book.get(ItsAliveDataComponents.CANAPLIA_CODEX);
|
||||
if (inscription == null) {
|
||||
open(player);
|
||||
return;
|
||||
}
|
||||
CanapliaKnowledgeData data = CanapliaKnowledgeData.get(player.level().getServer());
|
||||
open(player, player.getUUID(), data.personal(inscription.ownerId()));
|
||||
}
|
||||
|
||||
public static boolean learnRandomUnknown(ServerPlayer player) {
|
||||
CanapliaKnowledgeData data = CanapliaKnowledgeData.get(player.level().getServer());
|
||||
List<CanapliaVariantKey> unknown = unknownVariants(data.personal(player.getUUID()));
|
||||
if (unknown.isEmpty()) {
|
||||
player.sendSystemMessage(Component.translatable("message.canaplia.codex_page.complete"));
|
||||
return false;
|
||||
}
|
||||
CanapliaVariantKey selected = unknown.get(player.getRandom().nextInt(unknown.size()));
|
||||
return discover(player, selected, "codex_page");
|
||||
}
|
||||
|
||||
public static void inscribe(ItemStack stack, ServerPlayer player) {
|
||||
if (!stack.is(CanapliaContent.CODEX)) return;
|
||||
stack.set(ItsAliveDataComponents.CANAPLIA_CODEX, CanapliaCodexInscription.of(
|
||||
player.getUUID(), player.getGameProfile().name()));
|
||||
}
|
||||
|
||||
public static void forgetInscription(ItemStack stack) {
|
||||
if (stack.is(CanapliaContent.CODEX)) stack.remove(ItsAliveDataComponents.CANAPLIA_CODEX);
|
||||
}
|
||||
|
||||
static List<CanapliaVariantKey> unknownVariants(Set<String> learned) {
|
||||
return Arrays.stream(CanapliaVariantKey.values())
|
||||
.filter(variant -> !learned.contains(variant.key()))
|
||||
.toList();
|
||||
}
|
||||
|
||||
private static void open(ServerPlayer player, UUID personalOwner, Set<String> shared) {
|
||||
if (!ServerPlayNetworking.canSend(player, CanapliaCodexPayload.ID)) return;
|
||||
CanapliaKnowledgeData data = CanapliaKnowledgeData.get(player.level().getServer());
|
||||
Set<String> personal = data.personal(player.getUUID());
|
||||
Set<String> shared = data.shared();
|
||||
Set<String> personal = data.personal(personalOwner);
|
||||
int personalMask = mask(personal);
|
||||
int sharedMask = mask(shared);
|
||||
int originHints = 0;
|
||||
|
||||
+2
-3
@@ -15,7 +15,7 @@ import net.minecraft.world.level.Level;
|
||||
import net.minecraft.world.level.saveddata.SavedData;
|
||||
import net.minecraft.world.level.saveddata.SavedDataType;
|
||||
|
||||
/** Versioned personal and shared discoveries; unknown future keys survive round-trips. */
|
||||
/** Versioned personal discoveries; the legacy shared field is preserved but no longer exposed globally. */
|
||||
public final class CanapliaKnowledgeData extends SavedData {
|
||||
public static final int DATA_VERSION = 1;
|
||||
private static final Codec<CanapliaKnowledgeData> CODEC = RecordCodecBuilder.create(instance -> instance.group(
|
||||
@@ -57,8 +57,7 @@ public final class CanapliaKnowledgeData extends SavedData {
|
||||
public boolean discover(UUID player, String key) {
|
||||
if (!validKey(key)) return false;
|
||||
boolean personal = players.computeIfAbsent(player.toString(), ignored -> new LinkedHashSet<>()).add(key);
|
||||
boolean community = shared.add(key);
|
||||
if (personal || community) setDirty();
|
||||
if (personal) setDirty();
|
||||
return personal;
|
||||
}
|
||||
|
||||
|
||||
+77
@@ -0,0 +1,77 @@
|
||||
package fr.koka99cab.sanctuary26.itsalive.canaplia;
|
||||
|
||||
import net.fabricmc.fabric.api.event.player.PlayerBlockBreakEvents;
|
||||
import net.fabricmc.fabric.api.event.player.UseBlockCallback;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.sounds.SoundEvents;
|
||||
import net.minecraft.sounds.SoundSource;
|
||||
import net.minecraft.world.InteractionHand;
|
||||
import net.minecraft.world.InteractionResult;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.level.Level;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.block.LecternBlock;
|
||||
import net.minecraft.world.level.block.entity.LecternBlockEntity;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.phys.BlockHitResult;
|
||||
|
||||
/** Shares only the placer's Canaplia knowledge while the Forbidden Book remains on a lectern. */
|
||||
public final class CanapliaLectern {
|
||||
private CanapliaLectern() {
|
||||
}
|
||||
|
||||
public static void initialize() {
|
||||
UseBlockCallback.EVENT.register(CanapliaLectern::use);
|
||||
PlayerBlockBreakEvents.BEFORE.register((level, player, pos, state, blockEntity) -> {
|
||||
if (blockEntity instanceof LecternBlockEntity lectern) {
|
||||
CanapliaKnowledge.forgetInscription(lectern.getBook());
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
private static InteractionResult use(Player player, Level level, InteractionHand hand, BlockHitResult hit) {
|
||||
if (hand != InteractionHand.MAIN_HAND || player.isSpectator()) return InteractionResult.PASS;
|
||||
BlockPos pos = hit.getBlockPos();
|
||||
BlockState state = level.getBlockState(pos);
|
||||
if (!state.is(Blocks.LECTERN)) return InteractionResult.PASS;
|
||||
|
||||
ItemStack held = player.getItemInHand(hand);
|
||||
if (!state.getValue(LecternBlock.HAS_BOOK)) {
|
||||
if (held.is(CanapliaContent.CODEX)
|
||||
&& !level.isClientSide()
|
||||
&& player instanceof ServerPlayer serverPlayer) {
|
||||
CanapliaKnowledge.inscribe(held, serverPlayer);
|
||||
}
|
||||
return InteractionResult.PASS;
|
||||
}
|
||||
if (!(level.getBlockEntity(pos) instanceof LecternBlockEntity lectern)) {
|
||||
return InteractionResult.PASS;
|
||||
}
|
||||
ItemStack book = lectern.getBook();
|
||||
if (!book.is(CanapliaContent.CODEX)) return InteractionResult.PASS;
|
||||
if (level.isClientSide()) return InteractionResult.SUCCESS;
|
||||
if (!(player instanceof ServerPlayer serverPlayer) || !level.mayInteract(serverPlayer, pos)) {
|
||||
return InteractionResult.FAIL;
|
||||
}
|
||||
if (serverPlayer.isSecondaryUseActive()) {
|
||||
return takeBook(serverPlayer, level, pos, state, lectern);
|
||||
}
|
||||
CanapliaKnowledge.open(serverPlayer, book);
|
||||
return InteractionResult.SUCCESS;
|
||||
}
|
||||
|
||||
private static InteractionResult takeBook(ServerPlayer player, Level level, BlockPos pos,
|
||||
BlockState state, LecternBlockEntity lectern) {
|
||||
ItemStack taken = lectern.getBook().copy();
|
||||
if (taken.isEmpty()) return InteractionResult.PASS;
|
||||
CanapliaKnowledge.forgetInscription(taken);
|
||||
lectern.setBook(ItemStack.EMPTY);
|
||||
LecternBlock.resetBookState(player, level, pos, state, false);
|
||||
player.getInventory().placeItemBackInInventory(taken);
|
||||
level.playSound(null, pos, SoundEvents.ITEM_FRAME_REMOVE_ITEM, SoundSource.BLOCKS, 1.0F, 1.0F);
|
||||
return InteractionResult.SUCCESS;
|
||||
}
|
||||
}
|
||||
+9
-10
@@ -16,8 +16,7 @@ public final class CanapliaCodexScreen extends BookViewScreen {
|
||||
}
|
||||
|
||||
private static List<Component> pages(CanapliaCodexPayload snapshot) {
|
||||
List<Component> pages = new ArrayList<>(CanapliaVariantKey.values().length + 1);
|
||||
pages.add(Component.translatable("screen.canaplia.codex.introduction"));
|
||||
List<Component> pages = new ArrayList<>(CanapliaVariantKey.values().length);
|
||||
CanapliaVariantKey[] variants = CanapliaVariantKey.values();
|
||||
for (int index = 0; index < variants.length; index++) {
|
||||
pages.add(variantPage(snapshot, variants[index], index));
|
||||
@@ -39,16 +38,16 @@ public final class CanapliaCodexScreen extends BookViewScreen {
|
||||
.withStyle(ChatFormatting.GRAY));
|
||||
}
|
||||
|
||||
page.append((personal
|
||||
? Component.translatable("variant.canaplia." + variant.key())
|
||||
: Component.translatable("screen.canaplia.codex.silhouette"))
|
||||
.withStyle(personal ? ChatFormatting.DARK_GREEN : ChatFormatting.GRAY));
|
||||
if (personal || originHint) {
|
||||
page.append(Component.translatable("variant.canaplia." + variant.key())
|
||||
.withStyle(personal ? ChatFormatting.DARK_GREEN : ChatFormatting.DARK_PURPLE));
|
||||
if (personal || shared || originHint) {
|
||||
page.append("\n\n").append(Component.translatable("screen.canaplia.codex.origin",
|
||||
Component.translatable("origin.canaplia." + variant.key())));
|
||||
Component.translatable("origin.canaplia." + variant.key()))
|
||||
.withStyle(ChatFormatting.DARK_GRAY));
|
||||
}
|
||||
if (personal || conditionHint) {
|
||||
page.append("\n\n").append(Component.translatable("hint.canaplia." + variant.key()));
|
||||
if (personal || shared || conditionHint) {
|
||||
page.append("\n\n").append(Component.translatable("hint.canaplia." + variant.key())
|
||||
.withStyle(ChatFormatting.DARK_GRAY));
|
||||
}
|
||||
return page;
|
||||
}
|
||||
|
||||
+13
@@ -1,11 +1,17 @@
|
||||
package fr.koka99cab.sanctuary26.itsalive.canaplia.item;
|
||||
|
||||
import fr.koka99cab.sanctuary26.itsalive.canaplia.CanapliaKnowledge;
|
||||
import java.util.function.Consumer;
|
||||
import net.minecraft.ChatFormatting;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.world.InteractionHand;
|
||||
import net.minecraft.world.InteractionResult;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import net.minecraft.world.item.Item;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.item.TooltipFlag;
|
||||
import net.minecraft.world.item.component.TooltipDisplay;
|
||||
import net.minecraft.world.level.Level;
|
||||
|
||||
/** Opens the server-authored personal/shared Canaplia collection snapshot. */
|
||||
@@ -20,4 +26,11 @@ public final class CanapliaCodexItem extends Item {
|
||||
if (player instanceof ServerPlayer serverPlayer) CanapliaKnowledge.open(serverPlayer);
|
||||
return InteractionResult.SUCCESS_SERVER;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void appendHoverText(ItemStack stack, TooltipContext context, TooltipDisplay display,
|
||||
Consumer<Component> tooltip, TooltipFlag flag) {
|
||||
tooltip.accept(Component.translatable("tooltip.canaplia.codex.personal")
|
||||
.withStyle(ChatFormatting.DARK_PURPLE));
|
||||
}
|
||||
}
|
||||
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
package fr.koka99cab.sanctuary26.itsalive.canaplia.item;
|
||||
|
||||
import fr.koka99cab.sanctuary26.itsalive.canaplia.CanapliaKnowledge;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.world.InteractionHand;
|
||||
import net.minecraft.world.InteractionResult;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import net.minecraft.world.item.Item;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.level.Level;
|
||||
|
||||
/** Consumable unidentified page which discovers one random unknown Canaplia entry. */
|
||||
public final class CanapliaCodexPageItem extends Item {
|
||||
public CanapliaCodexPageItem(Properties properties) {
|
||||
super(properties);
|
||||
}
|
||||
|
||||
@Override
|
||||
public InteractionResult use(Level level, Player player, InteractionHand hand) {
|
||||
if (level.isClientSide()) return InteractionResult.SUCCESS;
|
||||
if (!(player instanceof ServerPlayer serverPlayer)) return InteractionResult.PASS;
|
||||
if (CanapliaKnowledge.learnRandomUnknown(serverPlayer)) {
|
||||
ItemStack held = player.getItemInHand(hand);
|
||||
if (!player.getAbilities().instabuild) held.shrink(1);
|
||||
}
|
||||
return InteractionResult.SUCCESS_SERVER;
|
||||
}
|
||||
}
|
||||
+10
@@ -1,6 +1,7 @@
|
||||
package fr.koka99cab.sanctuary26.itsalive.registry;
|
||||
|
||||
import fr.koka99cab.sanctuary26.itsalive.ItsAliveMod;
|
||||
import fr.koka99cab.sanctuary26.itsalive.canaplia.CanapliaCodexInscription;
|
||||
import fr.koka99cab.sanctuary26.itsalive.culinary.knowledge.CulinaryBookInscription;
|
||||
import net.minecraft.core.Registry;
|
||||
import net.minecraft.core.component.DataComponentType;
|
||||
@@ -8,6 +9,15 @@ import net.minecraft.core.registries.BuiltInRegistries;
|
||||
|
||||
/** Persistent item components owned by It's Alive. */
|
||||
public final class ItsAliveDataComponents {
|
||||
public static final DataComponentType<CanapliaCodexInscription> CANAPLIA_CODEX = Registry.register(
|
||||
BuiltInRegistries.DATA_COMPONENT_TYPE,
|
||||
ItsAliveMod.id("canaplia_codex"),
|
||||
DataComponentType.<CanapliaCodexInscription>builder()
|
||||
.persistent(CanapliaCodexInscription.CODEC)
|
||||
.networkSynchronized(CanapliaCodexInscription.STREAM_CODEC)
|
||||
.cacheEncoding()
|
||||
.build()
|
||||
);
|
||||
public static final DataComponentType<CulinaryBookInscription> CULINARY_BOOK = Registry.register(
|
||||
BuiltInRegistries.DATA_COMPONENT_TYPE,
|
||||
ItsAliveMod.id("culinary_book"),
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
{
|
||||
"description.canaplia": "Complete Canaplia gameplay: sixteen plants, server-authoritative discovery, hybrids, effects and an Ambiance-owned visual layer.",
|
||||
"screen.canaplia.codex.title": "Canaplia Codex",
|
||||
"screen.canaplia.codex.title": "Forbidden Book",
|
||||
"screen.canaplia.codex.page": "Page %s / %s",
|
||||
"screen.canaplia.codex.introduction": "CANAPLIA CODEX\n\nSlay monsters to recover scattered pages and, very rarely, a complete Codex.\n\nGrow and discover varieties to fill its pages.",
|
||||
"screen.canaplia.codex.entry_number": "Entry %s",
|
||||
"screen.canaplia.codex.origin": "Origin: %s",
|
||||
"screen.canaplia.codex.hidden": "Undiscovered",
|
||||
"screen.canaplia.codex.silhouette": "Known silhouette",
|
||||
"message.canaplia.discovered": "Canaplia discovered: %s",
|
||||
"message.canaplia.codex_page.complete": "You already know every entry in the Forbidden Book.",
|
||||
"message.canaplia.composter.mixed": "This composter already contains another Canaplia variant.",
|
||||
"tooltip.canaplia.codex.personal": "Personal in hand; shares its owner's knowledge on a lectern",
|
||||
"block.canaplia.hemp_crop": "Canaplia Plant",
|
||||
"block.canaplia.hemp_top": "Canaplia Plant Top",
|
||||
"block.canaplia.resin_block": "Canaplia Resin Block",
|
||||
"item.canaplia.codex": "Canaplia Codex",
|
||||
"item.canaplia.codex": "Forbidden Book",
|
||||
"item.canaplia.codex_page": "Canaplia Page",
|
||||
"variant.canaplia.common": "Common",
|
||||
"origin.canaplia.common": "Wild",
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
{
|
||||
"description.canaplia": "Gameplay Canaplia complet : seize plantes, découvertes, hybrides et effets autoritaires côté serveur, avec une couche visuelle gérée par Ambiance.",
|
||||
"screen.canaplia.codex.title": "Codex Canaplia",
|
||||
"screen.canaplia.codex.title": "Livre interdit",
|
||||
"screen.canaplia.codex.page": "Page %s / %s",
|
||||
"screen.canaplia.codex.introduction": "CODEX CANAPLIA\n\nTuez des monstres pour retrouver des pages éparses et, très rarement, un Codex complet.\n\nCultivez et découvrez les variétés pour remplir ses pages.",
|
||||
"screen.canaplia.codex.entry_number": "Entrée %s",
|
||||
"screen.canaplia.codex.origin": "Origine : %s",
|
||||
"screen.canaplia.codex.hidden": "Non découverte",
|
||||
"screen.canaplia.codex.silhouette": "Silhouette connue",
|
||||
"message.canaplia.discovered": "Canaplia découverte : %s",
|
||||
"message.canaplia.codex_page.complete": "Tu connais déjà toutes les entrées du Livre interdit.",
|
||||
"message.canaplia.composter.mixed": "Ce composteur contient déjà une autre variante de Canaplia.",
|
||||
"tooltip.canaplia.codex.personal": "Personnel en main ; partage le savoir du propriétaire sur un pupitre",
|
||||
"block.canaplia.hemp_crop": "Plant de Canaplia",
|
||||
"block.canaplia.hemp_top": "Sommet de Canaplia",
|
||||
"block.canaplia.resin_block": "Bloc de résine Canaplia",
|
||||
"item.canaplia.codex": "Codex Canaplia",
|
||||
"item.canaplia.codex": "Livre interdit",
|
||||
"item.canaplia.codex_page": "Page de Canaplia",
|
||||
"variant.canaplia.common": "Commune",
|
||||
"origin.canaplia.common": "Sauvage",
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
{
|
||||
"description.canaplia": "Полная механика Canaplia: шестнадцать растений, серверные открытия, гибриды и эффекты с визуальным слоем Ambiance.",
|
||||
"screen.canaplia.codex.title": "Кодекс Canaplia",
|
||||
"screen.canaplia.codex.title": "Запретная книга",
|
||||
"screen.canaplia.codex.page": "Страница %s / %s",
|
||||
"screen.canaplia.codex.introduction": "КОДЕКС CANAPLIA\n\nУбивайте монстров, чтобы находить отдельные страницы и, очень редко, полный Кодекс.\n\nВыращивайте и открывайте сорта, чтобы заполнить его.",
|
||||
"screen.canaplia.codex.entry_number": "Запись %s",
|
||||
"screen.canaplia.codex.origin": "Происхождение: %s",
|
||||
"screen.canaplia.codex.hidden": "Не открыто",
|
||||
"screen.canaplia.codex.silhouette": "Известный силуэт",
|
||||
"message.canaplia.discovered": "Открыта Canaplia: %s",
|
||||
"message.canaplia.codex_page.complete": "Вы уже знаете все записи Запретной книги.",
|
||||
"message.canaplia.composter.mixed": "В этом компостере уже находится другой вариант Canaplia.",
|
||||
"tooltip.canaplia.codex.personal": "Личная в руке; на кафедре делится знаниями владельца",
|
||||
"block.canaplia.hemp_crop": "Растение Canaplia",
|
||||
"block.canaplia.hemp_top": "Верхушка Canaplia",
|
||||
"block.canaplia.resin_block": "Блок смолы Canaplia",
|
||||
"item.canaplia.codex": "Кодекс Canaplia",
|
||||
"item.canaplia.codex": "Запретная книга",
|
||||
"item.canaplia.codex_page": "Страница Canaplia",
|
||||
"variant.canaplia.common": "Обычная",
|
||||
"origin.canaplia.common": "Дикая",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"replace": false,
|
||||
"values": [
|
||||
"itsalive:culinary_recipe_book"
|
||||
"itsalive:culinary_recipe_book",
|
||||
"canaplia:codex"
|
||||
]
|
||||
}
|
||||
|
||||
+22
@@ -97,6 +97,28 @@ public final class CanapliaGameplaySmoke {
|
||||
"unknown future personal discovery preserved");
|
||||
assertTrue(futureKnowledge.shared().contains("future_variant_17"),
|
||||
"unknown future shared discovery preserved");
|
||||
assertTrue(futureKnowledge.discover(owner, CanapliaVariantKey.COMMON.key()),
|
||||
"new personal discovery recorded");
|
||||
assertTrue(futureKnowledge.personal(owner).contains(CanapliaVariantKey.COMMON.key()),
|
||||
"new discovery remains personal");
|
||||
assertTrue(!futureKnowledge.shared().contains(CanapliaVariantKey.COMMON.key()),
|
||||
"new discovery is not published globally");
|
||||
|
||||
assertEquals(16, CanapliaKnowledge.unknownVariants(Set.of()).size(),
|
||||
"blank Codex page can select every variant");
|
||||
Set<String> almostComplete = new HashSet<>(CanapliaVariants.all().stream()
|
||||
.map(definition -> definition.key().key()).toList());
|
||||
almostComplete.remove(CanapliaVariantKey.EMBER.key());
|
||||
assertEquals(java.util.List.of(CanapliaVariantKey.EMBER),
|
||||
CanapliaKnowledge.unknownVariants(almostComplete), "last unknown Codex entry");
|
||||
assertTrue(CanapliaKnowledge.unknownVariants(
|
||||
CanapliaVariants.all().stream().map(definition -> definition.key().key()).collect(
|
||||
java.util.stream.Collectors.toSet())).isEmpty(), "complete Codex has no unknown entry");
|
||||
|
||||
CanapliaCodexInscription inscription = CanapliaCodexInscription.of(owner,
|
||||
" Can\u0000aplia owner with a deliberately long name ");
|
||||
assertTrue(inscription.ownerName().length() <= 32, "Codex owner name bound");
|
||||
assertTrue(!inscription.ownerName().contains("\u0000"), "Codex owner control characters removed");
|
||||
|
||||
CanapliaCodexPayload payload = new CanapliaCodexPayload(99, -1, 0x1FFFF, -2, Integer.MAX_VALUE);
|
||||
assertEquals(CanapliaCodexPayload.PROTOCOL_VERSION, payload.protocol(), "codex protocol bound");
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
# Abjection dans Only Fun
|
||||
|
||||
Le runtime historique GPL-3.0-or-later de `Fun/abjection` est intégré au JAR **Only Fun**. Le
|
||||
namespace `abjection` et les IDs `abjection:shit_ball` et `abjection:shit_cube` sont conservés ; le
|
||||
manifeste Only Fun fournit l’alias Fabric `abjection`, sans créer un onzième module.
|
||||
|
||||
## ABJ-01 — actions serveur
|
||||
|
||||
- `/piss` émet son jet pendant 15 secondes et peut activer un bouton touché ;
|
||||
- `/vomit` émet son jet pendant 8 secondes ;
|
||||
- `/shit` émet son jet pendant 6 secondes et crée régulièrement la même
|
||||
`abjection:shit_ball` que les lots suivants ;
|
||||
- `/smoke` émet de la fumée pendant 10 secondes.
|
||||
|
||||
`AbjectionActions.start` est l’unique API publique de déclenchement. Chaque nouvelle demande
|
||||
redémarre seulement l’action correspondante, sans dupliquer la boule initiale de `/shit`. L’état
|
||||
reste en mémoire serveur, n’est jamais sauvegardé et est supprimé à la mort, au passage spectateur
|
||||
ou à la déconnexion.
|
||||
|
||||
Le bloc `abjection:shit_cube` reprend les propriétés de la boue, l’apparence 16×16, la recette
|
||||
neuf boules, le butin et l’outil pelle historiques. Les textes sont disponibles en français,
|
||||
anglais et russe. Only Fun reste en données v2 et protocole réseau 2 pour ABJ-01.
|
||||
|
||||
## ABJ-02 — projectile Ouch
|
||||
|
||||
Only Fun expose l’objet et le point d’installation `AbjectionProjectileApi`. Ouch, qui dépend
|
||||
explicitement d’Only Fun sans cycle inverse, enregistre le type d’entité sous le même ID historique
|
||||
`abjection:shit_ball` et installe la fabrique commune au lancer manuel et au dispenser. Le
|
||||
projectile mesure 0,25 bloc, consomme une unité hors créatif, joue le son de boue à l’impact et
|
||||
ajoute sept secondes à la nausée déjà présente sans dupliquer d’objet.
|
||||
|
||||
## ABJ-03 — raccourci configurable
|
||||
|
||||
La catégorie de contrôles Only Fun expose « Chier », assigné à `J` par défaut. Le client utilise
|
||||
`consumeClick`, puis envoie un payload vide `onlyfun:c2s/shit_action`. Le serveur accepte au plus
|
||||
une demande par joueur et par tick, revalide que le joueur est toujours connecté, vivant et non
|
||||
spectateur, puis appelle le même `AbjectionActions.start(SHIT)` que `/shit`. Maintenir la touche
|
||||
ne produit donc pas de répétition par tick et la commande reste disponible. Only Fun conserve ses
|
||||
données v2 et passe son protocole réseau de 2 à 3.
|
||||
@@ -1,6 +1,9 @@
|
||||
package fr.koka99cab.sanctuary26.onlyfun;
|
||||
|
||||
import fr.koka99cab.sanctuary26.onlyfun.chickencarry.ChickenCarryController;
|
||||
import fr.koka99cab.sanctuary26.onlyfun.abjection.AbjectionRegistries;
|
||||
import fr.koka99cab.sanctuary26.onlyfun.api.abjection.AbjectionActions;
|
||||
import fr.koka99cab.sanctuary26.onlyfun.command.AbjectionCommands;
|
||||
import fr.koka99cab.sanctuary26.onlyfun.network.OnlyFunNetworking;
|
||||
import fr.koka99cab.sanctuary26.onlyfun.registry.OnlyFunRegistries;
|
||||
import fr.koka99cab.sanctuary26.onlyfun.registry.OnlyFunCreativeTab;
|
||||
@@ -31,6 +34,7 @@ public final class OnlyFunMod implements ModInitializer {
|
||||
|
||||
@Override
|
||||
public void onInitialize() {
|
||||
AbjectionRegistries.initialize();
|
||||
OnlyFunRegistries.initialize();
|
||||
OnlyFunCreativeTab.initialize();
|
||||
OnlyFunGenerator.initialize();
|
||||
@@ -42,6 +46,8 @@ public final class OnlyFunMod implements ModInitializer {
|
||||
OnlyFunNetworking.initialize();
|
||||
ChickenCarryController.register();
|
||||
OnlyFunCommands.register();
|
||||
AbjectionActions.initialize();
|
||||
AbjectionCommands.register();
|
||||
LOGGER.info("[{}] Loot generator, natural Lucky Blocks, mob lootboxes and chicken rules ready (data v{}, network v{}).",
|
||||
DISPLAY_NAME, CURRENT_DATA_VERSION, OnlyFunNetworking.PROTOCOL_VERSION);
|
||||
}
|
||||
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
package fr.koka99cab.sanctuary26.onlyfun.abjection;
|
||||
|
||||
import java.util.function.Function;
|
||||
import net.minecraft.core.Registry;
|
||||
import net.minecraft.core.registries.BuiltInRegistries;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.world.item.BlockItem;
|
||||
import net.minecraft.world.item.Item;
|
||||
import net.minecraft.world.level.block.Block;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.block.state.BlockBehaviour;
|
||||
|
||||
/** Historical Abjection registry IDs, owned and distributed by Only Fun. */
|
||||
public final class AbjectionRegistries {
|
||||
public static final String NAMESPACE = "abjection";
|
||||
public static final Block SHIT_CUBE = registerBlock("shit_cube", Block::new,
|
||||
BlockBehaviour.Properties.ofFullCopy(Blocks.MUD));
|
||||
public static final Item SHIT_BALL = registerItem("shit_ball",
|
||||
properties -> new ShitBallItem(properties.stacksTo(16).useItemDescriptionPrefix()));
|
||||
public static final Item SHIT_CUBE_ITEM = registerBlockItem("shit_cube", SHIT_CUBE);
|
||||
|
||||
private AbjectionRegistries() {
|
||||
}
|
||||
|
||||
public static Identifier id(String path) {
|
||||
return Identifier.fromNamespaceAndPath(NAMESPACE, path);
|
||||
}
|
||||
|
||||
public static void initialize() {
|
||||
// Class initialization owns registration; this method makes the startup order explicit.
|
||||
}
|
||||
|
||||
private static Item registerItem(String path, Function<Item.Properties, Item> factory) {
|
||||
ResourceKey<Item> key = ResourceKey.create(Registries.ITEM, id(path));
|
||||
return Registry.register(BuiltInRegistries.ITEM, key,
|
||||
factory.apply(new Item.Properties().setId(key)));
|
||||
}
|
||||
|
||||
private static Block registerBlock(String path, Function<BlockBehaviour.Properties, Block> factory,
|
||||
BlockBehaviour.Properties properties) {
|
||||
ResourceKey<Block> key = ResourceKey.create(Registries.BLOCK, id(path));
|
||||
return Registry.register(BuiltInRegistries.BLOCK, key, factory.apply(properties.setId(key)));
|
||||
}
|
||||
|
||||
private static Item registerBlockItem(String path, Block block) {
|
||||
ResourceKey<Item> key = ResourceKey.create(Registries.ITEM, id(path));
|
||||
return Registry.register(BuiltInRegistries.ITEM, key,
|
||||
new BlockItem(block, new Item.Properties().setId(key).useBlockDescriptionPrefix()));
|
||||
}
|
||||
}
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
package fr.koka99cab.sanctuary26.onlyfun.abjection;
|
||||
|
||||
import fr.koka99cab.sanctuary26.onlyfun.api.abjection.AbjectionActions;
|
||||
import fr.koka99cab.sanctuary26.onlyfun.network.ShitActionRequestPayload;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import net.fabricmc.fabric.api.networking.v1.ServerPlayConnectionEvents;
|
||||
import net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
|
||||
/** Validates connection/player state and deduplicates malicious same-tick key requests. */
|
||||
public final class ShitActionRequestHandler {
|
||||
private static final Map<UUID, Integer> LAST_REQUEST_TICK = new HashMap<>();
|
||||
|
||||
private ShitActionRequestHandler() {
|
||||
}
|
||||
|
||||
public static void register() {
|
||||
ServerPlayNetworking.registerGlobalReceiver(ShitActionRequestPayload.ID,
|
||||
(payload, context) -> handle(context.player()));
|
||||
ServerPlayConnectionEvents.DISCONNECT.register((handler, server) ->
|
||||
clear(handler.player.getUUID()));
|
||||
}
|
||||
|
||||
private static void handle(ServerPlayer player) {
|
||||
MinecraftServer server = player.level().getServer();
|
||||
if (server == null || server.getPlayerList().getPlayer(player.getUUID()) != player
|
||||
|| !player.isAlive() || player.isSpectator()
|
||||
|| !acceptTick(player.getUUID(), server.getTickCount())) return;
|
||||
AbjectionActions.start(player, AbjectionActions.Action.SHIT);
|
||||
}
|
||||
|
||||
static boolean acceptTick(UUID playerId, int tick) {
|
||||
Integer previous = LAST_REQUEST_TICK.put(playerId, tick);
|
||||
return previous == null || previous != tick;
|
||||
}
|
||||
|
||||
static void clear(UUID playerId) {
|
||||
LAST_REQUEST_TICK.remove(playerId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package fr.koka99cab.sanctuary26.onlyfun.abjection;
|
||||
|
||||
import fr.koka99cab.sanctuary26.onlyfun.api.abjection.AbjectionProjectileApi;
|
||||
import net.minecraft.core.Direction;
|
||||
import net.minecraft.core.Position;
|
||||
import net.minecraft.world.entity.projectile.Projectile;
|
||||
import net.minecraft.world.item.Item;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.item.ProjectileItem;
|
||||
import net.minecraft.world.level.Level;
|
||||
|
||||
/** Historical item identity with a projectile contract implemented by the owning Ouch module. */
|
||||
public final class ShitBallItem extends Item implements ProjectileItem {
|
||||
public ShitBallItem(Properties properties) {
|
||||
super(properties);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Projectile asProjectile(Level level, Position position, ItemStack stack, Direction direction) {
|
||||
return AbjectionProjectileApi.create(level, position, stack, direction);
|
||||
}
|
||||
}
|
||||
+383
@@ -0,0 +1,383 @@
|
||||
package fr.koka99cab.sanctuary26.onlyfun.api.abjection;
|
||||
|
||||
import fr.koka99cab.sanctuary26.onlyfun.abjection.AbjectionRegistries;
|
||||
import java.util.EnumMap;
|
||||
import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import net.fabricmc.fabric.api.event.lifecycle.v1.ServerTickEvents;
|
||||
import net.fabricmc.fabric.api.networking.v1.ServerPlayConnectionEvents;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.particles.ColorParticleOption;
|
||||
import net.minecraft.core.particles.DustParticleOptions;
|
||||
import net.minecraft.core.particles.ItemParticleOption;
|
||||
import net.minecraft.core.particles.ParticleTypes;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.util.Mth;
|
||||
import net.minecraft.world.entity.HumanoidArm;
|
||||
import net.minecraft.world.entity.item.ItemEntity;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.item.Items;
|
||||
import net.minecraft.world.level.ClipContext;
|
||||
import net.minecraft.world.level.block.ButtonBlock;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.phys.BlockHitResult;
|
||||
import net.minecraft.world.phys.HitResult;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
|
||||
/** Public server API shared by Abjection commands and bounded client requests. */
|
||||
public final class AbjectionActions {
|
||||
public enum Action {
|
||||
PISS(15 * 20, "piss"),
|
||||
VOMIT(8 * 20, "vomit"),
|
||||
SHIT(6 * 20, "shit"),
|
||||
SMOKE(10 * 20, "smoke");
|
||||
|
||||
private final int durationTicks;
|
||||
private final String id;
|
||||
|
||||
Action(int durationTicks, String id) {
|
||||
this.durationTicks = durationTicks;
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public int durationTicks() {
|
||||
return durationTicks;
|
||||
}
|
||||
|
||||
public String id() {
|
||||
return id;
|
||||
}
|
||||
}
|
||||
|
||||
public enum StartResult {
|
||||
STARTED,
|
||||
RESTARTED,
|
||||
REJECTED
|
||||
}
|
||||
|
||||
private static final Map<UUID, EnumMap<Action, Integer>> ACTIVE = new HashMap<>();
|
||||
private static final int STREAM_SEGMENTS = 64;
|
||||
private static final double STREAM_STEP_LENGTH = 0.05D;
|
||||
private static final double STREAM_STEP_SCALE = STREAM_STEP_LENGTH / 0.28D;
|
||||
private static final DustParticleOptions PISS_BRIGHT = new DustParticleOptions(0xEDD62E, 0.24F);
|
||||
private static final DustParticleOptions PISS_GOLD = new DustParticleOptions(0xD8B825, 0.2F);
|
||||
private static final DustParticleOptions PISS_AMBER = new DustParticleOptions(0xC6941A, 0.19F);
|
||||
private static final DustParticleOptions PISS_DEEP_GOLD = new DustParticleOptions(0xB88714, 0.17F);
|
||||
private static final DustParticleOptions VOMIT_GREEN = new DustParticleOptions(0x88A436, 0.24F);
|
||||
private static final DustParticleOptions VOMIT_YELLOW = new DustParticleOptions(0xB6B342, 0.2F);
|
||||
private static final DustParticleOptions VOMIT_ORANGE = new DustParticleOptions(0xA96A32, 0.19F);
|
||||
private static final DustParticleOptions VOMIT_BROWN = new DustParticleOptions(0x6E5828, 0.17F);
|
||||
private static final DustParticleOptions SHIT_SPLAT = new DustParticleOptions(0x4A2B14, 1.4F);
|
||||
private static final DustParticleOptions SHIT_MUD = new DustParticleOptions(0x6A4521, 0.22F);
|
||||
private static final ColorParticleOption PISS_BRIGHT_MIST =
|
||||
ColorParticleOption.create(ParticleTypes.ENTITY_EFFECT, 0xF0E35A);
|
||||
private static final ColorParticleOption PISS_GOLD_MIST =
|
||||
ColorParticleOption.create(ParticleTypes.ENTITY_EFFECT, 0xD8C43A);
|
||||
private static final ColorParticleOption PISS_AMBER_MIST =
|
||||
ColorParticleOption.create(ParticleTypes.ENTITY_EFFECT, 0xCDA128);
|
||||
private static final ColorParticleOption VOMIT_GREEN_MIST =
|
||||
ColorParticleOption.create(ParticleTypes.ENTITY_EFFECT, 0x95B33F);
|
||||
private static final ColorParticleOption VOMIT_YELLOW_MIST =
|
||||
ColorParticleOption.create(ParticleTypes.ENTITY_EFFECT, 0xC4C35B);
|
||||
private static final ColorParticleOption VOMIT_ORANGE_MIST =
|
||||
ColorParticleOption.create(ParticleTypes.ENTITY_EFFECT, 0xB97A3D);
|
||||
private static final ItemParticleOption VOMIT_FLESH =
|
||||
new ItemParticleOption(ParticleTypes.ITEM, Items.ROTTEN_FLESH);
|
||||
private static final ItemParticleOption VOMIT_EYE =
|
||||
new ItemParticleOption(ParticleTypes.ITEM, Items.SPIDER_EYE);
|
||||
private static final ItemParticleOption VOMIT_POTATO =
|
||||
new ItemParticleOption(ParticleTypes.ITEM, Items.POISONOUS_POTATO);
|
||||
private static boolean initialized;
|
||||
|
||||
private AbjectionActions() {
|
||||
}
|
||||
|
||||
public static void initialize() {
|
||||
if (initialized) return;
|
||||
initialized = true;
|
||||
ServerTickEvents.END_SERVER_TICK.register(AbjectionActions::tick);
|
||||
ServerPlayConnectionEvents.DISCONNECT.register((handler, server) -> clear(handler.player.getUUID()));
|
||||
}
|
||||
|
||||
public static StartResult start(ServerPlayer player, Action action) {
|
||||
if (player == null || action == null || !player.isAlive() || player.isSpectator()
|
||||
|| player.level().getServer() == null) return StartResult.REJECTED;
|
||||
boolean restarted = restart(player.getUUID(), action);
|
||||
if (action == Action.SHIT && !restarted) spawnShitDrop(player.level(), player);
|
||||
return restarted ? StartResult.RESTARTED : StartResult.STARTED;
|
||||
}
|
||||
|
||||
static boolean restart(UUID playerId, Action action) {
|
||||
EnumMap<Action, Integer> actions = ACTIVE.computeIfAbsent(playerId, ignored ->
|
||||
new EnumMap<>(Action.class));
|
||||
return actions.put(action, action.durationTicks()) != null;
|
||||
}
|
||||
|
||||
public static void clear(UUID playerId) {
|
||||
ACTIVE.remove(playerId);
|
||||
}
|
||||
|
||||
static int remaining(UUID playerId, Action action) {
|
||||
return ACTIVE.getOrDefault(playerId, new EnumMap<>(Action.class)).getOrDefault(action, 0);
|
||||
}
|
||||
|
||||
private static void tick(MinecraftServer server) {
|
||||
Iterator<Map.Entry<UUID, EnumMap<Action, Integer>>> players = ACTIVE.entrySet().iterator();
|
||||
while (players.hasNext()) {
|
||||
Map.Entry<UUID, EnumMap<Action, Integer>> entry = players.next();
|
||||
ServerPlayer player = server.getPlayerList().getPlayer(entry.getKey());
|
||||
if (player == null || !player.isAlive() || player.isSpectator()) {
|
||||
players.remove();
|
||||
continue;
|
||||
}
|
||||
Iterator<Map.Entry<Action, Integer>> actions = entry.getValue().entrySet().iterator();
|
||||
while (actions.hasNext()) {
|
||||
Map.Entry<Action, Integer> action = actions.next();
|
||||
emit(player, action.getKey(), action.getValue());
|
||||
int remaining = action.getValue() - 1;
|
||||
if (remaining <= 0) actions.remove();
|
||||
else action.setValue(remaining);
|
||||
}
|
||||
if (entry.getValue().isEmpty()) players.remove();
|
||||
}
|
||||
}
|
||||
|
||||
private static void emit(ServerPlayer player, Action action, int remaining) {
|
||||
switch (action) {
|
||||
case PISS -> spawnPissStream(player);
|
||||
case VOMIT -> spawnVomitStream(player);
|
||||
case SHIT -> spawnShitStream(player, remaining);
|
||||
case SMOKE -> spawnSmoke(player);
|
||||
}
|
||||
}
|
||||
|
||||
private static void spawnPissStream(ServerPlayer player) {
|
||||
ServerLevel level = player.level();
|
||||
Vec3 position = pissOrigin(player);
|
||||
Vec3 velocity = pissVelocity(player);
|
||||
for (int step = 0; step < STREAM_SEGMENTS; step++) {
|
||||
Vec3 next = position.add(velocity.scale(STREAM_STEP_LENGTH));
|
||||
Vec3 midpoint = position.lerp(next, 0.5D);
|
||||
level.sendParticles(pissCore(step), midpoint.x, midpoint.y, midpoint.z, 2,
|
||||
0.012D, 0.012D, 0.012D, 0.0D);
|
||||
if ((step & 1) == 0) level.sendParticles(step % 4 == 0 ? PISS_DEEP_GOLD : PISS_BRIGHT,
|
||||
midpoint.x, midpoint.y, midpoint.z, 2, 0.01D, 0.01D, 0.01D, 0.0D);
|
||||
if (step % 3 == 0) level.sendParticles(pissMist(step), midpoint.x, midpoint.y, midpoint.z,
|
||||
2, 0.012D, 0.012D, 0.012D, 0.0D);
|
||||
HitResult hit = clip(level, player, position, next);
|
||||
if (hit.getType() != HitResult.Type.MISS) {
|
||||
activateButton(level, player, hit);
|
||||
pissImpact(level, hit.getLocation());
|
||||
return;
|
||||
}
|
||||
position = next;
|
||||
velocity = velocity.add(0.0D, -0.03D * STREAM_STEP_SCALE, 0.0D)
|
||||
.scale(Math.pow(0.985D, STREAM_STEP_SCALE));
|
||||
if (position.y < level.getMinY()) return;
|
||||
}
|
||||
}
|
||||
|
||||
private static void spawnVomitStream(ServerPlayer player) {
|
||||
ServerLevel level = player.level();
|
||||
Vec3 position = vomitOrigin(player);
|
||||
Vec3 velocity = vomitVelocity(player);
|
||||
level.sendParticles(ParticleTypes.WHITE_SMOKE, position.x, position.y, position.z,
|
||||
2, 0.012D, 0.01D, 0.012D, 0.002D);
|
||||
for (int step = 0; step < STREAM_SEGMENTS; step++) {
|
||||
Vec3 next = position.add(velocity.scale(STREAM_STEP_LENGTH));
|
||||
Vec3 midpoint = position.lerp(next, 0.5D);
|
||||
level.sendParticles(vomitCore(step), midpoint.x, midpoint.y, midpoint.z, 2,
|
||||
0.012D, 0.012D, 0.012D, 0.0D);
|
||||
if ((step & 1) == 0) level.sendParticles(step % 4 == 0 ? VOMIT_BROWN : VOMIT_GREEN,
|
||||
midpoint.x, midpoint.y, midpoint.z, 2, 0.01D, 0.01D, 0.01D, 0.0D);
|
||||
HitResult hit = clip(level, player, position, next);
|
||||
if (hit.getType() != HitResult.Type.MISS) {
|
||||
vomitImpact(level, hit.getLocation());
|
||||
return;
|
||||
}
|
||||
position = next;
|
||||
velocity = velocity.add(0.0D, -0.038D * STREAM_STEP_SCALE, 0.0D)
|
||||
.scale(Math.pow(0.982D, STREAM_STEP_SCALE));
|
||||
if (position.y < level.getMinY()) return;
|
||||
}
|
||||
}
|
||||
|
||||
private static void spawnShitStream(ServerPlayer player, int remaining) {
|
||||
ServerLevel level = player.level();
|
||||
Vec3 position = shitOrigin(player);
|
||||
Vec3 velocity = shitVelocity(player);
|
||||
if (remaining < Action.SHIT.durationTicks() && remaining % 20 == 0) spawnShitDrop(level, player);
|
||||
for (int step = 0; step < STREAM_SEGMENTS; step++) {
|
||||
Vec3 next = position.add(velocity.scale(STREAM_STEP_LENGTH));
|
||||
Vec3 midpoint = position.lerp(next, 0.5D);
|
||||
float progress = step / (float) (STREAM_SEGMENTS - 1);
|
||||
float spread = 0.002F + progress * 0.018F;
|
||||
level.sendParticles(new DustParticleOptions(0x5B3417,
|
||||
Mth.lerp(progress * progress, 0.05F, 1.55F)), midpoint.x, midpoint.y, midpoint.z,
|
||||
1, spread, spread, spread, 0.0D);
|
||||
if ((step & 1) == 0) level.sendParticles(new DustParticleOptions(0x75461F,
|
||||
Mth.lerp(progress * progress, 0.04F, 0.85F)), midpoint.x, midpoint.y, midpoint.z,
|
||||
1, spread, spread, spread, 0.0D);
|
||||
HitResult hit = clip(level, player, position, next);
|
||||
if (hit.getType() != HitResult.Type.MISS) {
|
||||
level.sendParticles(SHIT_SPLAT, hit.getLocation().x, hit.getLocation().y,
|
||||
hit.getLocation().z, 2, 0.03D, 0.02D, 0.03D, 0.0D);
|
||||
level.sendParticles(SHIT_MUD, hit.getLocation().x, hit.getLocation().y,
|
||||
hit.getLocation().z, 4, 0.04D, 0.025D, 0.04D, 0.0D);
|
||||
return;
|
||||
}
|
||||
position = next;
|
||||
velocity = velocity.add(0.0D, -0.032D * STREAM_STEP_SCALE, 0.0D)
|
||||
.scale(Math.pow(0.986D, STREAM_STEP_SCALE));
|
||||
if (position.y < level.getMinY()) return;
|
||||
}
|
||||
}
|
||||
|
||||
private static void spawnSmoke(ServerPlayer player) {
|
||||
ServerLevel level = player.level();
|
||||
Vec3 mouth = mouthOrigin(player);
|
||||
level.sendParticles(ParticleTypes.SMOKE, mouth.x, mouth.y, mouth.z,
|
||||
2, 0.015D, 0.01D, 0.015D, 0.003D);
|
||||
if (level.getRandom().nextInt(3) == 0) level.sendParticles(ParticleTypes.WHITE_SMOKE,
|
||||
mouth.x, mouth.y, mouth.z, 1, 0.01D, 0.008D, 0.01D, 0.002D);
|
||||
}
|
||||
|
||||
private static HitResult clip(ServerLevel level, ServerPlayer player, Vec3 from, Vec3 to) {
|
||||
return level.clip(new ClipContext(from, to, ClipContext.Block.COLLIDER, ClipContext.Fluid.ANY, player));
|
||||
}
|
||||
|
||||
private static Vec3 pissOrigin(ServerPlayer player) {
|
||||
Vec3 forward = Vec3.directionFromRotation(0.0F, player.getYRot()).normalize();
|
||||
Vec3 right = new Vec3(-forward.z, 0.0D, forward.x);
|
||||
double side = player.getMainArm() == HumanoidArm.RIGHT ? -0.12D : 0.12D;
|
||||
return player.position().add(0.0D, player.getBbHeight() * 0.55D, 0.0D)
|
||||
.add(forward.scale(0.24D)).add(right.scale(side));
|
||||
}
|
||||
|
||||
private static Vec3 pissVelocity(ServerPlayer player) {
|
||||
float pitch = Mth.clamp(player.getXRot() + 22.0F, 6.0F, 55.0F);
|
||||
return Vec3.directionFromRotation(pitch, player.getYRot()).normalize().scale(1.05D)
|
||||
.add(player.getDeltaMovement().multiply(0.18D, 0.0D, 0.18D));
|
||||
}
|
||||
|
||||
private static Vec3 vomitOrigin(ServerPlayer player) {
|
||||
Vec3 forward = Vec3.directionFromRotation(player.getXRot(), player.getYRot()).normalize();
|
||||
return player.position().add(0.0D, player.getBbHeight() * 0.82D, 0.0D)
|
||||
.add(forward.scale(0.18D));
|
||||
}
|
||||
|
||||
private static Vec3 vomitVelocity(ServerPlayer player) {
|
||||
float pitch = Mth.clamp(player.getXRot() + 8.0F, -18.0F, 42.0F);
|
||||
return Vec3.directionFromRotation(pitch, player.getYRot()).normalize().scale(0.8D)
|
||||
.add(player.getDeltaMovement().multiply(0.15D, 0.0D, 0.15D));
|
||||
}
|
||||
|
||||
private static Vec3 shitOrigin(ServerPlayer player) {
|
||||
Vec3 backward = Vec3.directionFromRotation(0.0F, player.getYRot()).normalize().scale(-1.0D);
|
||||
return player.position().add(0.0D, player.getBbHeight() * 0.44D, 0.0D)
|
||||
.add(backward.scale(0.16D));
|
||||
}
|
||||
|
||||
private static Vec3 shitVelocity(ServerPlayer player) {
|
||||
Vec3 backward = Vec3.directionFromRotation(0.0F, player.getYRot()).normalize().scale(-1.0D);
|
||||
return backward.scale(0.68D).add(0.0D, 0.08D, 0.0D)
|
||||
.add(player.getDeltaMovement().multiply(0.1D, 0.0D, 0.1D));
|
||||
}
|
||||
|
||||
private static Vec3 mouthOrigin(ServerPlayer player) {
|
||||
Vec3 forward = Vec3.directionFromRotation(player.getXRot(), player.getYRot()).normalize();
|
||||
return player.position().add(0.0D, player.getBbHeight() * 0.82D, 0.0D)
|
||||
.add(forward.scale(0.12D));
|
||||
}
|
||||
|
||||
private static void spawnShitDrop(ServerLevel level, ServerPlayer player) {
|
||||
Vec3 backward = Vec3.directionFromRotation(0.0F, player.getYRot()).normalize().scale(-1.0D);
|
||||
Vec3 position = shitOrigin(player).add(backward.scale(0.08D)).add(0.0D, -0.08D, 0.0D);
|
||||
Vec3 velocity = backward.scale(0.05D).add(0.0D, 0.02D, 0.0D);
|
||||
ItemEntity item = new ItemEntity(level, position.x, position.y, position.z,
|
||||
new ItemStack(AbjectionRegistries.SHIT_BALL), velocity.x, velocity.y, velocity.z);
|
||||
item.setPickUpDelay(10);
|
||||
item.setThrower(player);
|
||||
level.addFreshEntity(item);
|
||||
}
|
||||
|
||||
private static DustParticleOptions pissCore(int step) {
|
||||
return switch (step % 3) {
|
||||
case 0 -> PISS_BRIGHT;
|
||||
case 1 -> PISS_GOLD;
|
||||
default -> PISS_AMBER;
|
||||
};
|
||||
}
|
||||
|
||||
private static ColorParticleOption pissMist(int step) {
|
||||
return switch (step % 3) {
|
||||
case 0 -> PISS_BRIGHT_MIST;
|
||||
case 1 -> PISS_GOLD_MIST;
|
||||
default -> PISS_AMBER_MIST;
|
||||
};
|
||||
}
|
||||
|
||||
private static DustParticleOptions vomitCore(int step) {
|
||||
return switch (step % 3) {
|
||||
case 0 -> VOMIT_GREEN;
|
||||
case 1 -> VOMIT_YELLOW;
|
||||
default -> VOMIT_ORANGE;
|
||||
};
|
||||
}
|
||||
|
||||
private static ItemParticleOption vomitChunk(ServerLevel level) {
|
||||
return switch (level.getRandom().nextInt(3)) {
|
||||
case 0 -> VOMIT_FLESH;
|
||||
case 1 -> VOMIT_EYE;
|
||||
default -> VOMIT_POTATO;
|
||||
};
|
||||
}
|
||||
|
||||
private static void activateButton(ServerLevel level, ServerPlayer player, HitResult hit) {
|
||||
if (!(hit instanceof BlockHitResult blockHit)) return;
|
||||
BlockPos pos = blockHit.getBlockPos();
|
||||
BlockState state = level.getBlockState(pos);
|
||||
if (state.getBlock() instanceof ButtonBlock button && !state.getValue(ButtonBlock.POWERED)) {
|
||||
button.press(state, level, pos, player);
|
||||
}
|
||||
}
|
||||
|
||||
private static void pissImpact(ServerLevel level, Vec3 position) {
|
||||
for (DustParticleOptions particle : new DustParticleOptions[] {PISS_BRIGHT, PISS_GOLD, PISS_AMBER}) {
|
||||
level.sendParticles(particle, position.x, position.y, position.z, 3,
|
||||
0.02D, 0.02D, 0.02D, 0.0D);
|
||||
}
|
||||
for (ColorParticleOption particle : new ColorParticleOption[] {
|
||||
PISS_BRIGHT_MIST, PISS_GOLD_MIST, PISS_AMBER_MIST}) {
|
||||
level.sendParticles(particle, position.x, position.y, position.z, 2,
|
||||
0.03D, 0.022D, 0.03D, 0.0D);
|
||||
}
|
||||
level.sendParticles(ParticleTypes.BUBBLE_POP, position.x, position.y, position.z,
|
||||
8, 0.05D, 0.03D, 0.05D, 0.01D);
|
||||
}
|
||||
|
||||
private static void vomitImpact(ServerLevel level, Vec3 position) {
|
||||
for (DustParticleOptions particle : new DustParticleOptions[] {
|
||||
VOMIT_GREEN, VOMIT_YELLOW, VOMIT_ORANGE, VOMIT_BROWN}) {
|
||||
level.sendParticles(particle, position.x, position.y, position.z, 3,
|
||||
0.02D, 0.02D, 0.02D, 0.0D);
|
||||
}
|
||||
for (ColorParticleOption particle : new ColorParticleOption[] {
|
||||
VOMIT_GREEN_MIST, VOMIT_YELLOW_MIST, VOMIT_ORANGE_MIST}) {
|
||||
level.sendParticles(particle, position.x, position.y, position.z, 2,
|
||||
0.03D, 0.022D, 0.03D, 0.0D);
|
||||
}
|
||||
level.sendParticles(ParticleTypes.SPLASH, position.x, position.y, position.z,
|
||||
4, 0.035D, 0.015D, 0.035D, 0.01D);
|
||||
level.sendParticles(ParticleTypes.SNEEZE, position.x, position.y, position.z,
|
||||
2, 0.03D, 0.02D, 0.03D, 0.0D);
|
||||
level.sendParticles(ParticleTypes.ITEM_SLIME, position.x, position.y, position.z,
|
||||
3, 0.03D, 0.02D, 0.03D, 0.0D);
|
||||
for (int index = 0; index < 2; index++) level.sendParticles(vomitChunk(level),
|
||||
position.x, position.y, position.z, 1, 0.03D, 0.02D, 0.03D, 0.0D);
|
||||
}
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
package fr.koka99cab.sanctuary26.onlyfun.api.abjection;
|
||||
|
||||
import fr.koka99cab.sanctuary26.onlyfun.abjection.AbjectionRegistries;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.world.item.Item;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
|
||||
/** Minimal public object contract consumed by Ouch's Abjection projectile implementation. */
|
||||
public final class AbjectionItems {
|
||||
private AbjectionItems() {
|
||||
}
|
||||
|
||||
public static Item shitBall() {
|
||||
return AbjectionRegistries.SHIT_BALL;
|
||||
}
|
||||
|
||||
public static Identifier shitBallId() {
|
||||
return Identifier.fromNamespaceAndPath("abjection", "shit_ball");
|
||||
}
|
||||
|
||||
public static boolean isShitBall(ItemStack stack) {
|
||||
return stack.is(AbjectionRegistries.SHIT_BALL);
|
||||
}
|
||||
}
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
package fr.koka99cab.sanctuary26.onlyfun.api.abjection;
|
||||
|
||||
import java.util.Objects;
|
||||
import net.minecraft.core.Direction;
|
||||
import net.minecraft.core.Position;
|
||||
import net.minecraft.world.entity.projectile.Projectile;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.level.Level;
|
||||
|
||||
/**
|
||||
* Owner-neutral projectile factory contract: Only Fun owns the item, while Ouch installs its
|
||||
* combat implementation without creating a reverse dependency.
|
||||
*/
|
||||
public final class AbjectionProjectileApi {
|
||||
@FunctionalInterface
|
||||
public interface Factory {
|
||||
Projectile create(Level level, Position position, ItemStack stack, Direction direction);
|
||||
}
|
||||
|
||||
private static Factory factory;
|
||||
|
||||
private AbjectionProjectileApi() {
|
||||
}
|
||||
|
||||
public static void registerFactory(Factory implementation) {
|
||||
Objects.requireNonNull(implementation, "implementation");
|
||||
if (factory != null) throw new IllegalStateException("Abjection projectile factory already registered");
|
||||
factory = implementation;
|
||||
}
|
||||
|
||||
public static Projectile create(Level level, Position position, ItemStack stack, Direction direction) {
|
||||
if (factory == null) throw new IllegalStateException("Ouch has not registered the Abjection projectile factory");
|
||||
return factory.create(level, position, stack, direction);
|
||||
}
|
||||
|
||||
public static boolean isReady() {
|
||||
return factory != null;
|
||||
}
|
||||
}
|
||||
@@ -3,17 +3,34 @@ package fr.koka99cab.sanctuary26.onlyfun.client;
|
||||
import fr.koka99cab.sanctuary26.iliketomoveit.physics.VehicleFlightPhysics;
|
||||
import fr.koka99cab.sanctuary26.onlyfun.chickenboat.ChickenBoatControlPayload;
|
||||
import fr.koka99cab.sanctuary26.onlyfun.chickenboat.ChickenBoatUtil;
|
||||
import fr.koka99cab.sanctuary26.onlyfun.OnlyFunMod;
|
||||
import fr.koka99cab.sanctuary26.onlyfun.network.ShitActionRequestPayload;
|
||||
import com.mojang.blaze3d.platform.InputConstants;
|
||||
import net.fabricmc.api.ClientModInitializer;
|
||||
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
|
||||
import net.fabricmc.fabric.api.client.keymapping.v1.KeyMappingHelper;
|
||||
import net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking;
|
||||
import net.minecraft.client.KeyMapping;
|
||||
import net.minecraft.world.entity.Entity;
|
||||
import net.minecraft.world.entity.vehicle.boat.AbstractBoat;
|
||||
import org.lwjgl.glfw.GLFW;
|
||||
|
||||
public final class OnlyFunClient implements ClientModInitializer {
|
||||
private static final KeyMapping.Category ONLY_FUN_CATEGORY =
|
||||
KeyMapping.Category.register(OnlyFunMod.id("onlyfun"));
|
||||
private static KeyMapping shitKey;
|
||||
|
||||
@Override
|
||||
public void onInitializeClient() {
|
||||
shitKey = KeyMappingHelper.registerKeyMapping(new KeyMapping(
|
||||
"key.onlyfun.shit", InputConstants.Type.KEYSYM, GLFW.GLFW_KEY_J, ONLY_FUN_CATEGORY));
|
||||
ClientTickEvents.END_CLIENT_TICK.register(client -> {
|
||||
if (client.player == null || client.level == null) return;
|
||||
while (shitKey.consumeClick()) {
|
||||
if (ClientPlayNetworking.canSend(ShitActionRequestPayload.ID)) {
|
||||
ClientPlayNetworking.send(new ShitActionRequestPayload());
|
||||
}
|
||||
}
|
||||
Entity vehicle = client.player.getVehicle();
|
||||
if (!(vehicle instanceof AbstractBoat boat) || !ChickenBoatUtil.hasChickenPassenger(boat)) return;
|
||||
if (!ClientPlayNetworking.canSend(ChickenBoatControlPayload.ID)) return;
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
package fr.koka99cab.sanctuary26.onlyfun.command;
|
||||
|
||||
import com.mojang.brigadier.Command;
|
||||
import com.mojang.brigadier.exceptions.CommandSyntaxException;
|
||||
import fr.koka99cab.sanctuary26.onlyfun.api.abjection.AbjectionActions;
|
||||
import net.fabricmc.fabric.api.command.v2.CommandRegistrationCallback;
|
||||
import net.minecraft.commands.CommandSourceStack;
|
||||
import net.minecraft.commands.Commands;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
|
||||
/** Historical root commands, routed through the shared server action API. */
|
||||
public final class AbjectionCommands {
|
||||
private AbjectionCommands() {
|
||||
}
|
||||
|
||||
public static void register() {
|
||||
CommandRegistrationCallback.EVENT.register((dispatcher, registryAccess, environment) -> {
|
||||
for (AbjectionActions.Action action : AbjectionActions.Action.values()) {
|
||||
dispatcher.register(Commands.literal(action.id())
|
||||
.executes(context -> start(context.getSource(), action)));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static int start(CommandSourceStack source, AbjectionActions.Action action)
|
||||
throws CommandSyntaxException {
|
||||
ServerPlayer player = source.getPlayerOrException();
|
||||
AbjectionActions.StartResult result = AbjectionActions.start(player, action);
|
||||
if (result == AbjectionActions.StartResult.REJECTED) return 0;
|
||||
String suffix = result == AbjectionActions.StartResult.RESTARTED ? "restarted" : "started";
|
||||
source.sendSuccess(() -> Component.translatable(
|
||||
"command.abjection." + action.id() + "." + suffix), false);
|
||||
return Command.SINGLE_SUCCESS;
|
||||
}
|
||||
}
|
||||
+3
-1
@@ -9,7 +9,7 @@ import net.fabricmc.fabric.api.networking.v1.PayloadTypeRegistry;
|
||||
* Networking contract for Only Fun. Payload IDs must use the module namespace.
|
||||
*/
|
||||
public final class OnlyFunNetworking {
|
||||
public static final int PROTOCOL_VERSION = 2;
|
||||
public static final int PROTOCOL_VERSION = 3;
|
||||
private static boolean initialized;
|
||||
|
||||
private OnlyFunNetworking() {
|
||||
@@ -21,7 +21,9 @@ public final class OnlyFunNetworking {
|
||||
}
|
||||
initialized = true;
|
||||
PayloadTypeRegistry.serverboundPlay().register(ChickenBoatControlPayload.ID, ChickenBoatControlPayload.CODEC);
|
||||
PayloadTypeRegistry.serverboundPlay().register(ShitActionRequestPayload.ID, ShitActionRequestPayload.CODEC);
|
||||
ChickenBoatServerController.register();
|
||||
fr.koka99cab.sanctuary26.onlyfun.abjection.ShitActionRequestHandler.register();
|
||||
OnlyFunMod.LOGGER.debug("[{}] Networking contract v{} ready; chicken boat controls registered.",
|
||||
OnlyFunMod.DISPLAY_NAME, PROTOCOL_VERSION);
|
||||
}
|
||||
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
package fr.koka99cab.sanctuary26.onlyfun.network;
|
||||
|
||||
import fr.koka99cab.sanctuary26.onlyfun.OnlyFunMod;
|
||||
import net.minecraft.network.RegistryFriendlyByteBuf;
|
||||
import net.minecraft.network.codec.StreamCodec;
|
||||
import net.minecraft.network.protocol.common.custom.CustomPacketPayload;
|
||||
|
||||
/** Empty and therefore strictly bounded request to start the existing server-owned action. */
|
||||
public record ShitActionRequestPayload() implements CustomPacketPayload {
|
||||
public static final Type<ShitActionRequestPayload> ID =
|
||||
new Type<>(OnlyFunMod.id("c2s/shit_action"));
|
||||
public static final StreamCodec<RegistryFriendlyByteBuf, ShitActionRequestPayload> CODEC =
|
||||
CustomPacketPayload.codec((payload, buffer) -> {}, buffer -> new ShitActionRequestPayload());
|
||||
|
||||
@Override
|
||||
public Type<? extends CustomPacketPayload> type() {
|
||||
return ID;
|
||||
}
|
||||
}
|
||||
+3
@@ -1,6 +1,7 @@
|
||||
package fr.koka99cab.sanctuary26.onlyfun.registry;
|
||||
|
||||
import fr.koka99cab.sanctuary26.onlyfun.OnlyFunMod;
|
||||
import fr.koka99cab.sanctuary26.onlyfun.abjection.AbjectionRegistries;
|
||||
import net.minecraft.core.Registry;
|
||||
import net.minecraft.core.registries.BuiltInRegistries;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
@@ -27,6 +28,8 @@ public final class OnlyFunCreativeTab {
|
||||
output.accept(OnlyFunRegistries.CAPE_LOOTBOX);
|
||||
output.accept(OnlyFunRegistries.SPAWN_EGG_LOOTBOX);
|
||||
output.accept(OnlyFunRegistries.ANOMALY_LOOTBOX);
|
||||
output.accept(AbjectionRegistries.SHIT_BALL);
|
||||
output.accept(AbjectionRegistries.SHIT_CUBE_ITEM);
|
||||
})
|
||||
.build()
|
||||
);
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"variants": {
|
||||
"": {
|
||||
"model": "abjection:block/shit_cube"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"model": {
|
||||
"type": "minecraft:model",
|
||||
"model": "abjection:item/shit_ball"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"model": {
|
||||
"type": "minecraft:model",
|
||||
"model": "abjection:item/shit_cube"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"item.abjection.shit_ball": "Shit Ball",
|
||||
"block.abjection.shit_cube": "Shit Cube",
|
||||
"command.abjection.piss.started": "Here we go for 15 seconds.",
|
||||
"command.abjection.piss.restarted": "The timer restarts for 15 seconds.",
|
||||
"command.abjection.vomit.started": "Yuck. You vomit for 8 seconds.",
|
||||
"command.abjection.vomit.restarted": "You vomit again for 8 seconds.",
|
||||
"command.abjection.shit.started": "You shit in a stream for 6 seconds.",
|
||||
"command.abjection.shit.restarted": "The shit stream restarts for 6 seconds.",
|
||||
"command.abjection.smoke.started": "You blow smoke for 10 seconds.",
|
||||
"command.abjection.smoke.restarted": "You keep smoking for 10 seconds."
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"item.abjection.shit_ball": "Boule de merde",
|
||||
"block.abjection.shit_cube": "Cube de merde",
|
||||
"command.abjection.piss.started": "C'est parti pour 15 secondes.",
|
||||
"command.abjection.piss.restarted": "Le timer repart pour 15 secondes.",
|
||||
"command.abjection.vomit.started": "Beurk. Tu vomis pendant 8 secondes.",
|
||||
"command.abjection.vomit.restarted": "Tu revomis pour 8 secondes.",
|
||||
"command.abjection.shit.started": "Tu chies en jet pendant 6 secondes.",
|
||||
"command.abjection.shit.restarted": "Le jet de merde repart pour 6 secondes.",
|
||||
"command.abjection.smoke.started": "Tu souffles de la fumée pendant 10 secondes.",
|
||||
"command.abjection.smoke.restarted": "Tu continues de fumer pendant 10 secondes."
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"item.abjection.shit_ball": "Шарик дерьма",
|
||||
"block.abjection.shit_cube": "Куб дерьма",
|
||||
"command.abjection.piss.started": "Струя запущена на 15 секунд.",
|
||||
"command.abjection.piss.restarted": "Таймер струи перезапущен на 15 секунд.",
|
||||
"command.abjection.vomit.started": "Фу. Тебя рвёт 8 секунд.",
|
||||
"command.abjection.vomit.restarted": "Тебя снова рвёт 8 секунд.",
|
||||
"command.abjection.shit.started": "Ты испражняешься струёй 6 секунд.",
|
||||
"command.abjection.shit.restarted": "Струя дерьма перезапущена на 6 секунд.",
|
||||
"command.abjection.smoke.started": "Ты выпускаешь дым 10 секунд.",
|
||||
"command.abjection.smoke.restarted": "Ты продолжаешь курить ещё 10 секунд."
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"parent": "minecraft:block/cube_all",
|
||||
"textures": {
|
||||
"all": "abjection:block/shit_cube"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"parent": "minecraft:item/generated",
|
||||
"textures": {
|
||||
"layer0": "abjection:item/shit_ball"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"parent": "abjection:block/shit_cube"
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 852 B |
Binary file not shown.
|
After Width: | Height: | Size: 261 B |
@@ -41,5 +41,7 @@
|
||||
"command.onlyfun.history": "Rolls: %s | dry streak: %s | anomalies: %s | last: %s",
|
||||
"command.onlyfun.give": "Granted Only Fun source: %s",
|
||||
"command.onlyfun.roll": "Outcome: %s (%s)",
|
||||
"command.onlyfun.simulate": "Simulation %s × %s: %s"
|
||||
"command.onlyfun.simulate": "Simulation %s × %s: %s",
|
||||
"key.categories.onlyfun.onlyfun": "Only Fun",
|
||||
"key.onlyfun.shit": "Shit"
|
||||
}
|
||||
|
||||
@@ -41,5 +41,7 @@
|
||||
"command.onlyfun.history": "Tirages : %s | série sèche : %s | anomalies : %s | dernier : %s",
|
||||
"command.onlyfun.give": "Source Only Fun donnée : %s",
|
||||
"command.onlyfun.roll": "Résultat : %s (%s)",
|
||||
"command.onlyfun.simulate": "Simulation %s × %s : %s"
|
||||
"command.onlyfun.simulate": "Simulation %s × %s : %s",
|
||||
"key.categories.onlyfun.onlyfun": "Only Fun",
|
||||
"key.onlyfun.shit": "Chier"
|
||||
}
|
||||
|
||||
@@ -41,5 +41,7 @@
|
||||
"command.onlyfun.history": "Розыгрыши: %s | серия: %s | аномалии: %s | последнее: %s",
|
||||
"command.onlyfun.give": "Выдан источник Only Fun: %s",
|
||||
"command.onlyfun.roll": "Результат: %s (%s)",
|
||||
"command.onlyfun.simulate": "Симуляция %s × %s: %s"
|
||||
"command.onlyfun.simulate": "Симуляция %s × %s: %s",
|
||||
"key.categories.onlyfun.onlyfun": "Only Fun",
|
||||
"key.onlyfun.shit": "Испражниться"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"type": "minecraft:block",
|
||||
"pools": [
|
||||
{
|
||||
"bonus_rolls": 0.0,
|
||||
"conditions": [
|
||||
{
|
||||
"condition": "minecraft:survives_explosion"
|
||||
}
|
||||
],
|
||||
"entries": [
|
||||
{
|
||||
"type": "minecraft:item",
|
||||
"name": "abjection:shit_cube"
|
||||
}
|
||||
],
|
||||
"rolls": 1.0
|
||||
}
|
||||
],
|
||||
"random_sequence": "abjection:blocks/shit_cube"
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"type": "minecraft:crafting_shaped",
|
||||
"category": "building",
|
||||
"key": {
|
||||
"#": "abjection:shit_ball"
|
||||
},
|
||||
"pattern": [
|
||||
"###",
|
||||
"###",
|
||||
"###"
|
||||
],
|
||||
"result": {
|
||||
"count": 1,
|
||||
"id": "abjection:shit_cube"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"replace": false,
|
||||
"values": [
|
||||
"abjection:shit_cube"
|
||||
]
|
||||
}
|
||||
@@ -8,6 +8,9 @@
|
||||
"KOKA99CAB"
|
||||
],
|
||||
"license": "GPL-3.0-or-later",
|
||||
"provides": [
|
||||
"abjection"
|
||||
],
|
||||
"icon": "assets/onlyfun/icon.png",
|
||||
"environment": "*",
|
||||
"entrypoints": {
|
||||
@@ -31,7 +34,7 @@
|
||||
"system_type": "gameplay-system",
|
||||
"namespace": "onlyfun",
|
||||
"data_version": 2,
|
||||
"network_protocol": 2,
|
||||
"network_protocol": 3,
|
||||
"lifecycle": "${lifecycle}",
|
||||
"pack_version": "${pack_version}",
|
||||
"descriptions": {
|
||||
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
package fr.koka99cab.sanctuary26.onlyfun.abjection;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
/** Executable check for one physical press and same-tick spam deduplication. */
|
||||
public final class ShitActionRequestSmoke {
|
||||
private ShitActionRequestSmoke() {
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
UUID playerId = UUID.fromString("00000000-0000-0000-0000-000000000059");
|
||||
require(ShitActionRequestHandler.acceptTick(playerId, 100), "first press was rejected");
|
||||
require(!ShitActionRequestHandler.acceptTick(playerId, 100), "same-tick spam was accepted");
|
||||
require(ShitActionRequestHandler.acceptTick(playerId, 101), "next physical press was rejected");
|
||||
ShitActionRequestHandler.clear(playerId);
|
||||
require(ShitActionRequestHandler.acceptTick(playerId, 101), "disconnect cleanup retained input state");
|
||||
ShitActionRequestHandler.clear(playerId);
|
||||
}
|
||||
|
||||
private static void require(boolean condition, String message) {
|
||||
if (!condition) throw new AssertionError(message);
|
||||
}
|
||||
}
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
package fr.koka99cab.sanctuary26.onlyfun.api.abjection;
|
||||
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import net.minecraft.SharedConstants;
|
||||
import net.minecraft.server.Bootstrap;
|
||||
|
||||
/** Executable regression check for the bounded historical action contract. */
|
||||
public final class AbjectionActionContractSmoke {
|
||||
private AbjectionActionContractSmoke() {
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
SharedConstants.tryDetectVersion();
|
||||
Bootstrap.bootStrap();
|
||||
require(AbjectionActions.Action.PISS.durationTicks() == 300, "piss duration changed");
|
||||
require(AbjectionActions.Action.VOMIT.durationTicks() == 160, "vomit duration changed");
|
||||
require(AbjectionActions.Action.SHIT.durationTicks() == 120, "shit duration changed");
|
||||
require(AbjectionActions.Action.SMOKE.durationTicks() == 200, "smoke duration changed");
|
||||
require(Set.of("piss", "vomit", "shit", "smoke").equals(Set.of(
|
||||
AbjectionActions.Action.PISS.id(), AbjectionActions.Action.VOMIT.id(),
|
||||
AbjectionActions.Action.SHIT.id(), AbjectionActions.Action.SMOKE.id())),
|
||||
"historical command surface changed");
|
||||
|
||||
UUID playerId = UUID.fromString("00000000-0000-0000-0000-000000000057");
|
||||
require(!AbjectionActions.restart(playerId, AbjectionActions.Action.SHIT),
|
||||
"first action incorrectly marked as a restart");
|
||||
require(AbjectionActions.remaining(playerId, AbjectionActions.Action.SHIT) == 120,
|
||||
"initial duration was not installed");
|
||||
require(AbjectionActions.restart(playerId, AbjectionActions.Action.SHIT),
|
||||
"second action was not marked as a restart");
|
||||
require(AbjectionActions.remaining(playerId, AbjectionActions.Action.SHIT) == 120,
|
||||
"restart did not reset the full duration");
|
||||
AbjectionActions.clear(playerId);
|
||||
require(AbjectionActions.remaining(playerId, AbjectionActions.Action.SHIT) == 0,
|
||||
"cleanup left a temporary action behind");
|
||||
}
|
||||
|
||||
private static void require(boolean condition, String message) {
|
||||
if (!condition) throw new AssertionError(message);
|
||||
}
|
||||
}
|
||||
+45
-3
@@ -12,9 +12,10 @@ def miningRifleTextureHash = "4e4295c5c8a8f71d03307809448ff64363eec2d2aefdc538c4
|
||||
|
||||
tasks.register("verifyOuch") {
|
||||
group = "verification"
|
||||
description = "Checks Ouch alpha.9, including the Ambiance-backed Weather TNT and migrated Gunz arsenal."
|
||||
description = "Checks Ouch alpha.10, including the shared Abjection projectile implementation."
|
||||
inputs.files(fileTree("src/main/java"))
|
||||
inputs.files(fileTree("src/main/resources"))
|
||||
inputs.files(rootProject.fileTree("onlyfun/src/main/java"))
|
||||
inputs.file(rootProject.file("pack/migrations/26.1.2-gunz-to-26.2.0-alpha.41.json"))
|
||||
inputs.file(rootProject.file("pack/migrations/26.2.0-alpha.99-canon-weather-ownership-to-alpha.100.json"))
|
||||
inputs.file(rootProject.file("pack/migrations/26.2.0-alpha.184-silver-engineering-to-alpha.185.json"))
|
||||
@@ -25,6 +26,34 @@ tasks.register("verifyOuch") {
|
||||
def weatherRegistry = file("src/main/java/fr/koka99cab/sanctuary26/ouch/registry/OuchWeatherTnt.java").text
|
||||
def weatherEntity = file("src/main/java/fr/koka99cab/sanctuary26/ouch/entity/WeatherTntEntity.java").text
|
||||
def manifest = new JsonSlurper().parse(file("src/main/resources/fabric.mod.json"))
|
||||
def abjectionRegistry = file(
|
||||
"src/main/java/fr/koka99cab/sanctuary26/ouch/registry/OuchAbjectionProjectiles.java").text
|
||||
def abjectionEntity = file(
|
||||
"src/main/java/fr/koka99cab/sanctuary26/ouch/entity/ShitBallEntity.java").text
|
||||
def abjectionClient = file(
|
||||
"src/main/java/fr/koka99cab/sanctuary26/ouch/client/OuchClient.java").text
|
||||
def onlyFunProjectileApi = rootProject.file(
|
||||
"onlyfun/src/main/java/fr/koka99cab/sanctuary26/onlyfun/api/abjection/AbjectionProjectileApi.java").text
|
||||
if (manifest.depends?.onlyfun != ">=0.0.0-alpha.23"
|
||||
|| manifest.custom?.sanctuary26?.data_version != 1
|
||||
|| manifest.custom?.sanctuary26?.network_protocol != 1
|
||||
|| !rootProject.file("build.gradle").text.contains('implementation project(":onlyfun")')
|
||||
|| !onlyFunProjectileApi.contains("public interface Factory")
|
||||
|| !abjectionRegistry.contains('AbjectionProjectileApi.registerFactory(')
|
||||
|| !abjectionRegistry.contains('AbjectionItems.shitBallId()')
|
||||
|| abjectionRegistry.contains('AbjectionRegistries')
|
||||
|| !abjectionRegistry.contains('DispenserBlock.registerProjectileBehavior(AbjectionItems.shitBall())')
|
||||
|| !abjectionRegistry.contains('UseItemCallback.EVENT.register')
|
||||
|| !abjectionRegistry.contains('stack.consume(1, player)')
|
||||
|| !abjectionRegistry.contains('Projectile.spawnProjectileFromRotation(')
|
||||
|| !abjectionRegistry.contains('.sized(0.25F, 0.25F)')
|
||||
|| !abjectionEntity.contains('NAUSEA_DURATION_TICKS = 7 * 20')
|
||||
|| !abjectionEntity.contains('current == null ? 0 : current.getDuration()')
|
||||
|| !abjectionEntity.contains('SoundEvents.MUD_PLACE')
|
||||
|| !abjectionEntity.contains('discard()')
|
||||
|| !abjectionClient.contains('new ThrownItemRenderer<>(context)')) {
|
||||
throw new GradleException("Ouch alpha.10 Abjection projectile contract is incomplete")
|
||||
}
|
||||
if (!itemRegistry.contains("MIGRATED_WEAPONS = List.of(CREEPERLOCK, MITRAILLETTE, EXPULSEUR, ASPIRATEUR)")
|
||||
|| !itemRegistry.contains("WEAPONS = List.of(CREEPERLOCK, MITRAILLETTE, EXPULSEUR, ASPIRATEUR, MINING_RIFLE, BETA_BOW)")
|
||||
|| !creativeTab.contains('OuchMod.id("arsenal")')
|
||||
@@ -89,7 +118,7 @@ tasks.register("verifyOuch") {
|
||||
def creeperlockRecipe = new JsonSlurper().parse(file("src/main/resources/data/ouch/recipe/creeperlock.json"))
|
||||
def silverEngineeringMigration = new JsonSlurper().parse(rootProject.file(
|
||||
"pack/migrations/26.2.0-alpha.184-silver-engineering-to-alpha.185.json"))
|
||||
if (project.version.toString() != "0.0.0-alpha.9"
|
||||
if (project.version.toString() != "0.0.0-alpha.10"
|
||||
|| mitrailletteRecipe.pattern != ["RRP", "IGI", " B "]
|
||||
|| mitrailletteRecipe.key != [B: "minecraft:stone_button", G: "minecraft:redstone",
|
||||
I: "anotherworld:silver_ingot", P: "anotherworld:silver_plate", R: "anotherworld:silver_rod"]
|
||||
@@ -317,10 +346,23 @@ tasks.register("verifyOuch") {
|
||||
throw new GradleException("Runtime Ouch resources must not re-register the legacy gunz namespace: ${forbiddenNamespaceReferences}")
|
||||
}
|
||||
|
||||
logger.lifecycle("Ouch verified: 6 enchantable items, 4 custom enchantments, debuffed bases, shared headshots, 6 recipes and FR/EN/RU translations.")
|
||||
logger.lifecycle("Ouch verified: arsenal, Weather TNT and shared Abjection projectile contract.")
|
||||
}
|
||||
}
|
||||
|
||||
tasks.named("test") {
|
||||
failOnNoDiscoveredTests = false
|
||||
}
|
||||
|
||||
tasks.register("shitBallContractSmoke", JavaExec) {
|
||||
group = "verification"
|
||||
description = "Checks additive and overflow-safe Shit Ball nausea."
|
||||
dependsOn tasks.named("testClasses")
|
||||
classpath = sourceSets.test.runtimeClasspath
|
||||
mainClass = "fr.koka99cab.sanctuary26.ouch.entity.ShitBallContractSmoke"
|
||||
}
|
||||
|
||||
tasks.named("check") {
|
||||
dependsOn tasks.named("shitBallContractSmoke")
|
||||
dependsOn tasks.named("verifyOuch")
|
||||
}
|
||||
|
||||
@@ -2,16 +2,20 @@ package fr.koka99cab.sanctuary26.ouch.client;
|
||||
|
||||
import fr.koka99cab.sanctuary26.ouch.OuchMod;
|
||||
import fr.koka99cab.sanctuary26.ouch.entity.WeatherTntEntity;
|
||||
import fr.koka99cab.sanctuary26.ouch.registry.OuchAbjectionProjectiles;
|
||||
import fr.koka99cab.sanctuary26.ouch.registry.OuchWeatherTnt;
|
||||
import net.fabricmc.api.ClientModInitializer;
|
||||
import net.fabricmc.fabric.api.client.rendering.v1.EntityRendererRegistry;
|
||||
import net.minecraft.client.renderer.entity.EntityRenderer;
|
||||
import net.minecraft.client.renderer.entity.TntRenderer;
|
||||
import net.minecraft.client.renderer.entity.ThrownItemRenderer;
|
||||
import net.minecraft.world.entity.EntityType;
|
||||
|
||||
public final class OuchClient implements ClientModInitializer {
|
||||
@Override
|
||||
public void onInitializeClient() {
|
||||
EntityRendererRegistry.register(OuchAbjectionProjectiles.ENTITY,
|
||||
context -> new ThrownItemRenderer<>(context));
|
||||
registerWeatherRenderer(OuchWeatherTnt.ENTITY);
|
||||
registerWeatherRenderer(OuchWeatherTnt.REDSTONER_COMPAT_ENTITY);
|
||||
registerWeatherRenderer(OuchWeatherTnt.LEGACY_ENTITY);
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
package fr.koka99cab.sanctuary26.ouch.entity;
|
||||
|
||||
import fr.koka99cab.sanctuary26.onlyfun.api.abjection.AbjectionItems;
|
||||
import fr.koka99cab.sanctuary26.ouch.registry.OuchAbjectionProjectiles;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.sounds.SoundEvents;
|
||||
import net.minecraft.sounds.SoundSource;
|
||||
import net.minecraft.world.effect.MobEffectInstance;
|
||||
import net.minecraft.world.effect.MobEffects;
|
||||
import net.minecraft.world.entity.EntityType;
|
||||
import net.minecraft.world.entity.LivingEntity;
|
||||
import net.minecraft.world.entity.projectile.throwableitemprojectile.ThrowableItemProjectile;
|
||||
import net.minecraft.world.item.Item;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.level.Level;
|
||||
import net.minecraft.world.phys.EntityHitResult;
|
||||
import net.minecraft.world.phys.HitResult;
|
||||
|
||||
/** Server-authoritative historical Abjection projectile, owned by Ouch. */
|
||||
public final class ShitBallEntity extends ThrowableItemProjectile {
|
||||
public static final int NAUSEA_DURATION_TICKS = 7 * 20;
|
||||
|
||||
public ShitBallEntity(EntityType<? extends ShitBallEntity> type, Level level) {
|
||||
super(type, level);
|
||||
}
|
||||
|
||||
public ShitBallEntity(Level level, LivingEntity owner, ItemStack stack) {
|
||||
super(OuchAbjectionProjectiles.ENTITY, owner, level, stack.copyWithCount(1));
|
||||
}
|
||||
|
||||
public ShitBallEntity(Level level, double x, double y, double z, ItemStack stack) {
|
||||
super(OuchAbjectionProjectiles.ENTITY, x, y, z, level, stack.copyWithCount(1));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Item getDefaultItem() {
|
||||
return AbjectionItems.shitBall();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onHitEntity(EntityHitResult hit) {
|
||||
super.onHitEntity(hit);
|
||||
if (!(level() instanceof ServerLevel) || !(hit.getEntity() instanceof LivingEntity target)) return;
|
||||
MobEffectInstance current = target.getEffect(MobEffects.NAUSEA);
|
||||
int duration = extendedNauseaDuration(current == null ? 0 : current.getDuration());
|
||||
target.addEffect(new MobEffectInstance(MobEffects.NAUSEA, duration), getOwner());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onHit(HitResult hit) {
|
||||
super.onHit(hit);
|
||||
if (level() instanceof ServerLevel level) {
|
||||
level.playSound(null, getX(), getY(), getZ(), SoundEvents.MUD_PLACE,
|
||||
SoundSource.PLAYERS, 1.0F, 0.75F);
|
||||
discard();
|
||||
}
|
||||
}
|
||||
|
||||
static int extendedNauseaDuration(int currentDuration) {
|
||||
return (int) Math.min(Integer.MAX_VALUE, Math.max(0L, currentDuration) + NAUSEA_DURATION_TICKS);
|
||||
}
|
||||
}
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
package fr.koka99cab.sanctuary26.ouch.registry;
|
||||
|
||||
import fr.koka99cab.sanctuary26.onlyfun.api.abjection.AbjectionItems;
|
||||
import fr.koka99cab.sanctuary26.onlyfun.api.abjection.AbjectionProjectileApi;
|
||||
import fr.koka99cab.sanctuary26.ouch.entity.ShitBallEntity;
|
||||
import net.fabricmc.fabric.api.event.player.UseItemCallback;
|
||||
import net.minecraft.core.Direction;
|
||||
import net.minecraft.core.Position;
|
||||
import net.minecraft.core.Registry;
|
||||
import net.minecraft.core.registries.BuiltInRegistries;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.sounds.SoundEvents;
|
||||
import net.minecraft.sounds.SoundSource;
|
||||
import net.minecraft.stats.Stats;
|
||||
import net.minecraft.world.InteractionResult;
|
||||
import net.minecraft.world.entity.EntityType;
|
||||
import net.minecraft.world.entity.LivingEntity;
|
||||
import net.minecraft.world.entity.MobCategory;
|
||||
import net.minecraft.world.entity.projectile.Projectile;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.level.Level;
|
||||
import net.minecraft.world.level.block.DispenserBlock;
|
||||
|
||||
/** Shared manual/dispenser creation path for {@code abjection:shit_ball}. */
|
||||
public final class OuchAbjectionProjectiles {
|
||||
private static final float SHOOT_POWER = 1.5F;
|
||||
private static final ResourceKey<EntityType<?>> KEY = ResourceKey.create(
|
||||
Registries.ENTITY_TYPE, AbjectionItems.shitBallId());
|
||||
public static final EntityType<ShitBallEntity> ENTITY = Registry.register(
|
||||
BuiltInRegistries.ENTITY_TYPE, KEY,
|
||||
EntityType.Builder.<ShitBallEntity>of(ShitBallEntity::new, MobCategory.MISC)
|
||||
.sized(0.25F, 0.25F).clientTrackingRange(4).updateInterval(10).build(KEY));
|
||||
private static boolean initialized;
|
||||
|
||||
private OuchAbjectionProjectiles() {
|
||||
}
|
||||
|
||||
public static void initialize() {
|
||||
if (initialized) return;
|
||||
initialized = true;
|
||||
AbjectionProjectileApi.registerFactory(OuchAbjectionProjectiles::createFromDispenser);
|
||||
DispenserBlock.registerProjectileBehavior(AbjectionItems.shitBall());
|
||||
UseItemCallback.EVENT.register((player, level, hand) -> {
|
||||
ItemStack stack = player.getItemInHand(hand);
|
||||
if (!AbjectionItems.isShitBall(stack) || player.isSpectator()) return InteractionResult.PASS;
|
||||
if (level.isClientSide()) return InteractionResult.SUCCESS;
|
||||
if (!(level instanceof ServerLevel serverLevel)) return InteractionResult.FAIL;
|
||||
serverLevel.playSound(null, player.getX(), player.getY(), player.getZ(),
|
||||
SoundEvents.SNOWBALL_THROW, SoundSource.NEUTRAL, 0.5F,
|
||||
0.4F / (serverLevel.getRandom().nextFloat() * 0.4F + 0.8F));
|
||||
Projectile.spawnProjectileFromRotation(OuchAbjectionProjectiles::createFromPlayer,
|
||||
serverLevel, stack, player, 0.0F, SHOOT_POWER, 1.0F);
|
||||
player.awardStat(Stats.ITEM_USED.get(AbjectionItems.shitBall()));
|
||||
stack.consume(1, player);
|
||||
return InteractionResult.SUCCESS_SERVER;
|
||||
});
|
||||
}
|
||||
|
||||
public static ShitBallEntity createFromPlayer(ServerLevel level, LivingEntity owner, ItemStack stack) {
|
||||
return new ShitBallEntity(level, owner, stack);
|
||||
}
|
||||
|
||||
public static Projectile createFromDispenser(Level level, Position position, ItemStack stack,
|
||||
Direction direction) {
|
||||
return new ShitBallEntity(level, position.x(), position.y(), position.z(), stack);
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,7 @@ public final class OuchRegistries {
|
||||
initialized = true;
|
||||
OuchItems.initialize();
|
||||
OuchWeatherTnt.initialize();
|
||||
OuchAbjectionProjectiles.initialize();
|
||||
OuchCreativeTab.initialize();
|
||||
OuchMod.LOGGER.debug("[{}] Weapon registry hub initialized.", OuchMod.DISPLAY_NAME);
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
"fabric-api": ">=0.154.2+26.2",
|
||||
"anotherworld": ">=0.0.0-alpha.46",
|
||||
"ambiance": ">=0.0.0-alpha.12",
|
||||
"onlyfun": ">=0.0.0-alpha.23",
|
||||
"sanctuary": ">=0.0.0-alpha.110"
|
||||
},
|
||||
"custom": {
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
package fr.koka99cab.sanctuary26.ouch.entity;
|
||||
|
||||
import net.minecraft.SharedConstants;
|
||||
import net.minecraft.server.Bootstrap;
|
||||
|
||||
/** Executable regression test for additive and overflow-safe nausea duration. */
|
||||
public final class ShitBallContractSmoke {
|
||||
private ShitBallContractSmoke() {
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
SharedConstants.tryDetectVersion();
|
||||
Bootstrap.bootStrap();
|
||||
require(ShitBallEntity.NAUSEA_DURATION_TICKS == 140, "historical duration changed");
|
||||
require(ShitBallEntity.extendedNauseaDuration(0) == 140, "first hit duration is wrong");
|
||||
require(ShitBallEntity.extendedNauseaDuration(60) == 200,
|
||||
"a new impact did not add seven seconds");
|
||||
require(ShitBallEntity.extendedNauseaDuration(-10) == 140,
|
||||
"negative input was not normalized");
|
||||
require(ShitBallEntity.extendedNauseaDuration(Integer.MAX_VALUE) == Integer.MAX_VALUE,
|
||||
"duration overflow was not saturated");
|
||||
}
|
||||
|
||||
private static void require(boolean condition, String message) {
|
||||
if (!condition) throw new AssertionError(message);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
{
|
||||
"schema_version": 1,
|
||||
"source": {
|
||||
"pack_version": "26.2.0-alpha.209",
|
||||
"modules": {
|
||||
"sanctuary": "0.0.0-alpha.117",
|
||||
"onlyfun": "0.0.0-alpha.20",
|
||||
"iliketomoveit": "0.0.0-alpha.14",
|
||||
"itsalive": "0.0.0-alpha.34"
|
||||
}
|
||||
},
|
||||
"target": {
|
||||
"pack_version": "26.2.0-alpha.210",
|
||||
"modules": {
|
||||
"sanctuary": "0.0.0-alpha.118",
|
||||
"onlyfun": "0.0.0-alpha.21",
|
||||
"iliketomoveit": "0.0.0-alpha.15",
|
||||
"itsalive": "0.0.0-alpha.35"
|
||||
}
|
||||
},
|
||||
"features": {
|
||||
"shop_status_layout_separated": true,
|
||||
"shop_progression_bypasses_removed": true,
|
||||
"zombie_xbox_360_names": true,
|
||||
"cape_reconnect_resynchronization": true,
|
||||
"quest_requests_require_dedicated_signs": true,
|
||||
"ridden_happy_ghast_cruise_speed_multiplier": 2.0,
|
||||
"canaplia_codex_name": "Forbidden Book",
|
||||
"canaplia_page_learns_random_unknown_entry": true,
|
||||
"canaplia_handheld_knowledge_personal": true,
|
||||
"canaplia_lectern_shares_owner_knowledge": true
|
||||
},
|
||||
"compatibility": {
|
||||
"new_world_required": false,
|
||||
"save_reset_required": false,
|
||||
"registry_ids_changed": false,
|
||||
"sanctuary_data_version": 11,
|
||||
"sanctuary_network_protocol": 21,
|
||||
"onlyfun_data_version": 2,
|
||||
"onlyfun_network_protocol": 2,
|
||||
"iliketomoveit_data_version": 3,
|
||||
"iliketomoveit_network_protocol": 5,
|
||||
"itsalive_data_version": 3,
|
||||
"itsalive_network_protocol": 4
|
||||
},
|
||||
"distribution": {
|
||||
"pack_release_updated": true,
|
||||
"packwiz_index_refreshed": true,
|
||||
"prism_modrinth_index_updated": true,
|
||||
"shared_client_server_mrpack": true,
|
||||
"jei": {
|
||||
"source": "30.28.0.191",
|
||||
"target": "30.28.0.193",
|
||||
"modrinth_version_id": "qaCQ7LGO",
|
||||
"packwiz_and_prism_synchronized": true
|
||||
}
|
||||
}
|
||||
}
|
||||
+15
-15
@@ -14,15 +14,15 @@ hash = "41f4f4c88c71d0a23446d5014f7407d55ad477441847099cad24492b032335a0"
|
||||
|
||||
[[files]]
|
||||
file = "mods/ambiance-0.0.0-alpha.14.jar"
|
||||
hash = "733c1b328fffcfc4f692439391b9e54be5c5485fd1c5894f2a94a887b3df2abb"
|
||||
hash = "a4b2a36e046a3095ada58e3dbf5fd30ab7c8ba42eaf2abcf513b38b8cc2f3e4a"
|
||||
|
||||
[[files]]
|
||||
file = "mods/anotherworld-0.0.0-alpha.55.jar"
|
||||
hash = "3e8966d914ca20519181aee22b3af701e64f7c57716bd395b32c134e116dba78"
|
||||
hash = "da7163a34ca71e1c2385a84221cddccc09f0f83d9a00bce0ed6263b19ec519cb"
|
||||
|
||||
[[files]]
|
||||
file = "mods/events-0.0.0-alpha.0.jar"
|
||||
hash = "cb1334425cd7f4972d30148ff80917ddafc3109d58300b73e711d15d7c23869e"
|
||||
hash = "7400d63c905d1c87a5290adaa2fe91305fa1f067827ca8f0610a6ae4e17624da"
|
||||
|
||||
[[files]]
|
||||
file = "mods/fabric-api.pw.toml"
|
||||
@@ -30,8 +30,8 @@ hash = "df032a6b051d0262cf24945c43e0e6b915e8cc1d62af62fe933932e7f35960c4"
|
||||
metafile = true
|
||||
|
||||
[[files]]
|
||||
file = "mods/iliketomoveit-0.0.0-alpha.14.jar"
|
||||
hash = "8ad908d29ab282f4e2efffb6ee8960c06d6f0f9f6e253baf90ebd8cb62934d11"
|
||||
file = "mods/iliketomoveit-0.0.0-alpha.15.jar"
|
||||
hash = "a084cf25a4afa50086ce220f40b6b3946a06389de105f23c0246df1577deb3c8"
|
||||
|
||||
[[files]]
|
||||
file = "mods/iris.pw.toml"
|
||||
@@ -39,33 +39,33 @@ hash = "84a3cef87679c0d21dceb4641167dc0368b9a3593698f6a3632485e9b3a8d5da"
|
||||
metafile = true
|
||||
|
||||
[[files]]
|
||||
file = "mods/itsalive-0.0.0-alpha.34.jar"
|
||||
hash = "febf235da00540ff365aeb11990b71a790411606b7d24563cb1e333defff01de"
|
||||
file = "mods/itsalive-0.0.0-alpha.35.jar"
|
||||
hash = "4a6433abdfc03a0872f7c810fb17016a0f20acda5e223e0fef479dd786cc73ac"
|
||||
|
||||
[[files]]
|
||||
file = "mods/jei.pw.toml"
|
||||
hash = "a6f7f6eaec2aa5842c54f8c9c4a228b028e99b680eb4e9112291763692027fd4"
|
||||
hash = "a213ae237f9b3934bdcd0d282119d4166d0ccbb7c7223daa8d7ed956c69a0e3f"
|
||||
metafile = true
|
||||
|
||||
[[files]]
|
||||
file = "mods/masterkey-0.0.0-alpha.8.jar"
|
||||
hash = "9fd559ab559fb2ba6d0ae78fa7e48db15462e6f957fb23f1895469435745fe00"
|
||||
hash = "eaf70f6c5a78ee7ab2d3be498d4f7d67fcba433ed61b65b305e958f03b387d94"
|
||||
|
||||
[[files]]
|
||||
file = "mods/onlyfun-0.0.0-alpha.20.jar"
|
||||
hash = "043d5c7d864c980329f707f0e16e39d646ea9d346aad1c8853faf42fefefaaa8"
|
||||
file = "mods/onlyfun-0.0.0-alpha.21.jar"
|
||||
hash = "7e319a0cc6149d5819ef2359fe3ad4035066a9cddd5ae0a16c12c9db64bc3f6a"
|
||||
|
||||
[[files]]
|
||||
file = "mods/ouch-0.0.0-alpha.9.jar"
|
||||
hash = "75d64d7260ea351576ad678d1eedde9a65b70e0b208b98abe48f6df345a205d8"
|
||||
hash = "f3cf2d8c83afa32b1c70e8239dd514be3c74834397f9d09efe177bc8c5ef9ac4"
|
||||
|
||||
[[files]]
|
||||
file = "mods/redstoner-0.0.0-alpha.10.jar"
|
||||
hash = "fed903b6faad120f5018997bb017b759c0be29013e130db9b9c68492b1440616"
|
||||
hash = "a9049b9d3a323015545194c92ce9da7a5dca4ff9a28ea26b02bead96748328ab"
|
||||
|
||||
[[files]]
|
||||
file = "mods/sanctuary-0.0.0-alpha.117.jar"
|
||||
hash = "293032d52220f77f13c4795f6c5293c73e7e7b74c5186d2c56284e5cfe5b228b"
|
||||
file = "mods/sanctuary-0.0.0-alpha.118.jar"
|
||||
hash = "1a1319e5f09de941ad8a7561aff9d2fb8c7460a8cd8d2374817d8f39c5682030"
|
||||
|
||||
[[files]]
|
||||
file = "mods/simple-voice-chat.pw.toml"
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
name = "Just Enough Items (JEI)"
|
||||
filename = "jei-26.2-fabric-30.28.0.191.jar"
|
||||
filename = "jei-26.2-fabric-30.28.0.193.jar"
|
||||
side = "both"
|
||||
pin = true
|
||||
|
||||
[download]
|
||||
url = "https://cdn.modrinth.com/data/u6dRKJwZ/versions/FQUVLzeD/jei-26.2-fabric-30.28.0.191.jar"
|
||||
url = "https://cdn.modrinth.com/data/u6dRKJwZ/versions/qaCQ7LGO/jei-26.2-fabric-30.28.0.193.jar"
|
||||
hash-format = "sha512"
|
||||
hash = "aa7dca89026fea2293a2e25fa4a84faf0eeaa6407b7e84991e69626ff1ee0f9df9ba3a5e096cbf631d08842693b115653c5c8373e5ef3953318a892dbdc0a33d"
|
||||
hash = "0869f4d5840a732a04bedc7684b40140f087f3d759066c206d9d1ec710007f7c9585e4f5ebb04f10e56bef1cf2eddc6978bb2b2c93541cff5fcf7e1609859468"
|
||||
|
||||
[update]
|
||||
[update.modrinth]
|
||||
mod-id = "u6dRKJwZ"
|
||||
version = "FQUVLzeD"
|
||||
version = "qaCQ7LGO"
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
name = "Sanctuary 26.2"
|
||||
author = "KOKA99CAB"
|
||||
version = "26.2.0-alpha.209"
|
||||
version = "26.2.0-alpha.210"
|
||||
pack-format = "packwiz:1.1.0"
|
||||
|
||||
[index]
|
||||
file = "index.toml"
|
||||
hash-format = "sha256"
|
||||
hash = "546e7fd1111ca64d1c7c7d158dc8607c1bcc269ca87d546414282fdf511b5886"
|
||||
hash = "e20c051f8895ce3164ec72b7764b8904f411e36f63bf1e046b1bcc9108d00a31"
|
||||
|
||||
[versions]
|
||||
fabric = "0.19.3"
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"game": "minecraft",
|
||||
"name": "Sanctuary 26.2",
|
||||
"summary": "Sanctuary 26.2 — progression, exploration, worldgen and multiplayer systems by KOKA99CAB.",
|
||||
"versionId": "26.2.0-alpha.209",
|
||||
"versionId": "26.2.0-alpha.210",
|
||||
"files": [
|
||||
{
|
||||
"path": "mods/fabric-api-0.158.0+26.2.jar",
|
||||
@@ -21,19 +21,19 @@
|
||||
"fileSize": 2535765
|
||||
},
|
||||
{
|
||||
"path": "mods/jei-26.2-fabric-30.28.0.191.jar",
|
||||
"path": "mods/jei-26.2-fabric-30.28.0.193.jar",
|
||||
"hashes": {
|
||||
"sha1": "35353e18b1cf2a61339463447fce1f3e2f716b80",
|
||||
"sha512": "aa7dca89026fea2293a2e25fa4a84faf0eeaa6407b7e84991e69626ff1ee0f9df9ba3a5e096cbf631d08842693b115653c5c8373e5ef3953318a892dbdc0a33d"
|
||||
"sha1": "494f9dc787d6c9cd9d32793da2784ab16834faf3",
|
||||
"sha512": "0869f4d5840a732a04bedc7684b40140f087f3d759066c206d9d1ec710007f7c9585e4f5ebb04f10e56bef1cf2eddc6978bb2b2c93541cff5fcf7e1609859468"
|
||||
},
|
||||
"env": {
|
||||
"client": "required",
|
||||
"server": "required"
|
||||
},
|
||||
"downloads": [
|
||||
"https://cdn.modrinth.com/data/u6dRKJwZ/versions/FQUVLzeD/jei-26.2-fabric-30.28.0.191.jar"
|
||||
"https://cdn.modrinth.com/data/u6dRKJwZ/versions/qaCQ7LGO/jei-26.2-fabric-30.28.0.193.jar"
|
||||
],
|
||||
"fileSize": 1822520
|
||||
"fileSize": 1822836
|
||||
},
|
||||
{
|
||||
"path": "mods/voicechat-fabric-2.6.22+26.2.jar",
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"schema_version": 1,
|
||||
"pack_version": "26.2.0-alpha.209",
|
||||
"pack_version": "26.2.0-alpha.210",
|
||||
"release_channel": "alpha",
|
||||
"minecraft_version": "26.2",
|
||||
"fabric_loader_version": "0.19.3",
|
||||
@@ -9,7 +9,7 @@
|
||||
"project_id": "FgOqyAXd",
|
||||
"project_slug": "sanctuary-koka99",
|
||||
"project_url": "https://modrinth.com/modpack/sanctuary-koka99",
|
||||
"primary_file": "sanctuary-26.2.0-alpha.209.mrpack",
|
||||
"primary_file": "sanctuary-26.2.0-alpha.210.mrpack",
|
||||
"shared_client_server_pack": true,
|
||||
"update_owner": "packwiz-installer",
|
||||
"check_on_launch": true,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user