release: Sanctuary 26.2.0-alpha.196 #1

Merged
koka merged 5 commits from release/26.2.0-alpha.196 into main 2026-08-26 21:11:57 +00:00
41 changed files with 1010 additions and 167 deletions
+15
View File
@@ -6,6 +6,21 @@ 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.
## `26.2.0-alpha.196`
Modules modifiés : **It's Alive `0.0.0-alpha.28`**, **I Like To Move It `0.0.0-alpha.13`** et **Sanctuary `0.0.0-alpha.112`**. Les sept autres modules restent inchangés.
- donne au vin une texture bordeaux distincte sans changer son identifiant ;
- détruit le contenu interne d'une presse alimentaire cassée, tout en laissant tomber le bloc de presse, afin de supprimer la duplication des bols et fioles pendant une extraction ;
- transforme directement un bloc de neuf olives en neuf portions d'huile récupérables avec des fioles et retire le second pressage manuel de la pâte d'olive ;
- expose l'état de la presse, du moulin, de la poêle, de la marmite, du four et du tonneau aux comparateurs, de 0 pour vide à 15 pour un résultat prêt ;
- notifie les observateurs lors des changements visibles de niveau, de mélange ou d'allumage, comme les blocs vanilla, sans impulsion à chaque tick interne ;
- éjecte en fontaine les contenants rendus par les recettes de marmite et verrouille brièvement les nouvelles insertions pendant leur restitution ;
- rend la boîte aux lettres orientable dans les quatre directions horizontales ;
- ajoute au grappin une prédiction locale fondée sur la même physique que le serveur et une réconciliation bornée, sans retirer l'autorité serveur ;
- porte le protocole I Like To Move It de 4 à 5 pour l'état borné du grappin, sans migration de sauvegarde ni changement d'identifiant persistant ;
- publie l'index Packwiz alpha.196 et actualise JEI de `30.25.0.177` vers `30.26.0.182`.
## `26.2.0-alpha.195`
Modules modifiés : **Only Fun `0.0.0-alpha.19`** et **Sanctuary `0.0.0-alpha.111`**. Les huit autres modules restent inchangés.
+6 -4
View File
@@ -22,13 +22,15 @@ 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 local : **`26.2.0-alpha.195`**. Only Fun passe en **`0.0.0-alpha.19`** et Sanctuary en **`0.0.0-alpha.111`** ; les huit autres modules restent inchangés.
Version courante du pack local : **`26.2.0-alpha.196`**. It's Alive passe en **`0.0.0-alpha.28`**, I Like To Move It en **`0.0.0-alpha.13`** et Sanctuary en **`0.0.0-alpha.112`** ; les sept autres modules restent inchangés.
Lalpha.196 fiabilise les postes culinaires : la presse détruit son contenu si elle est cassée, lhuile dolive se récupère directement en fioles et les six machines exposent leur état au comparateur. La marmite éjecte les contenants consommés avant daccepter un nouvel ingrédient, et le vin possède une teinte bordeaux distincte. La boîte aux lettres devient orientable horizontalement. Enfin, le grappin conserve son autorité serveur tout en ajoutant une prédiction locale et une réconciliation bornée pour supprimer les à-coups multijoueur.
Lalpha.195 charge le catalogue Shop depuis le CSV rééquilibré et conserve les contrôles serveur sur chaque offre. Le Magic Cloud et le Magic Carpet restent catalogués avec leur prix futur, mais sont invisibles et impossibles à acheter tant que les boss correspondants et leur état persistant ne sont pas implémentés.
Lalpha.194 ouvre la cuisine avec huit cultures supplémentaires — riz, soja, haricot, pois chiche, piment, basilic, menthe et coriandre — ainsi que le café, la vanille, lolivier et le citronnier. Le riz pousse réellement immergé sur de la boue, le haricot se récolte jeune ou sec, le café se torréfie et la vanille se sèche au smoker. Le climat détermine seulement la découverte : les nouvelles plantes sauvages apparaissent dans les nouveaux chunks, tandis quune chance rare de semence sur la végétation permet de continuer un monde existant. Aucune réinitialisation de sauvegarde nest requise.
Le [wiki cuisine de lalpha.194](itsalive/FOOD_WIKI.md) détaille les cultures, les six postes, lautomatisation, les ingrédients transformés, le livre et ses 64 recettes.
Le [wiki cuisine de lalpha.196](itsalive/FOOD_WIKI.md) détaille les cultures, les six postes, lautomatisation, les ingrédients transformés, le livre et ses 64 recettes.
Lalpha.193 donne à chaque item du Shop une petite variation de prix stable et reproductible, tout en réservant des tarifs explicites aux vrais objets exceptionnels. L’œuf doré, porte dentrée extrêmement rare vers les poules à gemmes, coûte désormais 811 rubis ou 59 saphirs dans le catalogue. Les six emplacements verrouillés occupent directement les six cartes restantes de la grille : ils se débloquent toujours dans lordre pour 2, 4, 8, 12, 18 puis 25 niveaux. Laide de boîte aux lettres remonte légèrement et la bourse rubis/saphirs sancre en bas à droite.
@@ -188,7 +190,7 @@ Le détail de chaque publication est conservé dans le [journal des changements]
- `Mod.id(path)` est l'unique fabrique locale d'identifiants.
- Les anciennes sauvegardes gardent leurs IDs historiques pendant la migration. Tout changement futur exige une table de migration explicite ; il ne faut pas réenregistrer silencieusement un contenu sous un nouvel ID.
- Chaque module possède un seul hub `registry/*Registries`. Une feature ajoute son registre à ce hub, sans effet de bord dispersé dans des initialisations statiques.
- Les payloads utilisent un namespace stable. Chaque module expose `PROTOCOL_VERSION`; Red-Stoner utilise le protocole 3 depuis l'intégration de Canon, I Like To Move It le protocole 4 et Only Fun conserve le protocole 2.
- Les payloads utilisent un namespace stable. Chaque module expose `PROTOCOL_VERSION`; Red-Stoner utilise le protocole 3 depuis l'intégration de Canon, I Like To Move It le protocole 5 et Only Fun conserve le protocole 2.
- Les données monde appartiennent au module conceptuellement responsable. Elles sont chargées paresseusement, utilisent une clé stable et sérialisent toujours `data_version`.
- Une migration de données est monotone et marque la donnée comme modifiée. Une donnée créée par une version plus récente est signalée sans être réécrite à l'aveugle.
- Chaque log porte le logger du module et un préfixe public identifiable. Le niveau debug de MasterKey est persistant mais ne modifie pas globalement la configuration SLF4J.
@@ -317,7 +319,7 @@ La donnée globale v1 `iliketomoveit:portal_links` affecte un UUID à chaque moi
## Grappin et poulies
I Like To Move It possède désormais la migration complète de Spider-Man sous les IDs `iliketomoveit:grappling_hand`, `iliketomoveit:grappling_rod`, `iliketomoveit:grappling_hook` et `iliketomoveit:pulley_head`. Le grappin conserve sa charge, sa portée, son balancement autoritaire côté serveur, ses commandes de rappel/descente et la restitution de l'élan au lâcher. La main se fabrique, tandis qu'une seconde peut aussi apparaître dans les coffres de la cité perdue de Portals avec une probabilité de 12 %.
I Like To Move It possède désormais la migration complète de Spider-Man sous les IDs `iliketomoveit:grappling_hand`, `iliketomoveit:grappling_rod`, `iliketomoveit:grappling_hook` et `iliketomoveit:pulley_head`. Le grappin conserve sa charge, sa portée, son balancement autoritaire côté serveur, ses commandes de rappel/descente et la restitution de l'élan au lâcher. En multijoueur, le client prédit le même pendule puis se réconcilie avec des états serveur bornés ; le serveur reste la source de vérité. La main se fabrique, tandis qu'une seconde peut aussi apparaître dans les coffres de la cité perdue de Portals avec une probabilité de 12 %.
Deux têtes de poulie peuvent être liées jusqu'à 128 blocs afin de créer une tyrolienne. La traversée vérifie les collisions, transporte le joueur à vitesse constante et émet un signal redstone de 15 pendant le passage : Red-Stoner pourra donc automatiser la mécanique plus tard sans en devenir propriétaire. Another World reste propriétaire des seuls matériaux éventuellement employés dans les recettes.
+53 -9
View File
@@ -3427,8 +3427,8 @@ 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.195"
|| rootProject.sanctuary_version != "0.0.0-alpha.111"
if (rootProject.pack_version != "26.2.0-alpha.196"
|| rootProject.sanctuary_version != "0.0.0-alpha.112"
|| rootProject.onlyfun_version != "0.0.0-alpha.19"
|| 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"]
@@ -3477,7 +3477,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.195"
if (rootProject.pack_version != "26.2.0-alpha.196"
|| rootProject.anotherworld_version != "0.0.0-alpha.49"
|| rootProject.redstoner_version != "0.0.0-alpha.10"
|| release.pack_version != rootProject.pack_version || index.versionId != rootProject.pack_version
@@ -4121,9 +4121,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.195"
if (rootProject.pack_version != "26.2.0-alpha.196"
|| rootProject.onlyfun_version != "0.0.0-alpha.19"
|| rootProject.sanctuary_version != "0.0.0-alpha.111"
|| rootProject.sanctuary_version != "0.0.0-alpha.112"
|| 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"
@@ -4166,8 +4166,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.195"
|| rootProject.itsalive_version != "0.0.0-alpha.27"
if (rootProject.pack_version != "26.2.0-alpha.196"
|| rootProject.itsalive_version != "0.0.0-alpha.28"
|| 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"]
@@ -4215,9 +4215,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.195"
if (rootProject.pack_version != "26.2.0-alpha.196"
|| rootProject.onlyfun_version != "0.0.0-alpha.19"
|| rootProject.sanctuary_version != "0.0.0-alpha.111"
|| rootProject.sanctuary_version != "0.0.0-alpha.112"
|| 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"]
@@ -4248,6 +4248,50 @@ tasks.named("verifyWorkspace") {
dependsOn tasks.named("verifyShopCsvBossGatesRelease")
}
tasks.register("verifyCulinaryMailboxGrappleFixesRelease") {
group = "verification"
description = "Checks the complete Sanctuary 26.2.0-alpha.196 reliability release contract."
inputs.file(file("pack/migrations/26.2.0-alpha.195-culinary-mailbox-grapple-fixes-to-alpha.196.json"))
inputs.file(file("pack/release.json"))
inputs.file(file("pack/prism/modrinth.index.json"))
inputs.file(file("pack/packwiz/pack.toml"))
doLast {
def migration = new JsonSlurper().parse(file(
"pack/migrations/26.2.0-alpha.195-culinary-mailbox-grapple-fixes-to-alpha.196.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
if (rootProject.pack_version != "26.2.0-alpha.196"
|| migration.source?.pack_version != "26.2.0-alpha.195"
|| migration.target?.pack_version != "26.2.0-alpha.196"
|| migration.target?.modules != [
itsalive: "0.0.0-alpha.28",
iliketomoveit: "0.0.0-alpha.13",
sanctuary: "0.0.0-alpha.112"]
|| migration.culinary_fixes?.olive_block_output?.count != 9
|| migration.culinary_fixes?.comparator_contract?.blocks?.size() != 6
|| migration.culinary_fixes?.observer_contract?.behavior != "vanilla_block_state_changes"
|| migration.culinary_fixes?.observer_contract?.inventory_or_progress_ticks != false
|| migration.mailbox?.horizontal_facing != true
|| migration.grappling?.payload_fixed_size_and_bounded != true
|| migration.grappling?.server_authoritative_collisions != true
|| migration.distribution?.packwiz_index_refreshed != true
|| 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.196"
|| release.modrinth?.primary_file != "sanctuary-26.2.0-alpha.196.mrpack"
|| prism.versionId != "26.2.0-alpha.196"
|| !packwiz.contains('version = "26.2.0-alpha.196"')) {
throw new GradleException("The alpha.196 culinary, mailbox and grapple release contract is incomplete")
}
}
}
tasks.named("verifyWorkspace") {
dependsOn tasks.named("verifyCulinaryMailboxGrappleFixesRelease")
}
tasks.named("check") {
dependsOn tasks.named("verifyWorkspace")
dependsOn tasks.named("verifyTransport")
+4 -4
View File
@@ -18,17 +18,17 @@ redstoner_version=0.0.0-alpha.10
redstoner_lifecycle=active
anotherworld_version=0.0.0-alpha.49
anotherworld_lifecycle=active
itsalive_version=0.0.0-alpha.27
itsalive_version=0.0.0-alpha.28
itsalive_lifecycle=active
ouch_version=0.0.0-alpha.8
ouch_lifecycle=active
masterkey_version=0.0.0-alpha.8
masterkey_lifecycle=active
iliketomoveit_version=0.0.0-alpha.12
iliketomoveit_version=0.0.0-alpha.13
iliketomoveit_lifecycle=active
onlyfun_version=0.0.0-alpha.19
onlyfun_lifecycle=active
sanctuary_version=0.0.0-alpha.111
sanctuary_version=0.0.0-alpha.112
sanctuary_lifecycle=active
pack_version=26.2.0-alpha.195
pack_version=26.2.0-alpha.196
maven_group=fr.koka99cab.sanctuary26
+55 -6
View File
@@ -1,5 +1,18 @@
import groovy.json.JsonSlurper
tasks.register("grapplePhysicsSmoke", JavaExec) {
group = "verification"
description = "Checks deterministic shared grappling physics and client rope projection."
classpath = sourceSets.test.runtimeClasspath
mainClass = "fr.koka99cab.sanctuary26.iliketomoveit.grapple.GrappleSwingPhysicsSmoke"
dependsOn tasks.named("testClasses")
}
tasks.named("test") {
// The physics contract is an executable smoke task, not a JUnit engine test.
failOnNoDiscoveredTests = false
}
tasks.register("verifyWaystones") {
group = "verification"
description = "Checks the dimension-local Waystone migration, direct elevator controls, persistence, networking and Atlas."
@@ -60,10 +73,10 @@ tasks.register("verifyWaystones") {
def manifest = new JsonSlurper().parse(file("src/main/resources/fabric.mod.json"))
if (manifest.custom?.sanctuary26?.data_version != 3
|| manifest.custom?.sanctuary26?.network_protocol != 4
|| manifest.custom?.sanctuary26?.network_protocol != 5
|| manifest.depends?.anotherworld != ">=0.0.0-alpha.46"
|| manifest.depends?.sanctuary != ">=0.0.0-alpha.21") {
throw new GradleException("I Like To Move It must expose data v3, protocol 4 and its Another World alpha.46/Sanctuary alpha.21 dependencies")
throw new GradleException("I Like To Move It must expose data v3, protocol 5 and its Another World alpha.46/Sanctuary alpha.21 dependencies")
}
["fr_fr", "en_us", "ru_ru"].each { language ->
def translations = new JsonSlurper().parse(file("src/main/resources/assets/iliketomoveit/lang/${language}.json"))
@@ -97,6 +110,9 @@ tasks.register("verifyGrappling") {
'class GrapplingRodItem',
'class GrapplingHookEntity',
'class GrappleController',
'class GrappleSwingPhysics',
'class ClientGrapplePrediction',
'id("s2c/grapple_state")',
'MAX_CHARGED_FLIGHT_DISTANCE = 120.0D',
'MAX_SESSION_DISTANCE = 152.0D',
'MAX_SWING_SPEED = 6.5D',
@@ -158,8 +174,8 @@ 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" }
|| manifest.custom?.sanctuary26?.data_version != 3
|| manifest.custom?.sanctuary26?.network_protocol != 4) {
throw new GradleException("Grappling must keep I Like To Move It aggregate data v3/protocol 4 and register its client mixin")
|| 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")
}
["fr_fr", "en_us", "ru_ru"].each { language ->
def translations = new JsonSlurper().parse(file("src/main/resources/assets/iliketomoveit/lang/${language}.json"))
@@ -267,7 +283,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.12"
if (project.version.toString() != "0.0.0-alpha.13"
|| atlasCavernsMigration.target?.pack_version != "26.2.0-alpha.150"
|| atlasCavernsMigration.target?.modules?.iliketomoveit != "0.0.0-alpha.9"
|| atlasCavernsMigration.portals?.active_routes != [
@@ -303,7 +319,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.12"
if (project.version.toString() != "0.0.0-alpha.13"
|| recipe.pattern != ["PGP", "GCG", "PBP"]
|| recipe.key != [B: "anotherworld:silver_block", C: "anotherworld:aircraft_cockpit",
G: "anotherworld:silver_gear", P: "anotherworld:silver_plate"]
@@ -318,9 +334,42 @@ tasks.register("verifySilverAircraft") {
}
}
tasks.register("verifyGrapplePrediction") {
group = "verification"
description = "Checks the alpha.13 bounded server-authoritative grapple prediction protocol."
inputs.file(rootProject.file("pack/migrations/26.2.0-alpha.195-culinary-mailbox-grapple-fixes-to-alpha.196.json"))
inputs.file(file("src/main/java/fr/koka99cab/sanctuary26/iliketomoveit/network/GrappleStatePayload.java"))
inputs.file(file("src/main/java/fr/koka99cab/sanctuary26/iliketomoveit/grapple/GrappleController.java"))
inputs.file(file("src/main/java/fr/koka99cab/sanctuary26/iliketomoveit/grapple/GrappleSwingPhysics.java"))
inputs.file(file("src/main/java/fr/koka99cab/sanctuary26/iliketomoveit/client/grapple/ClientGrapplePrediction.java"))
doLast {
def migration = new JsonSlurper().parse(rootProject.file(
"pack/migrations/26.2.0-alpha.195-culinary-mailbox-grapple-fixes-to-alpha.196.json"))
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.13"
|| 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
|| migration.compatibility?.iliketomoveit_target_network_protocol != 5
|| !payload.contains('id("s2c/grapple_state")')
|| !payload.contains("MAX_COORDINATE = 30_000_000.0D")
|| !payload.contains("MAX_VELOCITY = 12.0D")
|| !controller.contains("GrappleSwingPhysics.step")
|| !controller.contains("syncPrediction(player, session)")
|| !prediction.contains("HARD_DIVERGENCE_SQ")
|| !prediction.contains("GrappleSwingPhysics.step")) {
throw new GradleException("I Like To Move It alpha.13 grapple prediction contract is incomplete")
}
}
}
tasks.named("check") {
dependsOn tasks.named("grapplePhysicsSmoke")
dependsOn tasks.named("verifyWaystones")
dependsOn tasks.named("verifyGrappling")
dependsOn tasks.named("verifyGrapplePrediction")
dependsOn tasks.named("verifyPortals")
dependsOn tasks.named("verifySilverAircraft")
}
@@ -3,6 +3,7 @@ package fr.koka99cab.sanctuary26.iliketomoveit.client;
import fr.koka99cab.sanctuary26.iliketomoveit.ILikeToMoveItMod;
import fr.koka99cab.sanctuary26.iliketomoveit.client.gui.AircraftEngineScreen;
import fr.koka99cab.sanctuary26.iliketomoveit.client.gui.WaystoneScreen;
import fr.koka99cab.sanctuary26.iliketomoveit.client.grapple.ClientGrapplePrediction;
import fr.koka99cab.sanctuary26.iliketomoveit.client.item.GrapplingRodCastProperty;
import fr.koka99cab.sanctuary26.iliketomoveit.client.model.AircraftModel;
import fr.koka99cab.sanctuary26.iliketomoveit.client.model.MagicCloudModel;
@@ -14,6 +15,7 @@ import fr.koka99cab.sanctuary26.iliketomoveit.client.sound.MagicCloudSoundManage
import fr.koka99cab.sanctuary26.iliketomoveit.client.waystone.ClientWaystones;
import fr.koka99cab.sanctuary26.iliketomoveit.entity.AircraftEntity;
import fr.koka99cab.sanctuary26.iliketomoveit.network.AircraftControlPayload;
import fr.koka99cab.sanctuary26.iliketomoveit.network.GrappleStatePayload;
import fr.koka99cab.sanctuary26.iliketomoveit.network.WaystoneOpenPayload;
import fr.koka99cab.sanctuary26.iliketomoveit.network.WaystoneSyncPayload;
import fr.koka99cab.sanctuary26.iliketomoveit.registry.ILikeToMoveItEntityTypes;
@@ -57,10 +59,16 @@ public final class ILikeToMoveItClient implements ClientModInitializer {
payload.elevatorFloorCount(), payload.destinations()))));
ClientPlayNetworking.registerGlobalReceiver(WaystoneSyncPayload.ID, (payload, context) ->
context.client().execute(() -> ClientWaystones.update(payload)));
ClientPlayConnectionEvents.DISCONNECT.register((handler, client) -> ClientWaystones.clear());
ClientPlayNetworking.registerGlobalReceiver(GrappleStatePayload.ID, (payload, context) ->
context.client().execute(() -> ClientGrapplePrediction.update(context.client(), payload)));
ClientPlayConnectionEvents.DISCONNECT.register((handler, client) -> {
ClientWaystones.clear();
ClientGrapplePrediction.clear();
});
SanctuaryAtlasMarkers.register(ILikeToMoveItMod.id("waystones"), ClientWaystones::atlasMarkers);
ClientTickEvents.END_CLIENT_TICK.register(client -> {
if (client.player == null) return;
ClientGrapplePrediction.tick(client);
Entity vehicle = client.player.getVehicle();
if (vehicle instanceof AircraftEntity aircraft) {
boolean throttle = client.options.keyJump.isDown();
@@ -0,0 +1,147 @@
package fr.koka99cab.sanctuary26.iliketomoveit.client.grapple;
import fr.koka99cab.sanctuary26.iliketomoveit.grapple.GrappleSwingPhysics;
import fr.koka99cab.sanctuary26.iliketomoveit.network.GrappleStatePayload;
import net.minecraft.client.Minecraft;
import net.minecraft.client.player.LocalPlayer;
import net.minecraft.world.entity.MoverType;
import net.minecraft.world.entity.player.Input;
import net.minecraft.world.phys.Vec3;
/** Local rope constraint that prevents vanilla movement prediction fighting the server pendulum. */
public final class ClientGrapplePrediction {
private static final double PLAYER_CENTER_FACTOR = 0.5D;
private static final double REEL_IN_SPEED = 0.18D;
private static final double LOWER_ROPE_SPEED = 0.22D;
private static final double MIN_ROPE_LENGTH = 1.0D;
private static final int PACKET_TIMEOUT_TICKS = 10;
private static final double HARD_DIVERGENCE_SQ = 36.0D;
private static int sessionId = -1;
private static int sequence = -1;
private static boolean anchored;
private static boolean ropeTaut;
private static Vec3 anchor = Vec3.ZERO;
private static Vec3 authoritativeCenter = Vec3.ZERO;
private static Vec3 swingVelocity = Vec3.ZERO;
private static double ropeLength;
private static double maxRopeLength;
private static int ticksWithoutPacket;
private ClientGrapplePrediction() {
}
public static void update(Minecraft client, GrappleStatePayload payload) {
if (!payload.active()) {
// A stop for an older hook must not cancel a newer session if the client
// drains both packets during the same frame.
if (payload.sessionId() == sessionId && payload.sequence() > sequence) {
clear();
}
return;
}
if (payload.sessionId() == sessionId && payload.sequence() <= sequence) {
return;
}
boolean newSession = payload.sessionId() != sessionId;
sessionId = payload.sessionId();
sequence = payload.sequence();
anchored = payload.anchored();
ropeTaut = payload.ropeTaut();
anchor = payload.anchor();
authoritativeCenter = payload.center();
ropeLength = newSession ? payload.ropeLength() : lerp(ropeLength, payload.ropeLength(), 0.35D);
maxRopeLength = payload.maxRopeLength();
swingVelocity = newSession
? payload.velocity()
: swingVelocity.lerp(payload.velocity(), 0.2D);
ticksWithoutPacket = 0;
if (client.player != null && anchored && ropeTaut) {
Vec3 center = playerCenter(client.player);
if (center.distanceToSqr(authoritativeCenter) > HARD_DIVERGENCE_SQ) {
client.player.setPos(
authoritativeCenter.x,
authoritativeCenter.y - client.player.getBbHeight() * PLAYER_CENTER_FACTOR,
authoritativeCenter.z
);
client.player.setDeltaMovement(payload.velocity());
swingVelocity = payload.velocity();
}
}
}
public static void tick(Minecraft client) {
if (sessionId < 0 || client.player == null || client.level == null) {
return;
}
if (++ticksWithoutPacket > PACKET_TIMEOUT_TICKS) {
clear();
return;
}
if (!anchored || !ropeTaut) {
return;
}
boolean reel = client.options.keyJump.isDown();
boolean lower = client.options.keyShift.isDown();
if (reel && !lower) {
ropeLength = Math.max(MIN_ROPE_LENGTH, ropeLength - REEL_IN_SPEED);
} else if (lower && !reel) {
ropeLength = Math.min(maxRopeLength, ropeLength + LOWER_ROPE_SPEED);
}
LocalPlayer player = client.player;
Vec3 center = playerCenter(player);
Vec3 constrainedCenter = GrappleSwingPhysics.constrainCenter(anchor, ropeLength, center);
Vec3 radialCorrection = constrainedCenter.subtract(center);
if (radialCorrection.lengthSqr() > 1.0E-6D) {
player.move(MoverType.SELF, radialCorrection);
constrainedCenter = playerCenter(player);
}
GrappleSwingPhysics.Step predicted = GrappleSwingPhysics.step(
anchor, ropeLength, constrainedCenter, swingVelocity, moveIntent(player));
swingVelocity = predicted.velocity();
player.setDeltaMovement(swingVelocity);
player.resetFallDistance();
}
public static void clear() {
sessionId = -1;
sequence = -1;
anchored = false;
ropeTaut = false;
anchor = Vec3.ZERO;
authoritativeCenter = Vec3.ZERO;
swingVelocity = Vec3.ZERO;
ropeLength = 0.0D;
maxRopeLength = 0.0D;
ticksWithoutPacket = 0;
}
private static Vec3 playerCenter(LocalPlayer player) {
return player.position().add(0.0D, player.getBbHeight() * PLAYER_CENTER_FACTOR, 0.0D);
}
/** Mirrors ServerPlayer#getLastClientMoveIntent from the local key state. */
private static Vec3 moveIntent(LocalPlayer player) {
Input input = player.input.keyPresses;
double strafe = input.left() == input.right() ? 0.0D : input.left() ? 1.0D : -1.0D;
double forward = input.forward() == input.backward() ? 0.0D : input.forward() ? 1.0D : -1.0D;
Vec3 local = new Vec3(strafe, 0.0D, forward);
if (local.lengthSqr() < 1.0E-7D) {
return Vec3.ZERO;
}
if (local.lengthSqr() > 1.0D) {
local = local.normalize();
}
double yaw = player.getYRot() * Math.PI / 180.0D;
double sine = Math.sin(yaw);
double cosine = Math.cos(yaw);
return new Vec3(local.x * cosine - local.z * sine, 0.0D,
local.z * cosine + local.x * sine);
}
private static double lerp(double start, double end, double factor) {
return start + (end - start) * factor;
}
}
@@ -1,12 +1,14 @@
package fr.koka99cab.sanctuary26.iliketomoveit.grapple;
import fr.koka99cab.sanctuary26.iliketomoveit.entity.GrapplingHookEntity;
import fr.koka99cab.sanctuary26.iliketomoveit.network.GrappleStatePayload;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import net.fabricmc.fabric.api.event.lifecycle.v1.ServerTickEvents;
import net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking;
import net.minecraft.core.BlockPos;
import net.minecraft.resources.ResourceKey;
import net.minecraft.server.level.ServerLevel;
@@ -47,7 +49,6 @@ public final class GrappleController {
private static final double COLLISION_MISMATCH_TOLERANCE_SQ = 1.0E-4D;
private static final double WALL_RELEASE_HORIZONTAL_DAMPING = 0.25D;
private static final double WALL_RELEASE_DROP_SPEED = 0.18D;
private static final double SWING_PUMP = 0.055D;
private static final double REEL_IN_SPEED = 0.18D;
private static final double REEL_SPEED_FROM_SWING_SPEED = 0.035D;
private static final double MAX_DYNAMIC_REEL_IN_SPEED = 0.46D;
@@ -55,13 +56,10 @@ public final class GrappleController {
private static final double STUCK_REEL_IN_SPEED = 0.24D;
private static final double STUCK_SPEED_SQ = 0.018D;
private static final int STUCK_REEL_DELAY = 8;
private static final double PENDULUM_GRAVITY = 0.08D;
private static final double SWING_DAMPING = 0.998D;
private static final double FALL_INERTIA_TO_SWING = 1.22D;
private static final double CATCH_LIFT_FROM_SPEED = 0.18D;
private static final double CATCH_LIFT_FROM_FALL = 0.22D;
private static final double MAX_CATCH_LIFT = 0.72D;
private static final double MAX_SWING_SPEED = 6.5D;
private static final double PLAYER_CENTER_FACTOR = 0.5D;
private static final double RELEASE_BOOST_SPEED = 0.85D;
private static final double RELEASE_BOOST_FROM_SWING_SPEED = 0.12D;
@@ -113,7 +111,10 @@ public final class GrappleController {
GrapplingHookEntity hook = new GrapplingHookEntity(level, player, startPosition, velocity, maxFlightDistance, maxFlightTicks);
level.addFreshEntity(hook);
ACTIVE.put(player.getUUID(), new GrappleSession(level.dimension(), startPosition, BlockPos.containing(startPosition), hook.getId()));
GrappleSession session = new GrappleSession(
level.dimension(), startPosition, BlockPos.containing(startPosition), hook.getId());
ACTIVE.put(player.getUUID(), session);
syncPrediction(player, session);
stack.hurtAndBreak(1, player, hand);
level.playSound(null, player.getX(), player.getY(), player.getZ(), SoundEvents.FISHING_BOBBER_THROW, SoundSource.PLAYERS, 0.8F, 1.15F);
@@ -145,6 +146,7 @@ public final class GrappleController {
player.resetFallDistance();
levelSound(player, SoundEvents.LEAD_TIED, 0.8F, 1.1F);
syncPrediction(player, session);
}
public static void expire(ServerPlayer player, GrapplingHookEntity hook) {
@@ -169,6 +171,7 @@ public final class GrappleController {
applyReleaseBoost(player, session);
}
stopPrediction(player, session);
discardVisualHook((ServerLevel) player.level(), session);
return true;
}
@@ -192,6 +195,7 @@ public final class GrappleController {
if (!session.anchored) {
tickFlyingHook(player, session, hook);
syncPrediction(player, session);
return;
}
@@ -209,6 +213,7 @@ public final class GrappleController {
session.ropeTaut = false;
session.swingVelocity = player.getDeltaMovement();
session.jumpHeldLastTick = session.manualReel;
syncPrediction(player, session);
return;
}
@@ -220,6 +225,7 @@ public final class GrappleController {
session.ropeTaut = true;
session.jumpHeldLastTick = session.manualReel;
syncPrediction(player, session);
}
private static void tickFlyingHook(ServerPlayer player, GrappleSession session, GrapplingHookEntity hook) {
@@ -271,24 +277,16 @@ public final class GrappleController {
private static boolean simulatePendulum(ServerPlayer player, GrappleSession session, Vec3 center, boolean firstCatch) {
Vec3 radial = center.subtract(session.anchorPosition).normalize();
Vec3 currentCenter = session.anchorPosition.add(radial.scale(session.ropeLength));
Vec3 swingVelocity = firstCatch ? preservedCatchVelocity(player, radial) : projectOntoSwingPlane(session.swingVelocity, radial);
Vec3 gravity = new Vec3(0.0D, -PENDULUM_GRAVITY, 0.0D);
swingVelocity = swingVelocity.add(projectOntoSwingPlane(gravity, radial));
swingVelocity = swingVelocity.add(swingInput(player, radial)).scale(SWING_DAMPING);
swingVelocity = limitSpeed(swingVelocity);
Vec3 nextCenter = currentCenter.add(swingVelocity);
Vec3 nextRadial = nextCenter.subtract(session.anchorPosition);
if (nextRadial.lengthSqr() < 0.001D) {
nextRadial = radial;
} else {
nextRadial = nextRadial.normalize();
}
Vec3 constrainedCenter = session.anchorPosition.add(nextRadial.scale(session.ropeLength));
Vec3 actualVelocity = limitSpeed(constrainedCenter.subtract(currentCenter));
Vec3 carriedVelocity = firstCatch ? preservedCatchVelocity(player, radial) : session.swingVelocity;
GrappleSwingPhysics.Step step = GrappleSwingPhysics.step(
session.anchorPosition,
session.ropeLength,
center,
carriedVelocity,
player.getLastClientMoveIntent()
);
Vec3 constrainedCenter = step.center();
Vec3 actualVelocity = step.velocity();
session.swingVelocity = actualVelocity;
if (!movePlayerAlongRope(player, constrainedCenter)) {
@@ -298,7 +296,6 @@ public final class GrappleController {
player.setDeltaMovement(actualVelocity);
player.resetFallDistance();
player.hurtMarked = true;
return true;
}
@@ -351,7 +348,8 @@ public final class GrappleController {
private static Vec3 preservedCatchVelocity(ServerPlayer player, Vec3 radial) {
Vec3 incomingVelocity = player.getDeltaMovement();
Vec3 tangentVelocity = projectOntoSwingPlane(incomingVelocity, radial).add(catchLift(radial, incomingVelocity));
Vec3 tangentVelocity = GrappleSwingPhysics.projectOntoSwingPlane(incomingVelocity, radial)
.add(catchLift(radial, incomingVelocity));
double fallSpeed = Math.max(0.0D, -incomingVelocity.y);
double outwardSpeed = Math.max(0.0D, incomingVelocity.dot(radial));
double targetSwingSpeed = Math.max(fallSpeed, outwardSpeed) * FALL_INERTIA_TO_SWING;
@@ -370,7 +368,7 @@ public final class GrappleController {
return Vec3.ZERO;
}
Vec3 upwardTangent = projectOntoSwingPlane(new Vec3(0.0D, 1.0D, 0.0D), radial);
Vec3 upwardTangent = GrappleSwingPhysics.projectOntoSwingPlane(new Vec3(0.0D, 1.0D, 0.0D), radial);
if (upwardTangent.lengthSqr() < 0.001D) {
return Vec3.ZERO;
}
@@ -380,28 +378,19 @@ public final class GrappleController {
return upwardTangent.normalize().scale(lift);
}
private static Vec3 swingInput(ServerPlayer player, Vec3 radial) {
Vec3 tangentIntent = projectOntoSwingPlane(player.getLastClientMoveIntent(), radial);
if (tangentIntent.lengthSqr() < 0.001D) {
return Vec3.ZERO;
}
return tangentIntent.normalize().scale(SWING_PUMP);
}
private static Vec3 catchSwingDirection(ServerPlayer player, Vec3 radial, Vec3 tangentVelocity) {
if (tangentVelocity.lengthSqr() > 0.001D) {
return tangentVelocity.normalize();
}
Vec3 moveIntent = player.getLastClientMoveIntent();
Vec3 tangentIntent = projectOntoSwingPlane(moveIntent, radial);
Vec3 tangentIntent = GrappleSwingPhysics.projectOntoSwingPlane(moveIntent, radial);
if (tangentIntent.lengthSqr() > 0.001D) {
return tangentIntent.normalize();
}
Vec3 look = player.calculateViewVector(player.getXRot(), player.getYRot());
Vec3 tangentLook = projectOntoSwingPlane(look, radial);
Vec3 tangentLook = GrappleSwingPhysics.projectOntoSwingPlane(look, radial);
if (tangentLook.lengthSqr() > 0.001D) {
return tangentLook.normalize();
}
@@ -414,19 +403,6 @@ public final class GrappleController {
return new Vec3(1.0D, 0.0D, 0.0D);
}
private static Vec3 projectOntoSwingPlane(Vec3 direction, Vec3 radial) {
return direction.subtract(radial.scale(direction.dot(radial)));
}
private static Vec3 limitSpeed(Vec3 velocity) {
double speed = velocity.length();
if (speed <= MAX_SWING_SPEED) {
return velocity;
}
return velocity.scale(MAX_SWING_SPEED / speed);
}
private static void applyReleaseBoost(ServerPlayer player, GrappleSession session) {
if (!player.getLastClientInput().sprint() || !canBoostOnRelease(session)) {
return;
@@ -488,6 +464,31 @@ public final class GrappleController {
}
}
private static void syncPrediction(ServerPlayer player, GrappleSession session) {
if (!ServerPlayNetworking.canSend(player, GrappleStatePayload.ID)) {
return;
}
ServerPlayNetworking.send(player, new GrappleStatePayload(
session.visualHookId,
++session.sequence,
true,
session.anchored,
session.ropeTaut,
session.anchorPosition,
session.ropeLength,
session.maxRopeLength,
playerCenter(player),
session.swingVelocity
));
}
private static void stopPrediction(ServerPlayer player, GrappleSession session) {
if (ServerPlayNetworking.canSend(player, GrappleStatePayload.ID)) {
ServerPlayNetworking.send(player,
GrappleStatePayload.stopped(session.visualHookId, ++session.sequence));
}
}
private static void playRetrieveSound(ServerLevel level, ServerPlayer player) {
level.playSound(null, player.getX(), player.getY(), player.getZ(), SoundEvents.LEAD_UNTIED, SoundSource.PLAYERS, 0.9F, 1.0F);
}
@@ -515,6 +516,7 @@ public final class GrappleController {
private boolean manualLower;
private boolean jumpHeldLastTick;
private int stillTicks;
private int sequence;
private GrappleSession(ResourceKey<Level> dimension, Vec3 anchorPosition, BlockPos anchorBlockPos, int visualHookId) {
this.dimension = dimension;
@@ -0,0 +1,61 @@
package fr.koka99cab.sanctuary26.iliketomoveit.grapple;
import net.minecraft.world.phys.Vec3;
/** Deterministic pendulum step shared by the authoritative server and local prediction. */
public final class GrappleSwingPhysics {
public static final double MAX_SWING_SPEED = 6.5D;
private static final double PENDULUM_GRAVITY = 0.08D;
private static final double SWING_DAMPING = 0.998D;
private static final double SWING_PUMP = 0.055D;
private GrappleSwingPhysics() {
}
public static Step step(
Vec3 anchor,
double ropeLength,
Vec3 center,
Vec3 carriedVelocity,
Vec3 inputIntent
) {
Vec3 radial = center.subtract(anchor);
if (radial.lengthSqr() < 0.001D || ropeLength <= 0.0D) {
return new Step(center, Vec3.ZERO);
}
radial = radial.normalize();
Vec3 currentCenter = anchor.add(radial.scale(ropeLength));
Vec3 swingVelocity = projectOntoSwingPlane(carriedVelocity, radial);
Vec3 gravity = new Vec3(0.0D, -PENDULUM_GRAVITY, 0.0D);
swingVelocity = swingVelocity.add(projectOntoSwingPlane(gravity, radial));
Vec3 tangentIntent = projectOntoSwingPlane(inputIntent, radial);
if (tangentIntent.lengthSqr() >= 0.001D) {
swingVelocity = swingVelocity.add(tangentIntent.normalize().scale(SWING_PUMP));
}
swingVelocity = limitSpeed(swingVelocity.scale(SWING_DAMPING));
Vec3 nextRadial = currentCenter.add(swingVelocity).subtract(anchor);
nextRadial = nextRadial.lengthSqr() < 0.001D ? radial : nextRadial.normalize();
Vec3 constrainedCenter = anchor.add(nextRadial.scale(ropeLength));
return new Step(constrainedCenter, limitSpeed(constrainedCenter.subtract(currentCenter)));
}
public static Vec3 constrainCenter(Vec3 anchor, double ropeLength, Vec3 center) {
Vec3 radial = center.subtract(anchor);
return radial.lengthSqr() < 0.001D || ropeLength <= 0.0D
? center
: anchor.add(radial.normalize().scale(ropeLength));
}
public static Vec3 projectOntoSwingPlane(Vec3 direction, Vec3 radial) {
return direction.subtract(radial.scale(direction.dot(radial)));
}
private static Vec3 limitSpeed(Vec3 velocity) {
double speed = velocity.length();
return speed <= MAX_SWING_SPEED ? velocity : velocity.scale(MAX_SWING_SPEED / speed);
}
public record Step(Vec3 center, Vec3 velocity) {
}
}
@@ -0,0 +1,107 @@
package fr.koka99cab.sanctuary26.iliketomoveit.network;
import fr.koka99cab.sanctuary26.iliketomoveit.ILikeToMoveItMod;
import net.minecraft.network.RegistryFriendlyByteBuf;
import net.minecraft.network.codec.StreamCodec;
import net.minecraft.network.protocol.common.custom.CustomPacketPayload;
import net.minecraft.world.phys.Vec3;
/** Fixed-size authoritative state used only to predict the owning player's swing. */
public record GrappleStatePayload(
int sessionId,
int sequence,
boolean active,
boolean anchored,
boolean ropeTaut,
Vec3 anchor,
double ropeLength,
double maxRopeLength,
Vec3 center,
Vec3 velocity
) implements CustomPacketPayload {
private static final double MAX_COORDINATE = 30_000_000.0D;
private static final double MAX_ROPE_LENGTH = 216.0D;
private static final double MAX_VELOCITY = 12.0D;
public static final Type<GrappleStatePayload> ID =
new Type<>(ILikeToMoveItMod.id("s2c/grapple_state"));
public static final StreamCodec<RegistryFriendlyByteBuf, GrappleStatePayload> CODEC =
StreamCodec.ofMember(GrappleStatePayload::write, GrappleStatePayload::read);
public GrappleStatePayload {
sessionId = Math.max(-1, sessionId);
sequence = Math.max(0, sequence);
anchor = bounded(anchor, MAX_COORDINATE);
center = bounded(center, MAX_COORDINATE);
velocity = bounded(velocity, MAX_VELOCITY);
ropeLength = boundedScalar(ropeLength, 0.0D, MAX_ROPE_LENGTH);
maxRopeLength = boundedScalar(maxRopeLength, ropeLength, MAX_ROPE_LENGTH);
maxRopeLength = Math.max(ropeLength, maxRopeLength);
if (!active) {
anchored = false;
ropeTaut = false;
}
}
public static GrappleStatePayload stopped(int sessionId, int sequence) {
return new GrappleStatePayload(sessionId, sequence, false, false, false,
Vec3.ZERO, 0.0D, 0.0D, Vec3.ZERO, Vec3.ZERO);
}
private static GrappleStatePayload read(RegistryFriendlyByteBuf buffer) {
return new GrappleStatePayload(
buffer.readVarInt(),
buffer.readVarInt(),
buffer.readBoolean(),
buffer.readBoolean(),
buffer.readBoolean(),
readVec(buffer),
buffer.readDouble(),
buffer.readDouble(),
readVec(buffer),
readVec(buffer)
);
}
private void write(RegistryFriendlyByteBuf buffer) {
buffer.writeVarInt(sessionId);
buffer.writeVarInt(sequence);
buffer.writeBoolean(active);
buffer.writeBoolean(anchored);
buffer.writeBoolean(ropeTaut);
writeVec(buffer, anchor);
buffer.writeDouble(ropeLength);
buffer.writeDouble(maxRopeLength);
writeVec(buffer, center);
writeVec(buffer, velocity);
}
private static Vec3 readVec(RegistryFriendlyByteBuf buffer) {
return new Vec3(buffer.readDouble(), buffer.readDouble(), buffer.readDouble());
}
private static void writeVec(RegistryFriendlyByteBuf buffer, Vec3 value) {
buffer.writeDouble(value.x);
buffer.writeDouble(value.y);
buffer.writeDouble(value.z);
}
private static Vec3 bounded(Vec3 value, double maximum) {
if (value == null) {
return Vec3.ZERO;
}
return new Vec3(
boundedScalar(value.x, -maximum, maximum),
boundedScalar(value.y, -maximum, maximum),
boundedScalar(value.z, -maximum, maximum)
);
}
private static double boundedScalar(double value, double minimum, double maximum) {
return Double.isFinite(value) ? Math.clamp(value, minimum, maximum) : 0.0D;
}
@Override
public Type<? extends CustomPacketPayload> type() {
return ID;
}
}
@@ -10,7 +10,7 @@ import net.minecraft.world.entity.Entity;
* Networking contract for I Like To Move It. Payload IDs must use the module namespace.
*/
public final class ILikeToMoveItNetworking {
public static final int PROTOCOL_VERSION = 4;
public static final int PROTOCOL_VERSION = 5;
private static boolean initialized;
private ILikeToMoveItNetworking() {
@@ -26,6 +26,7 @@ public final class ILikeToMoveItNetworking {
PayloadTypeRegistry.serverboundPlay().register(WaystoneRenamePayload.ID, WaystoneRenamePayload.CODEC);
PayloadTypeRegistry.clientboundPlay().register(WaystoneOpenPayload.ID, WaystoneOpenPayload.CODEC);
PayloadTypeRegistry.clientboundPlay().register(WaystoneSyncPayload.ID, WaystoneSyncPayload.CODEC);
PayloadTypeRegistry.clientboundPlay().register(GrappleStatePayload.ID, GrappleStatePayload.CODEC);
ServerPlayNetworking.registerGlobalReceiver(AircraftControlPayload.ID, (payload, context) -> {
Entity vehicle = context.player().getVehicle();
if (vehicle instanceof AircraftEntity aircraft && aircraft.hasPassenger(context.player())) {
@@ -37,7 +38,7 @@ public final class ILikeToMoveItNetworking {
ServerPlayNetworking.registerGlobalReceiver(WaystoneRenamePayload.ID, (payload, context) ->
WaystoneTeleportation.rename(context.player(), payload.pos(), payload.name()));
WaystoneSyncService.initialize();
ILikeToMoveItMod.LOGGER.debug("[{}] Networking contract v{} ready; aircraft and waystone payloads registered.",
ILikeToMoveItMod.LOGGER.debug("[{}] Networking contract v{} ready; aircraft, grapple and waystone payloads registered.",
ILikeToMoveItMod.DISPLAY_NAME, PROTOCOL_VERSION);
}
}
@@ -37,7 +37,7 @@
"system_type": "gameplay-system",
"namespace": "iliketomoveit",
"data_version": 3,
"network_protocol": 4,
"network_protocol": 5,
"lifecycle": "${lifecycle}",
"pack_version": "${pack_version}",
"descriptions": {
@@ -0,0 +1,38 @@
package fr.koka99cab.sanctuary26.iliketomoveit.grapple;
import net.minecraft.world.phys.Vec3;
/** Determinism and rope-constraint smoke for the shared client/server pendulum. */
public final class GrappleSwingPhysicsSmoke {
private GrappleSwingPhysicsSmoke() {
}
public static void main(String[] args) {
Vec3 anchor = new Vec3(4.0D, 30.0D, -2.0D);
Vec3 center = new Vec3(4.0D, 20.0D, -2.0D);
Vec3 velocity = new Vec3(1.2D, -0.4D, 0.7D);
Vec3 input = new Vec3(0.4D, 0.0D, 1.0D);
GrappleSwingPhysics.Step first = GrappleSwingPhysics.step(anchor, 10.0D, center, velocity, input);
GrappleSwingPhysics.Step repeated = GrappleSwingPhysics.step(anchor, 10.0D, center, velocity, input);
assertNear(first.center().distanceTo(anchor), 10.0D, 1.0E-9D, "rope length drifted");
assertNear(first.velocity().length(), repeated.velocity().length(), 1.0E-12D,
"identical steps were not deterministic");
if (!first.center().equals(repeated.center()) || !first.velocity().equals(repeated.velocity())) {
throw new IllegalStateException("Shared pendulum state is not bit-for-bit deterministic");
}
if (first.velocity().length() > GrappleSwingPhysics.MAX_SWING_SPEED + 1.0E-9D) {
throw new IllegalStateException("Pendulum exceeded its bounded speed");
}
Vec3 constrained = GrappleSwingPhysics.constrainCenter(anchor, 7.5D,
new Vec3(19.0D, 3.0D, 8.0D));
assertNear(constrained.distanceTo(anchor), 7.5D, 1.0E-9D, "client rope projection drifted");
}
private static void assertNear(double actual, double expected, double tolerance, String message) {
if (!Double.isFinite(actual) || Math.abs(actual - expected) > tolerance) {
throw new IllegalStateException(message + ": expected " + expected + ", got " + actual);
}
}
}
+15 -6
View File
@@ -1,6 +1,6 @@
# Wiki cuisine — Sanctuary 26.2 alpha.194
# Wiki cuisine — Sanctuary 26.2 alpha.196
Ce guide décrit le système de nourriture réellement disponible dans **Sanctuary 26.2.0-alpha.194**, fourni par **It's Alive 0.0.0-alpha.27**. Il couvre les cultures, la découverte climatique, les machines, les ingrédients transformés et les 64 pages du livre de recettes.
Ce guide décrit le système de nourriture réellement disponible dans **Sanctuary 26.2.0-alpha.196**, fourni par **It's Alive 0.0.0-alpha.28**. Il couvre les cultures, la découverte climatique, les machines, les ingrédients transformés et les 64 pages du livre de recettes.
## Démarrage rapide
@@ -134,13 +134,14 @@ La presse est un réservoir en verre cerclé de métal.
|---|---|
| Bloc de tomates | 9 pulpes de tomate |
| Bloc de raisins | 9 moûts de raisin |
| Bloc d'olives | 9 pâtes d'olive |
| Pâte d'olive insérée à la main | Huile d'olive |
| Bloc d'olives | 9 huiles d'olive |
La pulpe de tomate et la pâte d'olive se récupèrent avec un **bol**. Le moût de raisin et l'huile d'olive se récupèrent avec une **fiole en verre**.
La pulpe de tomate se récupère avec un **bol**. Le moût de raisin et l'huile d'olive se récupèrent avec une **fiole en verre**. La pâte d'olive historique reste enregistrée uniquement pour relire les anciennes sauvegardes ; la presse n'en produit plus et ne demande plus de second pressage manuel.
Automatisation : un hopper au-dessus insère uniquement le récipient adapté au contenu visible ; un inventaire ou hopper sous la presse récupère les portions emballées. Sans inventaire dessous, les bols et fioles ne sont pas avalés en stockage caché.
Casser une presse détruit désormais ses ingrédients, son liquide et ses récipients en attente : seul le bloc de presse tombe. Ce comportement empêche de dupliquer des bols ou des fioles en interrompant une extraction.
### Moulin redstone
Recette :
@@ -190,6 +191,8 @@ La marmite n'a pas de recette séparée : utilise un **chaudron vanilla rempli d
L'eau initiale compte comme une bouteille d'eau pour les recettes qui en demandent. Une bouteille d'eau inutilisée en trop est tolérée. Quand la marmite est entièrement vidée, elle redevient un chaudron vanilla vide. Le café colore la mixture en brun.
Quand une recette consomme un contenant, par exemple un seau de lait, le contenant vide est projeté au-dessus de la marmite. Pendant cette courte fontaine, la marmite refuse tout nouvel ingrédient afin de ne pas avaler aussitôt l'objet rendu.
### Four de cuisine
Recette : un four vanilla au centre, entouré de huit blocs de briques.
@@ -216,6 +219,12 @@ Planche Dalle bois Planche
- Un clic droit récupère le produit fini ; sinon il retire le dernier ingrédient.
- Hopper au-dessus ou sur les côtés : ingrédients ; hopper dessous : résultat.
### Redstone et comparateurs
Les six postes — presse, moulin, poêle, marmite, four et tonneau — fournissent un signal de comparateur sémantique. Le signal vaut 0 quand le poste est vide, varie de 1 à 14 selon son remplissage ou son avancement, puis atteint 15 lorsqu'un résultat est prêt à extraire. Les changements d'inventaire et de progression actualisent automatiquement les comparateurs voisins.
Les observateurs suivent le comportement vanilla : ils émettent une impulsion quand le bloc change réellement d'état visible, par exemple lorsque la presse change de niveau, que la marmite change de mélange ou qu'un appareil s'allume ou s'éteint. Ils ne réagissent pas à chaque tick de progression ni aux changements d'inventaire invisibles ; pour ces informations, il faut utiliser un comparateur.
## Ingrédients de base connus
Ces transformations sont disponibles sans page secrète.
@@ -403,4 +412,4 @@ Les recettes sont dans `data/itsalive/recipe/`. Les familles utilisent les préf
- `itsalive:kitchen_oven`
- `itsalive:fermentation_barrel`
Ce wiki reflète les données et comportements présents dans le workspace alpha.194 ; les recettes JSON restent la source de vérité si un datapack les surcharge.
Ce wiki reflète les données et comportements présents dans le workspace alpha.196 ; les recettes JSON restent la source de vérité si un datapack les surcharge.
+84 -1
View File
@@ -396,7 +396,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.27"
if (project.version.toString() != "0.0.0-alpha.28"
|| climateMigration.target?.pack_version != "26.2.0-alpha.157"
|| climateMigration.target?.modules?.itsalive != "0.0.0-alpha.24"
|| !climateSource.contains("PlayerBlockBreakEvents.AFTER.register")
@@ -1069,6 +1069,89 @@ tasks.named("sourcesJar") {
dependsOn tasks.named("generateAgricultureResources")
}
tasks.register("verifyCulinaryReliability") {
group = "verification"
description = "Checks the alpha.28 press, olive oil, comparator, cauldron and wine contracts."
inputs.file(rootProject.file("pack/migrations/26.2.0-alpha.195-culinary-mailbox-grapple-fixes-to-alpha.196.json"))
inputs.file(file("src/main/resources/data/itsalive/recipe/press_olive_block.json"))
inputs.file(file("src/main/resources/assets/itsalive/textures/item/wine.png"))
inputs.files(fileTree("src/main/java/fr/koka99cab/sanctuary26/itsalive/culinary"))
inputs.files(fileTree("src/main/java/fr/koka99cab/sanctuary26/itsalive/machine"))
doLast {
def migration = new JsonSlurper().parse(rootProject.file(
"pack/migrations/26.2.0-alpha.195-culinary-mailbox-grapple-fixes-to-alpha.196.json"))
def oliveRecipe = new JsonSlurper().parse(
file("src/main/resources/data/itsalive/recipe/press_olive_block.json"))
def press = file("src/main/java/fr/koka99cab/sanctuary26/itsalive/machine/FoodPressBlock.java").text
def cauldron = file("src/main/java/fr/koka99cab/sanctuary26/itsalive/culinary/block/entity/CookingCauldronBlockEntity.java").text
def container = file("src/main/java/fr/koka99cab/sanctuary26/itsalive/culinary/block/entity/AbstractCulinaryContainerBlockEntity.java").text
def observableStateUpdates = [
"machine/FoodPressBlockEntity.java": [
".setValue(FoodPressBlock.LEVEL, visibleLevel), Block.UPDATE_ALL)"],
"machine/RedstoneMillBlockEntity.java": [
"state.setValue(RedstoneMillBlock.LIT, lit), Block.UPDATE_ALL"],
"culinary/block/entity/FryingPanBlockEntity.java": [
"state.setValue(FryingPanBlock.LIT, heated), Block.UPDATE_ALL"],
"culinary/block/entity/CookingCauldronBlockEntity.java": [
"currentState.setValue(CookingCauldronBlock.LIT, heated), Block.UPDATE_ALL",
"state.setValue(CookingCauldronBlock.MIXTURE, mixture),\n\t\t\t\tBlock.UPDATE_ALL);"],
"culinary/block/entity/KitchenOvenBlockEntity.java": [
"state.setValue(KitchenOvenBlock.LIT, lit), Block.UPDATE_ALL"]
]
def incompleteObserverUpdates = observableStateUpdates.findAll { relativePath, snippets ->
def source = file("src/main/java/fr/koka99cab/sanctuary26/itsalive/${relativePath}").text
snippets.any { !source.contains(it) }
}
def comparatorBlocks = [
"machine/FoodPressBlock.java",
"machine/RedstoneMillBlock.java",
"culinary/block/FryingPanBlock.java",
"culinary/block/CookingCauldronBlock.java",
"culinary/block/KitchenOvenBlock.java",
"culinary/block/FermentationBarrelBlock.java"
]
def incompleteComparators = comparatorBlocks.findAll { relativePath ->
def source = file("src/main/java/fr/koka99cab/sanctuary26/itsalive/${relativePath}").text
!source.contains("hasAnalogOutputSignal") || !source.contains("getAnalogOutputSignal")
}
def wine = ImageIO.read(file("src/main/resources/assets/itsalive/textures/item/wine.png"))
int bordeauxPixels = 0
for (int y = 0; y < wine.height; y++) {
for (int x = 0; x < wine.width; x++) {
int argb = wine.getRGB(x, y)
int alpha = (argb >>> 24) & 0xff
int red = (argb >>> 16) & 0xff
int green = (argb >>> 8) & 0xff
int blue = argb & 0xff
if (alpha > 0 && red >= 90 && red > green * 1.8D && red > blue * 1.15D) {
bordeauxPixels++
}
}
}
if (project.version.toString() != "0.0.0-alpha.28"
|| 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
|| migration.culinary_fixes?.observer_contract?.behavior != "vanilla_block_state_changes"
|| migration.culinary_fixes?.observer_contract?.inventory_or_progress_ticks != false
|| oliveRecipe.ingredient != "itsalive:olive_block"
|| oliveRecipe.result?.id != "itsalive:olive_oil"
|| oliveRecipe.result?.count != 9
|| file("src/main/resources/data/itsalive/recipe/press_olive_paste.json").exists()
|| press.contains("Containers.dropContents(level, pos, press)")
|| !incompleteComparators.isEmpty()
|| !incompleteObserverUpdates.isEmpty()
|| !container.contains("updateNeighbourForOutputSignal")
|| !cauldron.contains("REMAINDER_EJECTION_LOCK_TICKS = 20")
|| !cauldron.contains("level.addFreshEntity(ejected)")
|| !cauldron.contains("inputLockTicks <= 0")
|| wine.width != 16 || wine.height != 16 || bordeauxPixels < 24) {
throw new GradleException("It's Alive alpha.28 culinary reliability contract is incomplete: comparators=${incompleteComparators}, observers=${incompleteObserverUpdates}, bordeauxPixels=${bordeauxPixels}")
}
}
}
tasks.named("check") {
dependsOn tasks.named("verifyItsAlive")
dependsOn tasks.named("verifyCulinaryReliability")
}
@@ -7,6 +7,7 @@ import fr.koka99cab.sanctuary26.itsalive.culinary.CulinaryExperience;
import fr.koka99cab.sanctuary26.itsalive.registry.ItsAliveFoodProcessing;
import java.util.function.Supplier;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.Containers;
import net.minecraft.world.InteractionHand;
@@ -206,6 +207,18 @@ public final class CookingCauldronBlock extends BaseEntityBlock {
Containers.updateNeighboursAfterDestroy(state, level, pos);
}
@Override
protected boolean hasAnalogOutputSignal(BlockState state) {
return true;
}
@Override
protected int getAnalogOutputSignal(BlockState state, Level level, BlockPos pos, Direction direction) {
return level.getBlockEntity(pos) instanceof CookingCauldronBlockEntity cauldron
? cauldron.analogOutputSignal()
: 0;
}
@Override
protected RenderShape getRenderShape(BlockState state) {
return RenderShape.MODEL;
@@ -139,6 +139,18 @@ public final class FermentationBarrelBlock extends BaseEntityBlock {
Containers.updateNeighboursAfterDestroy(state, level, pos);
}
@Override
protected boolean hasAnalogOutputSignal(BlockState state) {
return true;
}
@Override
protected int getAnalogOutputSignal(BlockState state, Level level, BlockPos pos, Direction direction) {
return level.getBlockEntity(pos) instanceof FermentationBarrelBlockEntity barrel
? barrel.analogOutputSignal()
: 0;
}
@Override
public BlockState getStateForPlacement(BlockPlaceContext context) {
return defaultBlockState().setValue(FACING, context.getNearestLookingDirection().getOpposite());
@@ -5,6 +5,7 @@ import fr.koka99cab.sanctuary26.itsalive.culinary.CulinaryExperience;
import fr.koka99cab.sanctuary26.itsalive.culinary.block.entity.FryingPanBlockEntity;
import java.util.function.Supplier;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.Containers;
import net.minecraft.world.InteractionHand;
@@ -148,6 +149,18 @@ public final class FryingPanBlock extends BaseEntityBlock {
Containers.updateNeighboursAfterDestroy(state, level, pos);
}
@Override
protected boolean hasAnalogOutputSignal(BlockState state) {
return true;
}
@Override
protected int getAnalogOutputSignal(BlockState state, Level level, BlockPos pos, Direction direction) {
return level.getBlockEntity(pos) instanceof FryingPanBlockEntity pan
? pan.analogOutputSignal()
: 0;
}
@Override
protected RenderShape getRenderShape(BlockState state) {
return RenderShape.MODEL;
@@ -112,6 +112,18 @@ public final class KitchenOvenBlock extends BaseEntityBlock {
Containers.updateNeighboursAfterDestroy(state, level, pos);
}
@Override
protected boolean hasAnalogOutputSignal(BlockState state) {
return true;
}
@Override
protected int getAnalogOutputSignal(BlockState state, Level level, BlockPos pos, Direction direction) {
return level.getBlockEntity(pos) instanceof KitchenOvenBlockEntity oven
? oven.analogOutputSignal()
: 0;
}
@Override
public BlockState getStateForPlacement(BlockPlaceContext context) {
return defaultBlockState().setValue(FACING, context.getHorizontalDirection().getOpposite());
@@ -219,6 +219,7 @@ public abstract class AbstractCulinaryContainerBlockEntity extends BlockEntity
setChanged();
if (level != null) {
level.sendBlockUpdated(worldPosition, getBlockState(), getBlockState(), Block.UPDATE_CLIENTS);
level.updateNeighbourForOutputSignal(worldPosition, getBlockState().getBlock());
}
}
@@ -77,7 +77,7 @@ public abstract class AbstractTimedCulinaryBlockEntity extends AbstractCulinaryC
resetProgress();
return false;
}
consumeSlots(slots, 0);
consumeRecipeInputs(level, slots);
putResult(outputSlot, result);
progress = 0;
activeDuration = 0;
@@ -86,6 +86,31 @@ public abstract class AbstractTimedCulinaryBlockEntity extends AbstractCulinaryC
return true;
}
/** Hook for open appliances that need to eject crafting remainders. */
protected void consumeRecipeInputs(ServerLevel level, int[] slots) {
consumeSlots(slots, 0);
}
/** Semantic comparator contract: 0 empty, 1-14 loaded/working, 15 result ready. */
public int analogOutputSignal() {
if (!getItem(outputSlot).isEmpty()) {
return 15;
}
int occupied = 0;
for (int slot = 0; slot < inputSlots; slot++) {
if (!getItem(slot).isEmpty()) {
occupied++;
}
}
if (occupied == 0) {
return 0;
}
double loadedFraction = (double) occupied / inputSlots;
double workingFraction = progressFraction();
return Math.min(14, Math.max(1,
(int) Math.ceil(Math.max(loadedFraction, workingFraction) * 14.0D)));
}
protected boolean hasProcessableRecipe(ServerLevel level) {
return CulinaryRecipeTypes.find(level, new CulinaryRecipeInput(appliance, inputView(0, inputSlots)))
.map(holder -> canFitResult(outputSlot, holder.value().result()))
@@ -4,6 +4,8 @@ import fr.koka99cab.sanctuary26.itsalive.culinary.CulinaryAppliance;
import fr.koka99cab.sanctuary26.itsalive.culinary.CulinaryHeat;
import fr.koka99cab.sanctuary26.itsalive.culinary.block.CookingCauldronBlock;
import fr.koka99cab.sanctuary26.itsalive.registry.ItsAliveFoodProcessing;
import java.util.ArrayList;
import java.util.List;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.core.particles.ParticleTypes;
@@ -12,6 +14,7 @@ import net.minecraft.resources.Identifier;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.entity.item.ItemEntity;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.ItemStackTemplate;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.entity.BlockEntityType;
import net.minecraft.world.level.block.state.BlockState;
@@ -26,11 +29,13 @@ public final class CookingCauldronBlockEntity extends AbstractTimedCulinaryBlock
public static final int INPUT_SLOTS = 6;
public static final int OUTPUT_SLOT = 6;
public static final int SIZE = 7;
private static final int REMAINDER_EJECTION_LOCK_TICKS = 20;
private static final int[] INPUTS = {0, 1, 2, 3, 4, 5};
private static final int[] OUTPUT = {OUTPUT_SLOT};
private long assemblyStartedAtGameTime = -1L;
private long lastIngredientChangedAtGameTime = -1L;
private boolean assemblyComplete;
private int inputLockTicks;
public CookingCauldronBlockEntity(BlockPos pos, BlockState state) {
this(((CookingCauldronBlock) state.getBlock()).blockEntityType(), pos, state);
@@ -43,6 +48,9 @@ public final class CookingCauldronBlockEntity extends AbstractTimedCulinaryBlock
public static void serverTick(
ServerLevel level, BlockPos pos, BlockState state, CookingCauldronBlockEntity cauldron
) {
if (cauldron.inputLockTicks > 0) {
cauldron.inputLockTicks--;
}
cauldron.absorbDroppedItems(level, pos);
boolean heated = CulinaryHeat.hasHeatBelow(level, pos);
cauldron.tickAssembly(level, heated);
@@ -58,19 +66,66 @@ public final class CookingCauldronBlockEntity extends AbstractTimedCulinaryBlock
3, 0.22, 0.02, 0.22, 0.015
);
}
if (state.hasProperty(CookingCauldronBlock.LIT)
&& state.getValue(CookingCauldronBlock.LIT) != heated) {
level.setBlock(pos, state.setValue(CookingCauldronBlock.LIT, heated), Block.UPDATE_CLIENTS);
BlockState currentState = cauldron.getBlockState();
if (currentState.hasProperty(CookingCauldronBlock.LIT)
&& currentState.getValue(CookingCauldronBlock.LIT) != heated) {
level.setBlock(pos, currentState.setValue(CookingCauldronBlock.LIT, heated), Block.UPDATE_ALL);
}
}
public int insertIngredient(ItemStack source, int amount) {
if (hasIngredients() && assemblyWindowElapsed()) {
if (!canAcceptIngredients() || hasIngredients() && assemblyWindowElapsed()) {
return 0;
}
return insert(source, amount, 0, INPUT_SLOTS, stack -> true);
}
public boolean canAcceptIngredients() {
return inputLockTicks <= 0 && !hasFinishedProduct();
}
@Override
protected void consumeRecipeInputs(ServerLevel level, int[] slots) {
List<ItemStack> remainders = new ArrayList<>();
for (int relativeSlot : slots) {
ItemStack ingredient = items.get(relativeSlot);
if (ingredient.isEmpty()) {
continue;
}
ItemStackTemplate remainder = ingredient.getItem().getCraftingRemainder();
ingredient.shrink(1);
if (ingredient.isEmpty()) {
items.set(relativeSlot, ItemStack.EMPTY);
}
if (remainder != null) {
remainders.add(remainder.create());
}
}
if (remainders.isEmpty()) {
return;
}
inputLockTicks = REMAINDER_EJECTION_LOCK_TICKS;
for (int index = 0; index < remainders.size(); index++) {
double angle = Math.PI * 2.0D * index / remainders.size()
+ level.getRandom().nextDouble() * 0.35D;
double horizontalSpeed = 0.07D + level.getRandom().nextDouble() * 0.04D;
ItemEntity ejected = new ItemEntity(
level,
worldPosition.getX() + 0.5D,
worldPosition.getY() + 1.05D,
worldPosition.getZ() + 0.5D,
remainders.get(index)
);
ejected.setDeltaMovement(
Math.cos(angle) * horizontalSpeed,
0.34D + level.getRandom().nextDouble() * 0.08D,
Math.sin(angle) * horizontalSpeed
);
ejected.setPickUpDelay(REMAINDER_EJECTION_LOCK_TICKS);
level.addFreshEntity(ejected);
}
}
private void tickAssembly(ServerLevel level, boolean heated) {
if (hasFinishedProduct() || !hasIngredients()) {
resetAssembly();
@@ -204,6 +259,9 @@ public final class CookingCauldronBlockEntity extends AbstractTimedCulinaryBlock
}
private void absorbDroppedItems(ServerLevel level, BlockPos pos) {
if (!canAcceptIngredients()) {
return;
}
AABB bowl = new AABB(pos).inflate(0.05).expandTowards(0.0, 0.9, 0.0);
for (ItemEntity entity : level.getEntitiesOfClass(ItemEntity.class, bowl,
entity -> !entity.getItem().isEmpty())) {
@@ -243,7 +301,7 @@ public final class CookingCauldronBlockEntity extends AbstractTimedCulinaryBlock
if (state.hasProperty(CookingCauldronBlock.MIXTURE)
&& state.getValue(CookingCauldronBlock.MIXTURE) != mixture) {
level.setBlock(worldPosition, state.setValue(CookingCauldronBlock.MIXTURE, mixture),
Block.UPDATE_CLIENTS);
Block.UPDATE_ALL);
}
}
@@ -275,7 +333,7 @@ public final class CookingCauldronBlockEntity extends AbstractTimedCulinaryBlock
@Override
public boolean canPlaceItem(int slot, ItemStack stack) {
return slot >= 0 && slot < INPUT_SLOTS;
return canAcceptIngredients() && slot >= 0 && slot < INPUT_SLOTS;
}
@Override
@@ -140,6 +140,20 @@ public final class FermentationBarrelBlockEntity extends AbstractCulinaryContain
return !getItem(OUTPUT_SLOT).isEmpty();
}
public int analogOutputSignal() {
if (hasFinishedProduct()) {
return 15;
}
int occupied = 0;
for (int slot = 0; slot < INPUT_SLOTS; slot++) {
if (!getItem(slot).isEmpty()) {
occupied++;
}
}
return occupied == 0 ? 0 : Math.min(14, Math.max(1,
(int) Math.ceil((double) occupied / INPUT_SLOTS * 14.0D)));
}
public boolean hasIngredients() {
for (int slot = 0; slot < INPUT_SLOTS; slot++) {
if (!getItem(slot).isEmpty()) {
@@ -46,7 +46,7 @@ public final class FryingPanBlockEntity extends AbstractTimedCulinaryBlockEntity
pan.resetAssembly();
}
if (state.hasProperty(FryingPanBlock.LIT) && state.getValue(FryingPanBlock.LIT) != heated) {
level.setBlock(pos, state.setValue(FryingPanBlock.LIT, heated), Block.UPDATE_CLIENTS);
level.setBlock(pos, state.setValue(FryingPanBlock.LIT, heated), Block.UPDATE_ALL);
}
}
@@ -76,7 +76,7 @@ public final class KitchenOvenBlockEntity extends AbstractTimedCulinaryBlockEnti
boolean lit = oven.burnTime > 0;
if (state.hasProperty(KitchenOvenBlock.LIT) && state.getValue(KitchenOvenBlock.LIT) != lit) {
level.setBlock(pos, state.setValue(KitchenOvenBlock.LIT, lit), Block.UPDATE_CLIENTS);
level.setBlock(pos, state.setValue(KitchenOvenBlock.LIT, lit), Block.UPDATE_ALL);
}
if (lit && oven.burnTime % 20 == 0) {
oven.sync();
@@ -9,7 +9,6 @@ import net.minecraft.world.Containers;
import net.minecraft.world.InteractionHand;
import net.minecraft.world.InteractionResult;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.inventory.AbstractContainerMenu;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.context.BlockPlaceContext;
import net.minecraft.world.level.Level;
@@ -96,14 +95,7 @@ public final class FoodPressBlock extends BaseEntityBlock {
if (press.collectOne(stack, player)) {
return InteractionResult.SUCCESS_SERVER;
}
int inserted = press.insertSecondaryInput(stack);
if (inserted <= 0) {
return InteractionResult.PASS;
}
if (!player.hasInfiniteMaterials()) {
stack.shrink(inserted);
}
return InteractionResult.SUCCESS_SERVER;
return InteractionResult.PASS;
}
@Override
@@ -129,9 +121,10 @@ public final class FoodPressBlock extends BaseEntityBlock {
BlockPos pos,
boolean movedByPiston
) {
if (!movedByPiston && level.getBlockEntity(pos) instanceof FoodPressBlockEntity press) {
Containers.dropContents(level, pos, press);
}
// Loose portions and packaging slots are implementation details, not real
// item stacks that may exist without their bowl or bottle. Destroy them with
// the reservoir so breaking a partly emptied press can never mint unpackaged
// food. The block's loot table still returns the press itself.
Containers.updateNeighboursAfterDestroy(state, level, pos);
}
@@ -142,7 +135,9 @@ public final class FoodPressBlock extends BaseEntityBlock {
@Override
protected int getAnalogOutputSignal(BlockState state, Level level, BlockPos pos, Direction direction) {
return AbstractContainerMenu.getRedstoneSignalFromBlockEntity(level.getBlockEntity(pos));
return level.getBlockEntity(pos) instanceof FoodPressBlockEntity press
? press.analogOutputSignal()
: 0;
}
@Override
@@ -73,27 +73,7 @@ public final class FoodPressBlockEntity extends BlockEntity implements WorldlyCo
public boolean canInteractWith(ItemStack held) {
ItemStack output = items.get(OUTPUT_SLOT);
return !output.isEmpty() && held.is(requiredContainer(output))
|| output.isEmpty() && held.is(ItsAliveFoodProcessing.OLIVE_PASTE);
}
/** Olive paste is the only loose ingredient; whole produce must be compressed physically. */
public int insertSecondaryInput(ItemStack offered) {
if (!offered.is(ItsAliveFoodProcessing.OLIVE_PASTE)
|| !(level instanceof ServerLevel serverLevel)
|| !items.get(OUTPUT_SLOT).isEmpty()) {
return 0;
}
RecipeHolder<FoodPressingRecipe> holder = recipeCheck
.getRecipeFor(new SingleRecipeInput(offered), serverLevel)
.orElse(null);
if (holder == null) {
return 0;
}
items.set(OUTPUT_SLOT, holder.value().assemble(new SingleRecipeInput(offered)));
setChangedAndNotify();
syncVisualContents();
return 1;
return !output.isEmpty() && held.is(requiredContainer(output));
}
public boolean collectOne(ItemStack container, Player player) {
@@ -272,7 +252,7 @@ public final class FoodPressBlockEntity extends BlockEntity implements WorldlyCo
|| state.getValue(FoodPressBlock.LEVEL) != visibleLevel)) {
level.setBlock(worldPosition, state
.setValue(FoodPressBlock.CONTENTS, content)
.setValue(FoodPressBlock.LEVEL, visibleLevel), Block.UPDATE_CLIENTS);
.setValue(FoodPressBlock.LEVEL, visibleLevel), Block.UPDATE_ALL);
}
}
@@ -302,6 +282,15 @@ public final class FoodPressBlockEntity extends BlockEntity implements WorldlyCo
}
}
/** Comparator level follows the nine visible reservoir portions. */
public int analogOutputSignal() {
if (!items.get(PACKAGED_OUTPUT_SLOT).isEmpty()) {
return 15;
}
int portions = Math.min(9, items.get(OUTPUT_SLOT).getCount());
return portions <= 0 ? 0 : Math.min(15, (int) Math.ceil(portions * 15.0D / 9.0D));
}
@Override
protected void saveAdditional(ValueOutput output) {
super.saveAdditional(output);
@@ -12,7 +12,6 @@ import net.minecraft.world.Containers;
import net.minecraft.world.InteractionHand;
import net.minecraft.world.InteractionResult;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.inventory.AbstractContainerMenu;
import net.minecraft.world.item.Item;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.context.BlockPlaceContext;
@@ -148,7 +147,9 @@ public final class RedstoneMillBlock extends BaseEntityBlock {
@Override
protected int getAnalogOutputSignal(BlockState state, Level level, BlockPos pos, Direction direction) {
return AbstractContainerMenu.getRedstoneSignalFromBlockEntity(level.getBlockEntity(pos));
return level.getBlockEntity(pos) instanceof RedstoneMillBlockEntity mill
? mill.analogOutputSignal()
: 0;
}
@Override
@@ -249,7 +249,7 @@ public final class RedstoneMillBlockEntity extends BlockEntity implements Worldl
BlockState state = getBlockState();
boolean lit = burnTime > 0;
if (state.hasProperty(RedstoneMillBlock.LIT) && state.getValue(RedstoneMillBlock.LIT) != lit) {
level.setBlock(worldPosition, state.setValue(RedstoneMillBlock.LIT, lit), Block.UPDATE_CLIENTS);
level.setBlock(worldPosition, state.setValue(RedstoneMillBlock.LIT, lit), Block.UPDATE_ALL);
}
}
@@ -279,6 +279,21 @@ public final class RedstoneMillBlockEntity extends BlockEntity implements Worldl
}
}
public int analogOutputSignal() {
if (!items.get(OUTPUT_SLOT).isEmpty()) {
return 15;
}
boolean hasInput = !items.get(INPUT_SLOT).isEmpty();
boolean hasFuel = !items.get(FUEL_SLOT).isEmpty() || burnTime > 0;
if (!hasInput && !hasFuel) {
return 0;
}
double loadedFraction = (hasInput ? 0.5D : 0.0D) + (hasFuel ? 0.5D : 0.0D);
double workingFraction = totalProcessTime <= 0 ? 0.0D : (double) progress / totalProcessTime;
return Math.min(14, Math.max(1,
(int) Math.ceil(Math.max(loadedFraction, workingFraction) * 14.0D)));
}
@Override
protected void saveAdditional(ValueOutput output) {
super.saveAdditional(output);
Binary file not shown.

Before

Width:  |  Height:  |  Size: 357 B

After

Width:  |  Height:  |  Size: 561 B

@@ -2,7 +2,7 @@
"type": "itsalive:food_pressing",
"ingredient": "itsalive:olive_block",
"result": {
"id": "itsalive:olive_paste",
"id": "itsalive:olive_oil",
"count": 9
}
}
@@ -1,7 +0,0 @@
{
"type": "itsalive:food_pressing",
"ingredient": "itsalive:olive_paste",
"result": {
"id": "itsalive:olive_oil"
}
}
@@ -0,0 +1,79 @@
{
"schema_version": 1,
"source": {
"pack_version": "26.2.0-alpha.195",
"modules": {
"itsalive": "0.0.0-alpha.27",
"iliketomoveit": "0.0.0-alpha.12",
"sanctuary": "0.0.0-alpha.111"
}
},
"target": {
"pack_version": "26.2.0-alpha.196",
"modules": {
"itsalive": "0.0.0-alpha.28",
"iliketomoveit": "0.0.0-alpha.13",
"sanctuary": "0.0.0-alpha.112"
}
},
"culinary_fixes": {
"wine_palette": "bordeaux_garnet",
"food_press_contents_destroyed_on_break": true,
"olive_block_output": {
"item": "itsalive:olive_oil",
"count": 9,
"container": "minecraft:glass_bottle"
},
"legacy_olive_paste_id_preserved": true,
"comparator_contract": {
"blocks": [
"itsalive:food_press",
"itsalive:redstone_mill",
"itsalive:frying_pan",
"itsalive:cooking_cauldron",
"itsalive:kitchen_oven",
"itsalive:fermentation_barrel"
],
"empty": 0,
"loaded_or_working": "1-14",
"result_ready": 15
},
"observer_contract": {
"behavior": "vanilla_block_state_changes",
"observable_states": ["contents", "level", "mixture", "lit"],
"inventory_or_progress_ticks": false
},
"cauldron_remainders_ejected": true,
"cauldron_input_lock_ticks": 20
},
"mailbox": {
"id": "sanctuary:delivery_box",
"horizontal_facing": true,
"owner_inventory_and_queue_unchanged": true
},
"grappling": {
"owner_prediction_payload": "iliketomoveit:s2c/grapple_state",
"payload_fixed_size_and_bounded": true,
"shared_deterministic_pendulum": true,
"server_authoritative_collisions": true,
"per_tick_forced_velocity_packet_removed": true
},
"distribution": {
"packwiz_index_refreshed": true,
"jei": {
"source": "30.25.0.177",
"target": "30.26.0.182"
}
},
"compatibility": {
"itsalive_data_version": 1,
"itsalive_network_protocol": 2,
"iliketomoveit_data_version": 3,
"iliketomoveit_source_network_protocol": 4,
"iliketomoveit_target_network_protocol": 5,
"sanctuary_data_version": 11,
"sanctuary_network_protocol": 18,
"save_migration_required": false,
"registry_ids_changed": false
}
}
+14 -14
View File
@@ -14,15 +14,15 @@ hash = "41f4f4c88c71d0a23446d5014f7407d55ad477441847099cad24492b032335a0"
[[files]]
file = "mods/ambiance-0.0.0-alpha.11.jar"
hash = "bf780025630e2ee078078e1a6b0a9833503a02cffc9e3dddc274c44cd7d9d287"
hash = "a9bf37428a07f3dfbb506c0b99fc3ae40fb8654c1540603de96cd4326c231df9"
[[files]]
file = "mods/anotherworld-0.0.0-alpha.49.jar"
hash = "936db6fd7dbd6ce7d9e701a339e0695e2edd7fa03dd7bc3c789551d4418bf474"
hash = "6d151de5c07eef1f5eeb1af9c66ae9d8dc627432d9d84468b34272305fe02438"
[[files]]
file = "mods/events-0.0.0-alpha.0.jar"
hash = "e7d86ec2aab71105596a03148b91ccdabee42bf3e2ca8b2ab78ad4a2adf03442"
hash = "f0a2f9ac21865710183550f91e0d9ef44056269eefa4215b9c4e604569b7a6ab"
[[files]]
file = "mods/fabric-api.pw.toml"
@@ -30,8 +30,8 @@ hash = "df032a6b051d0262cf24945c43e0e6b915e8cc1d62af62fe933932e7f35960c4"
metafile = true
[[files]]
file = "mods/iliketomoveit-0.0.0-alpha.12.jar"
hash = "b20fb18dd55569f1e8250a2ff4dd15731e965d6fe7f20329087b0cd596ac3507"
file = "mods/iliketomoveit-0.0.0-alpha.13.jar"
hash = "90335af8d390dc874d7930b6d566f984c96d87f357ab0ba7b73d501b841d1871"
[[files]]
file = "mods/iris.pw.toml"
@@ -39,33 +39,33 @@ hash = "84a3cef87679c0d21dceb4641167dc0368b9a3593698f6a3632485e9b3a8d5da"
metafile = true
[[files]]
file = "mods/itsalive-0.0.0-alpha.27.jar"
hash = "24f61ff6d7300ce17faae7f8e3f9dab63cedd1b005371209b88859b9e389a2d2"
file = "mods/itsalive-0.0.0-alpha.28.jar"
hash = "de81dd16379277b372a442b15c847d2925bc89d45703cde91cd66f8c92af3854"
[[files]]
file = "mods/jei.pw.toml"
hash = "cb6b81977ed8f80dae5fddf3bb08255162d4ff918fcccadab697707d1228f1a9"
hash = "9b6ff8adcf51525a33f3180dad84327cb77bb818f3ca31547635689cea523cbf"
metafile = true
[[files]]
file = "mods/masterkey-0.0.0-alpha.8.jar"
hash = "62bf000ee8b7a1cad6fc4c95c3b9b8d51e005a111b300a6855dee8bbd9377bc7"
hash = "07bc6d9dc2df1f2b7be44408d05d36faef5a8c1c591e2ae9e4bdfa20c004a9d4"
[[files]]
file = "mods/onlyfun-0.0.0-alpha.19.jar"
hash = "f61fb53e6959eb356436b24c8347147e709b71834f5f0a517f4e48bfd6e27953"
hash = "7249ff62b9ef7e114c45d8083ede5b94cf20e2cf0eec216b12ab08c021a1caf1"
[[files]]
file = "mods/ouch-0.0.0-alpha.8.jar"
hash = "d58e50d3b48fc6bcc630a26c24732d94a09d326f4b1ff0de7bade187b2b3913e"
hash = "b3a641c015ae24ca958e03303b493c594ccb0b91969282f774da2d6849fd7936"
[[files]]
file = "mods/redstoner-0.0.0-alpha.10.jar"
hash = "a8207a5e00c646a33cf178ad598084dd85a6e538ea57096b5df082c8ac52d755"
hash = "db3a714ff043ac04a1ca8a1c752f614a7c1aecd557ea2ef2ca72af203cd25039"
[[files]]
file = "mods/sanctuary-0.0.0-alpha.111.jar"
hash = "cea043f3c0852d5e17cf19a6fd6a2caeb01faa8f2e7c44a8a8c0a617bd2452d5"
file = "mods/sanctuary-0.0.0-alpha.112.jar"
hash = "63976528da2a467bd1dbe1432e1444f3c161d24dcbfed3c9e4755669a84538d9"
[[files]]
file = "mods/simple-voice-chat.pw.toml"
+4 -4
View File
@@ -1,14 +1,14 @@
name = "Just Enough Items (JEI)"
filename = "jei-26.2-fabric-30.25.0.177.jar"
filename = "jei-26.2-fabric-30.26.0.182.jar"
side = "both"
pin = true
[download]
url = "https://cdn.modrinth.com/data/u6dRKJwZ/versions/AFgObZjc/jei-26.2-fabric-30.25.0.177.jar"
url = "https://cdn.modrinth.com/data/u6dRKJwZ/versions/x6nG9OT2/jei-26.2-fabric-30.26.0.182.jar"
hash-format = "sha512"
hash = "749454d81f0b8e9860995e4fea6703573453cc16d5fec7b24c97b8c58d319988bb4d8f49e20b5e0e18781ee1c5e90a6bc7c2ef0046bf7a3ffac5d885a15d9740"
hash = "a79a3aaa4d3c17659bdd4823b525890aea20c271a7168a0ece9aa926b0770b0571c0fba20cda7ed54f57abf80d55a7bfdcfc37a1c7c92ee6f7c3bcdb84404054"
[update]
[update.modrinth]
mod-id = "u6dRKJwZ"
version = "AFgObZjc"
version = "x6nG9OT2"
+2 -2
View File
@@ -1,12 +1,12 @@
name = "Sanctuary 26.2"
author = "KOKA99CAB"
version = "26.2.0-alpha.195"
version = "26.2.0-alpha.196"
pack-format = "packwiz:1.1.0"
[index]
file = "index.toml"
hash-format = "sha256"
hash = "b975524b49b564ed765a28636d6797bab332032c988f651dcdea0851e70bf7b0"
hash = "2491c55df3788a5384385ccb67d33c806bcedc24281f94bc1b38e6e37a578263"
[versions]
fabric = "0.19.3"
+1 -1
View File
@@ -3,7 +3,7 @@
"game": "minecraft",
"name": "Sanctuary 26.2",
"summary": "Sanctuary 26.2 — progression, exploration, worldgen and multiplayer systems by KOKA99CAB.",
"versionId": "26.2.0-alpha.195",
"versionId": "26.2.0-alpha.196",
"files": [
{
"path": "mods/fabric-api-0.155.0+26.2.jar",
+2 -2
View File
@@ -1,6 +1,6 @@
{
"schema_version": 1,
"pack_version": "26.2.0-alpha.195",
"pack_version": "26.2.0-alpha.196",
"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.195.mrpack",
"primary_file": "sanctuary-26.2.0-alpha.196.mrpack",
"shared_client_server_pack": true,
"update_owner": "packwiz-installer",
"check_on_launch": true,
+33 -3
View File
@@ -2878,7 +2878,7 @@ tasks.register("verifyProgressivePauseMenu") {
"pack/migrations/26.2.0-alpha.190-progressive-pause-menu-to-alpha.191.json"))
def pauseMenu = file("src/main/java/fr/koka99cab/sanctuary26/sanctuary/mixin/client/PauseScreenMixin.java").text
def costs = file("src/main/java/fr/koka99cab/sanctuary26/sanctuary/progression/SanctuaryProgressionCosts.java").text
if (project.version.toString() != "0.0.0-alpha.111"
if (project.version.toString() != "0.0.0-alpha.112"
|| migration.target?.pack_version != "26.2.0-alpha.191"
|| migration.target?.modules?.sanctuary != "0.0.0-alpha.109"
|| migration.pause_menu?.progression_full_width != true
@@ -2916,7 +2916,7 @@ tasks.register("verifyShopPricingLayout") {
"pack/migrations/26.2.0-alpha.192-shop-pricing-layout-to-alpha.193.json"))
def screen = file("src/main/java/fr/koka99cab/sanctuary26/sanctuary/client/SanctuaryShopScreen.java").text
def progression = file("src/main/java/fr/koka99cab/sanctuary26/sanctuary/shop/ShopProgression.java").text
if (project.version.toString() != "0.0.0-alpha.111"
if (project.version.toString() != "0.0.0-alpha.112"
|| migration.target?.modules?.sanctuary != "0.0.0-alpha.110"
|| migration.shop_layout?.locked_offer_buttons != 6
|| migration.shop_layout?.button_slots != [4, 5, 6, 7, 8, 9]
@@ -2947,7 +2947,7 @@ tasks.register("verifyShopCsvBossGates") {
"pack/migrations/26.2.0-alpha.194-shop-csv-boss-gates-to-alpha.195.json"))
def provider = file("src/main/java/fr/koka99cab/sanctuary26/sanctuary/shop/ShopCatalogProvider.java").text
def catalog = file("src/main/java/fr/koka99cab/sanctuary26/sanctuary/shop/ShopOfferCatalog.java").text
if (project.version.toString() != "0.0.0-alpha.111"
if (project.version.toString() != "0.0.0-alpha.112"
|| migration.target?.pack_version != "26.2.0-alpha.195"
|| migration.target?.modules?.sanctuary != "0.0.0-alpha.111"
|| migration.compatibility?.sanctuary_data_version != 11
@@ -2965,7 +2965,37 @@ tasks.named("verifySanctuary") {
dependsOn tasks.named("verifyShopCsvBossGates")
}
tasks.register("verifyDeliveryBoxOrientation") {
group = "verification"
description = "Checks the alpha.112 four-way horizontal mailbox placement contract."
inputs.file(rootProject.file("pack/migrations/26.2.0-alpha.195-culinary-mailbox-grapple-fixes-to-alpha.196.json"))
inputs.file(file("src/main/java/fr/koka99cab/sanctuary26/sanctuary/shop/DeliveryBoxBlock.java"))
inputs.file(file("src/main/resources/assets/sanctuary/blockstates/delivery_box.json"))
doLast {
def migration = new JsonSlurper().parse(rootProject.file(
"pack/migrations/26.2.0-alpha.195-culinary-mailbox-grapple-fixes-to-alpha.196.json"))
def source = file("src/main/java/fr/koka99cab/sanctuary26/sanctuary/shop/DeliveryBoxBlock.java").text
def blockstate = new JsonSlurper().parse(
file("src/main/resources/assets/sanctuary/blockstates/delivery_box.json"))
def expectedVariants = ["facing=north", "facing=east", "facing=south", "facing=west"].toSet()
if (project.version.toString() != "0.0.0-alpha.112"
|| migration.target?.modules?.sanctuary != "0.0.0-alpha.112"
|| migration.mailbox?.id != "sanctuary:delivery_box"
|| migration.mailbox?.horizontal_facing != true
|| !source.contains("BlockStateProperties.HORIZONTAL_FACING")
|| !source.contains("getStateForPlacement(BlockPlaceContext context)")
|| !source.contains("builder.add(FACING)")
|| blockstate.variants.keySet() != expectedVariants
|| blockstate.variants["facing=east"].y != 90
|| blockstate.variants["facing=south"].y != 180
|| blockstate.variants["facing=west"].y != 270) {
throw new GradleException("Sanctuary alpha.112 delivery-box orientation contract is incomplete")
}
}
}
tasks.named("check") {
dependsOn tasks.named("verifySanctuary")
dependsOn tasks.named("verifyDeliveryBoxOrientation")
dependsOn tasks.named("celestialGraphSmoke")
}
@@ -3,6 +3,7 @@ package fr.koka99cab.sanctuary26.sanctuary.shop;
import com.mojang.serialization.MapCodec;
import java.util.UUID;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.network.chat.Component;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.Containers;
@@ -11,19 +12,42 @@ import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.inventory.AbstractContainerMenu;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.context.BlockPlaceContext;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.BaseEntityBlock;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.Mirror;
import net.minecraft.world.level.block.RenderShape;
import net.minecraft.world.level.block.Rotation;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.block.state.StateDefinition;
import net.minecraft.world.level.block.state.properties.BlockStateProperties;
import net.minecraft.world.level.block.state.properties.EnumProperty;
import net.minecraft.world.phys.BlockHitResult;
public final class DeliveryBoxBlock extends BaseEntityBlock {
public static final MapCodec<DeliveryBoxBlock> CODEC = simpleCodec(DeliveryBoxBlock::new);
public DeliveryBoxBlock(Properties properties) { super(properties); }
public static final EnumProperty<Direction> FACING = BlockStateProperties.HORIZONTAL_FACING;
public DeliveryBoxBlock(Properties properties) {
super(properties);
registerDefaultState(stateDefinition.any().setValue(FACING, Direction.NORTH));
}
@Override protected MapCodec<? extends BaseEntityBlock> codec() { return CODEC; }
@Override public BlockEntity newBlockEntity(BlockPos pos, BlockState state) { return new DeliveryBoxBlockEntity(pos, state); }
@Override protected RenderShape getRenderShape(BlockState state) { return RenderShape.MODEL; }
@Override public BlockState getStateForPlacement(BlockPlaceContext context) {
return defaultBlockState().setValue(FACING, context.getHorizontalDirection().getOpposite());
}
@Override protected BlockState rotate(BlockState state, Rotation rotation) {
return state.setValue(FACING, rotation.rotate(state.getValue(FACING)));
}
@Override protected BlockState mirror(BlockState state, Mirror mirror) {
return rotate(state, mirror.getRotation(state.getValue(FACING)));
}
@Override protected void createBlockStateDefinition(StateDefinition.Builder<Block, BlockState> builder) {
builder.add(FACING);
}
@Override public void setPlacedBy(Level level, BlockPos pos, BlockState state, LivingEntity placer, ItemStack stack) {
super.setPlacedBy(level, pos, state, placer, stack);
@@ -1,5 +1,8 @@
{
"variants": {
"": { "model": "sanctuary:block/delivery_box" }
"facing=north": { "model": "sanctuary:block/delivery_box" },
"facing=east": { "model": "sanctuary:block/delivery_box", "y": 90 },
"facing=south": { "model": "sanctuary:block/delivery_box", "y": 180 },
"facing=west": { "model": "sanctuary:block/delivery_box", "y": 270 }
}
}