Compare commits
17
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b39c271c96 | ||
|
|
a16156b005 | ||
|
|
2518c20071 | ||
|
|
b7497c81d1 | ||
|
|
97638e0fa2 | ||
|
|
a071a0b746 | ||
|
|
6fcc56552c | ||
|
|
66944ec492 | ||
|
|
b513ab1262 | ||
|
|
b2222c5d33 | ||
|
|
2d0be21c5b | ||
|
|
a0b0788869 | ||
|
|
106d730dd8 | ||
|
|
21dd089486 | ||
|
|
e3b6c6ed01 | ||
|
|
7387096d98 | ||
|
|
457f91b92e |
+41
-4
@@ -6,16 +6,53 @@ 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é
|
||||
## Non publié
|
||||
|
||||
Modules modifiés : **It's Alive `0.0.0-alpha.37`**, **Ouch `0.0.0-alpha.11`**, **Sanctuary `0.0.0-alpha.126`** et **MasterKey `0.0.0-alpha.9`**. Le pack public reste temporairement en `26.2.0-alpha.223` jusqu'à la publication.
|
||||
Modules modifiés : **Sanctuary `0.0.0-alpha.131`** et **Another World `0.0.0-alpha.57`**.
|
||||
|
||||
- ajoute la dimension persistante `sanctuary:backroom`, accessible en s’accroupissant devant un lit et quittée uniquement par un lit, avec arrivées variées dans une zone architecturale de 2 000 blocs ;
|
||||
- transfère atomiquement vers la Mailbox tout l’équipement d’entrée, y compris les rangées étendues, l’œuf compagnon et la cape, avec reprise idempotente après interruption ;
|
||||
- conserve les objets perdus au dépop, dans le vide et lors des morts en Backroom dans un registre borné et non duplicable ;
|
||||
- transforme tous les coffres de la dimension en butin renouvelable, pauvre la plupart du temps, qui privilégie les pertes personnelles dans les premiers coffres avant les pertes anciennes partagées ;
|
||||
- génère des villes, bureaux, plaines, agrégats flottants et pièces visuellement retournées à partir de palettes composées, en réservant les matériaux d’Another World aux anomalies et les blocs précieux à de rares cachettes ;
|
||||
- fait enregistrer la mousse turquoise par Another World via l’API publique de matériaux Backroom, sans dépendance circulaire ;
|
||||
- migre Sanctuary vers les données v14 sans changer le protocole réseau 24, sans réinitialiser les sauvegardes ni effacer les constructions.
|
||||
|
||||
## `26.2.0-alpha.226`
|
||||
|
||||
Module modifié : **Sanctuary `0.0.0-alpha.130`**. Les neuf autres modules conservent leur version issue de l'alpha.225.
|
||||
|
||||
- ajoute `sanctuary:white_cape`, une cape blanche craftable avec six laines blanches disposées comme une porte, en deux colonnes de trois ;
|
||||
- permet d'associer simplement la cape et n'importe quelle bannière dans l'établi afin d'en conserver la couleur de fond et jusqu'à six motifs ;
|
||||
- affiche les motifs dans l'inventaire et sur la cape équipée, avec la physique native des capes, y compris en multijoueur ;
|
||||
- conserve les sauvegardes Sanctuary en data v13 ; le payload d'apparence transporte désormais la couleur et les motifs bornés, donc le protocole Sanctuary passe de 23 à 24 et client comme serveur doivent être mis à jour, sans nouveau monde ni réinitialisation.
|
||||
|
||||
## `26.2.0-alpha.225`
|
||||
|
||||
Modules modifiés : **Another World `0.0.0-alpha.56`**, **It's Alive `0.0.0-alpha.38`**, **I Like To Move It `0.0.0-alpha.18`**, **Only Fun `0.0.0-alpha.27`** et **Sanctuary `0.0.0-alpha.129`**. Les cinq autres modules conservent leur version issue de l'alpha.224.
|
||||
|
||||
- exige que l’attaquant possède son propre coffre-fort actif avant de commencer un braquage, revalide cette condition pendant le forage et refuse défensivement tout transfert si le coffre disparaît entre-temps ;
|
||||
- empêche les familiers temporaires Sanctuary de produire du butin vanilla, de l’expérience ou une lootbox Only Fun, et les exclut des contrats de panneau comme des demandes communautaires ;
|
||||
- conserve la variante Canaplia des seize graines historiques pendant le placement, la croissance naturelle, la poudre d’os, la création de la partie haute, le rechargement et la récolte, ce qui réactive notamment l’hybridation Tendre + Olive vers Dorée ;
|
||||
- remplace la laine violette du Magic Carpet par la texture historique 16 × 16 du Totem de Notch, utilisée par l’objet canonique et son alias hérité sans modifier la plateforme 3 × 3 ;
|
||||
- conserve tous les IDs, sauvegardes et formats de données ; les protocoles réseau restent Another World 2, It's Alive 4, I Like To Move It 5, Only Fun 3 et Sanctuary 23, sans nouveau monde ni réinitialisation.
|
||||
|
||||
## `26.2.0-alpha.224`
|
||||
|
||||
Modules modifiés : **It's Alive `0.0.0-alpha.37`**, **Ouch `0.0.0-alpha.11`**, **I Like To Move It `0.0.0-alpha.17`**, **Only Fun `0.0.0-alpha.26`**, **Sanctuary `0.0.0-alpha.128`** et **MasterKey `0.0.0-alpha.9`**.
|
||||
|
||||
- étend le clic molette aux coffres, barils, boîtes de Shulker, entonnoirs et distributeurs, tout en excluant les fours, le Shop et le terminal Red-Stoner ;
|
||||
- trie l'inventaire du joueur lorsque le curseur survole l'une de ses cases dans un conteneur, sans toucher à la hotbar, et replace la hotbar en bas après connexion ou achat d'une rangée ;
|
||||
- ajoute Notch comme résident unique et persistant de la maison Indev, avec restauration automatique, déduplication et lecture des anciennes entités `portals:notch` ;
|
||||
- transforme Notch en Titan au premier coup volontaire, avec santé bornée selon les participants, poursuite, attaques télégraphiées, boules de feu renvoyables et réinitialisation si le combat est abandonné ;
|
||||
- limite la destruction de blocs à l'explosion initiale de transformation ; les attaques suivantes utilisent uniquement dégâts, poussée, particules et explosions non destructrices ;
|
||||
- enregistre la première victoire comme jalon mondial monotone, crédite les participants, inscrit un marqueur de boss dans leur Atlas déjà découvert et réserve un drapeau de récompense unique pour la suite ;
|
||||
- enregistre la première victoire comme jalon mondial monotone, crédite les participants, inscrit un marqueur de boss dans leur Atlas déjà découvert et réserve une récompense physique unique ;
|
||||
- place le Magic Carpet de Notch dans un coffre déterministe signalé par feu d'artifice, avec réservation et livraison persistées séparément pour récupérer une interruption sans duplication ;
|
||||
- conserve `portals:notch_totem` comme alias caché et convertit inventaires, conteneurs et items au sol vers `iliketomoveit:magic_carpet` en gardant quantité, composants et nom personnalisé ;
|
||||
- maintient la plateforme temporaire 3 × 3 sous autorité serveur, sans écraser le terrain et avec un suivi par propriétaire qui protège les tapis superposés pendant les nettoyages ;
|
||||
- rend l'offre Magic Carpet visible et achetable uniquement après `sanctuary:notch_defeated`, au prix exceptionnel existant, avec paiement normal, livraison Mailbox, provenance et journal de remplacement ;
|
||||
- ajoute les diagnostics `/masterkey notch inspect` et `/masterkey notch restore`, les traductions FR/EN/RU et les contrats de test déterministes associés ;
|
||||
- migre les données It's Alive de v3 à v4 et Sanctuary de v11 à v12, sans changer les protocoles réseau ni le pack public.
|
||||
- migre les données It's Alive de v3 à v4, I Like To Move It de v3 à v4 et Sanctuary de v11 à v13 ; le payload de tri ayant gagné un booléen borné, le protocole Sanctuary passe de 22 à 23 et client comme serveur doivent être mis à jour, sans nouveau monde ni réinitialisation.
|
||||
|
||||
## `26.2.0-alpha.223`
|
||||
|
||||
|
||||
@@ -22,16 +22,18 @@ 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.223`**. Cette release distribue Sanctuary **`0.0.0-alpha.125`** ; les neuf autres modules conservent leur version issue de l'alpha.222.
|
||||
Version courante du pack distribué : **`26.2.0-alpha.226`**. Cette release distribue Sanctuary **`0.0.0-alpha.130`** ; les neuf autres modules conservent leur version issue de l'alpha.225.
|
||||
|
||||
L’alpha.225 corrige quatre tickets : posséder un coffre-fort devient obligatoire pour en braquer un autre, les familiers temporaires ne donnent plus de butin, d’expérience, de lootbox ni de progression de quête, les seize variantes Canaplia survivent à tout leur cycle de culture, et le Magic Carpet retrouve la texture historique du Totem de Notch à la place de la laine violette. Les IDs, sauvegardes, formats de données et protocoles restent compatibles.
|
||||
|
||||
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.224 ajoute Notch comme résident unique de la maison Indev et son combat de Titan. It's Alive possède l'entité persistante, Ouch les attaques et dégâts, Sanctuary le jalon mondial de victoire et l'Atlas, tandis que MasterKey fournit les commandes d'inspection et de restauration. Les anciennes entités `portals:notch` restent chargeables. La première victoire réserve maintenant un unique `iliketomoveit:magic_carpet` dans un coffre signalé par feu d'artifice. L'ancien `portals:notch_totem` est migré sans perte ; après Notch, Only Fun ouvre l'offre de remplacement au Shop avec paiement serveur et livraison par Mailbox. Le clic molette trie aussi les coffres, barils, boîtes de Shulker, entonnoirs et distributeurs, ou l'inventaire du joueur lorsqu'il est survolé, sans déplacer la hotbar. Le protocole Sanctuary passe à 23 : client et serveur doivent utiliser l'alpha.224.
|
||||
|
||||
L’alpha.223 ajoute le tri natif des rangées de stockage par clic molette sans déplacer la hotbar ni les emplacements spéciaux. Le suivi des quêtes propose désormais le texte transparent aligné sous la colonne gauche du F3 ou le panneau original de Chris, avec bascule configurable sur `N` et contrats ordonnés par récompense. JEI passe à la bêta Fabric 26.2 `30.29.0.198`.
|
||||
|
||||
L’alpha.222 améliore le terminal de stockage avec une recherche Unicode, des quantités compactes et des emplacements qui ne glissent plus lorsqu'une pile est vidée. Elle ajoute aussi le suivi textuel des quêtes actives en haut à gauche, masquable avec une touche configurable, ainsi que des suggestions d’IDs d’items et de mobs dans l’éditeur de demandes. JEI passe à la bêta Fabric 26.2 `30.29.0.196`.
|
||||
|
||||
Le lot non distribué ajoute Notch comme résident unique de la maison Indev et son combat de Titan. It's Alive possède l'entité persistante, Ouch les attaques et dégâts, Sanctuary le jalon mondial de victoire et l'Atlas, tandis que MasterKey fournit les commandes d'inspection et de restauration. Les anciennes entités `portals:notch` restent chargeables.
|
||||
|
||||
L’alpha.221 corrige la cause réelle du disque blanc ralenti et trop grave : Ambiance décode désormais la source AAC en stéréo native à 48 kHz, puis moyenne explicitement chaque paire gauche/droite vers le flux mono positionnel de Minecraft. OpenAL reste à sa hauteur normale `1.0`, sans compensation artificielle ni changement de durée.
|
||||
|
||||
L’alpha.220 transforme le terminal de stockage en véritable menu de conteneur, sans bouton de fermeture propriétaire. Les clics gauche/droit et shift-clic permettent maintenant de retirer ou déposer des piles depuis toutes les rangées d’inventaire débloquées par Sanctuary, jusqu’à six ; la grille agrégée reste non réorganisable et remplit chaque coffre ou baril dans l’ordre de découverte avant de passer au suivant.
|
||||
@@ -114,6 +116,8 @@ L’alpha.173 centralise l’audio du Miner : une seule boucle de frappe sur le
|
||||
|
||||
L’alpha.171 rassemble les **46 capes** dans un onglet créatif dédié « Capes ». Elles ne sont plus mélangées à l’onglet vanilla Tools & Utilities et ne sont donc affichées qu’une seule fois.
|
||||
|
||||
L’alpha.226 ajoute une 47e cape, `sanctuary:white_cape`, distincte de la collection Only Fun. Elle se fabrique avec six laines blanches en deux colonnes de trois, puis accepte une bannière dans une recette simple pour en conserver la couleur et jusqu’à six motifs ; les motifs s’affichent dans l’inventaire comme une fois la cape équipée.
|
||||
|
||||
L’alpha.170 ajoute les pictogrammes fournis aux emplacements vides de la cape et du familier. Ils disparaissent dès qu’une cape ou un œuf est équipé, sans modifier les objets, la persistance ni le protocole.
|
||||
|
||||
L’alpha.169 agrandit la collection de 13 à **46 capes équipables**. Les 33 nouvelles textures 64×32 fournies sont conservées telles quelles, avec une icône d’inventaire, un modèle et des traductions FR/EN/RU pour chaque cape. Elles utilisent le slot, la persistance et le rendu multijoueur déjà existants, restent sans recette et rejoignent toutes la lootbox de capes Only Fun. Les caméras Canon restent accessibles dans la lootbox normale et peuvent désormais aussi apparaître directement, très rarement, dans les coffres d’exploration et du donjon de cobblestone.
|
||||
|
||||
@@ -1647,6 +1647,7 @@ tasks.register("generateAnotherWorldResources") {
|
||||
"message.anotherworld.safe_vault.empty_before_break": "Videz le coffre-fort et attendez la fin du forage avant de le démonter.",
|
||||
"message.anotherworld.safe_vault.drill_own": "Vous ne pouvez pas forer votre propre coffre-fort.",
|
||||
"message.anotherworld.safe_vault.inactive": "Ce coffre-fort n’est pas actif.",
|
||||
"message.anotherworld.safe_vault.raid_requires_vault": "Vous devez posséder un coffre-fort actif avant d’en braquer un autre.",
|
||||
"message.anotherworld.safe_vault.empty": "Ce coffre-fort est vide.",
|
||||
"message.anotherworld.safe_vault.owner_offline": "Le propriétaire doit être connecté pour que le coffre-fort soit attaquable.",
|
||||
"message.anotherworld.safe_vault.protected": "Ce coffre-fort reste protégé pendant environ %s minute(s).",
|
||||
@@ -1671,6 +1672,7 @@ tasks.register("generateAnotherWorldResources") {
|
||||
"message.anotherworld.safe_vault.empty_before_break": "Опустошите сейф и дождитесь окончания сверления перед разборкой.",
|
||||
"message.anotherworld.safe_vault.drill_own": "Нельзя сверлить собственный сейф.",
|
||||
"message.anotherworld.safe_vault.inactive": "Этот сейф не активен.",
|
||||
"message.anotherworld.safe_vault.raid_requires_vault": "Для ограбления чужого сейфа нужен собственный активный сейф.",
|
||||
"message.anotherworld.safe_vault.empty": "Этот сейф пуст.",
|
||||
"message.anotherworld.safe_vault.owner_offline": "Владелец должен быть в сети, чтобы атаковать сейф.",
|
||||
"message.anotherworld.safe_vault.protected": "Сейф защищён ещё примерно %s минут(ы).",
|
||||
@@ -1695,6 +1697,7 @@ tasks.register("generateAnotherWorldResources") {
|
||||
"message.anotherworld.safe_vault.empty_before_break": "Empty the safe and wait for drilling to end before dismantling it.",
|
||||
"message.anotherworld.safe_vault.drill_own": "You cannot drill your own safe.",
|
||||
"message.anotherworld.safe_vault.inactive": "This safe is not active.",
|
||||
"message.anotherworld.safe_vault.raid_requires_vault": "You need an active safe of your own before raiding another one.",
|
||||
"message.anotherworld.safe_vault.empty": "This safe is empty.",
|
||||
"message.anotherworld.safe_vault.owner_offline": "The owner must be online before the safe can be raided.",
|
||||
"message.anotherworld.safe_vault.protected": "This safe remains protected for about %s minute(s).",
|
||||
@@ -1953,7 +1956,7 @@ tasks.register("verifyAnotherWorldCatalog") {
|
||||
"src/main/resources/assets/redstoner/textures/item/weather_gauge.png",
|
||||
"src/main/resources/data/redstoner/recipe/weather_gauge.json"
|
||||
]
|
||||
if (project.version.toString() != "0.0.0-alpha.55"
|
||||
if (project.version.toString() != "0.0.0-alpha.57"
|
||||
|| sulfurMigration.target?.modules?.anotherworld != "0.0.0-alpha.44"
|
||||
|| sulfurMigration.sulfur_caves?.sanctuary?.dripstone_cell_period != 2
|
||||
|| !javaText.contains('Identifier.parse("sanctuary:sulfur_caves")')
|
||||
@@ -3270,7 +3273,7 @@ tasks.register("verifyClimateBiomeMixer") {
|
||||
"pack/migrations/26.2.0-alpha.188-beta-climate-provinces-to-alpha.189.json"))
|
||||
def mixer = file(
|
||||
"src/main/java/fr/koka99cab/sanctuary26/anotherworld/mixin/MultiNoiseBiomeSourceParameterListPresetMixin.java").text
|
||||
if (project.version.toString() != "0.0.0-alpha.55"
|
||||
if (project.version.toString() != "0.0.0-alpha.57"
|
||||
|| migration.source?.pack_version != "26.2.0-alpha.188"
|
||||
|| migration.target?.pack_version != "26.2.0-alpha.189"
|
||||
|| migration.target?.modules?.anotherworld != "0.0.0-alpha.48"
|
||||
@@ -3297,3 +3300,33 @@ tasks.register("verifyClimateBiomeMixer") {
|
||||
tasks.named("check") {
|
||||
dependsOn tasks.named("verifyClimateBiomeMixer")
|
||||
}
|
||||
|
||||
tasks.register("verifyBackroomMaterialIntegration") {
|
||||
group = "verification"
|
||||
description = "Checks the one-way Another World material integration with Sanctuary Backroom."
|
||||
inputs.file(file("src/main/java/fr/koka99cab/sanctuary26/anotherworld/AnotherWorldMod.java"))
|
||||
inputs.file(file("src/main/java/fr/koka99cab/sanctuary26/anotherworld/registry/AnotherWorldBlocks.java"))
|
||||
inputs.file(rootProject.file(
|
||||
"sanctuary/src/main/java/fr/koka99cab/sanctuary26/sanctuary/api/backroom/BackroomMaterialApi.java"))
|
||||
doLast {
|
||||
def initializer = file("src/main/java/fr/koka99cab/sanctuary26/anotherworld/AnotherWorldMod.java").text
|
||||
def blocks = file("src/main/java/fr/koka99cab/sanctuary26/anotherworld/registry/AnotherWorldBlocks.java").text
|
||||
def api = rootProject.file(
|
||||
"sanctuary/src/main/java/fr/koka99cab/sanctuary26/sanctuary/api/backroom/BackroomMaterialApi.java").text
|
||||
def rootBuild = rootProject.file("build.gradle").text
|
||||
def manifest = new JsonSlurper().parse(file("src/main/resources/fabric.mod.json"))
|
||||
if (project.version.toString() != "0.0.0-alpha.57"
|
||||
|| !initializer.contains("BackroomMaterialApi.registerAnomalyMaterial(AnotherWorldBlocks.TURQUOISE_MOSS)")
|
||||
|| !blocks.contains('"turquoise_moss"')
|
||||
|| !api.contains("public static void registerAnomalyMaterial")
|
||||
|| manifest.depends?.sanctuary != ">=0.0.0-alpha.131"
|
||||
|| !rootBuild.contains('project(":anotherworld")')
|
||||
|| !rootBuild.contains('implementation project(":sanctuary")')) {
|
||||
throw new GradleException("Another World alpha.57 Backroom material integration is incomplete")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tasks.named("check") {
|
||||
dependsOn tasks.named("verifyBackroomMaterialIntegration")
|
||||
}
|
||||
|
||||
@@ -4,7 +4,9 @@ import fr.koka99cab.canon.CanonMod;
|
||||
import fr.koka99cab.sanctuary26.anotherworld.item.WeatherCompassItem;
|
||||
import fr.koka99cab.sanctuary26.anotherworld.network.AnotherWorldNetworking;
|
||||
import fr.koka99cab.sanctuary26.anotherworld.registry.AnotherWorldRegistries;
|
||||
import fr.koka99cab.sanctuary26.anotherworld.registry.AnotherWorldBlocks;
|
||||
import fr.koka99cab.sanctuary26.anotherworld.vault.VaultService;
|
||||
import fr.koka99cab.sanctuary26.sanctuary.api.backroom.BackroomMaterialApi;
|
||||
import net.fabricmc.api.ModInitializer;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import org.slf4j.Logger;
|
||||
@@ -26,6 +28,7 @@ public final class AnotherWorldMod implements ModInitializer {
|
||||
@Override
|
||||
public void onInitialize() {
|
||||
AnotherWorldRegistries.initialize();
|
||||
BackroomMaterialApi.registerAnomalyMaterial(AnotherWorldBlocks.TURQUOISE_MOSS);
|
||||
VaultService.initialize();
|
||||
WeatherCompassItem.registerFrameInteraction();
|
||||
CanonMod.initialize();
|
||||
|
||||
+11
-1
@@ -113,6 +113,12 @@ public final class VaultData extends SavedData {
|
||||
&& address.pos() == center.asLong() && address.owner().equals(ownerId));
|
||||
}
|
||||
|
||||
public boolean hasRegisteredVault(UUID owner) {
|
||||
if (owner == null) return false;
|
||||
String ownerId = owner.toString();
|
||||
return vaults.stream().anyMatch(address -> address.owner().equals(ownerId));
|
||||
}
|
||||
|
||||
public void unregisterNear(ServerLevel level, BlockPos member) {
|
||||
String dimension = dimension(level);
|
||||
boolean changed = vaults.removeIf(address -> {
|
||||
@@ -163,7 +169,7 @@ public final class VaultData extends SavedData {
|
||||
|
||||
/** Transfers the exact 20% raid share into the attacker's unlimited balances without item spam. */
|
||||
public long transferRaid(UUID owner, UUID attacker) {
|
||||
if (owner == null || attacker == null || owner.equals(attacker)) return 0L;
|
||||
if (!canReceiveRaidLoot(owner, attacker, hasRegisteredVault(attacker))) return 0L;
|
||||
Account source = accounts.get(owner);
|
||||
if (source == null) return 0L;
|
||||
Account target = accounts.computeIfAbsent(attacker, ignored -> new Account());
|
||||
@@ -180,6 +186,10 @@ public final class VaultData extends SavedData {
|
||||
return total;
|
||||
}
|
||||
|
||||
static boolean canReceiveRaidLoot(UUID owner, UUID attacker, boolean attackerHasRegisteredVault) {
|
||||
return owner != null && attacker != null && !owner.equals(attacker) && attackerHasRegisteredVault;
|
||||
}
|
||||
|
||||
public long protectionRemaining(UUID owner, long gameTime) {
|
||||
Account account = accounts.get(owner);
|
||||
return account == null ? 0L : Math.max(0L, account.protectedUntil - gameTime);
|
||||
|
||||
+11
@@ -48,6 +48,10 @@ public final class VaultDrillingService {
|
||||
attacker.sendSystemMessage(Component.translatable("message.anotherworld.safe_vault.inactive"));
|
||||
return false;
|
||||
}
|
||||
if (!data.hasRegisteredVault(attacker.getUUID())) {
|
||||
attacker.sendSystemMessage(Component.translatable("message.anotherworld.safe_vault.raid_requires_vault"));
|
||||
return false;
|
||||
}
|
||||
if (!data.hasContents(formed.owner())) {
|
||||
attacker.sendSystemMessage(Component.translatable("message.anotherworld.safe_vault.empty"));
|
||||
return false;
|
||||
@@ -104,6 +108,13 @@ public final class VaultDrillingService {
|
||||
attacker.stopUsingItem();
|
||||
return;
|
||||
}
|
||||
if (!VaultData.get(level.getServer()).hasRegisteredVault(attacker.getUUID())) {
|
||||
cancel(attacker, false);
|
||||
attacker.sendOverlayMessage(Component.translatable(
|
||||
"message.anotherworld.safe_vault.raid_requires_vault"));
|
||||
attacker.stopUsingItem();
|
||||
return;
|
||||
}
|
||||
|
||||
int elapsed = session.elapsedTicks() + 1;
|
||||
Session progressed = session.withElapsed(elapsed);
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
"java": ">=25",
|
||||
"fabric-api": ">=0.154.2+26.2",
|
||||
"ambiance": "*",
|
||||
"sanctuary": ">=0.0.0-alpha.117"
|
||||
"sanctuary": ">=0.0.0-alpha.131"
|
||||
},
|
||||
"custom": {
|
||||
"sanctuary26": {
|
||||
|
||||
+10
@@ -1,5 +1,7 @@
|
||||
package fr.koka99cab.sanctuary26.anotherworld.vault;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
public final class VaultRulesSmoke {
|
||||
private VaultRulesSmoke() {
|
||||
}
|
||||
@@ -55,6 +57,14 @@ public final class VaultRulesSmoke {
|
||||
require(VaultRules.raidLoot(Integer.MAX_VALUE) == 429_496_730L, "32-bit balance raid");
|
||||
require(VaultRules.raidLoot(Long.MAX_VALUE) == 1_844_674_407_370_955_162L,
|
||||
"64-bit balance raid without overflow");
|
||||
UUID owner = UUID.fromString("00000000-0000-0000-0000-000000000001");
|
||||
UUID attacker = UUID.fromString("00000000-0000-0000-0000-000000000002");
|
||||
require(!VaultData.canReceiveRaidLoot(owner, attacker, false),
|
||||
"an attacker without a registered vault cannot receive raid loot");
|
||||
require(VaultData.canReceiveRaidLoot(owner, attacker, true),
|
||||
"an attacker with a registered vault can receive raid loot");
|
||||
require(!VaultData.canReceiveRaidLoot(owner, owner, true),
|
||||
"a vault owner cannot receive loot from their own vault");
|
||||
require(VaultRules.saturatingAdd(Long.MAX_VALUE - 2L, 10L) == Long.MAX_VALUE,
|
||||
"balance saturation without overflow");
|
||||
for (long balance : new long[] {0L, Integer.MAX_VALUE, (long) Integer.MAX_VALUE + 1L, Long.MAX_VALUE}) {
|
||||
|
||||
+760
-236
File diff suppressed because it is too large
Load Diff
+6
-6
@@ -16,19 +16,19 @@ ambiance_version=0.0.0-alpha.22
|
||||
ambiance_lifecycle=active
|
||||
redstoner_version=0.0.0-alpha.13
|
||||
redstoner_lifecycle=active
|
||||
anotherworld_version=0.0.0-alpha.55
|
||||
anotherworld_version=0.0.0-alpha.57
|
||||
anotherworld_lifecycle=active
|
||||
itsalive_version=0.0.0-alpha.37
|
||||
itsalive_version=0.0.0-alpha.38
|
||||
itsalive_lifecycle=active
|
||||
ouch_version=0.0.0-alpha.11
|
||||
ouch_lifecycle=active
|
||||
masterkey_version=0.0.0-alpha.9
|
||||
masterkey_lifecycle=active
|
||||
iliketomoveit_version=0.0.0-alpha.16
|
||||
iliketomoveit_version=0.0.0-alpha.18
|
||||
iliketomoveit_lifecycle=active
|
||||
onlyfun_version=0.0.0-alpha.25
|
||||
onlyfun_version=0.0.0-alpha.27
|
||||
onlyfun_lifecycle=active
|
||||
sanctuary_version=0.0.0-alpha.126
|
||||
sanctuary_version=0.0.0-alpha.131
|
||||
sanctuary_lifecycle=active
|
||||
pack_version=26.2.0-alpha.223
|
||||
pack_version=26.2.0-alpha.226
|
||||
maven_group=fr.koka99cab.sanctuary26
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
# Magic Carpet et Totem de Notch
|
||||
|
||||
I Like To Move It possède l'objet, la plateforme temporaire, la remise physique
|
||||
de la récompense et la compatibilité de l'ancien Totem. Sanctuary reste
|
||||
propriétaire du jalon mondial et de la réservation unique ; Only Fun possède le
|
||||
catalogue et applique le jalon avant d'exposer l'offre.
|
||||
|
||||
## Identités et provenance
|
||||
|
||||
| Rôle | ID | Statut |
|
||||
|---|---|---|
|
||||
| objet canonique | `iliketomoveit:magic_carpet` | distribué après Notch puis achetable au Shop |
|
||||
| Totem historique | `portals:notch_totem` | alias caché, lisible et converti vers l'objet canonique |
|
||||
| jalon mondial | `sanctuary:notch_defeated` | autorité serveur Sanctuary |
|
||||
|
||||
La conversion utilise `ItemStack.transmuteCopy` : quantité, composants et nom
|
||||
personnalisé sont conservés. Les inventaires joueurs sont traités à la connexion,
|
||||
les conteneurs vanilla lors de leur chargement et les entités d'item dès leur
|
||||
tick serveur. Un Totem produit exactement un tapis et les nouveaux mondes ne
|
||||
distribuent jamais l'ancien ID. La donnée I Like To Move It passe en v4 ; une
|
||||
pile déjà convertie ne correspond plus à l'alias et ne peut donc pas être
|
||||
convertie une seconde fois.
|
||||
|
||||
Chaque tapis porte une provenance Sanctuary sans autorité de gameplay :
|
||||
`notch_reward`, `shop_purchase` ou `legacy_notch_totem`. Le serveur ne se fie
|
||||
jamais à cette étiquette pour autoriser un achat ; elle sert à l'historique, au
|
||||
diagnostic et à l'infobulle.
|
||||
|
||||
## Première récompense
|
||||
|
||||
Après la première victoire mondiale, Sanctuary réserve atomiquement la
|
||||
récompense puis I Like To Move It crée un coffre déterministe près de la maison
|
||||
de Notch. Le tapis unique est placé au centre et un feu d'artifice signale le
|
||||
coffre. Sanctuary ne marque la livraison terminée qu'après avoir retrouvé ou
|
||||
créé ce coffre.
|
||||
|
||||
Les états `initial_reward_claimed` et `initial_reward_delivered` sont distincts.
|
||||
Une reprise entre les deux relance la création de façon idempotente ; un coffre
|
||||
déjà créé et marqué est reconnu sans recevoir de second tapis. Les anciennes
|
||||
sauvegardes v1 dont la récompense était déjà déclarée remise sont migrées vers
|
||||
l'état livré afin de ne rien dupliquer.
|
||||
|
||||
## Plateforme temporaire
|
||||
|
||||
Tenir le tapis en main principale ou secondaire maintient neuf blocs de verre
|
||||
sous le joueur. Seul l'air est remplacé : terrain, liquides, plantes et blocs
|
||||
construits restent intacts. Le joueur conserve ses capacités normales ; seul le
|
||||
dégât de chute est réinitialisé tant que la plateforme est active.
|
||||
|
||||
Chaque position possède un ensemble implicite de propriétaires. Deux tapis
|
||||
peuvent se chevaucher et le verre ne disparaît qu'après le départ du dernier.
|
||||
Le nettoyage est effectué côté serveur au relâchement, à la déconnexion, au
|
||||
changement de dimension et pendant l'arrêt normal du serveur.
|
||||
|
||||
## Remplacement au Shop
|
||||
|
||||
Only Fun associe le Magic Carpet au resolver public
|
||||
`SanctuaryWorldMilestones`. Avant `sanctuary:notch_defeated`, l'offre est
|
||||
absente des snapshots et toute tentative réseau est rejetée par Sanctuary.
|
||||
Après la victoire, elle rejoint la rotation normale au prix configuré de
|
||||
1 536 rubis et 128 saphirs : les saphirs débloquent le catalogue, les rubis
|
||||
paient chaque remplacement et la Mailbox effectue la livraison différée.
|
||||
|
||||
Un achat porte la provenance `shop_purchase`, est journalisé comme remplacement
|
||||
et ne touche jamais aux états de réservation ou de coffre de la récompense
|
||||
initiale.
|
||||
+19
-10
@@ -32,6 +32,14 @@ tasks.register("alphaPortalContractSmoke", JavaExec) {
|
||||
dependsOn tasks.named("testClasses")
|
||||
}
|
||||
|
||||
tasks.register("magicCarpetLedgerSmoke", JavaExec) {
|
||||
group = "verification"
|
||||
description = "Checks Magic Carpet overlap, dimension-change and shutdown cleanup ownership."
|
||||
classpath = sourceSets.test.runtimeClasspath
|
||||
mainClass = "fr.koka99cab.sanctuary26.iliketomoveit.transport.MagicCarpetLedgerSmoke"
|
||||
dependsOn tasks.named("testClasses")
|
||||
}
|
||||
|
||||
tasks.named("test") {
|
||||
// Runtime contracts are executable smoke tasks, not JUnit engine tests.
|
||||
failOnNoDiscoveredTests = false
|
||||
@@ -100,11 +108,11 @@ tasks.register("verifyWaystones") {
|
||||
if (!missingResources.isEmpty()) throw new GradleException("Missing Waystone resources: ${missingResources}")
|
||||
|
||||
def manifest = new JsonSlurper().parse(file("src/main/resources/fabric.mod.json"))
|
||||
if (manifest.custom?.sanctuary26?.data_version != 3
|
||||
if (manifest.custom?.sanctuary26?.data_version != 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 5 and its Another World alpha.46/Sanctuary alpha.21 dependencies")
|
||||
|| manifest.depends?.sanctuary != ">=0.0.0-alpha.127") {
|
||||
throw new GradleException("I Like To Move It must expose data v4, protocol 5 and its Another World alpha.46/Sanctuary alpha.127 dependencies")
|
||||
}
|
||||
["fr_fr", "en_us", "ru_ru"].each { language ->
|
||||
def translations = new JsonSlurper().parse(file("src/main/resources/assets/iliketomoveit/lang/${language}.json"))
|
||||
@@ -203,9 +211,9 @@ tasks.register("verifyGrappling") {
|
||||
def manifest = new JsonSlurper().parse(file("src/main/resources/fabric.mod.json"))
|
||||
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?.data_version != 4
|
||||
|| manifest.custom?.sanctuary26?.network_protocol != 5) {
|
||||
throw new GradleException("I Like To Move It must keep aggregate data v3/protocol 5 and register its common/client mixins")
|
||||
throw new GradleException("I Like To Move It must expose aggregate data v4/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"))
|
||||
@@ -313,7 +321,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.16"
|
||||
if (project.version.toString() != "0.0.0-alpha.18"
|
||||
|| atlasCavernsMigration.target?.pack_version != "26.2.0-alpha.150"
|
||||
|| atlasCavernsMigration.target?.modules?.iliketomoveit != "0.0.0-alpha.9"
|
||||
|| atlasCavernsMigration.portals?.active_routes != [
|
||||
@@ -349,7 +357,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.16"
|
||||
if (project.version.toString() != "0.0.0-alpha.18"
|
||||
|| recipe.pattern != ["PGP", "GCG", "PBP"]
|
||||
|| recipe.key != [B: "anotherworld:silver_block", C: "anotherworld:aircraft_cockpit",
|
||||
G: "anotherworld:silver_gear", P: "anotherworld:silver_plate"]
|
||||
@@ -378,7 +386,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.16"
|
||||
if (project.version.toString() != "0.0.0-alpha.18"
|
||||
|| 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
|
||||
@@ -410,7 +418,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.16"
|
||||
if (project.version.toString() != "0.0.0-alpha.18"
|
||||
|| 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
|
||||
@@ -444,7 +452,7 @@ tasks.register("verifyAlphaPortal") {
|
||||
def locales = ["fr_fr", "en_us", "ru_ru"].collect {
|
||||
new JsonSlurper().parse(file("src/main/resources/assets/iliketomoveit/lang/${it}.json"))
|
||||
}
|
||||
if (project.version.toString() != "0.0.0-alpha.16"
|
||||
if (project.version.toString() != "0.0.0-alpha.18"
|
||||
|| route.schema_version != 2 || route.source_dimension != "minecraft:overworld"
|
||||
|| route.destination_dimension != "sanctuary:alpha"
|
||||
|| route.destination_resolver != "fixed"
|
||||
@@ -466,6 +474,7 @@ tasks.register("verifyAlphaPortal") {
|
||||
|
||||
tasks.named("check") {
|
||||
dependsOn tasks.named("grapplePhysicsSmoke")
|
||||
dependsOn tasks.named("magicCarpetLedgerSmoke")
|
||||
dependsOn tasks.named("waystoneElevatorRulesSmoke")
|
||||
dependsOn tasks.named("happyGhastCruiseSpeedSmoke")
|
||||
dependsOn tasks.named("verifyWaystones")
|
||||
|
||||
+5
-1
@@ -8,6 +8,8 @@ import fr.koka99cab.sanctuary26.iliketomoveit.pulley.PulleyHeadController;
|
||||
import fr.koka99cab.sanctuary26.iliketomoveit.registry.GrapplingLootTables;
|
||||
import fr.koka99cab.sanctuary26.iliketomoveit.registry.ILikeToMoveItRegistries;
|
||||
import fr.koka99cab.sanctuary26.iliketomoveit.transport.MagicCarpetController;
|
||||
import fr.koka99cab.sanctuary26.iliketomoveit.transport.MagicCarpetMigrationService;
|
||||
import fr.koka99cab.sanctuary26.iliketomoveit.transport.NotchCarpetRewardService;
|
||||
import fr.koka99cab.sanctuary26.iliketomoveit.waystone.WaystoneElevatorController;
|
||||
import net.fabricmc.api.ModInitializer;
|
||||
import net.minecraft.resources.Identifier;
|
||||
@@ -17,7 +19,7 @@ import org.slf4j.LoggerFactory;
|
||||
public final class ILikeToMoveItMod implements ModInitializer {
|
||||
public static final String MOD_ID = "iliketomoveit";
|
||||
public static final String DISPLAY_NAME = "I Like To Move It";
|
||||
public static final int CURRENT_DATA_VERSION = 3;
|
||||
public static final int CURRENT_DATA_VERSION = 4;
|
||||
public static final Logger LOGGER = LoggerFactory.getLogger(MOD_ID);
|
||||
|
||||
public ILikeToMoveItMod() {
|
||||
@@ -37,6 +39,8 @@ public final class ILikeToMoveItMod implements ModInitializer {
|
||||
GrappleController.register();
|
||||
PulleyHeadController.register();
|
||||
MagicCarpetController.register();
|
||||
MagicCarpetMigrationService.register();
|
||||
NotchCarpetRewardService.register();
|
||||
WaystoneElevatorController.initialize();
|
||||
LOGGER.info("[{}] Portals, vehicles, grappling, pulleys, Magic Carpet and dimension-local Waystones ready (data v{}, network v{}).",
|
||||
DISPLAY_NAME, CURRENT_DATA_VERSION, ILikeToMoveItNetworking.PROTOCOL_VERSION);
|
||||
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
package fr.koka99cab.sanctuary26.iliketomoveit.item;
|
||||
|
||||
import fr.koka99cab.sanctuary26.sanctuary.api.item.SanctuaryItemProvenance;
|
||||
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;
|
||||
|
||||
/** Epic carpet with explicit behaviour and provenance in its tooltip. */
|
||||
public final class MagicCarpetItem extends Item {
|
||||
public MagicCarpetItem(Properties properties) {
|
||||
super(properties);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void appendHoverText(ItemStack stack, TooltipContext context, TooltipDisplay display,
|
||||
Consumer<Component> tooltip, TooltipFlag flag) {
|
||||
tooltip.accept(Component.translatable("item.iliketomoveit.magic_carpet.description")
|
||||
.withStyle(ChatFormatting.GRAY));
|
||||
var origin = SanctuaryItemProvenance.origin(stack);
|
||||
if (SanctuaryItemProvenance.NOTCH_REWARD.equals(origin)) {
|
||||
tooltip.accept(Component.translatable("item.iliketomoveit.magic_carpet.origin.notch")
|
||||
.withStyle(ChatFormatting.GOLD));
|
||||
} else if (SanctuaryItemProvenance.SHOP_PURCHASE.equals(origin)) {
|
||||
tooltip.accept(Component.translatable("item.iliketomoveit.magic_carpet.origin.shop")
|
||||
.withStyle(ChatFormatting.AQUA));
|
||||
} else if (SanctuaryItemProvenance.LEGACY_NOTCH_TOTEM.equals(origin)) {
|
||||
tooltip.accept(Component.translatable("item.iliketomoveit.magic_carpet.origin.legacy")
|
||||
.withStyle(ChatFormatting.DARK_PURPLE));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isFoil(ItemStack stack) {
|
||||
return SanctuaryItemProvenance.hasOrigin(stack, SanctuaryItemProvenance.NOTCH_REWARD)
|
||||
|| super.isFoil(stack);
|
||||
}
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
package fr.koka99cab.sanctuary26.iliketomoveit.mixin;
|
||||
|
||||
import fr.koka99cab.sanctuary26.iliketomoveit.transport.MagicCarpetMigration;
|
||||
import net.minecraft.world.Container;
|
||||
import net.minecraft.world.level.block.entity.BaseContainerBlockEntity;
|
||||
import net.minecraft.world.level.storage.ValueInput;
|
||||
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;
|
||||
|
||||
/** Converts historical Totems as loaded chests and other vanilla containers are decoded. */
|
||||
@Mixin(BaseContainerBlockEntity.class)
|
||||
abstract class MagicCarpetContainerMixin {
|
||||
@Inject(method = "loadAdditional", at = @At("TAIL"))
|
||||
private void sanctuary26$migrateNotchTotem(ValueInput input, CallbackInfo ci) {
|
||||
MagicCarpetMigration.migrateContainer((Container) (Object) this);
|
||||
}
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
package fr.koka99cab.sanctuary26.iliketomoveit.mixin;
|
||||
|
||||
import fr.koka99cab.sanctuary26.iliketomoveit.transport.MagicCarpetMigration;
|
||||
import net.minecraft.world.entity.item.ItemEntity;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.injection.At;
|
||||
import org.spongepowered.asm.mixin.injection.Inject;
|
||||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
|
||||
|
||||
/** Converts historical Totems already present as loaded item entities. */
|
||||
@Mixin(ItemEntity.class)
|
||||
abstract class MagicCarpetItemEntityMixin {
|
||||
@Inject(method = "tick", at = @At("HEAD"))
|
||||
private void sanctuary26$migrateNotchTotem(CallbackInfo ci) {
|
||||
ItemEntity self = (ItemEntity) (Object) this;
|
||||
ItemStack current = self.getItem();
|
||||
ItemStack replacement = MagicCarpetMigration.migrateStack(current);
|
||||
if (replacement != current) self.setItem(replacement);
|
||||
}
|
||||
}
|
||||
+6
-1
@@ -3,6 +3,7 @@ package fr.koka99cab.sanctuary26.iliketomoveit.registry;
|
||||
import fr.koka99cab.sanctuary26.iliketomoveit.ILikeToMoveItMod;
|
||||
import fr.koka99cab.sanctuary26.iliketomoveit.item.AircraftItem;
|
||||
import fr.koka99cab.sanctuary26.iliketomoveit.item.GrapplingRodItem;
|
||||
import fr.koka99cab.sanctuary26.iliketomoveit.item.MagicCarpetItem;
|
||||
import net.minecraft.core.Registry;
|
||||
import net.minecraft.core.registries.BuiltInRegistries;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
@@ -25,7 +26,11 @@ public final class ILikeToMoveItItems {
|
||||
key -> new BoatItem(ILikeToMoveItEntityTypes.MAGIC_CLOUD,
|
||||
new Item.Properties().stacksTo(1).rarity(Rarity.UNCOMMON).setId(key)));
|
||||
public static final Item MAGIC_CARPET = register("magic_carpet",
|
||||
key -> new Item(new Item.Properties().stacksTo(1).rarity(Rarity.EPIC).setId(key)));
|
||||
key -> new MagicCarpetItem(new Item.Properties().stacksTo(1).rarity(Rarity.EPIC).setId(key)));
|
||||
/** Hidden read-preserving alias for inventories saved by the historical MagicCarpet mod. */
|
||||
public static final Item LEGACY_NOTCH_TOTEM = register(
|
||||
Identifier.fromNamespaceAndPath("portals", "notch_totem"),
|
||||
key -> new MagicCarpetItem(new Item.Properties().stacksTo(1).rarity(Rarity.EPIC).setId(key)));
|
||||
public static final Item GRAPPLING_HAND = register("grappling_hand",
|
||||
key -> new Item(new Item.Properties().setId(key)));
|
||||
public static final Item GRAPPLING_ROD = register("grappling_rod",
|
||||
|
||||
+16
-25
@@ -1,10 +1,7 @@
|
||||
package fr.koka99cab.sanctuary26.iliketomoveit.transport;
|
||||
|
||||
import fr.koka99cab.sanctuary26.iliketomoveit.ILikeToMoveItMod;
|
||||
import fr.koka99cab.sanctuary26.iliketomoveit.registry.ILikeToMoveItItems;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Iterator;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
@@ -27,7 +24,7 @@ public final class MagicCarpetController {
|
||||
private static final int RADIUS = 1;
|
||||
private static final Block CARPET_BLOCK = Blocks.GLASS;
|
||||
private static final BlockState CARPET_STATE = CARPET_BLOCK.defaultBlockState();
|
||||
private static final Map<UUID, CarpetState> ACTIVE_CARPETS = new HashMap<>();
|
||||
private static final MagicCarpetLedger ACTIVE_CARPETS = new MagicCarpetLedger();
|
||||
|
||||
private MagicCarpetController() {
|
||||
}
|
||||
@@ -45,8 +42,8 @@ public final class MagicCarpetController {
|
||||
connected.add(id);
|
||||
boolean active = !player.isSpectator() && (isMagicCarpet(player.getMainHandItem()) || isMagicCarpet(player.getOffhandItem()));
|
||||
if (active) {
|
||||
if (!ACTIVE_CARPETS.containsKey(id)) {
|
||||
ACTIVE_CARPETS.put(id, CarpetState.empty(player.level().dimension()));
|
||||
if (!ACTIVE_CARPETS.contains(id)) {
|
||||
ACTIVE_CARPETS.track(id, player.level().dimension(), Set.of());
|
||||
player.sendOverlayMessage(Component.translatable("message.iliketomoveit.magic_carpet.enabled"));
|
||||
}
|
||||
updateCarpet(id, player);
|
||||
@@ -54,27 +51,26 @@ public final class MagicCarpetController {
|
||||
deactivate(server, id, true);
|
||||
}
|
||||
}
|
||||
Iterator<Map.Entry<UUID, CarpetState>> iterator = ACTIVE_CARPETS.entrySet().iterator();
|
||||
while (iterator.hasNext()) {
|
||||
Map.Entry<UUID, CarpetState> entry = iterator.next();
|
||||
if (!connected.contains(entry.getKey())) {
|
||||
removePositions(server, entry.getKey(), entry.getValue(), entry.getValue().positions());
|
||||
iterator.remove();
|
||||
for (UUID owner : ACTIVE_CARPETS.owners()) {
|
||||
if (!connected.contains(owner)) {
|
||||
MagicCarpetLedger.State state = ACTIVE_CARPETS.remove(owner);
|
||||
if (state != null) removePositions(server, owner, state, state.positions());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isMagicCarpet(ItemStack stack) {
|
||||
return !stack.isEmpty() && stack.is(ILikeToMoveItItems.MAGIC_CARPET);
|
||||
return MagicCarpetMigration.isAnyCarpet(stack);
|
||||
}
|
||||
|
||||
private static void updateCarpet(UUID owner, ServerPlayer player) {
|
||||
ServerLevel level = player.level();
|
||||
CarpetState previous = ACTIVE_CARPETS.getOrDefault(owner, CarpetState.empty(level.dimension()));
|
||||
MagicCarpetLedger.State previous = ACTIVE_CARPETS.state(owner, level.dimension());
|
||||
Set<BlockPos> target = carpetPositions(player.blockPosition().below(player.isCrouching() ? 2 : 1));
|
||||
if (!previous.dimension().equals(level.dimension())) {
|
||||
removePositions(level.getServer(), owner, previous, previous.positions());
|
||||
previous = CarpetState.empty(level.dimension());
|
||||
ACTIVE_CARPETS.remove(owner);
|
||||
previous = ACTIVE_CARPETS.state(owner, level.dimension());
|
||||
} else {
|
||||
Set<BlockPos> stale = new HashSet<>(previous.positions());
|
||||
stale.removeAll(target);
|
||||
@@ -91,7 +87,7 @@ public final class MagicCarpetController {
|
||||
placed.add(pos);
|
||||
}
|
||||
}
|
||||
ACTIVE_CARPETS.put(owner, new CarpetState(level.dimension(), Set.copyOf(placed)));
|
||||
ACTIVE_CARPETS.track(owner, level.dimension(), placed);
|
||||
player.resetFallDistance();
|
||||
}
|
||||
|
||||
@@ -104,14 +100,14 @@ public final class MagicCarpetController {
|
||||
}
|
||||
|
||||
private static void deactivate(MinecraftServer server, UUID owner, boolean notify) {
|
||||
CarpetState state = ACTIVE_CARPETS.remove(owner);
|
||||
MagicCarpetLedger.State state = ACTIVE_CARPETS.remove(owner);
|
||||
if (state == null) return;
|
||||
removePositions(server, owner, state, state.positions());
|
||||
ServerPlayer player = server.getPlayerList().getPlayer(owner);
|
||||
if (notify && player != null) player.sendOverlayMessage(Component.translatable("message.iliketomoveit.magic_carpet.disabled"));
|
||||
}
|
||||
|
||||
private static void removePositions(MinecraftServer server, UUID owner, CarpetState state, Set<BlockPos> positions) {
|
||||
private static void removePositions(MinecraftServer server, UUID owner, MagicCarpetLedger.State state, Set<BlockPos> positions) {
|
||||
ServerLevel level = server.getLevel(state.dimension());
|
||||
if (level == null) return;
|
||||
for (BlockPos pos : positions) {
|
||||
@@ -122,18 +118,13 @@ public final class MagicCarpetController {
|
||||
}
|
||||
|
||||
private static boolean isTrackedByAnother(UUID owner, ResourceKey<Level> dimension, BlockPos pos) {
|
||||
return ACTIVE_CARPETS.entrySet().stream().anyMatch(entry -> !entry.getKey().equals(owner)
|
||||
&& entry.getValue().dimension().equals(dimension) && entry.getValue().positions().contains(pos));
|
||||
return ACTIVE_CARPETS.trackedByAnother(owner, dimension, pos);
|
||||
}
|
||||
|
||||
private static void reset(MinecraftServer server) {
|
||||
Map<UUID, CarpetState> copy = new HashMap<>(ACTIVE_CARPETS);
|
||||
ACTIVE_CARPETS.clear();
|
||||
Map<UUID, MagicCarpetLedger.State> copy = ACTIVE_CARPETS.drain();
|
||||
copy.forEach((owner, state) -> removePositions(server, owner, state, state.positions()));
|
||||
ILikeToMoveItMod.LOGGER.debug("Magic Carpet temporary platforms cleaned.");
|
||||
}
|
||||
|
||||
private record CarpetState(ResourceKey<Level> dimension, Set<BlockPos> positions) {
|
||||
private static CarpetState empty(ResourceKey<Level> dimension) { return new CarpetState(dimension, Set.of()); }
|
||||
}
|
||||
}
|
||||
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
package fr.koka99cab.sanctuary26.iliketomoveit.transport;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.world.level.Level;
|
||||
|
||||
/** Pure ownership ledger preventing overlapping temporary platforms from deleting each other. */
|
||||
public final class MagicCarpetLedger {
|
||||
private final Map<UUID, State> carpets = new HashMap<>();
|
||||
|
||||
public boolean contains(UUID owner) {
|
||||
return carpets.containsKey(owner);
|
||||
}
|
||||
|
||||
public State state(UUID owner, ResourceKey<Level> dimension) {
|
||||
return carpets.getOrDefault(owner, State.empty(dimension));
|
||||
}
|
||||
|
||||
public void track(UUID owner, ResourceKey<Level> dimension, Set<BlockPos> positions) {
|
||||
carpets.put(owner, new State(dimension, Set.copyOf(positions)));
|
||||
}
|
||||
|
||||
public State remove(UUID owner) {
|
||||
return carpets.remove(owner);
|
||||
}
|
||||
|
||||
public Set<UUID> owners() {
|
||||
return Set.copyOf(carpets.keySet());
|
||||
}
|
||||
|
||||
public boolean trackedByAnother(UUID owner, ResourceKey<Level> dimension, BlockPos pos) {
|
||||
return carpets.entrySet().stream().anyMatch(entry -> !entry.getKey().equals(owner)
|
||||
&& entry.getValue().dimension().equals(dimension)
|
||||
&& entry.getValue().positions().contains(pos));
|
||||
}
|
||||
|
||||
public Map<UUID, State> drain() {
|
||||
Map<UUID, State> copy = Map.copyOf(carpets);
|
||||
carpets.clear();
|
||||
return copy;
|
||||
}
|
||||
|
||||
public record State(ResourceKey<Level> dimension, Set<BlockPos> positions) {
|
||||
public State {
|
||||
positions = Set.copyOf(positions);
|
||||
}
|
||||
|
||||
private static State empty(ResourceKey<Level> dimension) {
|
||||
return new State(dimension, Set.of());
|
||||
}
|
||||
}
|
||||
}
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
package fr.koka99cab.sanctuary26.iliketomoveit.transport;
|
||||
|
||||
import fr.koka99cab.sanctuary26.iliketomoveit.registry.ILikeToMoveItItems;
|
||||
import fr.koka99cab.sanctuary26.sanctuary.api.item.SanctuaryItemProvenance;
|
||||
import net.minecraft.world.Container;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
|
||||
/** Read-preserving conversion from the historical Totem identity to the canonical carpet. */
|
||||
public final class MagicCarpetMigration {
|
||||
private MagicCarpetMigration() {
|
||||
}
|
||||
|
||||
public static boolean isAnyCarpet(ItemStack stack) {
|
||||
return stack != null && !stack.isEmpty()
|
||||
&& (stack.is(ILikeToMoveItItems.MAGIC_CARPET)
|
||||
|| stack.is(ILikeToMoveItItems.LEGACY_NOTCH_TOTEM));
|
||||
}
|
||||
|
||||
public static ItemStack migrateStack(ItemStack stack) {
|
||||
if (stack == null || stack.isEmpty() || !stack.is(ILikeToMoveItItems.LEGACY_NOTCH_TOTEM)) return stack;
|
||||
ItemStack migrated = stack.transmuteCopy(ILikeToMoveItItems.MAGIC_CARPET, stack.getCount());
|
||||
SanctuaryItemProvenance.stampIfAbsent(migrated, SanctuaryItemProvenance.LEGACY_NOTCH_TOTEM);
|
||||
return migrated;
|
||||
}
|
||||
|
||||
public static int migrateContainer(Container container) {
|
||||
if (container == null) return 0;
|
||||
int migrated = 0;
|
||||
for (int slot = 0; slot < container.getContainerSize(); slot++) {
|
||||
ItemStack current = container.getItem(slot);
|
||||
ItemStack replacement = migrateStack(current);
|
||||
if (replacement == current) continue;
|
||||
container.setItem(slot, replacement);
|
||||
migrated++;
|
||||
}
|
||||
if (migrated > 0) container.setChanged();
|
||||
return migrated;
|
||||
}
|
||||
}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
package fr.koka99cab.sanctuary26.iliketomoveit.transport;
|
||||
|
||||
import fr.koka99cab.sanctuary26.iliketomoveit.ILikeToMoveItMod;
|
||||
import fr.koka99cab.sanctuary26.iliketomoveit.data.ILikeToMoveItData;
|
||||
import net.fabricmc.fabric.api.event.lifecycle.v1.ServerTickEvents;
|
||||
import net.fabricmc.fabric.api.networking.v1.ServerPlayConnectionEvents;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
|
||||
/** Converts online inventories once their historical alias has been decoded. */
|
||||
public final class MagicCarpetMigrationService {
|
||||
private MagicCarpetMigrationService() {
|
||||
}
|
||||
|
||||
public static void register() {
|
||||
ServerPlayConnectionEvents.JOIN.register((handler, sender, server) -> migrate(handler.player));
|
||||
ServerTickEvents.END_SERVER_TICK.register(MagicCarpetMigrationService::tick);
|
||||
}
|
||||
|
||||
private static void tick(MinecraftServer server) {
|
||||
ILikeToMoveItData.get(server);
|
||||
if (server.overworld().getGameTime() % 20L != 0L) return;
|
||||
for (ServerPlayer player : server.getPlayerList().getPlayers()) migrate(player);
|
||||
}
|
||||
|
||||
private static void migrate(ServerPlayer player) {
|
||||
int migrated = MagicCarpetMigration.migrateContainer(player.getInventory());
|
||||
migrated += MagicCarpetMigration.migrateContainer(player.getEnderChestInventory());
|
||||
if (migrated > 0) {
|
||||
ILikeToMoveItMod.LOGGER.info("[Magic Carpet] Migrated {} historical Totem stack(s) for {}.",
|
||||
migrated, player.getGameProfile().name());
|
||||
}
|
||||
}
|
||||
}
|
||||
+124
@@ -0,0 +1,124 @@
|
||||
package fr.koka99cab.sanctuary26.iliketomoveit.transport;
|
||||
|
||||
import fr.koka99cab.sanctuary26.iliketomoveit.ILikeToMoveItMod;
|
||||
import fr.koka99cab.sanctuary26.iliketomoveit.registry.ILikeToMoveItItems;
|
||||
import fr.koka99cab.sanctuary26.sanctuary.api.item.SanctuaryItemProvenance;
|
||||
import fr.koka99cab.sanctuary26.sanctuary.api.notch.SanctuaryNotchMilestone;
|
||||
import fr.koka99cab.sanctuary26.sanctuary.api.world.SanctuaryAlphaWorld;
|
||||
import fr.koka99cab.sanctuary26.sanctuary.world.SanctuaryDimensions;
|
||||
import it.unimi.dsi.fastutil.ints.IntArrayList;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import net.fabricmc.fabric.api.event.lifecycle.v1.ServerTickEvents;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.component.DataComponents;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.world.Container;
|
||||
import net.minecraft.world.entity.projectile.FireworkRocketEntity;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.item.Items;
|
||||
import net.minecraft.world.item.component.FireworkExplosion;
|
||||
import net.minecraft.world.item.component.Fireworks;
|
||||
import net.minecraft.world.level.block.Block;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.block.entity.ChestBlockEntity;
|
||||
|
||||
/** Idempotently materializes the globally reserved Magic Carpet in Alpha. */
|
||||
public final class NotchCarpetRewardService {
|
||||
private static final int REWARD_SLOT = 13;
|
||||
|
||||
private NotchCarpetRewardService() {
|
||||
}
|
||||
|
||||
public static void register() {
|
||||
ServerTickEvents.END_SERVER_TICK.register(NotchCarpetRewardService::tick);
|
||||
}
|
||||
|
||||
private static void tick(MinecraftServer server) {
|
||||
if (server.overworld().getGameTime() % 20L != 0L) return;
|
||||
var state = SanctuaryNotchMilestone.snapshot(server);
|
||||
if (!state.defeated() || state.initialRewardDelivered()) return;
|
||||
if (!state.initialRewardClaimed() && !SanctuaryNotchMilestone.claimInitialReward(server)) return;
|
||||
ServerLevel alpha = server.getLevel(SanctuaryDimensions.ALPHA);
|
||||
if (alpha == null) return;
|
||||
BlockPos home = SanctuaryAlphaWorld.residence(alpha.getSeed());
|
||||
if (!alpha.isLoaded(home)) return;
|
||||
RewardChest result = ensureRewardChest(alpha, home);
|
||||
if (result == null || !SanctuaryNotchMilestone.completeInitialReward(server)) return;
|
||||
if (result.created()) {
|
||||
launchFirework(alpha, result.pos());
|
||||
server.getPlayerList().broadcastSystemMessage(
|
||||
Component.translatable("message.iliketomoveit.magic_carpet.reward_created"), false);
|
||||
}
|
||||
ILikeToMoveItMod.LOGGER.info(
|
||||
"[Magic Carpet] Completed unique Notch reward at {} (origin=notch_reward, recovered={}).",
|
||||
result.pos().toShortString(), !result.created());
|
||||
}
|
||||
|
||||
private static RewardChest ensureRewardChest(ServerLevel level, BlockPos home) {
|
||||
List<BlockPos> candidates = candidates(home);
|
||||
for (BlockPos pos : candidates) {
|
||||
if (containsInitialReward(level, pos)) return new RewardChest(pos, false);
|
||||
}
|
||||
for (BlockPos pos : candidates) {
|
||||
if (!level.getBlockState(pos).isAir() || !level.getBlockState(pos.above()).isAir()
|
||||
|| level.getBlockState(pos.below()).isAir()) continue;
|
||||
if (!level.setBlock(pos, Blocks.CHEST.defaultBlockState(), Block.UPDATE_ALL)) continue;
|
||||
if (level.getBlockEntity(pos) instanceof ChestBlockEntity chest) {
|
||||
chest.setItem(REWARD_SLOT, rewardStack());
|
||||
chest.setChanged();
|
||||
level.sendBlockUpdated(pos, level.getBlockState(pos), level.getBlockState(pos), Block.UPDATE_ALL);
|
||||
return new RewardChest(pos, true);
|
||||
}
|
||||
level.setBlock(pos, Blocks.AIR.defaultBlockState(), Block.UPDATE_ALL);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static boolean containsInitialReward(ServerLevel level, BlockPos pos) {
|
||||
if (level.getBlockState(pos).getBlock() != Blocks.CHEST
|
||||
|| !(level.getBlockEntity(pos) instanceof Container container)) return false;
|
||||
for (ItemStack stack : container) {
|
||||
if (stack.is(ILikeToMoveItItems.MAGIC_CARPET)
|
||||
&& SanctuaryItemProvenance.hasOrigin(stack, SanctuaryItemProvenance.NOTCH_REWARD)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static ItemStack rewardStack() {
|
||||
ItemStack reward = new ItemStack(ILikeToMoveItItems.MAGIC_CARPET);
|
||||
reward.set(DataComponents.CUSTOM_NAME,
|
||||
Component.translatable("item.iliketomoveit.magic_carpet.notch_reward_name"));
|
||||
SanctuaryItemProvenance.stamp(reward, SanctuaryItemProvenance.NOTCH_REWARD);
|
||||
return reward;
|
||||
}
|
||||
|
||||
private static List<BlockPos> candidates(BlockPos home) {
|
||||
ArrayList<BlockPos> positions = new ArrayList<>();
|
||||
for (int y : new int[] {0, 1, -1, 2}) {
|
||||
for (int radius = 2; radius <= 7; radius++) {
|
||||
for (int x = -radius; x <= radius; x++) for (int z = -radius; z <= radius; z++) {
|
||||
if (Math.max(Math.abs(x), Math.abs(z)) != radius) continue;
|
||||
BlockPos candidate = home.offset(x, y, z).immutable();
|
||||
if (SanctuaryAlphaWorld.containsHorizontal(candidate)) positions.add(candidate);
|
||||
}
|
||||
}
|
||||
}
|
||||
return List.copyOf(positions);
|
||||
}
|
||||
|
||||
private static void launchFirework(ServerLevel level, BlockPos pos) {
|
||||
ItemStack rocket = new ItemStack(Items.FIREWORK_ROCKET);
|
||||
FireworkExplosion explosion = new FireworkExplosion(FireworkExplosion.Shape.STAR,
|
||||
new IntArrayList(new int[] {0xE8B923, 0xF1F1F1}),
|
||||
new IntArrayList(new int[] {0x9B59B6}), true, true);
|
||||
rocket.set(DataComponents.FIREWORKS, new Fireworks(1, List.of(explosion)));
|
||||
level.addFreshEntity(new FireworkRocketEntity(level, pos.getX() + 0.5D,
|
||||
pos.getY() + 1.2D, pos.getZ() + 0.5D, rocket));
|
||||
}
|
||||
|
||||
private record RewardChest(BlockPos pos, boolean created) {
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,13 @@
|
||||
"itemGroup.iliketomoveit.transport": "I Like To Move It",
|
||||
"item.iliketomoveit.aircraft": "Aircraft",
|
||||
"item.iliketomoveit.magic_cloud": "Magic Cloud",
|
||||
"item.iliketomoveit.magic_carpet": "Magic Carpet (laboratory)",
|
||||
"item.iliketomoveit.magic_carpet": "Magic Carpet",
|
||||
"item.portals.notch_totem": "Notch Totem (legacy)",
|
||||
"item.iliketomoveit.magic_carpet.notch_reward_name": "Notch's Magic Carpet",
|
||||
"item.iliketomoveit.magic_carpet.description": "Hold it to maintain a temporary 3 × 3 platform beneath your feet.",
|
||||
"item.iliketomoveit.magic_carpet.origin.notch": "Unique world reward from Notch",
|
||||
"item.iliketomoveit.magic_carpet.origin.shop": "Replacement purchased from the Shop",
|
||||
"item.iliketomoveit.magic_carpet.origin.legacy": "Converted from the historical Notch Totem",
|
||||
"block.iliketomoveit.waystone": "Waystone",
|
||||
"block.waystone.waystone": "Waystone (legacy)",
|
||||
"entity.iliketomoveit.aircraft": "Aircraft",
|
||||
@@ -11,6 +17,7 @@
|
||||
"container.iliketomoveit.aircraft_engine": "Aircraft Engine",
|
||||
"message.iliketomoveit.magic_carpet.enabled": "Magic Carpet enabled",
|
||||
"message.iliketomoveit.magic_carpet.disabled": "Magic Carpet disabled",
|
||||
"message.iliketomoveit.magic_carpet.reward_created": "Notch's unique Magic Carpet appeared in a chest near his house.",
|
||||
"subtitles.iliketomoveit.magic_cloud.fly": "Magic Cloud flies",
|
||||
"screen.iliketomoveit.waystone.title": "Waystones",
|
||||
"screen.iliketomoveit.waystone.empty": "No other waystone in this dimension",
|
||||
|
||||
@@ -3,7 +3,13 @@
|
||||
"itemGroup.iliketomoveit.transport": "I Like To Move It",
|
||||
"item.iliketomoveit.aircraft": "Avion",
|
||||
"item.iliketomoveit.magic_cloud": "Nuage magique",
|
||||
"item.iliketomoveit.magic_carpet": "Tapis magique (laboratoire)",
|
||||
"item.iliketomoveit.magic_carpet": "Tapis magique",
|
||||
"item.portals.notch_totem": "Totem de Notch (hérité)",
|
||||
"item.iliketomoveit.magic_carpet.notch_reward_name": "Tapis magique de Notch",
|
||||
"item.iliketomoveit.magic_carpet.description": "Tenu en main, il maintient une plateforme temporaire 3 × 3 sous vos pieds.",
|
||||
"item.iliketomoveit.magic_carpet.origin.notch": "Récompense mondiale unique de Notch",
|
||||
"item.iliketomoveit.magic_carpet.origin.shop": "Remplacement acheté au Shop",
|
||||
"item.iliketomoveit.magic_carpet.origin.legacy": "Converti depuis le Totem de Notch historique",
|
||||
"block.iliketomoveit.waystone": "Pierre de voyage",
|
||||
"block.waystone.waystone": "Pierre de voyage (héritée)",
|
||||
"entity.iliketomoveit.aircraft": "Avion",
|
||||
@@ -11,6 +17,7 @@
|
||||
"container.iliketomoveit.aircraft_engine": "Moteur de l'avion",
|
||||
"message.iliketomoveit.magic_carpet.enabled": "Tapis magique activé",
|
||||
"message.iliketomoveit.magic_carpet.disabled": "Tapis magique désactivé",
|
||||
"message.iliketomoveit.magic_carpet.reward_created": "Le Tapis magique unique de Notch est apparu dans un coffre près de sa maison.",
|
||||
"subtitles.iliketomoveit.magic_cloud.fly": "Le nuage magique vole",
|
||||
"screen.iliketomoveit.waystone.title": "Pierres de voyage",
|
||||
"screen.iliketomoveit.waystone.empty": "Aucune autre pierre dans cette dimension",
|
||||
|
||||
@@ -3,7 +3,13 @@
|
||||
"itemGroup.iliketomoveit.transport": "I Like To Move It",
|
||||
"item.iliketomoveit.aircraft": "Самолёт",
|
||||
"item.iliketomoveit.magic_cloud": "Волшебное облако",
|
||||
"item.iliketomoveit.magic_carpet": "Ковёр-самолёт (лаборатория)",
|
||||
"item.iliketomoveit.magic_carpet": "Ковёр-самолёт",
|
||||
"item.portals.notch_totem": "Тотем Нотча (старый)",
|
||||
"item.iliketomoveit.magic_carpet.notch_reward_name": "Ковёр-самолёт Нотча",
|
||||
"item.iliketomoveit.magic_carpet.description": "В руке поддерживает временную платформу 3 × 3 под ногами.",
|
||||
"item.iliketomoveit.magic_carpet.origin.notch": "Уникальная мировая награда за Нотча",
|
||||
"item.iliketomoveit.magic_carpet.origin.shop": "Замена, купленная в Магазине",
|
||||
"item.iliketomoveit.magic_carpet.origin.legacy": "Преобразован из исторического Тотема Нотча",
|
||||
"block.iliketomoveit.waystone": "Камень пути",
|
||||
"block.waystone.waystone": "Камень пути (старый)",
|
||||
"entity.iliketomoveit.aircraft": "Самолёт",
|
||||
@@ -11,6 +17,7 @@
|
||||
"container.iliketomoveit.aircraft_engine": "Двигатель самолёта",
|
||||
"message.iliketomoveit.magic_carpet.enabled": "Ковёр-самолёт включён",
|
||||
"message.iliketomoveit.magic_carpet.disabled": "Ковёр-самолёт выключен",
|
||||
"message.iliketomoveit.magic_carpet.reward_created": "Уникальный ковёр-самолёт Нотча появился в сундуке возле его дома.",
|
||||
"subtitles.iliketomoveit.magic_cloud.fly": "Волшебное облако летит",
|
||||
"screen.iliketomoveit.waystone.title": "Камни пути",
|
||||
"screen.iliketomoveit.waystone.empty": "В этом измерении нет других камней",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"parent": "minecraft:item/generated",
|
||||
"textures": {
|
||||
"layer0": "minecraft:block/purple_wool"
|
||||
"layer0": "iliketomoveit:item/magic_carpet"
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 366 B |
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"model": {
|
||||
"type": "minecraft:model",
|
||||
"model": "iliketomoveit:item/magic_carpet"
|
||||
}
|
||||
}
|
||||
@@ -33,13 +33,13 @@
|
||||
"java": ">=25",
|
||||
"fabric-api": ">=0.154.2+26.2",
|
||||
"anotherworld": ">=0.0.0-alpha.46",
|
||||
"sanctuary": ">=0.0.0-alpha.21"
|
||||
"sanctuary": ">=0.0.0-alpha.127"
|
||||
},
|
||||
"custom": {
|
||||
"sanctuary26": {
|
||||
"system_type": "gameplay-system",
|
||||
"namespace": "iliketomoveit",
|
||||
"data_version": 3,
|
||||
"data_version": 4,
|
||||
"network_protocol": 5,
|
||||
"lifecycle": "${lifecycle}",
|
||||
"pack_version": "${pack_version}",
|
||||
|
||||
@@ -3,7 +3,9 @@
|
||||
"package": "fr.koka99cab.sanctuary26.iliketomoveit.mixin",
|
||||
"compatibilityLevel": "JAVA_25",
|
||||
"mixins": [
|
||||
"HappyGhastMixin"
|
||||
"HappyGhastMixin",
|
||||
"MagicCarpetContainerMixin",
|
||||
"MagicCarpetItemEntityMixin"
|
||||
],
|
||||
"injectors": {
|
||||
"defaultRequire": 1
|
||||
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
package fr.koka99cab.sanctuary26.iliketomoveit.transport;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.world.level.Level;
|
||||
|
||||
/** Regression checks for overlap, world changes and shutdown cleanup ownership. */
|
||||
public final class MagicCarpetLedgerSmoke {
|
||||
private MagicCarpetLedgerSmoke() {
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
MagicCarpetLedger ledger = new MagicCarpetLedger();
|
||||
UUID first = UUID.fromString("00000000-0000-0000-0000-000000000001");
|
||||
UUID second = UUID.fromString("00000000-0000-0000-0000-000000000002");
|
||||
ResourceKey<Level> firstWorld = ResourceKey.create(Registries.DIMENSION,
|
||||
Identifier.fromNamespaceAndPath("test", "first"));
|
||||
ResourceKey<Level> secondWorld = ResourceKey.create(Registries.DIMENSION,
|
||||
Identifier.fromNamespaceAndPath("test", "second"));
|
||||
BlockPos shared = new BlockPos(4, 70, 4);
|
||||
ledger.track(first, firstWorld, Set.of(shared));
|
||||
ledger.track(second, firstWorld, Set.of(shared));
|
||||
if (!ledger.trackedByAnother(first, firstWorld, shared)) {
|
||||
throw new AssertionError("Overlapping carpets lost their second owner");
|
||||
}
|
||||
ledger.remove(first);
|
||||
if (ledger.trackedByAnother(second, firstWorld, shared)) {
|
||||
throw new AssertionError("Removed owner still controls the shared glass");
|
||||
}
|
||||
ledger.track(second, secondWorld, Set.of(new BlockPos(0, 64, 0)));
|
||||
if (!ledger.state(second, secondWorld).dimension().equals(secondWorld)
|
||||
|| !ledger.state(second, firstWorld).dimension().equals(secondWorld)) {
|
||||
throw new AssertionError("Dimension change did not replace the tracked world");
|
||||
}
|
||||
Map<UUID, MagicCarpetLedger.State> shutdown = ledger.drain();
|
||||
if (shutdown.size() != 1 || !ledger.owners().isEmpty()) {
|
||||
throw new AssertionError("Server shutdown did not atomically drain temporary platforms");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -565,7 +565,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.37"
|
||||
if (project.version.toString() != "0.0.0-alpha.38"
|
||||
|| climateMigration.target?.pack_version != "26.2.0-alpha.157"
|
||||
|| climateMigration.target?.modules?.itsalive != "0.0.0-alpha.24"
|
||||
|| !climateSource.contains("PlayerBlockBreakEvents.AFTER.register")
|
||||
@@ -1322,7 +1322,7 @@ tasks.register("verifyCulinaryReliability") {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (project.version.toString() != "0.0.0-alpha.37"
|
||||
if (project.version.toString() != "0.0.0-alpha.38"
|
||||
|| 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
|
||||
@@ -1360,7 +1360,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.37"
|
||||
if (project.version.toString() != "0.0.0-alpha.38"
|
||||
|| migration.target?.modules?.itsalive != "0.0.0-alpha.29"
|
||||
|| migration.cauldron_batches?.logical_stack_limit != 64
|
||||
|| migration.cauldron_batches?.identity != "same_item_same_components"
|
||||
@@ -1398,7 +1398,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.37"
|
||||
if (project.version.toString() != "0.0.0-alpha.38"
|
||||
|| 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
|
||||
@@ -1446,7 +1446,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.37"
|
||||
if (project.version.toString() != "0.0.0-alpha.38"
|
||||
|| 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
|
||||
@@ -1509,7 +1509,7 @@ tasks.register("verifyCanapliaForbiddenBook") {
|
||||
fr_fr: "Livre interdit",
|
||||
en_us: "Forbidden Book",
|
||||
ru_ru: "Запретная книга"]
|
||||
if (project.version.toString() != "0.0.0-alpha.37"
|
||||
if (project.version.toString() != "0.0.0-alpha.38"
|
||||
|| !pageItem.contains("CanapliaKnowledge.learnRandomUnknown(serverPlayer)")
|
||||
|| !knowledge.contains("unknownVariants(data.personal(player.getUUID()))")
|
||||
|| !knowledge.contains("data.personal(inscription.ownerId())")
|
||||
|
||||
+4
-2
@@ -34,7 +34,8 @@ public final class CanapliaHarvestService {
|
||||
|
||||
public static void onMatureHarvest(ServerLevel level, ServerPlayer player, BlockPos pos, BlockState state) {
|
||||
if (!state.is(CanapliaContent.HEMP_CROP) || !CanapliaContent.HEMP_CROP.isMaxAge(state)) return;
|
||||
CanapliaVariantKey parent = state.getValue(CanapliaHempCropBlock.VARIANT);
|
||||
CanapliaVariantKey parent = CanapliaVariantLifecycle.harvestedFromPlant(
|
||||
state.getValue(CanapliaHempCropBlock.VARIANT));
|
||||
CanapliaKnowledge.discover(player, parent, "harvest");
|
||||
ClimateCropForaging.CropClimate climate = ClimateCropForaging.climate(level.getBiome(pos));
|
||||
AmbianceLunarHarvestApi.Conditions lunar = AmbianceLunarHarvestApi.evaluate(level, pos);
|
||||
@@ -65,7 +66,8 @@ public final class CanapliaHarvestService {
|
||||
if (pos.equals(center)) continue;
|
||||
BlockState state = level.getBlockState(pos);
|
||||
if (state.is(CanapliaContent.HEMP_CROP) && CanapliaContent.HEMP_CROP.isMaxAge(state)) {
|
||||
variants.add(state.getValue(CanapliaHempCropBlock.VARIANT));
|
||||
variants.add(CanapliaVariantLifecycle.harvestedFromPlant(
|
||||
state.getValue(CanapliaHempCropBlock.VARIANT)));
|
||||
}
|
||||
}
|
||||
return Set.copyOf(variants);
|
||||
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
package fr.koka99cab.sanctuary26.itsalive.canaplia;
|
||||
|
||||
/** Stable variant hand-off across placement, growth, block-state persistence and harvest. */
|
||||
public final class CanapliaVariantLifecycle {
|
||||
private CanapliaVariantLifecycle() {
|
||||
}
|
||||
|
||||
public static CanapliaVariantKey plantedFromSeed(CanapliaVariantKey seedVariant) {
|
||||
return require(seedVariant);
|
||||
}
|
||||
|
||||
public static CanapliaVariantKey afterGrowth(CanapliaVariantKey currentVariant) {
|
||||
return require(currentVariant);
|
||||
}
|
||||
|
||||
public static CanapliaVariantKey upperFromLower(CanapliaVariantKey lowerVariant) {
|
||||
return require(lowerVariant);
|
||||
}
|
||||
|
||||
public static CanapliaVariantKey harvestedFromPlant(CanapliaVariantKey plantVariant) {
|
||||
return require(plantVariant);
|
||||
}
|
||||
|
||||
public static String persistedKey(CanapliaVariantKey variant) {
|
||||
return require(variant).key();
|
||||
}
|
||||
|
||||
public static CanapliaVariantKey fromPersistedKey(String key) {
|
||||
return CanapliaVariantKey.byKey(key).orElse(CanapliaVariantKey.COMMON);
|
||||
}
|
||||
|
||||
private static CanapliaVariantKey require(CanapliaVariantKey variant) {
|
||||
if (variant == null) throw new IllegalArgumentException("Canaplia variant cannot be null");
|
||||
return variant;
|
||||
}
|
||||
}
|
||||
+17
-6
@@ -2,6 +2,7 @@ package fr.koka99cab.sanctuary26.itsalive.canaplia.block;
|
||||
|
||||
import fr.koka99cab.sanctuary26.itsalive.canaplia.CanapliaContent;
|
||||
import fr.koka99cab.sanctuary26.itsalive.canaplia.CanapliaHarvestService;
|
||||
import fr.koka99cab.sanctuary26.itsalive.canaplia.CanapliaVariantLifecycle;
|
||||
import fr.koka99cab.sanctuary26.itsalive.canaplia.CanapliaVariantKey;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
@@ -43,19 +44,28 @@ public final class CanapliaHempCropBlock extends CropBlock {
|
||||
|
||||
@Override
|
||||
protected ItemStack getCloneItemStack(LevelReader level, BlockPos pos, BlockState state, boolean includeData) {
|
||||
return new ItemStack(CanapliaContent.family(state.getValue(VARIANT)).seeds());
|
||||
CanapliaVariantKey variant = CanapliaVariantLifecycle.harvestedFromPlant(state.getValue(VARIANT));
|
||||
return new ItemStack(CanapliaContent.family(variant).seeds());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void randomTick(BlockState state, ServerLevel level, BlockPos pos, RandomSource random) {
|
||||
super.randomTick(state, level, pos, random);
|
||||
synchronizeUpper(level, pos, level.getBlockState(pos));
|
||||
if (level.getRawBrightness(pos, 0) < 9) return;
|
||||
int age = getAge(state);
|
||||
if (age >= getMaxAge()) return;
|
||||
float speed = getGrowthSpeed(this, level, pos);
|
||||
if (random.nextInt((int) (25.0F / speed) + 1) != 0) return;
|
||||
BlockState grown = state.setValue(AGE, age + 1).setValue(VARIANT,
|
||||
CanapliaVariantLifecycle.afterGrowth(state.getValue(VARIANT)));
|
||||
level.setBlock(pos, grown, Block.UPDATE_CLIENTS);
|
||||
synchronizeUpper(level, pos, grown);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void growCrops(Level level, BlockPos pos, BlockState state) {
|
||||
int age = Math.min(getMaxAge(), getAge(state) + getBonemealAgeIncrease(level));
|
||||
BlockState grown = state.setValue(AGE, age);
|
||||
BlockState grown = state.setValue(AGE, age).setValue(VARIANT,
|
||||
CanapliaVariantLifecycle.afterGrowth(state.getValue(VARIANT)));
|
||||
level.setBlock(pos, grown, Block.UPDATE_CLIENTS);
|
||||
synchronizeUpper(level, pos, grown);
|
||||
}
|
||||
@@ -78,7 +88,7 @@ public final class CanapliaHempCropBlock extends CropBlock {
|
||||
if (!(level instanceof ServerLevel serverLevel) || !(player instanceof ServerPlayer serverPlayer)) {
|
||||
return InteractionResult.FAIL;
|
||||
}
|
||||
CanapliaVariantKey variant = state.getValue(VARIANT);
|
||||
CanapliaVariantKey variant = CanapliaVariantLifecycle.harvestedFromPlant(state.getValue(VARIANT));
|
||||
Block.popResource(serverLevel, pos,
|
||||
new ItemStack(CanapliaContent.family(variant).bud(), 1 + serverLevel.getRandom().nextInt(3)));
|
||||
CanapliaHarvestService.onMatureHarvest(serverLevel, serverPlayer, pos, state);
|
||||
@@ -97,7 +107,8 @@ public final class CanapliaHempCropBlock extends CropBlock {
|
||||
if (getAge(state) >= UPPER_START_AGE) {
|
||||
if (aboveState.isAir() || aboveState.is(CanapliaContent.HEMP_TOP)) {
|
||||
level.setBlock(above, CanapliaContent.HEMP_TOP.defaultBlockState()
|
||||
.setValue(AGE, getAge(state)).setValue(VARIANT, state.getValue(VARIANT)), Block.UPDATE_CLIENTS);
|
||||
.setValue(AGE, getAge(state)).setValue(VARIANT,
|
||||
CanapliaVariantLifecycle.upperFromLower(state.getValue(VARIANT))), Block.UPDATE_CLIENTS);
|
||||
}
|
||||
} else if (aboveState.is(CanapliaContent.HEMP_TOP)) {
|
||||
level.removeBlock(above, false);
|
||||
|
||||
+4
-1
@@ -1,6 +1,7 @@
|
||||
package fr.koka99cab.sanctuary26.itsalive.canaplia.block;
|
||||
|
||||
import fr.koka99cab.sanctuary26.itsalive.canaplia.CanapliaContent;
|
||||
import fr.koka99cab.sanctuary26.itsalive.canaplia.CanapliaVariantLifecycle;
|
||||
import fr.koka99cab.sanctuary26.itsalive.canaplia.CanapliaVariantKey;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
@@ -26,7 +27,9 @@ public final class CanapliaHempTopBlock extends CropBlock {
|
||||
|
||||
@Override
|
||||
protected ItemStack getCloneItemStack(LevelReader level, BlockPos pos, BlockState state, boolean includeData) {
|
||||
return new ItemStack(CanapliaContent.family(state.getValue(CanapliaHempCropBlock.VARIANT)).seeds());
|
||||
CanapliaVariantKey variant = CanapliaVariantLifecycle.harvestedFromPlant(
|
||||
state.getValue(CanapliaHempCropBlock.VARIANT));
|
||||
return new ItemStack(CanapliaContent.family(variant).seeds());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+9
-3
@@ -1,5 +1,6 @@
|
||||
package fr.koka99cab.sanctuary26.itsalive.canaplia.item;
|
||||
|
||||
import fr.koka99cab.sanctuary26.itsalive.canaplia.CanapliaVariantLifecycle;
|
||||
import fr.koka99cab.sanctuary26.itsalive.canaplia.CanapliaVariantKey;
|
||||
import fr.koka99cab.sanctuary26.itsalive.canaplia.block.CanapliaHempCropBlock;
|
||||
import fr.koka99cab.sanctuary26.itsalive.canaplia.block.CanapliaResinBlock;
|
||||
@@ -24,12 +25,17 @@ public final class CanapliaVariantBlockItem extends BlockItem {
|
||||
@Override
|
||||
protected BlockState getPlacementState(BlockPlaceContext context) {
|
||||
BlockState state = super.getPlacementState(context);
|
||||
if (state == null) return null;
|
||||
return applyVariant(state, variant);
|
||||
}
|
||||
|
||||
public static BlockState applyVariant(BlockState state, CanapliaVariantKey variant) {
|
||||
if (state == null || variant == null) return state;
|
||||
CanapliaVariantKey placed = CanapliaVariantLifecycle.plantedFromSeed(variant);
|
||||
if (state.hasProperty(CanapliaHempCropBlock.VARIANT)) {
|
||||
return state.setValue(CanapliaHempCropBlock.VARIANT, variant);
|
||||
return state.setValue(CanapliaHempCropBlock.VARIANT, placed);
|
||||
}
|
||||
if (state.hasProperty(CanapliaResinBlock.VARIANT)) {
|
||||
return state.setValue(CanapliaResinBlock.VARIANT, variant);
|
||||
return state.setValue(CanapliaResinBlock.VARIANT, placed);
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
+16
@@ -61,6 +61,22 @@ public final class CanapliaGameplaySmoke {
|
||||
Set.of(), Temperature.COLD, Humidity.BALANCED, 12, false, false, true, true, false)).isEmpty(),
|
||||
"lunar harvest requires a sky dimension");
|
||||
|
||||
for (CanapliaVariantKey variant : CanapliaVariantKey.values()) {
|
||||
CanapliaVariantKey placed = CanapliaVariantLifecycle.plantedFromSeed(variant);
|
||||
assertEquals(variant, placed,
|
||||
"historical seed placement variant " + variant.key());
|
||||
CanapliaVariantKey grown = CanapliaVariantLifecycle.afterGrowth(placed);
|
||||
assertEquals(variant, grown,
|
||||
"natural growth variant " + variant.key());
|
||||
CanapliaVariantKey persisted = CanapliaVariantLifecycle.fromPersistedKey(
|
||||
CanapliaVariantLifecycle.persistedKey(grown));
|
||||
assertEquals(variant, persisted, "persisted variant " + variant.key());
|
||||
assertEquals(variant, CanapliaVariantLifecycle.upperFromLower(persisted),
|
||||
"upper crop variant " + variant.key());
|
||||
assertEquals(variant, CanapliaVariantLifecycle.harvestedFromPlant(persisted),
|
||||
"harvest variant " + variant.key());
|
||||
}
|
||||
|
||||
CanapliaEffectRules.Plan minimum = CanapliaEffectRules.plan(0, 1, 0, 0);
|
||||
assertEquals(CanapliaEffectRules.MIN_PRIMARY_TICKS, minimum.primaryTicks(), "minimum primary duration");
|
||||
assertTrue(!minimum.badTrip(), "nonzero bad-trip roll");
|
||||
|
||||
@@ -48,7 +48,7 @@ public final class OnlyFunMod implements ModInitializer {
|
||||
OnlyFunCommands.register();
|
||||
AbjectionActions.initialize();
|
||||
AbjectionCommands.register();
|
||||
LOGGER.info("[{}] Loot generator, natural Lucky Blocks, mob lootboxes and chicken rules ready (data v{}, network v{}).",
|
||||
LOGGER.info("[{}] Loot generator, boss-gated Shop, natural Lucky Blocks, mob lootboxes and chicken rules ready (data v{}, network v{}).",
|
||||
DISPLAY_NAME, CURRENT_DATA_VERSION, OnlyFunNetworking.PROTOCOL_VERSION);
|
||||
}
|
||||
}
|
||||
|
||||
+3
-1
@@ -2,6 +2,7 @@ package fr.koka99cab.sanctuary26.onlyfun.loot;
|
||||
|
||||
import fr.koka99cab.sanctuary26.onlyfun.OnlyFunMod;
|
||||
import fr.koka99cab.sanctuary26.onlyfun.registry.OnlyFunRegistries;
|
||||
import fr.koka99cab.sanctuary26.sanctuary.api.entity.SanctuaryEntityPolicy;
|
||||
import net.fabricmc.fabric.api.entity.event.v1.ServerLivingEntityEvents;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
@@ -38,7 +39,8 @@ public final class OnlyFunLootboxDrops {
|
||||
}
|
||||
|
||||
private static void afterDeath(LivingEntity entity, DamageSource source) {
|
||||
if (!(entity instanceof Mob mob) || !(entity.level() instanceof ServerLevel level)) return;
|
||||
if (SanctuaryEntityPolicy.isTemporaryCompanion(entity)
|
||||
|| !(entity instanceof Mob mob) || !(entity.level() instanceof ServerLevel level)) return;
|
||||
ServerPlayer killer = playerKiller(entity, source);
|
||||
if (killer == null || killer.isSpectator()) return;
|
||||
|
||||
|
||||
+12
-17
@@ -1,29 +1,22 @@
|
||||
package fr.koka99cab.sanctuary26.onlyfun.shop;
|
||||
|
||||
import java.util.Objects;
|
||||
import fr.koka99cab.sanctuary26.sanctuary.api.notch.SanctuaryNotchMilestone;
|
||||
import fr.koka99cab.sanctuary26.sanctuary.api.progression.SanctuaryWorldMilestones;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
|
||||
/** Keeps exceptional boss rewards out of the Shop until their future boss owner unlocks them. */
|
||||
/** Keeps exceptional boss rewards out of the Shop until their public world milestone is reached. */
|
||||
public final class OnlyFunBossShopGate {
|
||||
public static final Identifier MAGIC_CLOUD = Identifier.fromNamespaceAndPath("iliketomoveit", "magic_cloud");
|
||||
public static final Identifier MAGIC_CARPET = Identifier.fromNamespaceAndPath("iliketomoveit", "magic_carpet");
|
||||
private static final Set<Identifier> BOSS_REWARDS = Set.of(MAGIC_CLOUD, MAGIC_CARPET);
|
||||
private static final BossUnlockResolver LOCKED = (server, reward) -> false;
|
||||
private static volatile BossUnlockResolver resolver = LOCKED;
|
||||
private static final Map<Identifier, Identifier> IMPLEMENTED_MILESTONES = Map.of(
|
||||
MAGIC_CARPET, SanctuaryNotchMilestone.NOTCH_DEFEATED);
|
||||
|
||||
private OnlyFunBossShopGate() {}
|
||||
|
||||
/** Called later by the module that owns the bosses and their persistent defeat state. */
|
||||
public static synchronized void registerUnlockResolver(BossUnlockResolver value) {
|
||||
Objects.requireNonNull(value, "value");
|
||||
if (resolver != LOCKED && resolver != value) {
|
||||
throw new IllegalStateException("A boss Shop unlock resolver is already registered");
|
||||
}
|
||||
resolver = value;
|
||||
}
|
||||
|
||||
public static boolean requiresBoss(String itemId) {
|
||||
Identifier id = Identifier.tryParse(itemId);
|
||||
return id != null && BOSS_REWARDS.contains(id);
|
||||
@@ -31,11 +24,13 @@ public final class OnlyFunBossShopGate {
|
||||
|
||||
public static boolean isAvailable(MinecraftServer server, String itemId) {
|
||||
Identifier id = Identifier.tryParse(itemId);
|
||||
return id == null || !BOSS_REWARDS.contains(id) || resolver.isUnlocked(server, id);
|
||||
if (id == null || !BOSS_REWARDS.contains(id)) return true;
|
||||
Identifier milestone = requiredMilestone(id);
|
||||
return milestone != null && SanctuaryWorldMilestones.isReached(server, milestone);
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
public interface BossUnlockResolver {
|
||||
boolean isUnlocked(MinecraftServer server, Identifier rewardItem);
|
||||
/** Returns null while the corresponding boss contract is not implemented. */
|
||||
public static Identifier requiredMilestone(Identifier rewardItem) {
|
||||
return IMPLEMENTED_MILESTONES.get(rewardItem);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,8 +26,8 @@
|
||||
"minecraft": "~26.2",
|
||||
"java": ">=25",
|
||||
"fabric-api": ">=0.154.2+26.2",
|
||||
"iliketomoveit": "*",
|
||||
"sanctuary": "*"
|
||||
"iliketomoveit": ">=0.0.0-alpha.17",
|
||||
"sanctuary": ">=0.0.0-alpha.129"
|
||||
},
|
||||
"custom": {
|
||||
"sanctuary26": {
|
||||
|
||||
+7
-6
@@ -3,6 +3,7 @@ package fr.koka99cab.sanctuary26.onlyfun.shop;
|
||||
import fr.koka99cab.sanctuary26.onlyfun.policy.OnlyFunItemPolicy;
|
||||
import fr.koka99cab.sanctuary26.sanctuary.shop.ShopOffer;
|
||||
import fr.koka99cab.sanctuary26.sanctuary.shop.ShopOfferCatalog;
|
||||
import fr.koka99cab.sanctuary26.sanctuary.api.notch.SanctuaryNotchMilestone;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
@@ -94,12 +95,12 @@ public final class OnlyFunShopCatalogSmoke {
|
||||
"Magic Cloud must remain boss-gated");
|
||||
require(!OnlyFunBossShopGate.isAvailable(null, "iliketomoveit:magic_carpet")
|
||||
&& !OnlyFunBossShopGate.isAvailable(null, "iliketomoveit:magic_cloud"),
|
||||
"Boss rewards must be unavailable while their bosses do not exist");
|
||||
OnlyFunBossShopGate.registerUnlockResolver((server, reward) ->
|
||||
reward.equals(OnlyFunBossShopGate.MAGIC_CARPET));
|
||||
require(OnlyFunBossShopGate.isAvailable(null, "iliketomoveit:magic_carpet")
|
||||
&& !OnlyFunBossShopGate.isAvailable(null, "iliketomoveit:magic_cloud"),
|
||||
"Each future boss must unlock only its corresponding reward");
|
||||
"Boss rewards must reject lookups without authoritative server state");
|
||||
require(SanctuaryNotchMilestone.NOTCH_DEFEATED.equals(
|
||||
OnlyFunBossShopGate.requiredMilestone(OnlyFunBossShopGate.MAGIC_CARPET)),
|
||||
"Magic Carpet must resolve the public Notch milestone");
|
||||
require(OnlyFunBossShopGate.requiredMilestone(OnlyFunBossShopGate.MAGIC_CLOUD) == null,
|
||||
"Magic Cloud must stay locked until its own boss is implemented");
|
||||
|
||||
for (String protectedId : List.of(
|
||||
"anotherworld:ruby", "anotherworld:ruby_block", "anotherworld:deepslate_ruby_ore",
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
{
|
||||
"schema_version": 1,
|
||||
"status": "released",
|
||||
"source": {
|
||||
"pack_version": "26.2.0-alpha.223",
|
||||
"modules": {
|
||||
"itsalive": "0.0.0-alpha.36",
|
||||
"ouch": "0.0.0-alpha.10",
|
||||
"masterkey": "0.0.0-alpha.8",
|
||||
"iliketomoveit": "0.0.0-alpha.16",
|
||||
"onlyfun": "0.0.0-alpha.25",
|
||||
"sanctuary": "0.0.0-alpha.125"
|
||||
}
|
||||
},
|
||||
"target": {
|
||||
"pack_version": "26.2.0-alpha.224",
|
||||
"modules": {
|
||||
"itsalive": "0.0.0-alpha.37",
|
||||
"ouch": "0.0.0-alpha.11",
|
||||
"masterkey": "0.0.0-alpha.9",
|
||||
"iliketomoveit": "0.0.0-alpha.17",
|
||||
"onlyfun": "0.0.0-alpha.26",
|
||||
"sanctuary": "0.0.0-alpha.128"
|
||||
}
|
||||
},
|
||||
"inventory_sort": {
|
||||
"owner": "sanctuary",
|
||||
"payload_container_flag": "bounded_boolean",
|
||||
"sortable_storage_menus": ["chest", "barrel", "shulker_box", "hopper", "dispenser"],
|
||||
"player_inventory_when_hovered": true,
|
||||
"hotbar_unchanged": true,
|
||||
"hotbar_restored_to_bottom_on_login": true,
|
||||
"hotbar_restored_to_bottom_after_row_purchase": true,
|
||||
"excluded_menus": ["furnace", "shop", "redstoner_storage_terminal"],
|
||||
"server_authoritative": true,
|
||||
"server_validates_open_menu": true
|
||||
},
|
||||
"notch_encounter": {
|
||||
"entity_owner": "itsalive",
|
||||
"combat_owner": "ouch",
|
||||
"milestone_owner": "sanctuary",
|
||||
"diagnostics_owner": "masterkey",
|
||||
"dimension": "sanctuary:alpha",
|
||||
"unique_persistent_resident": true,
|
||||
"legacy_entity": "portals:notch",
|
||||
"legacy_entity_migrated": true,
|
||||
"maximum_participants": 8,
|
||||
"first_transformation_breaks_blocks": true,
|
||||
"later_attacks_break_blocks": false,
|
||||
"returned_fireballs_supported": true,
|
||||
"abandoned_fight_resets": true,
|
||||
"global_victory_monotonic": true,
|
||||
"atlas_boss_marker_for_participants": true,
|
||||
"commands": ["/masterkey notch inspect", "/masterkey notch restore"]
|
||||
},
|
||||
"magic_carpet": {
|
||||
"owner": "iliketomoveit",
|
||||
"canonical_item": "iliketomoveit:magic_carpet",
|
||||
"legacy_item": "portals:notch_totem",
|
||||
"legacy_alias_hidden": true,
|
||||
"legacy_components_preserved": true,
|
||||
"legacy_custom_name_preserved": true,
|
||||
"legacy_locations": ["player_inventory", "container", "item_entity"],
|
||||
"one_legacy_stack_becomes_one_canonical_stack": true,
|
||||
"new_worlds_distribute_legacy_item": false,
|
||||
"temporary_platform": {
|
||||
"size": [3, 1, 3],
|
||||
"block": "minecraft:glass",
|
||||
"air_only_placement": true,
|
||||
"creative_flight_granted": false,
|
||||
"overlap_owner_tracking": true,
|
||||
"cleanup": ["deactivation", "disconnect", "dimension_change", "server_stop"]
|
||||
}
|
||||
},
|
||||
"notch_reward": {
|
||||
"milestone_owner": "sanctuary",
|
||||
"milestone": "sanctuary:notch_defeated",
|
||||
"reservation": "initial_reward_claimed",
|
||||
"completion": "initial_reward_delivered",
|
||||
"delivery_owner": "iliketomoveit",
|
||||
"delivery": "deterministic_chest_near_notch_residence",
|
||||
"firework_signal": true,
|
||||
"idempotent_recovery": true,
|
||||
"maximum_canonical_initial_rewards": 1,
|
||||
"provenance": "sanctuary:notch_reward"
|
||||
},
|
||||
"shop_replacement": {
|
||||
"catalog_owner": "onlyfun",
|
||||
"transaction_owner": "sanctuary",
|
||||
"unlock_resolver": "SanctuaryWorldMilestones",
|
||||
"hidden_before_notch": true,
|
||||
"server_authoritative_gate": true,
|
||||
"configured_ruby_price": 1536,
|
||||
"configured_sapphire_price": 128,
|
||||
"daily_rotation_rules_preserved": true,
|
||||
"mailbox_delivery": true,
|
||||
"recreates_initial_reward": false,
|
||||
"provenance": "sanctuary:shop_purchase",
|
||||
"replacement_logged": true
|
||||
},
|
||||
"compatibility": {
|
||||
"existing_saves_supported": true,
|
||||
"new_world_required": false,
|
||||
"save_reset_required": false,
|
||||
"registry_ids_preserved_or_migrated": true,
|
||||
"client_server_update_required": true,
|
||||
"itsalive_data_version": 4,
|
||||
"itsalive_network_protocol": 4,
|
||||
"ouch_data_version": 1,
|
||||
"ouch_network_protocol": 1,
|
||||
"masterkey_data_version": 1,
|
||||
"masterkey_network_protocol": 2,
|
||||
"iliketomoveit_data_version": 4,
|
||||
"iliketomoveit_network_protocol": 5,
|
||||
"onlyfun_data_version": 2,
|
||||
"onlyfun_network_protocol": 3,
|
||||
"sanctuary_data_version": 13,
|
||||
"sanctuary_notch_state_data_version": 2,
|
||||
"sanctuary_source_network_protocol": 22,
|
||||
"sanctuary_target_network_protocol": 23,
|
||||
"sanctuary_network_protocol_changed": true
|
||||
},
|
||||
"distribution": {
|
||||
"pack_release_updated": true,
|
||||
"packwiz_index_refreshed": true,
|
||||
"prism_modrinth_index_updated": true,
|
||||
"shared_client_server_mrpack": true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
{
|
||||
"schema_version": 1,
|
||||
"status": "released",
|
||||
"source": {
|
||||
"pack_version": "26.2.0-alpha.224",
|
||||
"modules": {
|
||||
"anotherworld": "0.0.0-alpha.55",
|
||||
"itsalive": "0.0.0-alpha.37",
|
||||
"iliketomoveit": "0.0.0-alpha.17",
|
||||
"onlyfun": "0.0.0-alpha.26",
|
||||
"sanctuary": "0.0.0-alpha.128"
|
||||
}
|
||||
},
|
||||
"target": {
|
||||
"pack_version": "26.2.0-alpha.225",
|
||||
"modules": {
|
||||
"anotherworld": "0.0.0-alpha.56",
|
||||
"itsalive": "0.0.0-alpha.38",
|
||||
"iliketomoveit": "0.0.0-alpha.18",
|
||||
"onlyfun": "0.0.0-alpha.27",
|
||||
"sanctuary": "0.0.0-alpha.129"
|
||||
}
|
||||
},
|
||||
"fixed_issues": [105, 116, 123, 124],
|
||||
"safe_vault_raid": {
|
||||
"owner": "anotherworld",
|
||||
"attacker_requires_registered_vault": true,
|
||||
"validated_when_drilling_starts": true,
|
||||
"revalidated_during_drilling": true,
|
||||
"revalidated_before_balance_transfer": true
|
||||
},
|
||||
"temporary_companions": {
|
||||
"owner": "sanctuary",
|
||||
"public_policy_api": "SanctuaryEntityPolicy",
|
||||
"vanilla_loot_suppressed": true,
|
||||
"experience_suppressed": true,
|
||||
"quest_board_progress_suppressed": true,
|
||||
"community_request_progress_suppressed": true,
|
||||
"onlyfun_lootboxes_suppressed": true
|
||||
},
|
||||
"canaplia_variants": {
|
||||
"owner": "itsalive",
|
||||
"historical_seed_ids_preserved": 16,
|
||||
"placement_preserves_variant": true,
|
||||
"natural_growth_preserves_variant": true,
|
||||
"bonemeal_preserves_variant": true,
|
||||
"upper_crop_preserves_variant": true,
|
||||
"block_state_persistence_preserves_variant": true,
|
||||
"harvest_and_break_drops_preserve_variant": true,
|
||||
"mature_neighbors_preserve_variant": true,
|
||||
"tender_plus_olive_can_produce_gold": true
|
||||
},
|
||||
"magic_carpet_texture": {
|
||||
"owner": "iliketomoveit",
|
||||
"canonical_item": "iliketomoveit:magic_carpet",
|
||||
"legacy_item": "portals:notch_totem",
|
||||
"texture": "iliketomoveit:item/magic_carpet",
|
||||
"historical_totem_art_restored": true,
|
||||
"purple_wool_reference_removed": true,
|
||||
"platform_behavior_changed": false
|
||||
},
|
||||
"compatibility": {
|
||||
"existing_saves_supported": true,
|
||||
"new_world_required": false,
|
||||
"save_reset_required": false,
|
||||
"registry_ids_changed": false,
|
||||
"persistent_data_formats_changed": false,
|
||||
"network_protocols_changed": false,
|
||||
"client_server_update_required": true,
|
||||
"anotherworld_data_version": 5,
|
||||
"anotherworld_network_protocol": 2,
|
||||
"itsalive_data_version": 4,
|
||||
"itsalive_network_protocol": 4,
|
||||
"iliketomoveit_data_version": 4,
|
||||
"iliketomoveit_network_protocol": 5,
|
||||
"onlyfun_data_version": 2,
|
||||
"onlyfun_network_protocol": 3,
|
||||
"sanctuary_data_version": 13,
|
||||
"sanctuary_network_protocol": 23
|
||||
},
|
||||
"distribution": {
|
||||
"pack_release_updated": true,
|
||||
"packwiz_index_refreshed": true,
|
||||
"prism_modrinth_index_updated": true,
|
||||
"shared_client_server_mrpack": true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
{
|
||||
"schema_version": 1,
|
||||
"status": "released",
|
||||
"source": {
|
||||
"pack_version": "26.2.0-alpha.225",
|
||||
"modules": {
|
||||
"sanctuary": "0.0.0-alpha.129"
|
||||
}
|
||||
},
|
||||
"target": {
|
||||
"pack_version": "26.2.0-alpha.226",
|
||||
"modules": {
|
||||
"sanctuary": "0.0.0-alpha.130"
|
||||
}
|
||||
},
|
||||
"white_cape": {
|
||||
"owner": "sanctuary",
|
||||
"item_id": "sanctuary:white_cape",
|
||||
"crafting_material": "minecraft:white_wool",
|
||||
"crafting_material_count": 6,
|
||||
"crafting_pattern": ["WW", "WW", "WW"],
|
||||
"banner_decoration_recipe": "cape_plus_banner",
|
||||
"accepted_banners": "#minecraft:banners",
|
||||
"base_color_preserved": true,
|
||||
"maximum_pattern_layers": 6,
|
||||
"inventory_pattern_rendering": true,
|
||||
"equipped_pattern_rendering": true,
|
||||
"vanilla_cape_physics": true,
|
||||
"included_in_onlyfun_lootbox": false
|
||||
},
|
||||
"compatibility": {
|
||||
"existing_saves_supported": true,
|
||||
"new_world_required": false,
|
||||
"save_reset_required": false,
|
||||
"registry_ids_added": ["sanctuary:white_cape"],
|
||||
"persistent_data_formats_changed": false,
|
||||
"sanctuary_data_version": 13,
|
||||
"sanctuary_source_network_protocol": 23,
|
||||
"sanctuary_target_network_protocol": 24,
|
||||
"sanctuary_network_protocol_changed": true,
|
||||
"client_server_update_required": true
|
||||
},
|
||||
"distribution": {
|
||||
"pack_release_updated": true,
|
||||
"packwiz_index_refreshed": true,
|
||||
"prism_modrinth_index_updated": true,
|
||||
"shared_client_server_mrpack": true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"schema_version": 1,
|
||||
"status": "pending_release",
|
||||
"module": "sanctuary",
|
||||
"source_module_version": "0.0.0-alpha.130",
|
||||
"target_module_version": "0.0.0-alpha.131",
|
||||
"integration_modules": {
|
||||
"anotherworld": {
|
||||
"source": "0.0.0-alpha.56",
|
||||
"target": "0.0.0-alpha.57",
|
||||
"data_version_changed": false,
|
||||
"network_protocol_changed": false
|
||||
}
|
||||
},
|
||||
"source_data_version": 13,
|
||||
"target_data_version": 14,
|
||||
"migration": {
|
||||
"kind": "monotone_addition",
|
||||
"save_key": "sanctuary:backroom",
|
||||
"existing_player_progress_preserved": true,
|
||||
"existing_shop_data_preserved": true,
|
||||
"existing_mailboxes_preserved": true,
|
||||
"mailbox_transaction_history_default": [],
|
||||
"backroom_loss_registry_default": [],
|
||||
"backroom_chest_registry_default": []
|
||||
},
|
||||
"compatibility": {
|
||||
"existing_saves_supported": true,
|
||||
"new_world_required": false,
|
||||
"save_reset_required": false,
|
||||
"network_protocol_changed": false,
|
||||
"registry_ids_added": [
|
||||
"sanctuary:backroom",
|
||||
"sanctuary:backroom_loss_id",
|
||||
"sanctuary:backroom_architecture"
|
||||
]
|
||||
}
|
||||
}
|
||||
+17
-17
@@ -14,15 +14,15 @@ hash = "41f4f4c88c71d0a23446d5014f7407d55ad477441847099cad24492b032335a0"
|
||||
|
||||
[[files]]
|
||||
file = "mods/ambiance-0.0.0-alpha.22.jar"
|
||||
hash = "34aa79ab54ba2506b0ffef062662917de8b29f6f3f9b8d7a920a664d01185641"
|
||||
hash = "0e862db1c5efb5c0745b82f991d5480f4305822c3d3f53ed38e2d2d47efabee1"
|
||||
|
||||
[[files]]
|
||||
file = "mods/anotherworld-0.0.0-alpha.55.jar"
|
||||
hash = "2c93c364af7f617a488a8ef5c64d584041eeb24b93e4ea727570c042bd73fe2d"
|
||||
file = "mods/anotherworld-0.0.0-alpha.56.jar"
|
||||
hash = "cec8ed493f3c86e4c4481081be110382407f7a5c631b7cf27dd80f80c6f2e92a"
|
||||
|
||||
[[files]]
|
||||
file = "mods/events-0.0.0-alpha.0.jar"
|
||||
hash = "6c94ba89f6daf64d7144e9ea2283a98a609a01a20b7913bd51821034ee8a26fd"
|
||||
hash = "16bf9fc194988086f51ba6987546b6aa5bc634fe8fcb118b88540ed16991049c"
|
||||
|
||||
[[files]]
|
||||
file = "mods/fabric-api.pw.toml"
|
||||
@@ -30,8 +30,8 @@ hash = "df032a6b051d0262cf24945c43e0e6b915e8cc1d62af62fe933932e7f35960c4"
|
||||
metafile = true
|
||||
|
||||
[[files]]
|
||||
file = "mods/iliketomoveit-0.0.0-alpha.16.jar"
|
||||
hash = "f0f54b9746977b085abb74fea61e8cd230f3dda905cc337866e7950fb2ee9293"
|
||||
file = "mods/iliketomoveit-0.0.0-alpha.18.jar"
|
||||
hash = "94044341de08c9b89e0c1e6af6857da64b472fadbc94051ab76fc3cf1a0f0006"
|
||||
|
||||
[[files]]
|
||||
file = "mods/iris.pw.toml"
|
||||
@@ -39,8 +39,8 @@ hash = "84a3cef87679c0d21dceb4641167dc0368b9a3593698f6a3632485e9b3a8d5da"
|
||||
metafile = true
|
||||
|
||||
[[files]]
|
||||
file = "mods/itsalive-0.0.0-alpha.36.jar"
|
||||
hash = "ae2201e91e4ab2eaf09f32ea41c920d460cf3cf426e71bd76be5241b7e81e1a0"
|
||||
file = "mods/itsalive-0.0.0-alpha.38.jar"
|
||||
hash = "a42885703439646687ac281b1c124d1cddb3ca0595a41d930733b94bd8f1aa2f"
|
||||
|
||||
[[files]]
|
||||
file = "mods/jei.pw.toml"
|
||||
@@ -48,24 +48,24 @@ hash = "cffb13bae33cd8df605c62e7fa66089428dc684e0ab973f662a8a710c45c306d"
|
||||
metafile = true
|
||||
|
||||
[[files]]
|
||||
file = "mods/masterkey-0.0.0-alpha.8.jar"
|
||||
hash = "60de65566b1434709cdfb4c915d5bc6a7f9019dd48ebeaa04fe474f0ec7fd3b4"
|
||||
file = "mods/masterkey-0.0.0-alpha.9.jar"
|
||||
hash = "979c14e0a527be63688e97af8349be07b027d3149107c3e9e728a2b1e1ec65ff"
|
||||
|
||||
[[files]]
|
||||
file = "mods/onlyfun-0.0.0-alpha.25.jar"
|
||||
hash = "165ffe713b09db64484cceb6c508ac5e53b5d7e69e86ea6658c3e8ea04ee1ccf"
|
||||
file = "mods/onlyfun-0.0.0-alpha.27.jar"
|
||||
hash = "023ba80a8f3326a6b534b343753d5630d3be0700d25613ea21057cbfe44fdc73"
|
||||
|
||||
[[files]]
|
||||
file = "mods/ouch-0.0.0-alpha.10.jar"
|
||||
hash = "3f66a1b0657c40b653ec9e7a7aa92e4edf72475af7e372a49d9254743696f0a5"
|
||||
file = "mods/ouch-0.0.0-alpha.11.jar"
|
||||
hash = "2c15c9d0adeb315b2c3a6796f27c9dc62dd33365f35ea584552d97500b833eb8"
|
||||
|
||||
[[files]]
|
||||
file = "mods/redstoner-0.0.0-alpha.13.jar"
|
||||
hash = "226c000f5febd841d69420316af2c14dd4ec0ef1c774c21d5926f7120f44b7e6"
|
||||
hash = "753835558c1037e39ca669737190013bd59a3ab16f7d5156c52e940c54ff9c60"
|
||||
|
||||
[[files]]
|
||||
file = "mods/sanctuary-0.0.0-alpha.125.jar"
|
||||
hash = "f0826d9be66f6498bc0455ea8075bfbaefc97c1f6a7b0d21b225003fe71ad958"
|
||||
file = "mods/sanctuary-0.0.0-alpha.130.jar"
|
||||
hash = "8906be26de369903a0d5152d80a95a1c47b3468e4afca17ab4683b8300ad69a9"
|
||||
|
||||
[[files]]
|
||||
file = "mods/simple-voice-chat.pw.toml"
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
name = "Sanctuary 26.2"
|
||||
author = "KOKA99CAB"
|
||||
version = "26.2.0-alpha.223"
|
||||
version = "26.2.0-alpha.226"
|
||||
pack-format = "packwiz:1.1.0"
|
||||
|
||||
[index]
|
||||
file = "index.toml"
|
||||
hash-format = "sha256"
|
||||
hash = "9e06da88d562c7a2ed5fbcd56a06779a37715d4f9cf26479ee1a664758ada599"
|
||||
hash = "d3b929a596081adc3960866a2f939d4155d1b67ecb22bfe6050931a7d1d0f0a2"
|
||||
|
||||
[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.223",
|
||||
"versionId": "26.2.0-alpha.226",
|
||||
"files": [
|
||||
{
|
||||
"path": "mods/fabric-api-0.158.0+26.2.jar",
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"schema_version": 1,
|
||||
"pack_version": "26.2.0-alpha.223",
|
||||
"pack_version": "26.2.0-alpha.226",
|
||||
"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.223.mrpack",
|
||||
"primary_file": "sanctuary-26.2.0-alpha.226.mrpack",
|
||||
"shared_client_server_pack": true,
|
||||
"update_owner": "packwiz-installer",
|
||||
"check_on_launch": true,
|
||||
|
||||
+12
-2
@@ -49,5 +49,15 @@ Ouch possède les attaques et les dégâts ; seule l'explosion de transformation
|
||||
peut casser des blocs. La victoire est enregistrée une seule fois dans les
|
||||
données globales Sanctuary, même après redémarrage ou restauration de l'entité.
|
||||
Les participants reçoivent le jalon `sanctuary:notch_defeated` et un marqueur
|
||||
de boss dans leur Atlas si Alpha y était déjà découverte. Le drapeau de
|
||||
récompense initiale reste séparé et monotone pour la future étape CARPET-01.
|
||||
de boss dans leur Atlas si Alpha y était déjà découverte. Sanctuary réserve
|
||||
ensuite une récompense mondiale unique : I Like To Move It place le
|
||||
`iliketomoveit:magic_carpet` dans un coffre près de la maison et le signale par
|
||||
un feu d'artifice. La réservation et la livraison sont persistées séparément
|
||||
afin qu'une reprise serveur termine le coffre sans créer un second tapis.
|
||||
|
||||
L'ancien `portals:notch_totem` reste lisible et devient un Magic Carpet en
|
||||
conservant ses composants et son nom. Après la victoire, Only Fun autorise
|
||||
l'offre exceptionnelle dans la rotation normale du Shop ; les remplacements
|
||||
sont payés et livrés par la Mailbox sans recréer la récompense initiale. Le
|
||||
contrat détaillé est dans
|
||||
[`iliketomoveit/MAGIC_CARPET.md`](../iliketomoveit/MAGIC_CARPET.md).
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
# Backroom
|
||||
|
||||
La Backroom est une dimension persistante de récupération et de construction. Elle n’est jamais
|
||||
réinitialisée : routes, bases, coffres et bâtiments posés par les habitants restent dans le monde.
|
||||
Sa zone architecturale forme un disque de 2 000 blocs autour de l’origine.
|
||||
|
||||
## Entrer et sortir
|
||||
|
||||
- S’accroupir et utiliser un lit hors de la Backroom déclenche l’entrée, sans remplacer l’usage
|
||||
normal du lit.
|
||||
- Avant toute téléportation, le serveur transfère atomiquement vers la Mailbox enregistrée les
|
||||
43 emplacements vanilla, les 18 emplacements étendus, l’œuf compagnon et la cape équipée.
|
||||
- Si aucune Mailbox extérieure n’est chargée, ou si elle ne peut pas recevoir l’intégralité du
|
||||
colis, rien ne bouge et l’entrée est refusée. Une Mailbox construite dans la Backroom n’est jamais
|
||||
une destination d’entrée et ne permet donc pas d’importer du matériel.
|
||||
- L’identifiant de transaction est conservé par la Mailbox. Après une interruption, la connexion
|
||||
suivante termine exactement une fois le vidage correspondant ou annule une transaction qui n’a
|
||||
jamais atteint la Mailbox.
|
||||
- Dans la Backroom, tout lit — généré ou posé par un joueur — ramène au lit de réapparition normal,
|
||||
ou au point d’apparition global s’il n’existe plus. Le joueur conserve alors ses trouvailles.
|
||||
- Aucun autre usage de lit ne définit un point de réapparition dans la dimension.
|
||||
- Toute autre téléportation vers la Backroom est refusée. Une téléportation forcée vers l’extérieur
|
||||
reste possible pour l’administration, mais replace d’abord le butin porté dans le registre : elle
|
||||
ne constitue jamais une extraction réussie.
|
||||
|
||||
## Objets perdus et coffres
|
||||
|
||||
Les piles qui disparaissent naturellement ou tombent dans le vide rejoignent un registre mondial
|
||||
borné. Une mort de joueur dans la Backroom y replace aussi tout ce qu’il transportait. Chaque pile
|
||||
réelle matérialisée reçoit un identifiant persistant : elle peut passer du registre à un coffre, puis
|
||||
à un joueur et revenir au registre, mais elle ne peut jamais exister deux fois.
|
||||
Une perte associée à un joueur lui reste réservée pendant 24 000 ticks avant de pouvoir rejoindre
|
||||
les tirages historiques partagés ; une perte sans propriétaire est immédiatement partagée.
|
||||
|
||||
Tous les coffres vanilla de la Backroom, générés ou posés, deviennent des nœuds de butin renouvelés
|
||||
toutes les 12 000 ticks lors de leur prochaine ouverture. Le renouvellement rend au registre les
|
||||
objets perdus non pris, efface le contenu ordinaire et produit le plus souvent du vide ou des
|
||||
ressources pauvres : terre, pierre, planches, torches, nourriture et outils simples. Le butin ancien
|
||||
partagé et les objets de valeur restent rares. Reposer un coffre au même emplacement pendant le
|
||||
même cycle ne crée pas un nouveau tirage ; multiplier les emplacements donne en revanche bien des
|
||||
tirages indépendants, au prix des ressources trouvées ou produites sur place.
|
||||
|
||||
Pendant une expédition, chaque coffre distinct est un essai personnel. Le premier a 75 % de chance
|
||||
de proposer une perte récente du joueur, le deuxième 90 %, le troisième 100 %, puis les suivants
|
||||
65 %. Sans perte personnelle disponible, le coffre suit simplement son tirage général.
|
||||
|
||||
## Architecture
|
||||
|
||||
Les plans sont déterministes par grandes zones et utilisent cinq familles composées :
|
||||
|
||||
- mégalopoles en béton, verre, pierre et voirie ;
|
||||
- bureaux superposés en bois, clay, textile et éléments techniques ;
|
||||
- plaines naturelles avec arbres et rares ruines ;
|
||||
- agrégats de plateformes et de pièces flottantes reliées verticalement ;
|
||||
- pièces construites visuellement à 90° ou à l’envers, sans modifier la gravité globale.
|
||||
|
||||
Les matériaux très marqués d’Another World n’apparaissent que ponctuellement aux anomalies. Aucun
|
||||
bloc précieux ne sert de matériau structurel ; de très rares blocs de diamant sont cachés dans les
|
||||
fondations.
|
||||
|
||||
## Exploitation
|
||||
|
||||
Les données `sanctuary:backroom` utilisent leur propre `data_version`, ainsi que des limites pour les
|
||||
transactions, pertes, coffres suivis, visites et arrivées récentes. Les opérateurs disposent de
|
||||
`/backroom status` et `/backroom inspect <joueur>` pour contrôler les registres sans les modifier.
|
||||
+182
-20
@@ -116,6 +116,14 @@ tasks.register("capeJoinSyncSmoke", JavaExec) {
|
||||
mainClass = "fr.koka99cab.sanctuary26.sanctuary.gameplay.CapeJoinSyncQueueSmoke"
|
||||
}
|
||||
|
||||
tasks.register("capeSyncPayloadSmoke", JavaExec) {
|
||||
group = "verification"
|
||||
description = "Checks banner cape colors and the six-pattern network bound."
|
||||
dependsOn tasks.named("testClasses")
|
||||
classpath = sourceSets.test.runtimeClasspath
|
||||
mainClass = "fr.koka99cab.sanctuary26.sanctuary.network.CapeSyncPayloadSmoke"
|
||||
}
|
||||
|
||||
tasks.register("inventorySortSmoke", JavaExec) {
|
||||
group = "verification"
|
||||
description = "Checks that storage stacks merge and sort while the hotbar row stays out of the sort."
|
||||
@@ -124,6 +132,14 @@ tasks.register("inventorySortSmoke", JavaExec) {
|
||||
mainClass = "fr.koka99cab.sanctuary26.sanctuary.gameplay.SanctuaryInventorySortSmoke"
|
||||
}
|
||||
|
||||
tasks.register("entityPolicySmoke", JavaExec) {
|
||||
group = "verification"
|
||||
description = "Checks that only tagged temporary companions are excluded from loot and quests."
|
||||
dependsOn tasks.named("testClasses")
|
||||
classpath = sourceSets.test.runtimeClasspath
|
||||
mainClass = "fr.koka99cab.sanctuary26.sanctuary.api.entity.SanctuaryEntityPolicySmoke"
|
||||
}
|
||||
|
||||
tasks.register("alphaIndevGeneratorSmoke", JavaExec) {
|
||||
group = "verification"
|
||||
description = "Checks deterministic finite Indev islands, residences and safe portal arrivals."
|
||||
@@ -140,6 +156,14 @@ tasks.register("notchVictoryStateSmoke", JavaExec) {
|
||||
mainClass = "fr.koka99cab.sanctuary26.sanctuary.data.NotchVictoryStateSmoke"
|
||||
}
|
||||
|
||||
tasks.register("backroomModelSmoke", JavaExec) {
|
||||
group = "verification"
|
||||
description = "Checks bounded Backroom entry data, personal loot priority and composed architecture zones."
|
||||
dependsOn tasks.named("testClasses")
|
||||
classpath = sourceSets.test.runtimeClasspath
|
||||
mainClass = "fr.koka99cab.sanctuary26.sanctuary.backroom.BackroomModelSmoke"
|
||||
}
|
||||
|
||||
tasks.register("verifyBlackMarket") {
|
||||
group = "verification"
|
||||
description = "Checks the physical sign market, flash Shop, escrow ledger and mailbox delivery."
|
||||
@@ -175,7 +199,7 @@ tasks.register("verifyBlackMarket") {
|
||||
def questData = file("src/main/java/fr/koka99cab/sanctuary26/sanctuary/shop/QuestBoardData.java").text
|
||||
def quests = file("src/main/java/fr/koka99cab/sanctuary26/sanctuary/shop/QuestBoardService.java").text
|
||||
def inventoryView = file("src/main/java/fr/koka99cab/sanctuary26/sanctuary/api/inventory/SanctuaryPlayerInventoryView.java").text
|
||||
if (project.version.toString() != "0.0.0-alpha.126"
|
||||
if (project.version.toString() != "0.0.0-alpha.131"
|
||||
|| !data.contains('SAVE_KEY = "black_market"') || !data.contains("DATA_VERSION = 2")
|
||||
|| !data.contains("ItemStack.CODEC.fieldOf(\"item\")") || !data.contains("replaceListing")
|
||||
|| !service.contains("LISTING_FEE_RUBIES = 1") || !service.contains("COMMISSION_PERCENT = 5")
|
||||
@@ -189,7 +213,7 @@ tasks.register("verifyBlackMarket") {
|
||||
|| !minecraftMixin.contains('method = "pickBlockOrEntity"')
|
||||
|| !middleClickPayload.contains('SanctuaryMod.id("c2s/black_market_interact")')
|
||||
|| !middleClickPayload.contains("No client position, listing ID, item or price is trusted")
|
||||
|| !network.contains("PROTOCOL_VERSION = 22")
|
||||
|| !network.contains("PROTOCOL_VERSION = 24")
|
||||
|| !network.contains("BlackMarketService.middleClick(context.player())")
|
||||
|| !service.contains("loadedDeliveryBox") || !service.contains("PARCEL_RETENTION_MILLIS")
|
||||
|| !policy.contains("DataComponents.CONTAINER") || !policy.contains("DataComponents.BUNDLE_CONTENTS")
|
||||
@@ -647,6 +671,14 @@ tasks.register("verifySanctuary") {
|
||||
,'matchesMouse'
|
||||
,'int bankIndex(int visualRow, int visibleRows)'
|
||||
,'playButtonClickSound'
|
||||
,'requestVisualRow(client, 0, currentRows)'
|
||||
,'isSortableStorageMenu'
|
||||
,'instanceof ChestMenu'
|
||||
,'instanceof ShulkerBoxMenu'
|
||||
,'instanceof HopperMenu'
|
||||
,'instanceof DispenserMenu'
|
||||
,'buffer.writeBoolean(container)'
|
||||
,'sortOpenContainer'
|
||||
,'GLFW.GLFW_KEY_TAB'
|
||||
,'HOTBAR_TEXTURE'
|
||||
,'0.0F, 0.0F, 182, 22, 182, 22'
|
||||
@@ -1176,13 +1208,13 @@ tasks.register("verifySanctuary") {
|
||||
}
|
||||
|
||||
def manifest = new JsonSlurper().parse(file("src/main/resources/fabric.mod.json"))
|
||||
if (manifest.custom?.sanctuary26?.data_version != 12
|
||||
|| manifest.custom?.sanctuary26?.network_protocol != 22
|
||||
if (manifest.custom?.sanctuary26?.data_version != 14
|
||||
|| manifest.custom?.sanctuary26?.network_protocol != 24
|
||||
|| manifest.depends?.jei != ">=30.24.0.176"
|
||||
|| manifest.depends?."fabric-api" != ">=0.155.0+26.2"
|
||||
|| !manifest.entrypoints?.client?.contains("fr.koka99cab.sanctuary26.sanctuary.client.SanctuaryClient")
|
||||
|| !manifest.mixins?.contains("sanctuary.mixins.json")) {
|
||||
throw new GradleException("Sanctuary must expose data v11, protocol 22, JEI and the client entrypoint")
|
||||
throw new GradleException("Sanctuary must expose data v14, protocol 24, JEI and the client entrypoint")
|
||||
}
|
||||
def capeMigration = new JsonSlurper().parse(rootProject.file(
|
||||
"pack/migrations/26.2.0-alpha.155-cape-equipment-to-alpha.156.json"))
|
||||
@@ -1667,8 +1699,8 @@ tasks.register("verifySanctuary") {
|
||||
|
||||
def worldgenRoot = file("src/main/resources/data/sanctuary/worldgen")
|
||||
def worldgenCounts = [
|
||||
density_function: 12, biome: 6, configured_feature: 13, configured_carver: 1,
|
||||
placed_feature: 19, noise_settings: 3, world_preset: 2
|
||||
density_function: 12, biome: 7, configured_feature: 14, configured_carver: 1,
|
||||
placed_feature: 20, noise_settings: 4, world_preset: 2
|
||||
]
|
||||
worldgenCounts.each { type, expected ->
|
||||
def actual = fileTree(new File(worldgenRoot, type.toString())).matching { include "**/*.json" }.files.size()
|
||||
@@ -2883,7 +2915,7 @@ tasks.register("verifyShopCurrencyRowsRelease") {
|
||||
"src/main/java/fr/koka99cab/sanctuary26/sanctuary/shop/BlackMarketService.java").text
|
||||
def shop = file(
|
||||
"src/main/java/fr/koka99cab/sanctuary26/sanctuary/shop/ShopService.java").text
|
||||
if (project.version.toString() != "0.0.0-alpha.126"
|
||||
if (project.version.toString() != "0.0.0-alpha.131"
|
||||
|| migration.target?.modules?.sanctuary != "0.0.0-alpha.117"
|
||||
|| migration.shop_currency_inventory?.all_unlocked_rows_counted != true
|
||||
|| migration.shop_currency_inventory?.overflow_rows_counted != true
|
||||
@@ -3049,7 +3081,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.126"
|
||||
if (project.version.toString() != "0.0.0-alpha.131"
|
||||
|| 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
|
||||
@@ -3087,7 +3119,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.126"
|
||||
if (project.version.toString() != "0.0.0-alpha.131"
|
||||
|| 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]
|
||||
@@ -3118,7 +3150,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.126"
|
||||
if (project.version.toString() != "0.0.0-alpha.131"
|
||||
|| migration.target?.pack_version != "26.2.0-alpha.195"
|
||||
|| migration.target?.modules?.sanctuary != "0.0.0-alpha.111"
|
||||
|| migration.compatibility?.sanctuary_data_version != 11
|
||||
@@ -3149,7 +3181,7 @@ tasks.register("verifyDeliveryBoxOrientation") {
|
||||
def blockstate = new JsonSlurper().parse(
|
||||
file("src/main/resources/assets/sanctuary/blockstates/delivery_box.json"))
|
||||
def expectedVariants = ["facing=north", "facing=east", "facing=south", "facing=west"].toSet()
|
||||
if (project.version.toString() != "0.0.0-alpha.126"
|
||||
if (project.version.toString() != "0.0.0-alpha.131"
|
||||
|| migration.target?.modules?.sanctuary != "0.0.0-alpha.112"
|
||||
|| migration.mailbox?.id != "sanctuary:delivery_box"
|
||||
|| migration.mailbox?.horizontal_facing != true
|
||||
@@ -3177,7 +3209,7 @@ tasks.register("verifyInventoryShiftClick") {
|
||||
def mixin = file("src/main/java/fr/koka99cab/sanctuary26/sanctuary/mixin/AbstractContainerMenuMixin.java").text
|
||||
def screen = file("src/main/java/fr/koka99cab/sanctuary26/sanctuary/mixin/client/AbstractContainerScreenMixin.java").text
|
||||
def gui = file("INVENTORY_GUI.md").text
|
||||
if (project.version.toString() != "0.0.0-alpha.126"
|
||||
if (project.version.toString() != "0.0.0-alpha.131"
|
||||
|| !transfer.contains("shouldAppendOverflow")
|
||||
|| !transfer.contains("isVanillaStorageRange")
|
||||
|| !transfer.contains("hotbarStart")
|
||||
@@ -3223,7 +3255,7 @@ tasks.register("verifyQuestRequests") {
|
||||
new JsonSlurper().parse(file("src/main/resources/data/sanctuary/loot_table/blocks/${path}.json"))
|
||||
.pools[0].entries[0].name
|
||||
}
|
||||
if (project.version.toString() != "0.0.0-alpha.126"
|
||||
if (project.version.toString() != "0.0.0-alpha.131"
|
||||
|| migration.target?.modules?.sanctuary != "0.0.0-alpha.113"
|
||||
|| migration.quest_requests?.save_key != "quest_requests"
|
||||
|| migration.quest_requests?.data_version != 1
|
||||
@@ -3235,7 +3267,7 @@ tasks.register("verifyQuestRequests") {
|
||||
|| !data.contains('SAVE_KEY = "quest_requests"') || !data.contains("DATA_VERSION = 3")
|
||||
|| !service.contains("PlayerBlockBreakEvents.BEFORE")
|
||||
|| !service.contains("player.blockPosition().distSqr(payload.pos()) > 64.0D")
|
||||
|| !network.contains("PROTOCOL_VERSION = 22")
|
||||
|| !network.contains("PROTOCOL_VERSION = 24")
|
||||
|| !network.contains("QuestRequestSubmitPayload.ID")
|
||||
|| !network.contains("QuestRequestAcceptPayload.ID")
|
||||
|| !registry.contains('SanctuaryMod.id("quest_request_sign")')
|
||||
@@ -3281,7 +3313,7 @@ tasks.register("verifyQuestRequestContracts") {
|
||||
def locales = ["fr_fr", "en_us", "ru_ru"].collectEntries { locale ->
|
||||
[(locale): new JsonSlurper().parse(file("src/main/resources/assets/sanctuary/lang/${locale}.json"))]
|
||||
}
|
||||
if (project.version.toString() != "0.0.0-alpha.126"
|
||||
if (project.version.toString() != "0.0.0-alpha.131"
|
||||
|| 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"
|
||||
@@ -3301,7 +3333,7 @@ tasks.register("verifyQuestRequestContracts") {
|
||||
|| !service.contains('REWARD_LOAN = "loan"')
|
||||
|| !service.contains('PLAYER.equals(targetKind)')
|
||||
|| !screen.contains("rewardQuantity") || !screen.contains("killTargetIndex")
|
||||
|| !network.contains("PROTOCOL_VERSION = 22")
|
||||
|| !network.contains("PROTOCOL_VERSION = 24")
|
||||
|| standingModel.textures?.all != "sanctuary:block/pink_quest_request_sign"
|
||||
|| itemModel.textures?.layer0 != "sanctuary:item/pink_quest_request_sign"
|
||||
|| blockTexture == null || blockTexture.width != 32 || blockTexture.height != 32
|
||||
@@ -3350,7 +3382,7 @@ tasks.register("verifyQuestRequestEscrow") {
|
||||
def locales = ["fr_fr", "en_us", "ru_ru"].collectEntries { locale ->
|
||||
[(locale): new JsonSlurper().parse(file("src/main/resources/assets/sanctuary/lang/${locale}.json"))]
|
||||
}
|
||||
if (project.version.toString() != "0.0.0-alpha.126"
|
||||
if (project.version.toString() != "0.0.0-alpha.131"
|
||||
|| 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"
|
||||
@@ -3371,7 +3403,7 @@ tasks.register("verifyQuestRequestEscrow") {
|
||||
|| !service.contains('"quest_request_reward"')
|
||||
|| !service.contains('"quest_request_delivery"')
|
||||
|| service.contains("sanctuary_quest_accept")
|
||||
|| !network.contains("PROTOCOL_VERSION = 22")
|
||||
|| !network.contains("PROTOCOL_VERSION = 24")
|
||||
|| !network.contains("QuestRequestAcceptPayload.ID")
|
||||
|| !network.contains("QuestRequestConfirmPayload.ID")
|
||||
|| !acceptPayload.contains("readUtf(36)")
|
||||
@@ -3395,6 +3427,68 @@ tasks.named("verifySanctuary") {
|
||||
dependsOn tasks.named("verifyInventoryShiftClick")
|
||||
}
|
||||
|
||||
tasks.register("verifyCraftableBannerCape") {
|
||||
group = "verification"
|
||||
description = "Checks the white wool recipe, banner decoration and equipped pattern rendering."
|
||||
inputs.file(rootProject.file(
|
||||
"pack/migrations/26.2.0-alpha.225-craftable-banner-cape-to-alpha.226.json"))
|
||||
inputs.file(file("src/main/resources/data/sanctuary/recipe/white_cape.json"))
|
||||
inputs.file(file("src/main/resources/data/sanctuary/recipe/white_cape_banner.json"))
|
||||
inputs.file(file("src/main/resources/assets/sanctuary/items/white_cape.json"))
|
||||
inputs.files(fileTree("src/main/resources/assets/sanctuary/lang") { include "*.json" })
|
||||
inputs.files(
|
||||
file("src/main/java/fr/koka99cab/sanctuary26/sanctuary/gameplay/SanctuaryCapes.java"),
|
||||
file("src/main/java/fr/koka99cab/sanctuary26/sanctuary/network/CapeSyncPayload.java"),
|
||||
file("src/main/java/fr/koka99cab/sanctuary26/sanctuary/network/SanctuaryNetworking.java"),
|
||||
file("src/main/java/fr/koka99cab/sanctuary26/sanctuary/client/SanctuaryBannerCapeLayer.java"))
|
||||
doLast {
|
||||
def migration = new JsonSlurper().parse(rootProject.file(
|
||||
"pack/migrations/26.2.0-alpha.225-craftable-banner-cape-to-alpha.226.json"))
|
||||
def baseRecipe = new JsonSlurper().parse(file(
|
||||
"src/main/resources/data/sanctuary/recipe/white_cape.json"))
|
||||
def bannerRecipe = new JsonSlurper().parse(file(
|
||||
"src/main/resources/data/sanctuary/recipe/white_cape_banner.json"))
|
||||
def itemDefinition = new JsonSlurper().parse(file(
|
||||
"src/main/resources/assets/sanctuary/items/white_cape.json"))
|
||||
def capes = file(
|
||||
"src/main/java/fr/koka99cab/sanctuary26/sanctuary/gameplay/SanctuaryCapes.java").text
|
||||
def payload = file(
|
||||
"src/main/java/fr/koka99cab/sanctuary26/sanctuary/network/CapeSyncPayload.java").text
|
||||
def networking = file(
|
||||
"src/main/java/fr/koka99cab/sanctuary26/sanctuary/network/SanctuaryNetworking.java").text
|
||||
def renderer = file(
|
||||
"src/main/java/fr/koka99cab/sanctuary26/sanctuary/client/SanctuaryBannerCapeLayer.java").text
|
||||
def locales = ["fr_fr", "en_us", "ru_ru"].collect {
|
||||
new JsonSlurper().parse(file("src/main/resources/assets/sanctuary/lang/${it}.json"))
|
||||
}
|
||||
if (project.version.toString() != "0.0.0-alpha.131"
|
||||
|| migration.target?.modules?.sanctuary != "0.0.0-alpha.130"
|
||||
|| migration.white_cape?.crafting_material_count != 6
|
||||
|| migration.white_cape?.maximum_pattern_layers != 6
|
||||
|| baseRecipe.pattern != ["WW", "WW", "WW"]
|
||||
|| baseRecipe.key?.W != "minecraft:white_wool"
|
||||
|| baseRecipe.result?.id != "sanctuary:white_cape"
|
||||
|| bannerRecipe.type != "minecraft:crafting_special_shielddecoration"
|
||||
|| bannerRecipe.banner != "#minecraft:banners"
|
||||
|| bannerRecipe.target != "sanctuary:white_cape"
|
||||
|| itemDefinition.model?.type != "minecraft:special"
|
||||
|| itemDefinition.model?.model?.type != "minecraft:banner"
|
||||
|| locales.any { !it.containsKey("item.sanctuary.white_cape") }
|
||||
|| !capes.contains('WHITE_CAPE_PATH = "white_cape"')
|
||||
|| !payload.contains("MAX_PATTERN_LAYERS = 6")
|
||||
|| !payload.contains("DataComponents.BASE_COLOR")
|
||||
|| !payload.contains("DataComponents.BANNER_PATTERNS")
|
||||
|| !networking.contains("PROTOCOL_VERSION = 24")
|
||||
|| !renderer.contains("BannerRenderer.submitPatterns")) {
|
||||
throw new GradleException("Sanctuary alpha.130 craftable banner cape is incomplete")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tasks.named("verifySanctuary") {
|
||||
dependsOn tasks.named("verifyCraftableBannerCape")
|
||||
}
|
||||
|
||||
tasks.register("verifyAlphaIndev") {
|
||||
group = "verification"
|
||||
description = "Checks the alpha.122 finite Indev world, migration and attribution contract."
|
||||
@@ -3422,7 +3516,7 @@ tasks.register("verifyAlphaIndev") {
|
||||
def locales = ["fr_fr", "en_us", "ru_ru"].collect {
|
||||
new JsonSlurper().parse(file("src/main/resources/assets/sanctuary/lang/${it}.json"))
|
||||
}
|
||||
if (project.version.toString() != "0.0.0-alpha.126"
|
||||
if (project.version.toString() != "0.0.0-alpha.131"
|
||||
|| migration.target?.modules?.sanctuary != "0.0.0-alpha.122"
|
||||
|| migration.alpha_world?.dimension != "sanctuary:alpha"
|
||||
|| migration.alpha_world?.legacy_dimension != "portals:alpha"
|
||||
@@ -3444,7 +3538,73 @@ tasks.register("verifyAlphaIndev") {
|
||||
}
|
||||
}
|
||||
|
||||
tasks.register("verifyBackroom") {
|
||||
group = "verification"
|
||||
description = "Checks Backroom dimension, migration, Mailbox boundary, loss registry and translations."
|
||||
inputs.file(file("BACKROOM.md"))
|
||||
inputs.file(rootProject.file("pack/migrations/sanctuary-backroom-data-v14.json"))
|
||||
inputs.files(fileTree("src/main/java/fr/koka99cab/sanctuary26/sanctuary/backroom"))
|
||||
inputs.files(fileTree("src/main/resources/data/sanctuary") { include "**/*backroom*.json" })
|
||||
inputs.files(fileTree("src/main/resources/assets/sanctuary/lang") { include "*.json" })
|
||||
inputs.files(
|
||||
file("src/main/java/fr/koka99cab/sanctuary26/sanctuary/api/backroom/BackroomMaterialApi.java"),
|
||||
file("src/main/java/fr/koka99cab/sanctuary26/sanctuary/shop/DeliveryBoxBlockEntity.java"),
|
||||
file("src/main/java/fr/koka99cab/sanctuary26/sanctuary/worldgen/feature/BackroomArchitectureFeature.java"),
|
||||
file("src/main/resources/data/minecraft/worldgen/world_preset/normal.json"),
|
||||
file("src/main/resources/data/sanctuary/worldgen/world_preset/overworld.json"),
|
||||
file("src/main/resources/data/sanctuary/worldgen/world_preset/sanctuary.json"))
|
||||
doLast {
|
||||
def migration = new JsonSlurper().parse(rootProject.file("pack/migrations/sanctuary-backroom-data-v14.json"))
|
||||
def dimensionType = new JsonSlurper().parse(file("src/main/resources/data/sanctuary/dimension_type/backroom.json"))
|
||||
def presets = [
|
||||
file("src/main/resources/data/minecraft/worldgen/world_preset/normal.json"),
|
||||
file("src/main/resources/data/sanctuary/worldgen/world_preset/overworld.json"),
|
||||
file("src/main/resources/data/sanctuary/worldgen/world_preset/sanctuary.json")
|
||||
].collect { new JsonSlurper().parse(it) }
|
||||
def data = file("src/main/java/fr/koka99cab/sanctuary26/sanctuary/backroom/BackroomData.java").text
|
||||
def inventory = file("src/main/java/fr/koka99cab/sanctuary26/sanctuary/backroom/BackroomInventory.java").text
|
||||
def service = file("src/main/java/fr/koka99cab/sanctuary26/sanctuary/backroom/BackroomService.java").text
|
||||
def chest = file("src/main/java/fr/koka99cab/sanctuary26/sanctuary/backroom/BackroomChestService.java").text
|
||||
def mailbox = file("src/main/java/fr/koka99cab/sanctuary26/sanctuary/shop/DeliveryBoxBlockEntity.java").text
|
||||
def architecture = file("src/main/java/fr/koka99cab/sanctuary26/sanctuary/worldgen/feature/BackroomArchitectureFeature.java").text
|
||||
def materialApi = file(
|
||||
"src/main/java/fr/koka99cab/sanctuary26/sanctuary/api/backroom/BackroomMaterialApi.java").text
|
||||
def locales = ["fr_fr", "en_us", "ru_ru"].collect {
|
||||
new JsonSlurper().parse(file("src/main/resources/assets/sanctuary/lang/${it}.json"))
|
||||
}
|
||||
if (project.version.toString() != "0.0.0-alpha.131"
|
||||
|| migration.source_data_version != 13 || migration.target_data_version != 14
|
||||
|| migration.integration_modules?.anotherworld?.source != "0.0.0-alpha.56"
|
||||
|| migration.integration_modules?.anotherworld?.target != "0.0.0-alpha.57"
|
||||
|| migration.compatibility?.network_protocol_changed != false
|
||||
|| presets.any { it.dimensions?.get("sanctuary:backroom") == null }
|
||||
|| dimensionType.attributes?.get("minecraft:gameplay/bed_rule")?.can_set_spawn != "never"
|
||||
|| dimensionType.attributes?.get("minecraft:gameplay/bed_rule")?.can_sleep != "never"
|
||||
|| !data.contains("MAX_LOST_ITEMS = 16_384") || !data.contains("MAX_CHEST_STATES = 65_536")
|
||||
|| !data.contains("PERSONAL_RESERVATION_TICKS = 24_000L")
|
||||
|| !data.contains("indexOfLoss(marker)") || !data.contains("BACKROOM_LOSS_COMPONENT")
|
||||
|| !inventory.contains("companionEgg") || !inventory.contains("equippedCape")
|
||||
|| !service.contains("ARRIVAL_RADIUS = 2_000") || !service.contains("insertTransaction")
|
||||
|| !service.contains("AUTHORIZED_TRANSITIONS")
|
||||
|| !service.contains("level -> !isBackroom(level)")
|
||||
|| !mailbox.contains("MAX_PROCESSED_TRANSACTIONS = 128")
|
||||
|| !chest.contains("REFRESH_TICKS = 12_000L") || !chest.contains("ordinal == 3")
|
||||
|| !architecture.contains("PLAYABLE_RADIUS = 2_000")
|
||||
|| !architecture.contains("BackroomMaterialApi.anomalyMaterial()")
|
||||
|| !materialApi.contains("registerAnomalyMaterial")
|
||||
|| locales.any { !it.containsKey("dimension.sanctuary.backroom")
|
||||
|| !it.containsKey("message.sanctuary.backroom.bed_entry_only")
|
||||
|| !it.containsKey("message.sanctuary.backroom.registry_busy")
|
||||
|| !it.containsKey("message.sanctuary.backroom.mailbox_full")
|
||||
|| !it.containsKey("message.sanctuary.backroom.exited_with_losses") }) {
|
||||
throw new GradleException("Sanctuary alpha.131 Backroom contract is incomplete")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tasks.named("check") {
|
||||
dependsOn tasks.named("backroomModelSmoke")
|
||||
dependsOn tasks.named("verifyBackroom")
|
||||
dependsOn tasks.named("notchVictoryStateSmoke")
|
||||
dependsOn tasks.named("verifySanctuary")
|
||||
dependsOn tasks.named("verifyDeliveryBoxOrientation")
|
||||
@@ -3459,6 +3619,8 @@ tasks.named("check") {
|
||||
dependsOn tasks.named("questIdSuggestionsSmoke")
|
||||
dependsOn tasks.named("alphaIndevGeneratorSmoke")
|
||||
dependsOn tasks.named("capeJoinSyncSmoke")
|
||||
dependsOn tasks.named("capeSyncPayloadSmoke")
|
||||
dependsOn tasks.named("inventorySortSmoke")
|
||||
dependsOn tasks.named("entityPolicySmoke")
|
||||
dependsOn tasks.named("verifyAlphaIndev")
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ import fr.koka99cab.sanctuary26.sanctuary.shop.ShopService;
|
||||
import fr.koka99cab.sanctuary26.sanctuary.shop.BlackMarketService;
|
||||
import fr.koka99cab.sanctuary26.sanctuary.shop.QuestBoardService;
|
||||
import fr.koka99cab.sanctuary26.sanctuary.shop.QuestRequestService;
|
||||
import fr.koka99cab.sanctuary26.sanctuary.backroom.BackroomService;
|
||||
import net.fabricmc.api.ModInitializer;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import org.slf4j.Logger;
|
||||
@@ -34,7 +35,7 @@ import org.slf4j.LoggerFactory;
|
||||
public final class SanctuaryMod implements ModInitializer {
|
||||
public static final String MOD_ID = "sanctuary";
|
||||
public static final String DISPLAY_NAME = "Sanctuary";
|
||||
public static final int CURRENT_DATA_VERSION = 12;
|
||||
public static final int CURRENT_DATA_VERSION = 14;
|
||||
public static final Logger LOGGER = LoggerFactory.getLogger(MOD_ID);
|
||||
|
||||
public SanctuaryMod() {
|
||||
@@ -53,6 +54,7 @@ public final class SanctuaryMod implements ModInitializer {
|
||||
SanctuarySpawn.register();
|
||||
SanctuaryWorldProfileData.register();
|
||||
SanctuaryRegistries.initialize();
|
||||
BackroomService.initialize();
|
||||
SanctuaryBulkActions.initialize();
|
||||
SanctuaryInventoryCapacity.initialize();
|
||||
SanctuaryHotbarRows.initialize();
|
||||
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
package fr.koka99cab.sanctuary26.sanctuary.api.backroom;
|
||||
|
||||
import java.util.Objects;
|
||||
import net.minecraft.world.level.block.Block;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
|
||||
/** Public, one-way integration point for rare materials owned by other modules. */
|
||||
public final class BackroomMaterialApi {
|
||||
private static Block anomalyMaterial = Blocks.WARPED_WART_BLOCK;
|
||||
|
||||
private BackroomMaterialApi() {}
|
||||
|
||||
public static void registerAnomalyMaterial(Block material) {
|
||||
anomalyMaterial = Objects.requireNonNull(material, "Backroom anomaly material");
|
||||
}
|
||||
|
||||
public static Block anomalyMaterial() {
|
||||
return anomalyMaterial;
|
||||
}
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
package fr.koka99cab.sanctuary26.sanctuary.api.entity;
|
||||
|
||||
import java.util.Set;
|
||||
import net.minecraft.world.entity.Entity;
|
||||
|
||||
/** Public cross-module policy for Sanctuary-owned temporary gameplay entities. */
|
||||
public final class SanctuaryEntityPolicy {
|
||||
public static final String TEMPORARY_COMPANION_TAG = "sanctuary_companion";
|
||||
|
||||
private SanctuaryEntityPolicy() {
|
||||
}
|
||||
|
||||
/** Temporary companions must never yield loot, experience or quest progress. */
|
||||
public static boolean isTemporaryCompanion(Entity entity) {
|
||||
return entity != null && containsTemporaryCompanionTag(entity.entityTags());
|
||||
}
|
||||
|
||||
static boolean containsTemporaryCompanionTag(Set<String> tags) {
|
||||
return tags != null && tags.contains(TEMPORARY_COMPANION_TAG);
|
||||
}
|
||||
}
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
package fr.koka99cab.sanctuary26.sanctuary.api.item;
|
||||
|
||||
import fr.koka99cab.sanctuary26.sanctuary.SanctuaryMod;
|
||||
import net.minecraft.core.component.DataComponents;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.item.component.CustomData;
|
||||
|
||||
/** Stable item metadata used to distinguish boss rewards, migrations and Shop replacements. */
|
||||
public final class SanctuaryItemProvenance {
|
||||
public static final Identifier NOTCH_REWARD = SanctuaryMod.id("notch_reward");
|
||||
public static final Identifier SHOP_PURCHASE = SanctuaryMod.id("shop_purchase");
|
||||
public static final Identifier LEGACY_NOTCH_TOTEM = SanctuaryMod.id("legacy_notch_totem");
|
||||
private static final String ORIGIN_KEY = "sanctuary_origin";
|
||||
private static final String VERSION_KEY = "sanctuary_origin_version";
|
||||
|
||||
private SanctuaryItemProvenance() {
|
||||
}
|
||||
|
||||
public static void stamp(ItemStack stack, Identifier origin) {
|
||||
if (stack == null || stack.isEmpty() || origin == null) return;
|
||||
CustomData.update(DataComponents.CUSTOM_DATA, stack, tag -> {
|
||||
tag.putString(ORIGIN_KEY, origin.toString());
|
||||
tag.putInt(VERSION_KEY, 1);
|
||||
});
|
||||
}
|
||||
|
||||
public static void stampIfAbsent(ItemStack stack, Identifier origin) {
|
||||
if (origin(stack) == null) stamp(stack, origin);
|
||||
}
|
||||
|
||||
public static Identifier origin(ItemStack stack) {
|
||||
if (stack == null || stack.isEmpty()) return null;
|
||||
CustomData data = stack.get(DataComponents.CUSTOM_DATA);
|
||||
if (data == null) return null;
|
||||
return Identifier.tryParse(data.copyTag().getStringOr(ORIGIN_KEY, ""));
|
||||
}
|
||||
|
||||
public static boolean hasOrigin(ItemStack stack, Identifier origin) {
|
||||
return origin != null && origin.equals(origin(stack));
|
||||
}
|
||||
}
|
||||
+8
-2
@@ -57,6 +57,11 @@ public final class SanctuaryNotchMilestone {
|
||||
return NotchVictoryData.get(server).claimInitialReward();
|
||||
}
|
||||
|
||||
/** Completes the reservation only after the physical reward chest exists. */
|
||||
public static boolean completeInitialReward(MinecraftServer server) {
|
||||
return NotchVictoryData.get(server).completeInitialReward();
|
||||
}
|
||||
|
||||
private static VictoryResult record(ServerLevel alphaLevel, List<UUID> participants,
|
||||
boolean rewardAlreadyClaimed, boolean announce) {
|
||||
NotchVictoryData data = NotchVictoryData.get(alphaLevel.getServer());
|
||||
@@ -73,7 +78,8 @@ public final class SanctuaryNotchMilestone {
|
||||
alphaLevel.getServer().getPlayerList().broadcastSystemMessage(
|
||||
Component.translatable("message.sanctuary.notch_defeated").withStyle(ChatFormatting.GOLD), false);
|
||||
}
|
||||
return new VictoryResult(newlyDefeated, state.participants(), state.initialRewardClaimed());
|
||||
return new VictoryResult(newlyDefeated, state.participants(), state.initialRewardClaimed(),
|
||||
state.initialRewardDelivered());
|
||||
}
|
||||
|
||||
private static List<UUID> sanitizeParticipants(ServerLevel level, Collection<UUID> candidates) {
|
||||
@@ -91,7 +97,7 @@ public final class SanctuaryNotchMilestone {
|
||||
}
|
||||
|
||||
public record VictoryResult(boolean newlyDefeated, List<UUID> participants,
|
||||
boolean initialRewardClaimed) {
|
||||
boolean initialRewardClaimed, boolean initialRewardDelivered) {
|
||||
public VictoryResult {
|
||||
participants = List.copyOf(participants);
|
||||
}
|
||||
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
package fr.koka99cab.sanctuary26.sanctuary.api.progression;
|
||||
|
||||
import fr.koka99cab.sanctuary26.sanctuary.api.notch.SanctuaryNotchMilestone;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
|
||||
/** Public, server-authoritative resolver for world-wide progression milestones. */
|
||||
public final class SanctuaryWorldMilestones {
|
||||
private SanctuaryWorldMilestones() {
|
||||
}
|
||||
|
||||
public static boolean isReached(MinecraftServer server, Identifier milestone) {
|
||||
if (server == null || milestone == null) return false;
|
||||
return milestone.equals(SanctuaryNotchMilestone.NOTCH_DEFEATED)
|
||||
&& SanctuaryNotchMilestone.isDefeated(server);
|
||||
}
|
||||
}
|
||||
+149
@@ -0,0 +1,149 @@
|
||||
package fr.koka99cab.sanctuary26.sanctuary.backroom;
|
||||
|
||||
import fr.koka99cab.sanctuary26.sanctuary.registry.SanctuaryRegistries;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.util.RandomSource;
|
||||
import net.minecraft.world.item.Item;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.item.Items;
|
||||
import net.minecraft.world.level.block.ChestBlock;
|
||||
import net.minecraft.world.level.block.entity.ChestBlockEntity;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.block.state.properties.ChestType;
|
||||
|
||||
/** Renewable loot nodes backed by the non-duplicating real-item registry. */
|
||||
public final class BackroomChestService {
|
||||
public static final long REFRESH_TICKS = 12_000L;
|
||||
|
||||
private BackroomChestService() {}
|
||||
|
||||
public static void open(ChestBlockEntity opened, ServerPlayer player) {
|
||||
if (!(opened.getLevel() instanceof ServerLevel level) || !BackroomService.isBackroom(level)) return;
|
||||
ChestGroup group = group(level, opened);
|
||||
BackroomData data = BackroomData.get(level.getServer());
|
||||
long cycle = Math.floorDiv(level.getGameTime(), REFRESH_TICKS);
|
||||
BackroomData.ChestState state = data.chest(group.anchor).orElse(null);
|
||||
ArrayList<UUID> assigned = state == null ? new ArrayList<>() : new ArrayList<>(state.assignedIds());
|
||||
boolean refreshed = state == null || state.cycle() != cycle;
|
||||
|
||||
if (refreshed) {
|
||||
data.reconcileChest(group.anchor, markers(group));
|
||||
group.clear();
|
||||
assigned.clear();
|
||||
}
|
||||
|
||||
BackroomData.ChestVisit visit = data.visitChest(player.getUUID(), group.anchor);
|
||||
boolean personalInserted = false;
|
||||
if (visit.firstVisit() && personalRoll(player.getRandom(), visit.ordinal())) {
|
||||
BackroomData.MaterializedLoss loss = data.takePersonal(player.getUUID(), group.anchor).orElse(null);
|
||||
if (loss != null && group.insertLoss(loss.stack())) {
|
||||
assigned.add(loss.id());
|
||||
personalInserted = true;
|
||||
} else if (loss != null) {
|
||||
data.capture(loss.stack(), player.getUUID(), "chest_full", level.getGameTime());
|
||||
}
|
||||
}
|
||||
if (refreshed) rollGeneralLoot(level, group, data, assigned, cycle);
|
||||
if (refreshed || personalInserted) data.updateChest(group.anchor, cycle, assigned);
|
||||
}
|
||||
|
||||
public static boolean personalRoll(RandomSource random, int ordinal) {
|
||||
if (ordinal <= 0) return false;
|
||||
if (ordinal == 1) return random.nextFloat() < 0.75F;
|
||||
if (ordinal == 2) return random.nextFloat() < 0.90F;
|
||||
if (ordinal == 3) return true;
|
||||
return random.nextFloat() < 0.65F;
|
||||
}
|
||||
|
||||
private static void rollGeneralLoot(ServerLevel level, ChestGroup group, BackroomData data,
|
||||
List<UUID> assigned, long cycle) {
|
||||
RandomSource random = RandomSource.create(level.getSeed() ^ group.anchor ^ cycle * 0x9E3779B97F4A7C15L);
|
||||
int roll = random.nextInt(100);
|
||||
if (roll < 55) return;
|
||||
if (roll < 74) {
|
||||
insert(group, random, Items.DIRT, 4, 20);
|
||||
if (random.nextBoolean()) insert(group, random, Items.OAK_PLANKS, 2, 10);
|
||||
return;
|
||||
}
|
||||
if (roll < 90) {
|
||||
insert(group, random, Items.COBBLESTONE, 6, 24);
|
||||
insert(group, random, random.nextBoolean() ? Items.TORCH : Items.BREAD, 2, 7);
|
||||
if (random.nextInt(3) == 0) group.insert(new ItemStack(Items.WOODEN_PICKAXE));
|
||||
return;
|
||||
}
|
||||
if (roll < 97) {
|
||||
data.takeShared(group.anchor, level.getGameTime()).ifPresent(loss -> {
|
||||
if (group.insertLoss(loss.stack())) assigned.add(loss.id());
|
||||
else data.capture(loss.stack(), null, "chest_full", level.getGameTime());
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (roll < 99) {
|
||||
group.insert(new ItemStack(Items.IRON_PICKAXE));
|
||||
insert(group, random, Items.ENDER_PEARL, 1, 2);
|
||||
} else {
|
||||
group.insert(new ItemStack(Items.DIAMOND));
|
||||
}
|
||||
}
|
||||
|
||||
private static void insert(ChestGroup group, RandomSource random, Item item, int minimum, int maximum) {
|
||||
group.insert(new ItemStack(item, minimum + random.nextInt(maximum - minimum + 1)));
|
||||
}
|
||||
|
||||
private static List<UUID> markers(ChestGroup group) {
|
||||
ArrayList<UUID> result = new ArrayList<>();
|
||||
for (ChestBlockEntity chest : group.chests) for (int slot = 0; slot < chest.getContainerSize(); slot++) {
|
||||
UUID marker = chest.getItem(slot).get(SanctuaryRegistries.BACKROOM_LOSS_COMPONENT);
|
||||
if (marker != null) result.add(marker);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static ChestGroup group(ServerLevel level, ChestBlockEntity opened) {
|
||||
BlockPos pos = opened.getBlockPos();
|
||||
BlockState state = opened.getBlockState();
|
||||
if (!(state.getBlock() instanceof ChestBlock) || state.getValue(ChestBlock.TYPE) == ChestType.SINGLE) {
|
||||
return new ChestGroup(pos.asLong(), List.of(opened));
|
||||
}
|
||||
BlockPos connectedPos = ChestBlock.getConnectedBlockPos(pos, state);
|
||||
if (!(level.getBlockEntity(connectedPos) instanceof ChestBlockEntity connected)) {
|
||||
return new ChestGroup(pos.asLong(), List.of(opened));
|
||||
}
|
||||
long anchor = Math.min(pos.asLong(), connectedPos.asLong());
|
||||
return pos.asLong() == anchor
|
||||
? new ChestGroup(anchor, List.of(opened, connected))
|
||||
: new ChestGroup(anchor, List.of(connected, opened));
|
||||
}
|
||||
|
||||
private record ChestGroup(long anchor, List<ChestBlockEntity> chests) {
|
||||
private void clear() {
|
||||
chests.forEach(chest -> { chest.clearContent(); chest.setChanged(); });
|
||||
}
|
||||
|
||||
private boolean insert(ItemStack offered) {
|
||||
if (offered == null || offered.isEmpty()) return false;
|
||||
for (ChestBlockEntity chest : chests) if (insertInto(chest, offered)) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
/** Keeps every real loss in the physical anchor chest tracked by the persisted state. */
|
||||
private boolean insertLoss(ItemStack offered) {
|
||||
return offered != null && !offered.isEmpty() && insertInto(chests.getFirst(), offered);
|
||||
}
|
||||
|
||||
private static boolean insertInto(ChestBlockEntity chest, ItemStack offered) {
|
||||
for (int slot = 0; slot < chest.getContainerSize(); slot++) {
|
||||
if (!chest.getItem(slot).isEmpty()) continue;
|
||||
chest.setItem(slot, offered.copy());
|
||||
chest.setChanged();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
+316
@@ -0,0 +1,316 @@
|
||||
package fr.koka99cab.sanctuary26.sanctuary.backroom;
|
||||
|
||||
import com.mojang.serialization.Codec;
|
||||
import com.mojang.serialization.codecs.RecordCodecBuilder;
|
||||
import fr.koka99cab.sanctuary26.sanctuary.SanctuaryMod;
|
||||
import fr.koka99cab.sanctuary26.sanctuary.registry.SanctuaryRegistries;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.level.Level;
|
||||
import net.minecraft.world.level.saveddata.SavedData;
|
||||
import net.minecraft.world.level.saveddata.SavedDataType;
|
||||
|
||||
/** Persistent, bounded state for Backroom entries and the real-item loss pool. */
|
||||
public final class BackroomData extends SavedData {
|
||||
public static final String SAVE_KEY = "backroom";
|
||||
public static final int DATA_VERSION = 1;
|
||||
public static final int MAX_PENDING_ENTRIES = 256;
|
||||
public static final int MAX_ENTRY_SLOTS = 64;
|
||||
public static final int MAX_LOST_ITEMS = 16_384;
|
||||
public static final int MAX_CHEST_STATES = 65_536;
|
||||
public static final int MAX_PLAYER_STATES = 4_096;
|
||||
public static final int MAX_VISITED_CHESTS = 128;
|
||||
public static final int MAX_RECENT_ARRIVALS = 8;
|
||||
public static final long PERSONAL_RESERVATION_TICKS = 24_000L;
|
||||
public static final String LOST_POOL = "pool";
|
||||
public static final String LOST_CHEST = "chest";
|
||||
|
||||
public static final Codec<EntrySlot> ENTRY_SLOT_CODEC = RecordCodecBuilder.create(instance -> instance.group(
|
||||
Codec.STRING.fieldOf("location").forGetter(EntrySlot::location),
|
||||
Codec.INT.fieldOf("index").forGetter(EntrySlot::index),
|
||||
ItemStack.CODEC.fieldOf("item").forGetter(EntrySlot::item)
|
||||
).apply(instance, EntrySlot::new));
|
||||
private static final Codec<PendingEntry> PENDING_CODEC = RecordCodecBuilder.create(instance -> instance.group(
|
||||
Codec.STRING.fieldOf("player").forGetter(PendingEntry::player),
|
||||
Codec.STRING.fieldOf("transaction").forGetter(PendingEntry::transaction),
|
||||
ENTRY_SLOT_CODEC.listOf(0, MAX_ENTRY_SLOTS).fieldOf("slots").forGetter(PendingEntry::slots)
|
||||
).apply(instance, PendingEntry::new));
|
||||
private static final Codec<LostItem> LOST_CODEC = RecordCodecBuilder.create(instance -> instance.group(
|
||||
Codec.STRING.fieldOf("id").forGetter(LostItem::id),
|
||||
Codec.STRING.optionalFieldOf("owner", "").forGetter(LostItem::owner),
|
||||
Codec.STRING.fieldOf("cause").forGetter(LostItem::cause),
|
||||
Codec.LONG.fieldOf("lost_at").forGetter(LostItem::lostAt),
|
||||
ItemStack.CODEC.fieldOf("item").forGetter(LostItem::item),
|
||||
Codec.STRING.optionalFieldOf("state", LOST_POOL).forGetter(LostItem::state),
|
||||
Codec.LONG.optionalFieldOf("chest", Long.MIN_VALUE).forGetter(LostItem::chest)
|
||||
).apply(instance, LostItem::new));
|
||||
private static final Codec<ChestState> CHEST_CODEC = RecordCodecBuilder.create(instance -> instance.group(
|
||||
Codec.LONG.fieldOf("pos").forGetter(ChestState::pos),
|
||||
Codec.LONG.fieldOf("cycle").forGetter(ChestState::cycle),
|
||||
Codec.STRING.listOf(0, 54).optionalFieldOf("assigned", List.of()).forGetter(ChestState::assigned)
|
||||
).apply(instance, ChestState::new));
|
||||
private static final Codec<PlayerState> PLAYER_CODEC = RecordCodecBuilder.create(instance -> instance.group(
|
||||
Codec.STRING.fieldOf("player").forGetter(PlayerState::player),
|
||||
Codec.INT.optionalFieldOf("personal_attempts", 0).forGetter(PlayerState::personalAttempts),
|
||||
Codec.LONG.listOf(0, MAX_VISITED_CHESTS).optionalFieldOf("visited_chests", List.of()).forGetter(PlayerState::visitedChests),
|
||||
Codec.LONG.listOf(0, MAX_RECENT_ARRIVALS).optionalFieldOf("recent_arrivals", List.of()).forGetter(PlayerState::recentArrivals)
|
||||
).apply(instance, PlayerState::new));
|
||||
private static final Codec<BackroomData> CODEC = RecordCodecBuilder.create(instance -> instance.group(
|
||||
Codec.INT.optionalFieldOf("data_version", DATA_VERSION).forGetter(data -> data.dataVersion),
|
||||
PENDING_CODEC.listOf(0, MAX_PENDING_ENTRIES).optionalFieldOf("pending_entries", List.of()).forGetter(data -> data.pendingEntries),
|
||||
LOST_CODEC.listOf(0, MAX_LOST_ITEMS).optionalFieldOf("lost_items", List.of()).forGetter(data -> data.lostItems),
|
||||
CHEST_CODEC.listOf(0, MAX_CHEST_STATES).optionalFieldOf("chests", List.of()).forGetter(data -> data.chests),
|
||||
PLAYER_CODEC.listOf(0, MAX_PLAYER_STATES).optionalFieldOf("players", List.of()).forGetter(data -> data.players)
|
||||
).apply(instance, BackroomData::new));
|
||||
private static final SavedDataType<BackroomData> TYPE = new SavedDataType<>(
|
||||
SanctuaryMod.id(SAVE_KEY), BackroomData::new, CODEC, null);
|
||||
|
||||
private int dataVersion;
|
||||
private final List<PendingEntry> pendingEntries;
|
||||
private final List<LostItem> lostItems;
|
||||
private final List<ChestState> chests;
|
||||
private final Map<Long, Integer> chestIndices;
|
||||
private final List<PlayerState> players;
|
||||
|
||||
public BackroomData() {
|
||||
this(DATA_VERSION, List.of(), List.of(), List.of(), List.of());
|
||||
}
|
||||
|
||||
private BackroomData(int dataVersion, List<PendingEntry> pendingEntries, List<LostItem> lostItems,
|
||||
List<ChestState> chests, List<PlayerState> players) {
|
||||
this.dataVersion = Math.max(DATA_VERSION, dataVersion);
|
||||
this.pendingEntries = bounded(pendingEntries, MAX_PENDING_ENTRIES);
|
||||
this.lostItems = bounded(lostItems, MAX_LOST_ITEMS);
|
||||
this.chests = bounded(chests, MAX_CHEST_STATES);
|
||||
this.chestIndices = new HashMap<>();
|
||||
for (int index = 0; index < this.chests.size(); index++) chestIndices.put(this.chests.get(index).pos, index);
|
||||
this.players = bounded(players, MAX_PLAYER_STATES);
|
||||
}
|
||||
|
||||
public static BackroomData get(MinecraftServer server) {
|
||||
ServerLevel overworld = server.getLevel(Level.OVERWORLD);
|
||||
if (overworld == null) throw new IllegalStateException("Overworld unavailable for Backroom data");
|
||||
BackroomData data = overworld.getDataStorage().computeIfAbsent(TYPE);
|
||||
if (data.dataVersion < DATA_VERSION) {
|
||||
data.dataVersion = DATA_VERSION;
|
||||
data.setDirty();
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
public Optional<PendingEntry> pendingEntry(UUID player) {
|
||||
String id = player.toString();
|
||||
return pendingEntries.stream().filter(entry -> entry.player.equals(id)).findFirst();
|
||||
}
|
||||
|
||||
public Optional<PendingEntry> prepareEntry(UUID player, List<EntrySlot> slots) {
|
||||
pendingEntries.removeIf(entry -> entry.player.equals(player.toString()));
|
||||
if (pendingEntries.size() >= MAX_PENDING_ENTRIES) return Optional.empty();
|
||||
PendingEntry entry = new PendingEntry(player.toString(), UUID.randomUUID().toString(), copySlots(slots));
|
||||
pendingEntries.add(entry);
|
||||
setDirty();
|
||||
return Optional.of(entry);
|
||||
}
|
||||
|
||||
public void finishEntry(UUID player) {
|
||||
if (pendingEntries.removeIf(entry -> entry.player.equals(player.toString()))) setDirty();
|
||||
}
|
||||
|
||||
/** Adds a new loss or returns a previously materialized loss to the pool. */
|
||||
public Optional<UUID> capture(ItemStack source, UUID owner, String cause, long gameTime) {
|
||||
if (source == null || source.isEmpty()) return Optional.empty();
|
||||
ItemStack clean = source.copy();
|
||||
UUID marker = clean.get(SanctuaryRegistries.BACKROOM_LOSS_COMPONENT);
|
||||
clean.remove(SanctuaryRegistries.BACKROOM_LOSS_COMPONENT);
|
||||
if (marker != null) {
|
||||
int existing = indexOfLoss(marker);
|
||||
if (existing >= 0) {
|
||||
LostItem previous = lostItems.get(existing);
|
||||
lostItems.set(existing, new LostItem(previous.id, owner == null ? previous.owner : owner.toString(),
|
||||
cause, gameTime, clean, LOST_POOL, Long.MIN_VALUE));
|
||||
setDirty();
|
||||
return Optional.of(marker);
|
||||
}
|
||||
}
|
||||
if (lostItems.size() >= MAX_LOST_ITEMS) return Optional.empty();
|
||||
UUID id = marker == null ? UUID.randomUUID() : marker;
|
||||
lostItems.add(new LostItem(id.toString(), owner == null ? "" : owner.toString(), cause,
|
||||
gameTime, clean, LOST_POOL, Long.MIN_VALUE));
|
||||
setDirty();
|
||||
return Optional.of(id);
|
||||
}
|
||||
|
||||
public Optional<MaterializedLoss> takePersonal(UUID owner, long chestPos) {
|
||||
String player = owner.toString();
|
||||
return lostItems.stream()
|
||||
.filter(loss -> loss.state.equals(LOST_POOL) && loss.owner.equals(player))
|
||||
.max(Comparator.comparingLong(LostItem::lostAt))
|
||||
.flatMap(loss -> materialize(loss, chestPos));
|
||||
}
|
||||
|
||||
public Optional<MaterializedLoss> takeShared(long chestPos, long gameTime) {
|
||||
return lostItems.stream()
|
||||
.filter(loss -> loss.state.equals(LOST_POOL)
|
||||
&& sharedEligible(loss.owner, loss.lostAt, gameTime))
|
||||
.min(Comparator.comparingLong(LostItem::lostAt))
|
||||
.flatMap(loss -> materialize(loss, chestPos));
|
||||
}
|
||||
|
||||
static boolean sharedEligible(String owner, long lostAt, long gameTime) {
|
||||
return owner == null || owner.isEmpty()
|
||||
|| gameTime >= lostAt && gameTime - lostAt >= PERSONAL_RESERVATION_TICKS;
|
||||
}
|
||||
|
||||
private Optional<MaterializedLoss> materialize(LostItem loss, long chestPos) {
|
||||
int index = lostItems.indexOf(loss);
|
||||
if (index < 0) return Optional.empty();
|
||||
ItemStack marked = loss.item.copy();
|
||||
UUID id = loss.uuid();
|
||||
marked.set(SanctuaryRegistries.BACKROOM_LOSS_COMPONENT, id);
|
||||
lostItems.set(index, new LostItem(loss.id, loss.owner, loss.cause, loss.lostAt,
|
||||
loss.item.copy(), LOST_CHEST, chestPos));
|
||||
setDirty();
|
||||
return Optional.of(new MaterializedLoss(id, marked));
|
||||
}
|
||||
|
||||
/** Reconciles real items allocated during the chest's previous loot cycle. */
|
||||
public void reconcileChest(long chestPos, List<UUID> markersStillPresent) {
|
||||
List<String> present = markersStillPresent.stream().map(UUID::toString).toList();
|
||||
boolean changed = false;
|
||||
for (int index = lostItems.size() - 1; index >= 0; index--) {
|
||||
LostItem loss = lostItems.get(index);
|
||||
if (!loss.state.equals(LOST_CHEST) || loss.chest != chestPos) continue;
|
||||
if (present.contains(loss.id)) {
|
||||
lostItems.set(index, new LostItem(loss.id, loss.owner, loss.cause, loss.lostAt,
|
||||
loss.item.copy(), LOST_POOL, Long.MIN_VALUE));
|
||||
} else {
|
||||
lostItems.remove(index);
|
||||
}
|
||||
changed = true;
|
||||
}
|
||||
if (changed) setDirty();
|
||||
}
|
||||
|
||||
/** Settles a marked item that successfully left the Backroom. */
|
||||
public void settle(UUID lossId) {
|
||||
if (lossId != null && lostItems.removeIf(loss -> loss.id.equals(lossId.toString()))) setDirty();
|
||||
}
|
||||
|
||||
public Optional<ChestState> chest(long pos) {
|
||||
Integer index = chestIndices.get(pos);
|
||||
return index == null ? Optional.empty() : Optional.of(chests.get(index));
|
||||
}
|
||||
|
||||
public void updateChest(long pos, long cycle, List<UUID> assigned) {
|
||||
ChestState replacement = new ChestState(pos, cycle, assigned.stream().limit(54).map(UUID::toString).toList());
|
||||
Integer existing = chestIndices.get(pos);
|
||||
if (existing != null) {
|
||||
chests.set(existing, replacement);
|
||||
} else {
|
||||
if (chests.size() >= MAX_CHEST_STATES) {
|
||||
chests.removeFirst();
|
||||
chestIndices.clear();
|
||||
for (int index = 0; index < chests.size(); index++) chestIndices.put(chests.get(index).pos, index);
|
||||
}
|
||||
chestIndices.put(pos, chests.size());
|
||||
chests.add(replacement);
|
||||
}
|
||||
setDirty();
|
||||
}
|
||||
|
||||
/** Marks a chest and returns whether it is a new personal-recovery attempt. */
|
||||
public ChestVisit visitChest(UUID player, long chestPos) {
|
||||
PlayerState state = playerState(player);
|
||||
ArrayList<Long> visited = new ArrayList<>(state.visitedChests);
|
||||
boolean firstVisit = !visited.contains(chestPos);
|
||||
if (firstVisit) {
|
||||
if (visited.size() >= MAX_VISITED_CHESTS) visited.removeFirst();
|
||||
visited.add(chestPos);
|
||||
state = replacePlayer(state, state.personalAttempts + 1, visited, state.recentArrivals);
|
||||
}
|
||||
return new ChestVisit(firstVisit, state.personalAttempts);
|
||||
}
|
||||
|
||||
public void beginExpedition(UUID player, long packedArrival) {
|
||||
PlayerState state = playerState(player);
|
||||
ArrayList<Long> arrivals = new ArrayList<>(state.recentArrivals);
|
||||
arrivals.remove(packedArrival);
|
||||
arrivals.addFirst(packedArrival);
|
||||
while (arrivals.size() > MAX_RECENT_ARRIVALS) arrivals.removeLast();
|
||||
replacePlayer(state, 0, List.of(), arrivals);
|
||||
}
|
||||
|
||||
public List<Long> recentArrivals(UUID player) {
|
||||
return List.copyOf(playerState(player).recentArrivals);
|
||||
}
|
||||
|
||||
public int lostItemCount() { return lostItems.size(); }
|
||||
public long pooledItemCount() { return lostItems.stream().filter(loss -> loss.state.equals(LOST_POOL)).count(); }
|
||||
public long pooledItemCount(UUID owner) {
|
||||
String id = owner.toString();
|
||||
return lostItems.stream().filter(loss -> loss.state.equals(LOST_POOL) && loss.owner.equals(id)).count();
|
||||
}
|
||||
public int trackedChestCount() { return chests.size(); }
|
||||
|
||||
private PlayerState playerState(UUID player) {
|
||||
String id = player.toString();
|
||||
return players.stream().filter(state -> state.player.equals(id)).findFirst().orElseGet(() -> {
|
||||
if (players.size() >= MAX_PLAYER_STATES) players.removeFirst();
|
||||
PlayerState state = new PlayerState(id, 0, List.of(), List.of());
|
||||
players.add(state);
|
||||
setDirty();
|
||||
return state;
|
||||
});
|
||||
}
|
||||
|
||||
private PlayerState replacePlayer(PlayerState old, int attempts, List<Long> visited, List<Long> arrivals) {
|
||||
PlayerState replacement = new PlayerState(old.player, Math.max(0, attempts),
|
||||
bounded(visited, MAX_VISITED_CHESTS), bounded(arrivals, MAX_RECENT_ARRIVALS));
|
||||
players.set(players.indexOf(old), replacement);
|
||||
setDirty();
|
||||
return replacement;
|
||||
}
|
||||
|
||||
private int indexOfLoss(UUID id) {
|
||||
String value = id.toString();
|
||||
for (int index = 0; index < lostItems.size(); index++) if (lostItems.get(index).id.equals(value)) return index;
|
||||
return -1;
|
||||
}
|
||||
|
||||
private static List<EntrySlot> copySlots(List<EntrySlot> slots) {
|
||||
return slots.stream().filter(slot -> slot != null && !slot.item.isEmpty()).limit(MAX_ENTRY_SLOTS)
|
||||
.map(slot -> new EntrySlot(slot.location, slot.index, slot.item.copy())).toList();
|
||||
}
|
||||
|
||||
private static <T> ArrayList<T> bounded(List<T> source, int maximum) {
|
||||
List<T> safe = source == null ? List.of() : source;
|
||||
int start = Math.max(0, safe.size() - maximum);
|
||||
return new ArrayList<>(safe.subList(start, safe.size()));
|
||||
}
|
||||
|
||||
public record EntrySlot(String location, int index, ItemStack item) {
|
||||
public EntrySlot { item = item == null ? ItemStack.EMPTY : item.copy(); }
|
||||
}
|
||||
public record PendingEntry(String player, String transaction, List<EntrySlot> slots) {
|
||||
public UUID transactionId() { return UUID.fromString(transaction); }
|
||||
}
|
||||
public record LostItem(String id, String owner, String cause, long lostAt, ItemStack item,
|
||||
String state, long chest) {
|
||||
public UUID uuid() { return UUID.fromString(id); }
|
||||
}
|
||||
public record ChestState(long pos, long cycle, List<String> assigned) {
|
||||
public List<UUID> assignedIds() { return assigned.stream().map(UUID::fromString).toList(); }
|
||||
}
|
||||
private record PlayerState(String player, int personalAttempts, List<Long> visitedChests,
|
||||
List<Long> recentArrivals) {}
|
||||
public record MaterializedLoss(UUID id, ItemStack stack) {}
|
||||
public record ChestVisit(boolean firstVisit, int ordinal) {}
|
||||
}
|
||||
+108
@@ -0,0 +1,108 @@
|
||||
package fr.koka99cab.sanctuary26.sanctuary.backroom;
|
||||
|
||||
import fr.koka99cab.sanctuary26.sanctuary.gameplay.SanctuaryInventoryCapacity;
|
||||
import fr.koka99cab.sanctuary26.sanctuary.progression.SanctuaryPlayerProgress;
|
||||
import fr.koka99cab.sanctuary26.sanctuary.registry.SanctuaryRegistries;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.world.entity.player.Inventory;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
|
||||
/** Exact inventory boundary used by Backroom entry, death and exit. */
|
||||
public final class BackroomInventory {
|
||||
private static final String INVENTORY = "inventory";
|
||||
private static final String OVERFLOW = "overflow";
|
||||
private static final String EGG = "companion_egg";
|
||||
private static final String CAPE = "cape";
|
||||
|
||||
private BackroomInventory() {}
|
||||
|
||||
public static List<BackroomData.EntrySlot> snapshot(ServerPlayer player) {
|
||||
ArrayList<BackroomData.EntrySlot> result = new ArrayList<>();
|
||||
Inventory inventory = player.getInventory();
|
||||
for (int slot = 0; slot < inventory.getContainerSize(); slot++) {
|
||||
add(result, INVENTORY, slot, inventory.getItem(slot));
|
||||
}
|
||||
for (int slot = 0; slot < SanctuaryPlayerProgress.OVERFLOW_SLOT_COUNT; slot++) {
|
||||
add(result, OVERFLOW, slot, SanctuaryInventoryCapacity.overflowItem(player, slot));
|
||||
}
|
||||
add(result, EGG, 0, SanctuaryInventoryCapacity.companionEgg(player));
|
||||
add(result, CAPE, 0, SanctuaryInventoryCapacity.equippedCape(player));
|
||||
return result;
|
||||
}
|
||||
|
||||
public static List<ItemStack> parcel(List<BackroomData.EntrySlot> slots) {
|
||||
return slots.stream().map(BackroomData.EntrySlot::item).filter(stack -> !stack.isEmpty())
|
||||
.map(ItemStack::copy).toList();
|
||||
}
|
||||
|
||||
/** Clears only exact snapshot values, so recovery never destroys a later replacement item. */
|
||||
public static void clearMatching(ServerPlayer player, List<BackroomData.EntrySlot> slots) {
|
||||
for (BackroomData.EntrySlot slot : slots) {
|
||||
ItemStack current = get(player, slot);
|
||||
if (ItemStack.matches(current, slot.item())) set(player, slot, ItemStack.EMPTY);
|
||||
}
|
||||
player.getInventory().setChanged();
|
||||
SanctuaryInventoryCapacity.markOverflowChanged(player);
|
||||
}
|
||||
|
||||
public static int captureAll(ServerPlayer player, String cause) {
|
||||
BackroomData data = BackroomData.get(player.level().getServer());
|
||||
long now = player.level().getGameTime();
|
||||
int captured = 0;
|
||||
for (BackroomData.EntrySlot slot : snapshot(player)) {
|
||||
if (data.capture(slot.item(), player.getUUID(), cause, now).isEmpty()) continue;
|
||||
set(player, slot, ItemStack.EMPTY);
|
||||
captured++;
|
||||
}
|
||||
player.getInventory().setChanged();
|
||||
SanctuaryInventoryCapacity.markOverflowChanged(player);
|
||||
return captured;
|
||||
}
|
||||
|
||||
/** Removes registry markers only when their real item successfully crosses a bed exit. */
|
||||
public static int settleCarriedLosses(ServerPlayer player) {
|
||||
BackroomData data = BackroomData.get(player.level().getServer());
|
||||
int settled = 0;
|
||||
for (BackroomData.EntrySlot slot : snapshot(player)) {
|
||||
ItemStack item = get(player, slot);
|
||||
UUID id = item.get(SanctuaryRegistries.BACKROOM_LOSS_COMPONENT);
|
||||
if (id == null) continue;
|
||||
item.remove(SanctuaryRegistries.BACKROOM_LOSS_COMPONENT);
|
||||
data.settle(id);
|
||||
set(player, slot, item);
|
||||
settled++;
|
||||
}
|
||||
return settled;
|
||||
}
|
||||
|
||||
private static void add(List<BackroomData.EntrySlot> target, String location, int index, ItemStack stack) {
|
||||
if (stack != null && !stack.isEmpty()) target.add(new BackroomData.EntrySlot(location, index, stack));
|
||||
}
|
||||
|
||||
private static ItemStack get(ServerPlayer player, BackroomData.EntrySlot slot) {
|
||||
return switch (slot.location()) {
|
||||
case INVENTORY -> slot.index() >= 0 && slot.index() < player.getInventory().getContainerSize()
|
||||
? player.getInventory().getItem(slot.index()) : ItemStack.EMPTY;
|
||||
case OVERFLOW -> SanctuaryInventoryCapacity.overflowItem(player, slot.index());
|
||||
case EGG -> SanctuaryInventoryCapacity.companionEgg(player);
|
||||
case CAPE -> SanctuaryInventoryCapacity.equippedCape(player);
|
||||
default -> ItemStack.EMPTY;
|
||||
};
|
||||
}
|
||||
|
||||
private static void set(ServerPlayer player, BackroomData.EntrySlot slot, ItemStack stack) {
|
||||
switch (slot.location()) {
|
||||
case INVENTORY -> {
|
||||
if (slot.index() >= 0 && slot.index() < player.getInventory().getContainerSize())
|
||||
player.getInventory().setItem(slot.index(), stack);
|
||||
}
|
||||
case OVERFLOW -> SanctuaryInventoryCapacity.setOverflowItem(player, slot.index(), stack);
|
||||
case EGG -> SanctuaryInventoryCapacity.setCompanionEgg(player, stack);
|
||||
case CAPE -> SanctuaryInventoryCapacity.setEquippedCape(player, stack);
|
||||
default -> { }
|
||||
}
|
||||
}
|
||||
}
|
||||
+240
@@ -0,0 +1,240 @@
|
||||
package fr.koka99cab.sanctuary26.sanctuary.backroom;
|
||||
|
||||
import fr.koka99cab.sanctuary26.sanctuary.SanctuaryMod;
|
||||
import fr.koka99cab.sanctuary26.sanctuary.shop.ShopData;
|
||||
import fr.koka99cab.sanctuary26.sanctuary.world.SanctuaryDimensions;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import net.fabricmc.fabric.api.networking.v1.ServerPlayConnectionEvents;
|
||||
import net.fabricmc.fabric.api.command.v2.CommandRegistrationCallback;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.commands.CommandSourceStack;
|
||||
import net.minecraft.commands.Commands;
|
||||
import net.minecraft.commands.arguments.EntityArgument;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.util.Mth;
|
||||
import net.minecraft.util.RandomSource;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.level.levelgen.Heightmap;
|
||||
import net.minecraft.world.level.portal.TeleportTransition;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
|
||||
/** Bed gateway and crash-safe Mailbox boundary for Backroom expeditions. */
|
||||
public final class BackroomService {
|
||||
public static final int ARRIVAL_RADIUS = 2_000;
|
||||
private static final int ARRIVAL_ATTEMPTS = 48;
|
||||
private static final Set<UUID> AUTHORIZED_TRANSITIONS = new HashSet<>();
|
||||
private static boolean initialized;
|
||||
|
||||
private BackroomService() {}
|
||||
|
||||
public static void initialize() {
|
||||
if (initialized) return;
|
||||
initialized = true;
|
||||
ServerPlayConnectionEvents.JOIN.register((handler, sender, server) -> reconcile(handler.player));
|
||||
CommandRegistrationCallback.EVENT.register((dispatcher, registryAccess, environment) -> {
|
||||
dispatcher.register(Commands.literal("backroom")
|
||||
.requires(Commands.hasPermission(Commands.LEVEL_GAMEMASTERS))
|
||||
.executes(context -> status(context.getSource()))
|
||||
.then(Commands.literal("status").executes(context -> status(context.getSource())))
|
||||
.then(Commands.literal("inspect")
|
||||
.then(Commands.argument("player", EntityArgument.player())
|
||||
.executes(context -> inspect(context.getSource(), EntityArgument.getPlayer(context, "player"))))));
|
||||
});
|
||||
SanctuaryMod.LOGGER.info("[{}] Backroom bed gateway ready (arrival radius {} blocks).",
|
||||
SanctuaryMod.DISPLAY_NAME, ARRIVAL_RADIUS);
|
||||
}
|
||||
|
||||
/** Returns true when the bed interaction belongs to the Backroom feature. */
|
||||
public static boolean useBed(ServerPlayer player) {
|
||||
if (isBackroom(player.level())) {
|
||||
exit(player);
|
||||
return true;
|
||||
}
|
||||
if (!player.isSecondaryUseActive()) return false;
|
||||
enter(player);
|
||||
return true;
|
||||
}
|
||||
|
||||
public static boolean isBackroom(ServerLevel level) {
|
||||
return level != null && level.dimension().equals(SanctuaryDimensions.BACKROOM);
|
||||
}
|
||||
|
||||
public static boolean enter(ServerPlayer player) {
|
||||
ServerLevel destination = player.level().getServer().getLevel(SanctuaryDimensions.BACKROOM);
|
||||
if (destination == null) {
|
||||
notify(player, "message.sanctuary.backroom.unavailable");
|
||||
return false;
|
||||
}
|
||||
if (!reconcile(player)) {
|
||||
notify(player, "message.sanctuary.backroom.pending");
|
||||
return false;
|
||||
}
|
||||
|
||||
List<BackroomData.EntrySlot> slots = BackroomInventory.snapshot(player);
|
||||
ShopData.DeliveryDestination mailbox = null;
|
||||
if (!slots.isEmpty()) {
|
||||
mailbox = ShopData.get(player.level().getServer()).loadedDeliveryBox(
|
||||
player.level().getServer(), player.getUUID(), level -> !isBackroom(level)).orElse(null);
|
||||
if (mailbox == null) {
|
||||
notify(player, "message.sanctuary.backroom.mailbox_missing");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
BackroomData data = BackroomData.get(player.level().getServer());
|
||||
BackroomData.PendingEntry pending = data.prepareEntry(player.getUUID(), slots).orElse(null);
|
||||
if (pending == null) {
|
||||
notify(player, "message.sanctuary.backroom.registry_busy");
|
||||
return false;
|
||||
}
|
||||
if (mailbox != null && !mailbox.box().insertTransaction(pending.transactionId(), BackroomInventory.parcel(slots))) {
|
||||
data.finishEntry(player.getUUID());
|
||||
notify(player, "message.sanctuary.backroom.mailbox_full");
|
||||
return false;
|
||||
}
|
||||
BackroomInventory.clearMatching(player, slots);
|
||||
data.finishEntry(player.getUUID());
|
||||
|
||||
BlockPos arrival = chooseArrival(destination, player.getUUID(), data);
|
||||
data.beginExpedition(player.getUUID(), arrival.asLong());
|
||||
TeleportTransition transition = new TeleportTransition(destination, Vec3.atBottomCenterOf(arrival),
|
||||
Vec3.ZERO, player.getYRot(), player.getXRot(),
|
||||
TeleportTransition.PLAY_PORTAL_SOUND.then(TeleportTransition.PLACE_PORTAL_TICKET));
|
||||
ServerPlayer teleported;
|
||||
AUTHORIZED_TRANSITIONS.add(player.getUUID());
|
||||
try {
|
||||
teleported = player.teleport(transition);
|
||||
} finally {
|
||||
AUTHORIZED_TRANSITIONS.remove(player.getUUID());
|
||||
}
|
||||
if (teleported == null) {
|
||||
SanctuaryMod.LOGGER.error("Backroom teleport failed after Mailbox transaction {} for {}",
|
||||
pending.transaction(), player.getGameProfile().name());
|
||||
return false;
|
||||
}
|
||||
notify(player, "message.sanctuary.backroom.entered");
|
||||
return true;
|
||||
}
|
||||
|
||||
public static boolean exit(ServerPlayer player) {
|
||||
if (!isBackroom(player.level())) return false;
|
||||
TeleportTransition transition = player.findRespawnPositionAndUseSpawnBlock(false,
|
||||
TeleportTransition.PLAY_PORTAL_SOUND.then(TeleportTransition.PLACE_PORTAL_TICKET));
|
||||
if (isBackroom(transition.newLevel())) {
|
||||
transition = TeleportTransition.createDefault(player,
|
||||
TeleportTransition.PLAY_PORTAL_SOUND.then(TeleportTransition.PLACE_PORTAL_TICKET));
|
||||
}
|
||||
ServerPlayer teleported;
|
||||
AUTHORIZED_TRANSITIONS.add(player.getUUID());
|
||||
try {
|
||||
teleported = player.teleport(transition);
|
||||
} finally {
|
||||
AUTHORIZED_TRANSITIONS.remove(player.getUUID());
|
||||
}
|
||||
if (teleported == null) return false;
|
||||
int recovered = BackroomInventory.settleCarriedLosses(teleported);
|
||||
notify(teleported, recovered == 0
|
||||
? "message.sanctuary.backroom.exited"
|
||||
: "message.sanctuary.backroom.exited_with_losses");
|
||||
return true;
|
||||
}
|
||||
|
||||
public static boolean isAuthorizedTransition(UUID player) {
|
||||
return player != null && AUTHORIZED_TRANSITIONS.contains(player);
|
||||
}
|
||||
|
||||
/** Resolves a crash between Mailbox insertion and inventory clearing. */
|
||||
public static boolean reconcile(ServerPlayer player) {
|
||||
BackroomData data = BackroomData.get(player.level().getServer());
|
||||
BackroomData.PendingEntry pending = data.pendingEntry(player.getUUID()).orElse(null);
|
||||
if (pending == null) return true;
|
||||
ShopData.DeliveryDestination mailbox = ShopData.get(player.level().getServer())
|
||||
.loadedDeliveryBox(player.level().getServer(), player.getUUID(), level -> !isBackroom(level)).orElse(null);
|
||||
if (mailbox == null) return false;
|
||||
if (mailbox.box().hasProcessedTransaction(pending.transactionId())) {
|
||||
BackroomInventory.clearMatching(player, pending.slots());
|
||||
}
|
||||
data.finishEntry(player.getUUID());
|
||||
return true;
|
||||
}
|
||||
|
||||
private static BlockPos chooseArrival(ServerLevel level, UUID player, BackroomData data) {
|
||||
RandomSource random = level.getRandom();
|
||||
List<Long> recent = data.recentArrivals(player);
|
||||
BlockPos best = null;
|
||||
for (int attempt = 0; attempt < ARRIVAL_ATTEMPTS; attempt++) {
|
||||
double angle = random.nextDouble() * Math.PI * 2.0D;
|
||||
double radius = Math.sqrt(random.nextDouble()) * ARRIVAL_RADIUS;
|
||||
int x = Mth.floor(Math.cos(angle) * radius);
|
||||
int z = Mth.floor(Math.sin(angle) * radius);
|
||||
int y = Mth.clamp(level.getHeight(Heightmap.Types.MOTION_BLOCKING_NO_LEAVES, x, z),
|
||||
level.getMinY() + 2, level.getMaxY() - 3);
|
||||
BlockPos candidate = new BlockPos(x, y, z);
|
||||
best = candidate;
|
||||
if (farFromRecent(candidate, recent) && safe(level, candidate)) return candidate;
|
||||
}
|
||||
BlockPos fallback = findSafeNear(level, best == null ? BlockPos.ZERO : best, 32);
|
||||
if (fallback != null) return fallback;
|
||||
fallback = findSafeNear(level, BlockPos.ZERO, 48);
|
||||
if (fallback != null) return fallback;
|
||||
int originY = Mth.clamp(level.getHeight(Heightmap.Types.MOTION_BLOCKING_NO_LEAVES, 0, 0),
|
||||
level.getMinY() + 2, level.getMaxY() - 3);
|
||||
return new BlockPos(0, originY, 0);
|
||||
}
|
||||
|
||||
private static boolean farFromRecent(BlockPos candidate, List<Long> recent) {
|
||||
for (long packed : recent) {
|
||||
BlockPos old = BlockPos.of(packed);
|
||||
long dx = old.getX() - candidate.getX(), dz = old.getZ() - candidate.getZ();
|
||||
if (dx * dx + dz * dz < 128L * 128L) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private static boolean safe(ServerLevel level, BlockPos feet) {
|
||||
return level.getBlockState(feet).isAir() && level.getBlockState(feet.above()).isAir()
|
||||
&& level.getBlockState(feet.below()).isCollisionShapeFullBlock(level, feet.below());
|
||||
}
|
||||
|
||||
private static BlockPos findSafeNear(ServerLevel level, BlockPos center, int radius) {
|
||||
for (int ring = 0; ring <= radius; ring += 2) {
|
||||
for (int dx = -ring; dx <= ring; dx += 2) for (int dz = -ring; dz <= ring; dz += 2) {
|
||||
if (ring > 0 && Math.abs(dx) != ring && Math.abs(dz) != ring) continue;
|
||||
int x = center.getX() + dx, z = center.getZ() + dz;
|
||||
if ((long) x * x + (long) z * z > (long) ARRIVAL_RADIUS * ARRIVAL_RADIUS) continue;
|
||||
int y = Mth.clamp(level.getHeight(Heightmap.Types.MOTION_BLOCKING_NO_LEAVES, x, z),
|
||||
level.getMinY() + 2, level.getMaxY() - 3);
|
||||
BlockPos candidate = new BlockPos(x, y, z);
|
||||
if (safe(level, candidate)) return candidate;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static void notify(ServerPlayer player, String translationKey) {
|
||||
player.sendOverlayMessage(Component.translatable(translationKey));
|
||||
}
|
||||
|
||||
private static int status(CommandSourceStack source) {
|
||||
BackroomData data = BackroomData.get(source.getServer());
|
||||
source.sendSuccess(() -> Component.literal("Backroom: " + data.lostItemCount() + " lost stacks ("
|
||||
+ data.pooledItemCount() + " pooled), " + data.trackedChestCount() + " tracked chest positions; data v"
|
||||
+ BackroomData.DATA_VERSION + "."), false);
|
||||
return data.lostItemCount();
|
||||
}
|
||||
|
||||
private static int inspect(CommandSourceStack source, ServerPlayer player) {
|
||||
BackroomData data = BackroomData.get(source.getServer());
|
||||
boolean pending = data.pendingEntry(player.getUUID()).isPresent();
|
||||
source.sendSuccess(() -> Component.literal("Backroom player " + player.getGameProfile().name() + ": "
|
||||
+ data.pooledItemCount(player.getUUID()) + " personal pooled stacks, entry transaction "
|
||||
+ (pending ? "pending" : "clear") + ", " + data.recentArrivals(player.getUUID()).size()
|
||||
+ " remembered arrivals."), false);
|
||||
return pending ? 0 : 1;
|
||||
}
|
||||
}
|
||||
+98
@@ -0,0 +1,98 @@
|
||||
package fr.koka99cab.sanctuary26.sanctuary.client;
|
||||
|
||||
import com.mojang.blaze3d.vertex.PoseStack;
|
||||
import fr.koka99cab.sanctuary26.sanctuary.SanctuaryMod;
|
||||
import net.fabricmc.fabric.api.client.rendering.v1.LivingEntityRenderLayerRegistrationCallback;
|
||||
import net.fabricmc.fabric.api.client.rendering.v1.ModelLayerRegistry;
|
||||
import net.minecraft.client.model.geom.ModelLayerLocation;
|
||||
import net.minecraft.client.model.geom.PartPose;
|
||||
import net.minecraft.client.model.geom.builders.CubeDeformation;
|
||||
import net.minecraft.client.model.geom.builders.CubeListBuilder;
|
||||
import net.minecraft.client.model.geom.builders.LayerDefinition;
|
||||
import net.minecraft.client.model.geom.builders.MeshDefinition;
|
||||
import net.minecraft.client.model.geom.builders.PartDefinition;
|
||||
import net.minecraft.client.model.player.PlayerCapeModel;
|
||||
import net.minecraft.client.model.player.PlayerModel;
|
||||
import net.minecraft.client.renderer.SubmitNodeCollector;
|
||||
import net.minecraft.client.renderer.blockentity.BannerRenderer;
|
||||
import net.minecraft.client.renderer.entity.EntityRendererProvider;
|
||||
import net.minecraft.client.renderer.entity.RenderLayerParent;
|
||||
import net.minecraft.client.renderer.entity.layers.RenderLayer;
|
||||
import net.minecraft.client.renderer.entity.player.AvatarRenderer;
|
||||
import net.minecraft.client.renderer.entity.state.AvatarRenderState;
|
||||
import net.minecraft.client.renderer.texture.OverlayTexture;
|
||||
import net.minecraft.client.resources.model.EquipmentAssetManager;
|
||||
import net.minecraft.client.resources.model.EquipmentClientInfo;
|
||||
import net.minecraft.core.component.DataComponents;
|
||||
import net.minecraft.world.entity.EntityTypes;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.item.equipment.Equippable;
|
||||
|
||||
/** Renders a banner-decorated cape with vanilla cape physics and banner pattern sprites. */
|
||||
public final class SanctuaryBannerCapeLayer extends RenderLayer<AvatarRenderState, PlayerModel> {
|
||||
private static final ModelLayerLocation MODEL_LAYER =
|
||||
new ModelLayerLocation(SanctuaryMod.id("banner_cape"), "main");
|
||||
private final PlayerCapeModel model;
|
||||
private final EquipmentAssetManager equipmentAssets;
|
||||
private final EntityRendererProvider.Context context;
|
||||
|
||||
private SanctuaryBannerCapeLayer(RenderLayerParent<AvatarRenderState, PlayerModel> parent,
|
||||
EntityRendererProvider.Context context) {
|
||||
super(parent);
|
||||
this.context = context;
|
||||
this.model = new PlayerCapeModel(context.bakeLayer(MODEL_LAYER));
|
||||
this.equipmentAssets = context.getEquipmentAssets();
|
||||
}
|
||||
|
||||
public static void initialize() {
|
||||
ModelLayerRegistry.registerModelLayer(MODEL_LAYER, SanctuaryBannerCapeLayer::createLayer);
|
||||
LivingEntityRenderLayerRegistrationCallback.EVENT.register((entityType, renderer, helper, context) -> {
|
||||
if (entityType == EntityTypes.PLAYER && renderer instanceof AvatarRenderer<?> avatarRenderer) {
|
||||
register(helper, avatarRenderer, context);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@SuppressWarnings({"unchecked", "rawtypes"})
|
||||
private static void register(LivingEntityRenderLayerRegistrationCallback.RegistrationHelper helper,
|
||||
AvatarRenderer<?> renderer, EntityRendererProvider.Context context) {
|
||||
RenderLayerParent<AvatarRenderState, PlayerModel> parent =
|
||||
(RenderLayerParent<AvatarRenderState, PlayerModel>) (RenderLayerParent) renderer;
|
||||
helper.register(new SanctuaryBannerCapeLayer(parent, context));
|
||||
}
|
||||
|
||||
private static LayerDefinition createLayer() {
|
||||
MeshDefinition mesh = PlayerModel.createMesh(CubeDeformation.NONE, false);
|
||||
PartDefinition root = mesh.getRoot().clearRecursively();
|
||||
PartDefinition body = root.getChild("body");
|
||||
body.addOrReplaceChild("cape", CubeListBuilder.create().texOffs(0, 0)
|
||||
.addBox(-5.0F, 0.0F, -1.0F, 10.0F, 16.0F, 1.0F,
|
||||
CubeDeformation.NONE, 2.0F, 2.5F),
|
||||
PartPose.offsetAndRotation(0.0F, 0.0F, 2.0F, 0.0F, (float) Math.PI, 0.0F));
|
||||
return LayerDefinition.create(mesh, 64, 64);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void submit(PoseStack poseStack, SubmitNodeCollector collector, int packedLight,
|
||||
AvatarRenderState state, float yRot, float xRot) {
|
||||
SanctuaryClientCapes.Appearance appearance =
|
||||
((SanctuaryCapeRenderState) state).sanctuary$capeAppearance();
|
||||
if (state.isInvisible || appearance == null || !appearance.bannerCape()
|
||||
|| hasLayer(state.chestEquipment, EquipmentClientInfo.LayerType.WINGS)) return;
|
||||
|
||||
poseStack.pushPose();
|
||||
if (hasLayer(state.chestEquipment, EquipmentClientInfo.LayerType.HUMANOID)) {
|
||||
poseStack.translate(0.0F, -0.053125F, 0.06875F);
|
||||
}
|
||||
BannerRenderer.submitPatterns(context.getSprites(), poseStack, collector, packedLight,
|
||||
OverlayTexture.NO_OVERLAY, model, state, true, appearance.baseColor(),
|
||||
appearance.patterns(), null);
|
||||
poseStack.popPose();
|
||||
}
|
||||
|
||||
private boolean hasLayer(ItemStack stack, EquipmentClientInfo.LayerType layerType) {
|
||||
Equippable equippable = stack.get(DataComponents.EQUIPPABLE);
|
||||
return equippable != null && equippable.assetId().isPresent()
|
||||
&& !equipmentAssets.get(equippable.assetId().get()).getLayers(layerType).isEmpty();
|
||||
}
|
||||
}
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
package fr.koka99cab.sanctuary26.sanctuary.client;
|
||||
|
||||
/** Mixin-backed bridge carrying equipped cape data into the extracted render state. */
|
||||
public interface SanctuaryCapeRenderState {
|
||||
SanctuaryClientCapes.Appearance sanctuary$capeAppearance();
|
||||
|
||||
void sanctuary$setCapeAppearance(SanctuaryClientCapes.Appearance appearance);
|
||||
}
|
||||
@@ -119,6 +119,7 @@ public final class SanctuaryClient implements ClientModInitializer {
|
||||
SanctuaryBulkActionsClient.initialize();
|
||||
SanctuaryHealthPreview.register();
|
||||
CelestialSpyglassSelection.register();
|
||||
SanctuaryBannerCapeLayer.initialize();
|
||||
EntityRendererRegistry.register(SanctuaryRegistries.SEAT, NoopRenderer::new);
|
||||
ClientTickEvents.END_CLIENT_TICK.register(SanctuaryClient::tick);
|
||||
ClientPlayNetworking.registerGlobalReceiver(ProgressionSnapshotPayload.ID, (payload, context) ->
|
||||
|
||||
+14
-6
@@ -6,25 +6,33 @@ import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.world.item.DyeColor;
|
||||
import net.minecraft.world.level.block.entity.BannerPatternLayers;
|
||||
|
||||
/** Client mirror of server-authoritative equipped cape choices. */
|
||||
public final class SanctuaryClientCapes {
|
||||
private static final Map<UUID, Identifier> TEXTURES = new HashMap<>();
|
||||
private static final Map<UUID, Appearance> APPEARANCES = new HashMap<>();
|
||||
|
||||
private SanctuaryClientCapes() {
|
||||
}
|
||||
|
||||
public static void update(CapeSyncPayload payload) {
|
||||
Identifier texture = SanctuaryCapes.texture(payload.capeItemId());
|
||||
if (texture == null) TEXTURES.remove(payload.playerId());
|
||||
else TEXTURES.put(payload.playerId(), texture);
|
||||
boolean bannerCape = SanctuaryCapes.isBannerCape(payload.capeItemId());
|
||||
if (texture == null && !bannerCape) APPEARANCES.remove(payload.playerId());
|
||||
else APPEARANCES.put(payload.playerId(), new Appearance(
|
||||
texture, bannerCape, payload.baseColor(), payload.patterns()));
|
||||
}
|
||||
|
||||
public static Identifier texture(UUID playerId) {
|
||||
return TEXTURES.get(playerId);
|
||||
public static Appearance appearance(UUID playerId) {
|
||||
return APPEARANCES.get(playerId);
|
||||
}
|
||||
|
||||
public static void clear() {
|
||||
TEXTURES.clear();
|
||||
APPEARANCES.clear();
|
||||
}
|
||||
|
||||
public record Appearance(Identifier texture, boolean bannerCape, DyeColor baseColor,
|
||||
BannerPatternLayers patterns) {
|
||||
}
|
||||
}
|
||||
|
||||
+3
-1
@@ -33,7 +33,9 @@ public final class SanctuaryClientProgress {
|
||||
|
||||
Minecraft client = Minecraft.getInstance();
|
||||
if (currentRows > 1) {
|
||||
SanctuaryHotbarRowsClient.requestVisualRow(client, currentRows - 1, currentRows);
|
||||
// Visual 0 is the bottom bank line after bankIndex inversion. Keep the
|
||||
// live hotbar there on login and after an inventory upgrade.
|
||||
SanctuaryHotbarRowsClient.requestVisualRow(client, 0, currentRows);
|
||||
}
|
||||
if (client.gui.screen() instanceof AbstractContainerScreen<?> screen) {
|
||||
screen.resize(client.getWindow().getGuiScaledWidth(),
|
||||
|
||||
+1
-1
@@ -44,7 +44,7 @@ public final class SanctuaryHotbarRowsClient {
|
||||
return requestVisualRow(client, targetVisualRow, unlockedRows);
|
||||
}
|
||||
|
||||
/** Makes the requested visible row the real hotbar, notably after an inventory upgrade. */
|
||||
/** Makes the requested visible row the real hotbar. Visual 0 is the bottom line. */
|
||||
public static boolean requestVisualRow(Minecraft client, int targetVisualRow, int unlockedRows) {
|
||||
unlockedRows = Math.clamp(unlockedRows, 1, SanctuaryInventoryCapacity.MAX_ROW_COUNT);
|
||||
if (pending || unlockedRows <= 1 || client.player == null || client.getConnection() == null
|
||||
|
||||
+25
-4
@@ -1,5 +1,7 @@
|
||||
package fr.koka99cab.sanctuary26.sanctuary.client;
|
||||
|
||||
import fr.koka99cab.sanctuary26.sanctuary.gameplay.SanctuaryInventorySort;
|
||||
import fr.koka99cab.sanctuary26.sanctuary.mixin.client.AbstractContainerScreenAccessor;
|
||||
import fr.koka99cab.sanctuary26.sanctuary.network.SortInventoryPayload;
|
||||
import net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking;
|
||||
import net.minecraft.client.Minecraft;
|
||||
@@ -7,6 +9,8 @@ import net.minecraft.client.gui.components.AbstractWidget;
|
||||
import net.minecraft.client.gui.components.EditBox;
|
||||
import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen;
|
||||
import net.minecraft.client.gui.screens.inventory.CreativeModeInventoryScreen;
|
||||
import net.minecraft.client.gui.screens.inventory.InventoryScreen;
|
||||
import net.minecraft.world.inventory.Slot;
|
||||
|
||||
/** Sends a sort request while a survival container is open. */
|
||||
public final class SanctuaryInventorySortClient {
|
||||
@@ -17,18 +21,35 @@ public final class SanctuaryInventorySortClient {
|
||||
if (client.gui.screen() != null && client.gui.screen().getFocused() instanceof EditBox) {
|
||||
return false;
|
||||
}
|
||||
return request(client);
|
||||
return request(client, null);
|
||||
}
|
||||
|
||||
public static boolean request(Minecraft client) {
|
||||
return request(client, null);
|
||||
}
|
||||
|
||||
public static boolean request(Minecraft client, Slot underCursor) {
|
||||
if (client.player == null || client.level == null
|
||||
|| !(client.gui.screen() instanceof AbstractContainerScreen<?>)
|
||||
|| client.gui.screen() instanceof CreativeModeInventoryScreen
|
||||
|| !(client.gui.screen() instanceof AbstractContainerScreen<?> screen)
|
||||
|| screen instanceof CreativeModeInventoryScreen
|
||||
|| !ClientPlayNetworking.canSend(SortInventoryPayload.ID)) {
|
||||
return false;
|
||||
}
|
||||
boolean container = resolveContainerTarget(screen, underCursor);
|
||||
AbstractWidget.playButtonClickSound(client.getSoundManager());
|
||||
ClientPlayNetworking.send(new SortInventoryPayload());
|
||||
ClientPlayNetworking.send(new SortInventoryPayload(container));
|
||||
return true;
|
||||
}
|
||||
|
||||
private static boolean resolveContainerTarget(AbstractContainerScreen<?> screen,
|
||||
Slot underCursor) {
|
||||
if (screen instanceof InventoryScreen) return false;
|
||||
Slot hovered = underCursor != null ? underCursor
|
||||
: ((AbstractContainerScreenAccessor) screen).sanctuary$getHoveredSlot();
|
||||
if (hovered != null) {
|
||||
return SanctuaryInventorySort.isSortableStorageMenu(screen.getMenu())
|
||||
&& !SanctuaryInventorySort.isPlayerInventorySlot(hovered);
|
||||
}
|
||||
return SanctuaryInventorySort.isSortableStorageMenu(screen.getMenu());
|
||||
}
|
||||
}
|
||||
|
||||
+9
-1
@@ -10,7 +10,7 @@ import net.minecraft.world.level.Level;
|
||||
import net.minecraft.world.level.saveddata.SavedData;
|
||||
import net.minecraft.world.level.saveddata.SavedDataType;
|
||||
|
||||
/** SavedData wrapper for the world-wide Notch victory and future unique reward claim. */
|
||||
/** SavedData wrapper for the world-wide Notch victory and two-phase unique reward delivery. */
|
||||
public final class NotchVictoryData extends SavedData {
|
||||
private static final SavedDataType<NotchVictoryData> TYPE = new SavedDataType<>(
|
||||
SanctuaryMod.id("notch_victory"),
|
||||
@@ -57,4 +57,12 @@ public final class NotchVictoryData extends SavedData {
|
||||
setDirty();
|
||||
return true;
|
||||
}
|
||||
|
||||
public boolean completeInitialReward() {
|
||||
NotchVictoryState updated = state.completeInitialReward();
|
||||
if (updated.equals(state)) return false;
|
||||
state = updated;
|
||||
setDirty();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
+25
-9
@@ -13,34 +13,42 @@ public record NotchVictoryState(
|
||||
boolean defeated,
|
||||
long defeatedAt,
|
||||
List<String> participantIds,
|
||||
boolean initialRewardClaimed
|
||||
boolean initialRewardClaimed,
|
||||
boolean initialRewardDelivered
|
||||
) {
|
||||
public static final int DATA_VERSION = 1;
|
||||
public static final int DATA_VERSION = 2;
|
||||
public static final int MAX_PARTICIPANTS = 16;
|
||||
public static final Codec<NotchVictoryState> CODEC = RecordCodecBuilder.create(instance -> instance.group(
|
||||
Codec.INT.optionalFieldOf("data_version", DATA_VERSION).forGetter(NotchVictoryState::dataVersion),
|
||||
Codec.INT.optionalFieldOf("data_version", 1).forGetter(NotchVictoryState::dataVersion),
|
||||
Codec.BOOL.optionalFieldOf("notch_defeated", false).forGetter(NotchVictoryState::defeated),
|
||||
Codec.LONG.optionalFieldOf("defeated_at", 0L).forGetter(NotchVictoryState::defeatedAt),
|
||||
Codec.STRING.listOf().optionalFieldOf("participants", List.of()).forGetter(NotchVictoryState::participantIds),
|
||||
Codec.BOOL.optionalFieldOf("initial_reward_claimed", false)
|
||||
.forGetter(NotchVictoryState::initialRewardClaimed)
|
||||
.forGetter(NotchVictoryState::initialRewardClaimed),
|
||||
Codec.BOOL.optionalFieldOf("initial_reward_delivered", false)
|
||||
.forGetter(NotchVictoryState::initialRewardDelivered)
|
||||
).apply(instance, NotchVictoryState::new));
|
||||
|
||||
public NotchVictoryState {
|
||||
int storedVersion = dataVersion;
|
||||
dataVersion = DATA_VERSION;
|
||||
defeatedAt = Math.max(0L, defeatedAt);
|
||||
participantIds = normalizeStrings(participantIds);
|
||||
initialRewardClaimed = defeated && initialRewardClaimed;
|
||||
// Version 1 only knew the claimed flag. Treat those historical claims as
|
||||
// already delivered so migrated worlds never receive a duplicate carpet.
|
||||
initialRewardDelivered = initialRewardClaimed
|
||||
&& (initialRewardDelivered || storedVersion < DATA_VERSION);
|
||||
}
|
||||
|
||||
public static NotchVictoryState empty() {
|
||||
return new NotchVictoryState(DATA_VERSION, false, 0L, List.of(), false);
|
||||
return new NotchVictoryState(DATA_VERSION, false, 0L, List.of(), false, false);
|
||||
}
|
||||
|
||||
public NotchVictoryState defeat(Collection<UUID> participants, long gameTime, boolean rewardAlreadyClaimed) {
|
||||
if (defeated) {
|
||||
return rewardAlreadyClaimed && !initialRewardClaimed
|
||||
? new NotchVictoryState(DATA_VERSION, true, defeatedAt, participantIds, true)
|
||||
return rewardAlreadyClaimed && !initialRewardDelivered
|
||||
? new NotchVictoryState(DATA_VERSION, true, defeatedAt, participantIds, true, true)
|
||||
: this;
|
||||
}
|
||||
List<String> ids = participants == null ? List.of() : participants.stream()
|
||||
@@ -48,12 +56,20 @@ public record NotchVictoryState(
|
||||
.map(UUID::toString)
|
||||
.toList();
|
||||
return new NotchVictoryState(DATA_VERSION, true, Math.max(0L, gameTime), ids,
|
||||
rewardAlreadyClaimed);
|
||||
rewardAlreadyClaimed, rewardAlreadyClaimed);
|
||||
}
|
||||
|
||||
/** Reserves the unique reward before the physical chest is created. */
|
||||
public NotchVictoryState claimInitialReward() {
|
||||
return defeated && !initialRewardClaimed
|
||||
? new NotchVictoryState(DATA_VERSION, true, defeatedAt, participantIds, true)
|
||||
? new NotchVictoryState(DATA_VERSION, true, defeatedAt, participantIds, true, false)
|
||||
: this;
|
||||
}
|
||||
|
||||
/** Marks the reserved reward delivered after its idempotent chest exists. */
|
||||
public NotchVictoryState completeInitialReward() {
|
||||
return defeated && initialRewardClaimed && !initialRewardDelivered
|
||||
? new NotchVictoryState(DATA_VERSION, true, defeatedAt, participantIds, true, true)
|
||||
: this;
|
||||
}
|
||||
|
||||
|
||||
+2
-2
@@ -62,7 +62,7 @@ public final class SanctuaryCapeService {
|
||||
|
||||
private static void send(ServerPlayer receiver, ServerPlayer owner) {
|
||||
if (!ServerPlayNetworking.canSend(receiver, CapeSyncPayload.ID)) return;
|
||||
String capeId = SanctuaryCapes.itemId(SanctuaryInventoryCapacity.equippedCape(owner));
|
||||
ServerPlayNetworking.send(receiver, new CapeSyncPayload(owner.getUUID(), capeId));
|
||||
ServerPlayNetworking.send(receiver, CapeSyncPayload.fromStack(
|
||||
owner.getUUID(), SanctuaryInventoryCapacity.equippedCape(owner)));
|
||||
}
|
||||
}
|
||||
|
||||
+11
-4
@@ -14,8 +14,10 @@ import net.minecraft.world.item.CreativeModeTab;
|
||||
import net.minecraft.world.item.Item;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
|
||||
/** Registration and immutable visual definitions for Sanctuary's collectible capes. */
|
||||
/** Registration and immutable visual definitions for Sanctuary's equippable capes. */
|
||||
public final class SanctuaryCapes {
|
||||
public static final String WHITE_CAPE_PATH = "white_cape";
|
||||
public static final String WHITE_CAPE_ID = "sanctuary:" + WHITE_CAPE_PATH;
|
||||
public static final List<String> PATHS = List.of(
|
||||
"transgender_cape", "mojang_cape", "founders_cape", "nes_cape", "pride_cape",
|
||||
"pink_cape", "feesh_gang_cape", "gameboy_cape", "blue_cape", "chicken_cape",
|
||||
@@ -28,7 +30,7 @@ public final class SanctuaryCapes {
|
||||
"vanilla_cape", "java_edition_cape", "migration_cape", "creeper_anniversary_cape",
|
||||
"java_twitch_cape", "minecon_enderman_cape", "iron_golem_minecon_cape", "minecon_2013_cape",
|
||||
"crafter_twitchcon_cape", "minecon_pickaxe_cape", "minecon_2011_cape", "mojang_classic_cape",
|
||||
"blue_scroll_cape"
|
||||
"blue_scroll_cape", WHITE_CAPE_PATH
|
||||
);
|
||||
private static final Map<Identifier, Definition> DEFINITIONS = registerDefinitions();
|
||||
private static final ResourceKey<CreativeModeTab> CREATIVE_TAB_KEY = ResourceKey.create(
|
||||
@@ -74,6 +76,10 @@ public final class SanctuaryCapes {
|
||||
return definition == null ? null : definition.texture();
|
||||
}
|
||||
|
||||
public static boolean isBannerCape(String itemId) {
|
||||
return WHITE_CAPE_ID.equals(itemId);
|
||||
}
|
||||
|
||||
public static List<Definition> definitions() {
|
||||
return List.copyOf(DEFINITIONS.values());
|
||||
}
|
||||
@@ -85,8 +91,9 @@ public final class SanctuaryCapes {
|
||||
ResourceKey<Item> key = ResourceKey.create(Registries.ITEM, id);
|
||||
Item item = Registry.register(BuiltInRegistries.ITEM, key,
|
||||
new SanctuaryCapeItem(new Item.Properties().stacksTo(1).setId(key)));
|
||||
definitions.put(id, new Definition(id, item,
|
||||
SanctuaryMod.id("entity/cape/" + path)));
|
||||
Identifier texture = WHITE_CAPE_PATH.equals(path)
|
||||
? null : SanctuaryMod.id("entity/cape/" + path);
|
||||
definitions.put(id, new Definition(id, item, texture));
|
||||
}
|
||||
return Map.copyOf(definitions);
|
||||
}
|
||||
|
||||
+2
-1
@@ -1,6 +1,7 @@
|
||||
package fr.koka99cab.sanctuary26.sanctuary.gameplay;
|
||||
|
||||
import fr.koka99cab.sanctuary26.sanctuary.SanctuaryMod;
|
||||
import fr.koka99cab.sanctuary26.sanctuary.api.entity.SanctuaryEntityPolicy;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
@@ -24,7 +25,7 @@ import net.minecraft.world.item.SpawnEggItem;
|
||||
|
||||
/** Server-owned lifecycle for the one companion equipped by each player. */
|
||||
public final class SanctuaryCompanions {
|
||||
public static final String ENTITY_TAG = "sanctuary_companion";
|
||||
public static final String ENTITY_TAG = SanctuaryEntityPolicy.TEMPORARY_COMPANION_TAG;
|
||||
private static final String OWNER_TAG_PREFIX = "sanctuary_companion_owner_";
|
||||
private static final Identifier MINI_SCALE = SanctuaryMod.id("companion/mini_scale");
|
||||
private static final double TELEPORT_DISTANCE_SQUARED = 144.0D;
|
||||
|
||||
+52
-3
@@ -10,11 +10,18 @@ import net.minecraft.core.registries.BuiltInRegistries;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.world.Container;
|
||||
import net.minecraft.world.entity.player.Inventory;
|
||||
import net.minecraft.world.inventory.AbstractContainerMenu;
|
||||
import net.minecraft.world.inventory.ChestMenu;
|
||||
import net.minecraft.world.inventory.DispenserMenu;
|
||||
import net.minecraft.world.inventory.HopperMenu;
|
||||
import net.minecraft.world.inventory.ShulkerBoxMenu;
|
||||
import net.minecraft.world.inventory.Slot;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
|
||||
/**
|
||||
* Server-authoritative sort of every unlocked inventory row except the live hotbar.
|
||||
* Armor, off-hand, crafting, cape and companion slots stay untouched.
|
||||
* Server-authoritative sort of player storage or an open chest-like container.
|
||||
* Armor, off-hand, crafting, cape, companion slots and the live hotbar stay
|
||||
* untouched. Furnaces, shops and the storage terminal are never rewritten.
|
||||
*/
|
||||
public final class SanctuaryInventorySort {
|
||||
private static final int COOLDOWN_TICKS = 8;
|
||||
@@ -85,11 +92,31 @@ public final class SanctuaryInventorySort {
|
||||
ServerPlayerEvents.LEAVE.register(player -> LAST_SORT_TICK.remove(player.getUUID()));
|
||||
}
|
||||
|
||||
public static void sort(ServerPlayer player) {
|
||||
public static boolean isSortableStorageMenu(AbstractContainerMenu menu) {
|
||||
return menu instanceof ChestMenu
|
||||
|| menu instanceof ShulkerBoxMenu
|
||||
|| menu instanceof HopperMenu
|
||||
|| menu instanceof DispenserMenu;
|
||||
}
|
||||
|
||||
public static boolean isPlayerInventorySlot(Slot slot) {
|
||||
if (slot == null) return false;
|
||||
Container container = slot.container;
|
||||
return container instanceof Inventory
|
||||
|| container instanceof SanctuaryOverflowContainer
|
||||
|| container instanceof SanctuaryCapeContainer
|
||||
|| container instanceof SanctuaryCompanionEggContainer;
|
||||
}
|
||||
|
||||
public static void sort(ServerPlayer player, boolean container) {
|
||||
long now = player.level().getGameTime();
|
||||
Long previous = LAST_SORT_TICK.get(player.getUUID());
|
||||
if (previous != null && now - previous < COOLDOWN_TICKS) return;
|
||||
LAST_SORT_TICK.put(player.getUUID(), now);
|
||||
if (container) {
|
||||
sortOpenContainer(player);
|
||||
return;
|
||||
}
|
||||
|
||||
int unlockedRows = SanctuaryInventoryCapacity.unlockedRows(player);
|
||||
if (unlockedRows <= 1) return;
|
||||
@@ -123,6 +150,28 @@ public final class SanctuaryInventorySort {
|
||||
if (player.containerMenu != player.inventoryMenu) player.containerMenu.broadcastChanges();
|
||||
}
|
||||
|
||||
private static void sortOpenContainer(ServerPlayer player) {
|
||||
AbstractContainerMenu menu = player.containerMenu;
|
||||
if (menu == null || menu == player.inventoryMenu || !isSortableStorageMenu(menu)
|
||||
|| !menu.stillValid(player)) {
|
||||
return;
|
||||
}
|
||||
List<Slot> sortable = new ArrayList<>();
|
||||
for (Slot slot : menu.slots) {
|
||||
if (!slot.isActive() || isPlayerInventorySlot(slot)) continue;
|
||||
sortable.add(slot);
|
||||
}
|
||||
if (sortable.isEmpty()) return;
|
||||
List<ItemStack> items = new ArrayList<>(sortable.size());
|
||||
for (Slot slot : sortable) items.add(slot.getItem().copy());
|
||||
SanctuaryInventorySorter.compactAndSort(items, STACKS);
|
||||
for (int index = 0; index < sortable.size(); index++) {
|
||||
Slot slot = sortable.get(index);
|
||||
slot.container.setItem(slot.getContainerSlot(), items.get(index));
|
||||
}
|
||||
menu.broadcastChanges();
|
||||
}
|
||||
|
||||
private static ItemStack getRowItem(Inventory inventory, Container overflow,
|
||||
int logicalRow, int column) {
|
||||
if (logicalRow < SanctuaryInventoryCapacity.VANILLA_ROW_COUNT) {
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
package fr.koka99cab.sanctuary26.sanctuary.mixin;
|
||||
|
||||
import fr.koka99cab.sanctuary26.sanctuary.backroom.BackroomService;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.world.InteractionResult;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import net.minecraft.world.level.Level;
|
||||
import net.minecraft.world.level.block.BedBlock;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.phys.BlockHitResult;
|
||||
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(BedBlock.class)
|
||||
abstract class BedBlockMixin {
|
||||
@Inject(method = "useWithoutItem", at = @At("HEAD"), cancellable = true)
|
||||
private void sanctuary$useBackroomBed(BlockState state, Level level, BlockPos pos,
|
||||
Player player, BlockHitResult hit, CallbackInfoReturnable<InteractionResult> callback) {
|
||||
if (player instanceof ServerPlayer serverPlayer && BackroomService.useBed(serverPlayer)) {
|
||||
callback.setReturnValue(InteractionResult.SUCCESS_SERVER);
|
||||
}
|
||||
}
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
package fr.koka99cab.sanctuary26.sanctuary.mixin;
|
||||
|
||||
import fr.koka99cab.sanctuary26.sanctuary.backroom.BackroomChestService;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.world.entity.ContainerUser;
|
||||
import net.minecraft.world.level.block.entity.ChestBlockEntity;
|
||||
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(ChestBlockEntity.class)
|
||||
abstract class ChestBlockEntityMixin {
|
||||
@Inject(method = "startOpen", at = @At("HEAD"))
|
||||
private void sanctuary$refreshBackroomChest(ContainerUser user, CallbackInfo callback) {
|
||||
if (user.getLivingEntity() instanceof ServerPlayer player) {
|
||||
BackroomChestService.open((ChestBlockEntity) (Object) this, player);
|
||||
}
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
package fr.koka99cab.sanctuary26.sanctuary.mixin;
|
||||
|
||||
import fr.koka99cab.sanctuary26.sanctuary.gameplay.SanctuaryCompanions;
|
||||
import fr.koka99cab.sanctuary26.sanctuary.api.entity.SanctuaryEntityPolicy;
|
||||
import net.minecraft.world.entity.Entity;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.injection.At;
|
||||
@@ -12,7 +12,7 @@ import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
|
||||
abstract class EntityCompanionMixin {
|
||||
@Inject(method = "shouldBeSaved", at = @At("HEAD"), cancellable = true)
|
||||
private void sanctuary$doNotSaveTemporaryCompanion(CallbackInfoReturnable<Boolean> cir) {
|
||||
if (((Entity) (Object) this).entityTags().contains(SanctuaryCompanions.ENTITY_TAG)) {
|
||||
if (SanctuaryEntityPolicy.isTemporaryCompanion((Entity) (Object) this)) {
|
||||
cir.setReturnValue(false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
package fr.koka99cab.sanctuary26.sanctuary.mixin;
|
||||
|
||||
import fr.koka99cab.sanctuary26.sanctuary.backroom.BackroomData;
|
||||
import java.util.UUID;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.world.entity.Entity;
|
||||
import net.minecraft.world.entity.item.ItemEntity;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.Shadow;
|
||||
import org.spongepowered.asm.mixin.injection.At;
|
||||
import org.spongepowered.asm.mixin.injection.Inject;
|
||||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
|
||||
|
||||
@Mixin(ItemEntity.class)
|
||||
abstract class ItemEntityMixin {
|
||||
@Shadow private @Nullable UUID target;
|
||||
|
||||
@Inject(method = "tick", at = @At("HEAD"), cancellable = true)
|
||||
private void sanctuary$captureLostItem(CallbackInfo callback) {
|
||||
ItemEntity item = (ItemEntity) (Object) this;
|
||||
if (!(item.level() instanceof ServerLevel level) || item.getItem().isEmpty()) return;
|
||||
boolean voidLoss = item.getY() < level.getMinY() - 64.0D;
|
||||
boolean despawn = item.getAge() >= 5_999;
|
||||
if (!voidLoss && !despawn) return;
|
||||
UUID owner = target;
|
||||
Entity thrower = item.getOwner();
|
||||
if (owner == null && thrower instanceof ServerPlayer player) owner = player.getUUID();
|
||||
if (BackroomData.get(level.getServer()).capture(item.getItem(), owner,
|
||||
voidLoss ? "void" : "despawn", level.getGameTime()).isEmpty()) return;
|
||||
item.discard();
|
||||
callback.cancel();
|
||||
}
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
package fr.koka99cab.sanctuary26.sanctuary.mixin;
|
||||
|
||||
import fr.koka99cab.sanctuary26.sanctuary.api.entity.SanctuaryEntityPolicy;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.world.damagesource.DamageSource;
|
||||
import net.minecraft.world.entity.LivingEntity;
|
||||
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;
|
||||
|
||||
/** Temporary companions can be defeated, but never become a renewable loot or XP source. */
|
||||
@Mixin(LivingEntity.class)
|
||||
abstract class LivingEntityCompanionMixin {
|
||||
@Inject(method = "dropAllDeathLoot", at = @At("HEAD"), cancellable = true)
|
||||
private void sanctuary$suppressTemporaryCompanionLoot(ServerLevel level, DamageSource source,
|
||||
CallbackInfo ci) {
|
||||
if (SanctuaryEntityPolicy.isTemporaryCompanion((LivingEntity) (Object) this)) ci.cancel();
|
||||
}
|
||||
}
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
package fr.koka99cab.sanctuary26.sanctuary.mixin;
|
||||
|
||||
import fr.koka99cab.sanctuary26.sanctuary.backroom.BackroomInventory;
|
||||
import fr.koka99cab.sanctuary26.sanctuary.backroom.BackroomService;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.world.damagesource.DamageSource;
|
||||
import net.minecraft.world.damagesource.DamageTypes;
|
||||
import net.minecraft.world.level.portal.TeleportTransition;
|
||||
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;
|
||||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
|
||||
|
||||
@Mixin(ServerPlayer.class)
|
||||
abstract class ServerPlayerBackroomMixin {
|
||||
@Inject(method = "teleport", at = @At("HEAD"), cancellable = true)
|
||||
private void sanctuary$enforceBackroomExtraction(TeleportTransition transition,
|
||||
CallbackInfoReturnable<ServerPlayer> callback) {
|
||||
ServerPlayer player = (ServerPlayer) (Object) this;
|
||||
boolean leaving = BackroomService.isBackroom(player.level())
|
||||
&& !BackroomService.isBackroom(transition.newLevel());
|
||||
boolean entering = !BackroomService.isBackroom(player.level())
|
||||
&& BackroomService.isBackroom(transition.newLevel());
|
||||
if ((!leaving && !entering) || BackroomService.isAuthorizedTransition(player.getUUID())) return;
|
||||
if (entering) {
|
||||
player.sendOverlayMessage(Component.translatable("message.sanctuary.backroom.bed_entry_only"));
|
||||
callback.setReturnValue(null);
|
||||
return;
|
||||
}
|
||||
BackroomInventory.captureAll(player, "backroom_forced_exit");
|
||||
}
|
||||
|
||||
@Inject(method = "die", at = @At("HEAD"))
|
||||
private void sanctuary$captureBackroomDeath(DamageSource source, CallbackInfo callback) {
|
||||
ServerPlayer player = (ServerPlayer) (Object) this;
|
||||
if (BackroomService.isBackroom(player.level())) {
|
||||
BackroomInventory.captureAll(player, "backroom_death");
|
||||
} else if (source.is(DamageTypes.FELL_OUT_OF_WORLD)) {
|
||||
BackroomInventory.captureAll(player, "void_death");
|
||||
}
|
||||
}
|
||||
}
|
||||
+4
@@ -1,6 +1,7 @@
|
||||
package fr.koka99cab.sanctuary26.sanctuary.mixin.client;
|
||||
|
||||
import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen;
|
||||
import net.minecraft.world.inventory.Slot;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.Mutable;
|
||||
import org.spongepowered.asm.mixin.gen.Accessor;
|
||||
@@ -21,4 +22,7 @@ public interface AbstractContainerScreenAccessor {
|
||||
@Mutable
|
||||
@Accessor("imageHeight")
|
||||
void sanctuary$setImageHeight(int imageHeight);
|
||||
|
||||
@Accessor("hoveredSlot")
|
||||
Slot sanctuary$getHoveredSlot();
|
||||
}
|
||||
|
||||
+2
-1
@@ -251,7 +251,8 @@ abstract class AbstractContainerScreenMixin implements SanctuaryContainerScreenG
|
||||
private void sanctuary$sortInventory(MouseButtonEvent event, boolean doubleClick,
|
||||
CallbackInfoReturnable<Boolean> cir) {
|
||||
if (!SanctuaryClient.isSortInventoryMouse(event)) return;
|
||||
if (SanctuaryInventorySortClient.request(Minecraft.getInstance())) {
|
||||
if (SanctuaryInventorySortClient.request(Minecraft.getInstance(),
|
||||
sanctuary$slotAt(event.x(), event.y()))) {
|
||||
cir.setReturnValue(true);
|
||||
}
|
||||
}
|
||||
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
package fr.koka99cab.sanctuary26.sanctuary.mixin.client;
|
||||
|
||||
import fr.koka99cab.sanctuary26.sanctuary.client.SanctuaryCapeRenderState;
|
||||
import fr.koka99cab.sanctuary26.sanctuary.client.SanctuaryClientCapes;
|
||||
import net.minecraft.client.renderer.entity.state.AvatarRenderState;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.Unique;
|
||||
|
||||
/** Stores the server-authoritative dynamic cape appearance on each extracted player state. */
|
||||
@Mixin(AvatarRenderState.class)
|
||||
abstract class AvatarRenderStateMixin implements SanctuaryCapeRenderState {
|
||||
@Unique private SanctuaryClientCapes.Appearance sanctuary$capeAppearance;
|
||||
|
||||
@Override
|
||||
public SanctuaryClientCapes.Appearance sanctuary$capeAppearance() {
|
||||
return sanctuary$capeAppearance;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sanctuary$setCapeAppearance(SanctuaryClientCapes.Appearance appearance) {
|
||||
sanctuary$capeAppearance = appearance;
|
||||
}
|
||||
}
|
||||
+8
-1
@@ -1,6 +1,7 @@
|
||||
package fr.koka99cab.sanctuary26.sanctuary.mixin.client;
|
||||
|
||||
import fr.koka99cab.sanctuary26.sanctuary.client.SanctuaryClientCapes;
|
||||
import fr.koka99cab.sanctuary26.sanctuary.client.SanctuaryCapeRenderState;
|
||||
import net.minecraft.client.renderer.entity.player.AvatarRenderer;
|
||||
import net.minecraft.client.renderer.entity.state.AvatarRenderState;
|
||||
import net.minecraft.core.ClientAsset;
|
||||
@@ -21,7 +22,13 @@ abstract class AvatarRendererMixin {
|
||||
)
|
||||
private void sanctuary$applyEquippedCape(Avatar avatar, AvatarRenderState state,
|
||||
float partialTick, CallbackInfo ci) {
|
||||
Identifier texture = SanctuaryClientCapes.texture(avatar.getUUID());
|
||||
SanctuaryClientCapes.Appearance appearance = SanctuaryClientCapes.appearance(avatar.getUUID());
|
||||
((SanctuaryCapeRenderState) state).sanctuary$setCapeAppearance(appearance);
|
||||
if (appearance == null || appearance.bannerCape()) {
|
||||
if (appearance != null) state.showCape = false;
|
||||
return;
|
||||
}
|
||||
Identifier texture = appearance.texture();
|
||||
PlayerSkin skin = state.skin;
|
||||
if (texture == null || skin == null) return;
|
||||
state.skin = PlayerSkin.insecure(skin.body(), new ClientAsset.ResourceTexture(texture),
|
||||
|
||||
+4
@@ -6,6 +6,7 @@ import fr.koka99cab.sanctuary26.sanctuary.client.SanctuaryInventorySortClient;
|
||||
import fr.koka99cab.sanctuary26.sanctuary.client.SanctuaryZoom;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.MouseHandler;
|
||||
import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen;
|
||||
import net.minecraft.client.input.MouseButtonEvent;
|
||||
import net.minecraft.client.input.MouseButtonInfo;
|
||||
import org.lwjgl.glfw.GLFW;
|
||||
@@ -32,6 +33,9 @@ public abstract class MouseHandlerMixin {
|
||||
if (action != GLFW.GLFW_PRESS) return;
|
||||
Minecraft client = Minecraft.getInstance();
|
||||
if (windowPointer != client.getWindow().handle()) return;
|
||||
if (client.gui.screen() instanceof AbstractContainerScreen<?>) {
|
||||
return;
|
||||
}
|
||||
MouseButtonEvent event = new MouseButtonEvent(0.0, 0.0, buttonInfo);
|
||||
if (!SanctuaryClient.isSortInventoryMouse(event)) return;
|
||||
if (SanctuaryInventorySortClient.request(client)) {
|
||||
|
||||
+43
-2
@@ -1,13 +1,22 @@
|
||||
package fr.koka99cab.sanctuary26.sanctuary.network;
|
||||
|
||||
import fr.koka99cab.sanctuary26.sanctuary.SanctuaryMod;
|
||||
import fr.koka99cab.sanctuary26.sanctuary.gameplay.SanctuaryCapes;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import net.minecraft.core.component.DataComponents;
|
||||
import net.minecraft.network.RegistryFriendlyByteBuf;
|
||||
import net.minecraft.network.codec.StreamCodec;
|
||||
import net.minecraft.network.protocol.common.custom.CustomPacketPayload;
|
||||
import net.minecraft.world.item.DyeColor;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.level.block.entity.BannerPatternLayers;
|
||||
|
||||
/** Announces one player's currently equipped Sanctuary cape to clients. */
|
||||
public record CapeSyncPayload(UUID playerId, String capeItemId) implements CustomPacketPayload {
|
||||
public record CapeSyncPayload(UUID playerId, String capeItemId, DyeColor baseColor,
|
||||
BannerPatternLayers patterns) implements CustomPacketPayload {
|
||||
public static final int MAX_PATTERN_LAYERS = 6;
|
||||
public static final Type<CapeSyncPayload> ID = new Type<>(SanctuaryMod.id("s2c/cape_sync"));
|
||||
public static final StreamCodec<RegistryFriendlyByteBuf, CapeSyncPayload> CODEC =
|
||||
CustomPacketPayload.codec(CapeSyncPayload::write, CapeSyncPayload::new);
|
||||
@@ -15,15 +24,47 @@ public record CapeSyncPayload(UUID playerId, String capeItemId) implements Custo
|
||||
public CapeSyncPayload {
|
||||
if (playerId == null) throw new IllegalArgumentException("Cape player UUID cannot be null");
|
||||
capeItemId = capeItemId == null ? "" : capeItemId;
|
||||
baseColor = baseColor == null ? DyeColor.WHITE : baseColor;
|
||||
patterns = bounded(patterns);
|
||||
}
|
||||
|
||||
private CapeSyncPayload(RegistryFriendlyByteBuf buffer) {
|
||||
this(buffer.readUUID(), buffer.readUtf(128));
|
||||
this(buffer.readUUID(), buffer.readUtf(128), DyeColor.STREAM_CODEC.decode(buffer), readPatterns(buffer));
|
||||
}
|
||||
|
||||
private void write(RegistryFriendlyByteBuf buffer) {
|
||||
buffer.writeUUID(playerId);
|
||||
buffer.writeUtf(capeItemId, 128);
|
||||
DyeColor.STREAM_CODEC.encode(buffer, baseColor);
|
||||
buffer.writeVarInt(patterns.layers().size());
|
||||
patterns.layers().forEach(layer -> BannerPatternLayers.Layer.STREAM_CODEC.encode(buffer, layer));
|
||||
}
|
||||
|
||||
public static CapeSyncPayload fromStack(UUID playerId, ItemStack stack) {
|
||||
String capeItemId = SanctuaryCapes.itemId(stack);
|
||||
DyeColor baseColor = stack.get(DataComponents.BASE_COLOR);
|
||||
BannerPatternLayers patterns = stack.get(DataComponents.BANNER_PATTERNS);
|
||||
return new CapeSyncPayload(playerId, capeItemId, baseColor, patterns);
|
||||
}
|
||||
|
||||
private static BannerPatternLayers readPatterns(RegistryFriendlyByteBuf buffer) {
|
||||
int size = buffer.readVarInt();
|
||||
if (size < 0 || size > MAX_PATTERN_LAYERS) {
|
||||
throw new IllegalArgumentException("Cape banner pattern count must be between 0 and "
|
||||
+ MAX_PATTERN_LAYERS + ": " + size);
|
||||
}
|
||||
List<BannerPatternLayers.Layer> layers = new ArrayList<>(size);
|
||||
for (int i = 0; i < size; i++) {
|
||||
layers.add(BannerPatternLayers.Layer.STREAM_CODEC.decode(buffer));
|
||||
}
|
||||
return layers.isEmpty() ? BannerPatternLayers.EMPTY : new BannerPatternLayers(List.copyOf(layers));
|
||||
}
|
||||
|
||||
private static BannerPatternLayers bounded(BannerPatternLayers patterns) {
|
||||
if (patterns == null || patterns.layers().isEmpty()) return BannerPatternLayers.EMPTY;
|
||||
List<BannerPatternLayers.Layer> layers = patterns.layers();
|
||||
if (layers.size() <= MAX_PATTERN_LAYERS) return patterns;
|
||||
return new BannerPatternLayers(List.copyOf(layers.subList(0, MAX_PATTERN_LAYERS)));
|
||||
}
|
||||
|
||||
@Override public Type<? extends CustomPacketPayload> type() { return ID; }
|
||||
|
||||
+3
-2
@@ -24,7 +24,7 @@ import net.minecraft.server.level.ServerPlayer;
|
||||
* Networking contract for Sanctuary. Payload IDs must use the module namespace.
|
||||
*/
|
||||
public final class SanctuaryNetworking {
|
||||
public static final int PROTOCOL_VERSION = 22;
|
||||
public static final int PROTOCOL_VERSION = 24;
|
||||
private static boolean initialized;
|
||||
|
||||
private SanctuaryNetworking() {
|
||||
@@ -75,7 +75,8 @@ public final class SanctuaryNetworking {
|
||||
}
|
||||
}));
|
||||
ServerPlayNetworking.registerGlobalReceiver(SortInventoryPayload.ID, (payload, context) ->
|
||||
context.server().execute(() -> SanctuaryInventorySort.sort(context.player())));
|
||||
context.server().execute(() ->
|
||||
SanctuaryInventorySort.sort(context.player(), payload.container())));
|
||||
ServerPlayNetworking.registerGlobalReceiver(CompanionPowerUsePayload.ID, (payload, context) ->
|
||||
context.server().execute(() -> SanctuaryCompanionPowers.use(context.player())));
|
||||
ServerPlayNetworking.registerGlobalReceiver(SitRequestPayload.ID, (payload, context) ->
|
||||
|
||||
+7
-4
@@ -5,19 +5,22 @@ import net.minecraft.network.RegistryFriendlyByteBuf;
|
||||
import net.minecraft.network.codec.StreamCodec;
|
||||
import net.minecraft.network.protocol.common.custom.CustomPacketPayload;
|
||||
|
||||
/** A parameterless client intent. The server owns which unlocked rows are sorted. */
|
||||
public record SortInventoryPayload() implements CustomPacketPayload {
|
||||
/**
|
||||
* Bounded client intent: player storage or the open storage container.
|
||||
* The server still decides which slots exist and ignores non-storage menus.
|
||||
*/
|
||||
public record SortInventoryPayload(boolean container) implements CustomPacketPayload {
|
||||
public static final Type<SortInventoryPayload> ID =
|
||||
new Type<>(SanctuaryMod.id("c2s/sort_inventory"));
|
||||
public static final StreamCodec<RegistryFriendlyByteBuf, SortInventoryPayload> CODEC =
|
||||
CustomPacketPayload.codec(SortInventoryPayload::write, SortInventoryPayload::new);
|
||||
|
||||
private SortInventoryPayload(RegistryFriendlyByteBuf buffer) {
|
||||
this();
|
||||
this(buffer.readBoolean());
|
||||
}
|
||||
|
||||
private void write(RegistryFriendlyByteBuf buffer) {
|
||||
// Intentionally empty: no client-supplied slot list or item identity can be trusted.
|
||||
buffer.writeBoolean(container);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+13
@@ -9,6 +9,9 @@ import fr.koka99cab.sanctuary26.sanctuary.shop.QuestRequestSignItem;
|
||||
import fr.koka99cab.sanctuary26.sanctuary.shop.QuestSignItem;
|
||||
import fr.koka99cab.sanctuary26.sanctuary.gameplay.SanctuaryCapes;
|
||||
import java.util.function.Function;
|
||||
import java.util.UUID;
|
||||
import net.minecraft.core.UUIDUtil;
|
||||
import net.minecraft.core.component.DataComponentType;
|
||||
import net.fabricmc.fabric.api.creativetab.v1.CreativeModeTabEvents;
|
||||
import net.fabricmc.fabric.api.object.builder.v1.block.entity.FabricBlockEntityType;
|
||||
import net.fabricmc.fabric.api.object.builder.v1.block.entity.FabricBlockEntityTypeBuilder;
|
||||
@@ -37,6 +40,16 @@ import net.minecraft.world.level.material.MapColor;
|
||||
* Feature registries must be called from here to keep initialization ordering explicit.
|
||||
*/
|
||||
public final class SanctuaryRegistries {
|
||||
public static final Identifier BACKROOM_LOSS_ID = SanctuaryMod.id("backroom_loss_id");
|
||||
public static final DataComponentType<UUID> BACKROOM_LOSS_COMPONENT = Registry.register(
|
||||
BuiltInRegistries.DATA_COMPONENT_TYPE,
|
||||
BACKROOM_LOSS_ID,
|
||||
DataComponentType.<UUID>builder()
|
||||
.persistent(UUIDUtil.CODEC)
|
||||
.networkSynchronized(UUIDUtil.STREAM_CODEC)
|
||||
.cacheEncoding()
|
||||
.build()
|
||||
);
|
||||
public static final Identifier DELIVERY_BOX_ID = SanctuaryMod.id("delivery_box");
|
||||
public static final DeliveryBoxBlock DELIVERY_BOX = registerBlock(
|
||||
DELIVERY_BOX_ID,
|
||||
|
||||
+58
@@ -1,6 +1,10 @@
|
||||
package fr.koka99cab.sanctuary26.sanctuary.shop;
|
||||
|
||||
import fr.koka99cab.sanctuary26.sanctuary.registry.SanctuaryRegistries;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.NonNullList;
|
||||
@@ -19,7 +23,9 @@ import net.minecraft.world.level.storage.ValueOutput;
|
||||
|
||||
public final class DeliveryBoxBlockEntity extends BaseContainerBlockEntity {
|
||||
public static final int SIZE = 27;
|
||||
private static final int MAX_PROCESSED_TRANSACTIONS = 128;
|
||||
private NonNullList<ItemStack> items = NonNullList.withSize(SIZE, ItemStack.EMPTY);
|
||||
private final Set<UUID> processedTransactions = new LinkedHashSet<>();
|
||||
private UUID owner;
|
||||
|
||||
public DeliveryBoxBlockEntity(BlockPos pos, BlockState state) {
|
||||
@@ -68,16 +74,68 @@ public final class DeliveryBoxBlockEntity extends BaseContainerBlockEntity {
|
||||
return remaining.isEmpty();
|
||||
}
|
||||
|
||||
/**
|
||||
* Atomically inserts a complete Backroom parcel. Replaying the same transaction is a no-op,
|
||||
* which makes the mailbox side of an entry safe across a server crash.
|
||||
*/
|
||||
public boolean insertTransaction(UUID transactionId, List<ItemStack> deliveries) {
|
||||
if (transactionId == null || deliveries == null) return false;
|
||||
if (processedTransactions.contains(transactionId)) return true;
|
||||
NonNullList<ItemStack> simulated = copyItems(items);
|
||||
for (ItemStack delivery : deliveries) {
|
||||
if (delivery == null || delivery.isEmpty()) continue;
|
||||
if (!insertInto(simulated, delivery.copy())) return false;
|
||||
}
|
||||
items = simulated;
|
||||
processedTransactions.add(transactionId);
|
||||
while (processedTransactions.size() > MAX_PROCESSED_TRANSACTIONS) {
|
||||
processedTransactions.remove(processedTransactions.iterator().next());
|
||||
}
|
||||
setChanged();
|
||||
return true;
|
||||
}
|
||||
|
||||
public boolean hasProcessedTransaction(UUID transactionId) {
|
||||
return transactionId != null && processedTransactions.contains(transactionId);
|
||||
}
|
||||
|
||||
private static NonNullList<ItemStack> copyItems(List<ItemStack> source) {
|
||||
NonNullList<ItemStack> copy = NonNullList.withSize(SIZE, ItemStack.EMPTY);
|
||||
for (int slot = 0; slot < Math.min(SIZE, source.size()); slot++) copy.set(slot, source.get(slot).copy());
|
||||
return copy;
|
||||
}
|
||||
|
||||
private static boolean insertInto(NonNullList<ItemStack> target, ItemStack offered) {
|
||||
for (int slot = 0; slot < target.size() && !offered.isEmpty(); slot++) {
|
||||
ItemStack existing = target.get(slot);
|
||||
if (existing.isEmpty() || !ItemStack.isSameItemSameComponents(existing, offered)) continue;
|
||||
int moved = Math.min(offered.getCount(), existing.getMaxStackSize() - existing.getCount());
|
||||
if (moved > 0) { existing.grow(moved); offered.shrink(moved); }
|
||||
}
|
||||
for (int slot = 0; slot < target.size() && !offered.isEmpty(); slot++) {
|
||||
if (!target.get(slot).isEmpty()) continue;
|
||||
int moved = Math.min(offered.getCount(), offered.getMaxStackSize());
|
||||
target.set(slot, offered.copyWithCount(moved));
|
||||
offered.shrink(moved);
|
||||
}
|
||||
return offered.isEmpty();
|
||||
}
|
||||
|
||||
@Override protected void saveAdditional(ValueOutput output) {
|
||||
super.saveAdditional(output);
|
||||
ContainerHelper.saveAllItems(output, items);
|
||||
if (owner != null) output.store("Owner", UUIDUtil.CODEC, owner);
|
||||
output.store("BackroomTransactions", UUIDUtil.CODEC.listOf(0, MAX_PROCESSED_TRANSACTIONS),
|
||||
new ArrayList<>(processedTransactions));
|
||||
}
|
||||
@Override protected void loadAdditional(ValueInput input) {
|
||||
super.loadAdditional(input);
|
||||
items = NonNullList.withSize(SIZE, ItemStack.EMPTY);
|
||||
ContainerHelper.loadAllItems(input, items);
|
||||
owner = input.read("Owner", UUIDUtil.CODEC).orElse(null);
|
||||
processedTransactions.clear();
|
||||
input.read("BackroomTransactions", UUIDUtil.CODEC.listOf(0, MAX_PROCESSED_TRANSACTIONS)).orElse(List.of()).stream()
|
||||
.limit(MAX_PROCESSED_TRANSACTIONS).forEach(processedTransactions::add);
|
||||
}
|
||||
@Override public int getContainerSize() { return SIZE; }
|
||||
@Override protected NonNullList<ItemStack> getItems() { return items; }
|
||||
|
||||
+3
-1
@@ -1,5 +1,6 @@
|
||||
package fr.koka99cab.sanctuary26.sanctuary.shop;
|
||||
|
||||
import fr.koka99cab.sanctuary26.sanctuary.api.entity.SanctuaryEntityPolicy;
|
||||
import fr.koka99cab.sanctuary26.sanctuary.api.inventory.SanctuaryPlayerInventoryView;
|
||||
import fr.koka99cab.sanctuary26.sanctuary.registry.SanctuaryRegistries;
|
||||
import java.time.Duration;
|
||||
@@ -199,7 +200,8 @@ public final class QuestBoardService {
|
||||
}
|
||||
|
||||
private static void afterDeath(LivingEntity entity, DamageSource source) {
|
||||
if (!(entity.level() instanceof ServerLevel level)) return;
|
||||
if (SanctuaryEntityPolicy.isTemporaryCompanion(entity)
|
||||
|| !(entity.level() instanceof ServerLevel level)) return;
|
||||
ServerPlayer player = killer(entity, source);
|
||||
if (player == null || player.isSpectator()) return;
|
||||
Identifier killed = BuiltInRegistries.ENTITY_TYPE.getKey(entity.getType());
|
||||
|
||||
+3
-1
@@ -1,5 +1,6 @@
|
||||
package fr.koka99cab.sanctuary26.sanctuary.shop;
|
||||
|
||||
import fr.koka99cab.sanctuary26.sanctuary.api.entity.SanctuaryEntityPolicy;
|
||||
import fr.koka99cab.sanctuary26.sanctuary.api.inventory.SanctuaryPlayerInventoryView;
|
||||
import fr.koka99cab.sanctuary26.sanctuary.network.QuestRequestAcceptPayload;
|
||||
import fr.koka99cab.sanctuary26.sanctuary.network.QuestRequestConfirmPayload;
|
||||
@@ -323,7 +324,8 @@ public final class QuestRequestService {
|
||||
}
|
||||
|
||||
private static void afterDeath(LivingEntity entity, DamageSource source) {
|
||||
if (!(entity.level() instanceof ServerLevel level)) return;
|
||||
if (SanctuaryEntityPolicy.isTemporaryCompanion(entity)
|
||||
|| !(entity.level() instanceof ServerLevel level)) return;
|
||||
ServerPlayer killer = killer(entity, source);
|
||||
if (killer == null || killer.isSpectator()) return;
|
||||
Identifier killedType = BuiltInRegistries.ENTITY_TYPE.getKey(entity.getType());
|
||||
|
||||
@@ -8,6 +8,7 @@ import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import java.util.function.Predicate;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
@@ -107,6 +108,11 @@ public final class ShopData extends SavedData {
|
||||
}
|
||||
|
||||
public Optional<DeliveryDestination> loadedDeliveryBox(MinecraftServer server, UUID owner) {
|
||||
return loadedDeliveryBox(server, owner, level -> true);
|
||||
}
|
||||
|
||||
public Optional<DeliveryDestination> loadedDeliveryBox(MinecraftServer server, UUID owner,
|
||||
Predicate<ServerLevel> allowedLevel) {
|
||||
String ownerId = owner.toString();
|
||||
boolean changed = false;
|
||||
Iterator<DeliveryAddress> iterator = deliveryBoxes.iterator();
|
||||
@@ -117,7 +123,7 @@ public final class ShopData extends SavedData {
|
||||
for (ServerLevel candidate : server.getAllLevels()) {
|
||||
if (dimension(candidate).equals(address.dimension)) { level = candidate; break; }
|
||||
}
|
||||
if (level == null) continue;
|
||||
if (level == null || allowedLevel == null || !allowedLevel.test(level)) continue;
|
||||
BlockPos pos = BlockPos.of(address.pos);
|
||||
if (!level.isLoaded(pos)) continue;
|
||||
if (!(level.getBlockEntity(pos) instanceof DeliveryBoxBlockEntity box) || !owner.equals(box.owner())) {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package fr.koka99cab.sanctuary26.sanctuary.shop;
|
||||
|
||||
import fr.koka99cab.sanctuary26.sanctuary.SanctuaryMod;
|
||||
import fr.koka99cab.sanctuary26.sanctuary.api.item.SanctuaryItemProvenance;
|
||||
import fr.koka99cab.sanctuary26.sanctuary.network.ShopActionPayload;
|
||||
import fr.koka99cab.sanctuary26.sanctuary.network.ShopSnapshotPayload;
|
||||
import fr.koka99cab.sanctuary26.sanctuary.progression.SanctuaryProgressionService;
|
||||
@@ -29,6 +30,7 @@ public final class ShopService {
|
||||
public static final boolean OPEN = true;
|
||||
private static final Identifier RUBY_ID = Identifier.fromNamespaceAndPath("anotherworld", "ruby");
|
||||
private static final Identifier SAPPHIRE_ID = Identifier.fromNamespaceAndPath("anotherworld", "sapphire");
|
||||
private static final String MAGIC_CARPET_ID = "iliketomoveit:magic_carpet";
|
||||
private static final int DELIVERY_RADIUS = 256;
|
||||
static final int MINIMUM_DELIVERY_TICKS = 20 * 20;
|
||||
static final int MAXIMUM_DELIVERY_TICKS = 5 * 60 * 20;
|
||||
@@ -118,6 +120,11 @@ public final class ShopService {
|
||||
delaySeconds / 60, delaySeconds % 60));
|
||||
SanctuaryMod.LOGGER.info("[Shop] Scheduled {} x{} for {} in {} ticks.", itemId, parcel.getCount(),
|
||||
player.getGameProfile().name(), delayTicks);
|
||||
if (MAGIC_CARPET_ID.equals(itemId)) {
|
||||
SanctuaryMod.LOGGER.info(
|
||||
"[Shop] Logged Magic Carpet replacement for {} (origin=shop_purchase, reward_recreated=false).",
|
||||
player.getGameProfile().name());
|
||||
}
|
||||
return "purchase_scheduled";
|
||||
}
|
||||
|
||||
@@ -141,6 +148,9 @@ public final class ShopService {
|
||||
Optional<ShopData.DeliveryDestination> destination = data.loadedDeliveryBox(server, pending.playerId());
|
||||
if (destination.isEmpty()) continue;
|
||||
ItemStack parcel = new ItemStack(item, pending.count());
|
||||
if (MAGIC_CARPET_ID.equals(pending.item())) {
|
||||
SanctuaryItemProvenance.stamp(parcel, SanctuaryItemProvenance.SHOP_PURCHASE);
|
||||
}
|
||||
DeliveryBoxBlockEntity box = destination.get().box();
|
||||
if (!box.insertDelivery(parcel)) continue;
|
||||
data.completeDelivery(pending);
|
||||
|
||||
+3
-1
@@ -26,6 +26,7 @@ public final class SanctuaryDimensions {
|
||||
public static final ResourceKey<Level> PAINTED_CANYONS = dimension("painted_canyons");
|
||||
public static final ResourceKey<Level> BREAKER_SEA = dimension("breaker_sea");
|
||||
public static final ResourceKey<Level> LAPUTA = dimension("laputa");
|
||||
public static final ResourceKey<Level> BACKROOM = dimension("backroom");
|
||||
|
||||
public static final ResourceKey<LevelStem> MAIN_STEM = stem("main");
|
||||
public static final ResourceKey<LevelStem> OVERWORLD_STEM = stem("overworld");
|
||||
@@ -37,11 +38,12 @@ public final class SanctuaryDimensions {
|
||||
public static final ResourceKey<LevelStem> PAINTED_CANYONS_STEM = stem("painted_canyons");
|
||||
public static final ResourceKey<LevelStem> BREAKER_SEA_STEM = stem("breaker_sea");
|
||||
public static final ResourceKey<LevelStem> LAPUTA_STEM = stem("laputa");
|
||||
public static final ResourceKey<LevelStem> BACKROOM_STEM = stem("backroom");
|
||||
|
||||
public static final Set<Identifier> RESERVED_IDS = Set.of(
|
||||
MAIN.identifier(), OVERWORLD.identifier(), CAVERNS.identifier(), ALPHA.identifier(),
|
||||
SHEEP_LAND.identifier(), SKELETON_WASTE.identifier(), LOST_CITY.identifier(),
|
||||
PAINTED_CANYONS.identifier(), BREAKER_SEA.identifier(), LAPUTA.identifier()
|
||||
PAINTED_CANYONS.identifier(), BREAKER_SEA.identifier(), LAPUTA.identifier(), BACKROOM.identifier()
|
||||
);
|
||||
|
||||
private SanctuaryDimensions() {
|
||||
|
||||
+5
@@ -5,6 +5,7 @@ import fr.koka99cab.sanctuary26.sanctuary.worldgen.feature.MatterDepthMaterializ
|
||||
import fr.koka99cab.sanctuary26.sanctuary.worldgen.feature.AlphaIndevChunkFeature;
|
||||
import fr.koka99cab.sanctuary26.sanctuary.worldgen.feature.SanctuaryWaterLakeFeature;
|
||||
import fr.koka99cab.sanctuary26.sanctuary.worldgen.feature.SulfurCaveGeologyFeature;
|
||||
import fr.koka99cab.sanctuary26.sanctuary.worldgen.feature.BackroomArchitectureFeature;
|
||||
import net.fabricmc.fabric.api.biome.v1.BiomeModifications;
|
||||
import net.fabricmc.fabric.api.biome.v1.BiomeSelectors;
|
||||
import net.minecraft.core.Registry;
|
||||
@@ -22,6 +23,9 @@ import net.minecraft.world.level.levelgen.placement.PlacedFeature;
|
||||
|
||||
/** Registers the two generator-specific features migrated from Sanctuary 26.1.2. */
|
||||
public final class SanctuaryFeatures {
|
||||
public static final Identifier BACKROOM_ARCHITECTURE_ID = SanctuaryMod.id("backroom_architecture");
|
||||
public static final Feature<NoneFeatureConfiguration> BACKROOM_ARCHITECTURE =
|
||||
new BackroomArchitectureFeature(NoneFeatureConfiguration.CODEC);
|
||||
public static final Identifier ALPHA_INDEV_CHUNK_ID = SanctuaryMod.id("alpha_indev_chunk");
|
||||
public static final Feature<NoneFeatureConfiguration> ALPHA_INDEV_CHUNK =
|
||||
new AlphaIndevChunkFeature(NoneFeatureConfiguration.CODEC);
|
||||
@@ -56,6 +60,7 @@ public final class SanctuaryFeatures {
|
||||
}
|
||||
|
||||
public static void register() {
|
||||
Registry.register(BuiltInRegistries.FEATURE, BACKROOM_ARCHITECTURE_ID, BACKROOM_ARCHITECTURE);
|
||||
Registry.register(BuiltInRegistries.FEATURE, ALPHA_INDEV_CHUNK_ID, ALPHA_INDEV_CHUNK);
|
||||
Registry.register(BuiltInRegistries.FEATURE,
|
||||
MATTER_DEPTH_MATERIALIZATION_ID, MATTER_DEPTH_MATERIALIZATION);
|
||||
|
||||
+243
@@ -0,0 +1,243 @@
|
||||
package fr.koka99cab.sanctuary26.sanctuary.worldgen.feature;
|
||||
|
||||
import com.mojang.serialization.Codec;
|
||||
import fr.koka99cab.sanctuary26.sanctuary.SanctuaryMod;
|
||||
import fr.koka99cab.sanctuary26.sanctuary.api.backroom.BackroomMaterialApi;
|
||||
import java.util.Random;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Direction;
|
||||
import net.minecraft.world.item.DyeColor;
|
||||
import net.minecraft.world.level.WorldGenLevel;
|
||||
import net.minecraft.world.level.block.BedBlock;
|
||||
import net.minecraft.world.level.block.Block;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.block.LeavesBlock;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.block.state.properties.BedPart;
|
||||
import net.minecraft.world.level.levelgen.feature.Feature;
|
||||
import net.minecraft.world.level.levelgen.feature.FeaturePlaceContext;
|
||||
import net.minecraft.world.level.levelgen.feature.configurations.NoneFeatureConfiguration;
|
||||
|
||||
/**
|
||||
* Deterministic, chunk-local architecture for the persistent Backroom.
|
||||
* Macro cells select authored palettes; randomness only varies plans inside a coherent family.
|
||||
*/
|
||||
public final class BackroomArchitectureFeature extends Feature<NoneFeatureConfiguration> {
|
||||
public static final int PLAYABLE_RADIUS = 2_000;
|
||||
private static final int CHUNK_SIZE = 16;
|
||||
private static final int MACRO_CHUNKS = 8;
|
||||
private static final BlockState CONCRETE_GRAY = Blocks.CONCRETE.pick(DyeColor.GRAY).defaultBlockState();
|
||||
private static final BlockState CONCRETE_LIGHT = Blocks.CONCRETE.pick(DyeColor.LIGHT_GRAY).defaultBlockState();
|
||||
private static final BlockState CONCRETE_WHITE = Blocks.CONCRETE.pick(DyeColor.WHITE).defaultBlockState();
|
||||
private static final BlockState GLASS_CYAN = Blocks.STAINED_GLASS.pick(DyeColor.CYAN).defaultBlockState();
|
||||
private static final BlockState CLAY_BROWN = Blocks.DYED_TERRACOTTA.pick(DyeColor.BROWN).defaultBlockState();
|
||||
private static final BlockState CLAY_ORANGE = Blocks.DYED_TERRACOTTA.pick(DyeColor.ORANGE).defaultBlockState();
|
||||
private static final BlockState LEAVES = Blocks.OAK_LEAVES.defaultBlockState().setValue(LeavesBlock.PERSISTENT, true);
|
||||
|
||||
public BackroomArchitectureFeature(Codec<NoneFeatureConfiguration> codec) {
|
||||
super(codec);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean place(FeaturePlaceContext<NoneFeatureConfiguration> context) {
|
||||
WorldGenLevel level = context.level();
|
||||
int chunkX = Math.floorDiv(context.origin().getX(), CHUNK_SIZE);
|
||||
int chunkZ = Math.floorDiv(context.origin().getZ(), CHUNK_SIZE);
|
||||
int startX = chunkX * CHUNK_SIZE;
|
||||
int startZ = chunkZ * CHUNK_SIZE;
|
||||
if (!withinPlayableArea(startX + 8, startZ + 8)) return false;
|
||||
|
||||
long seed = mix(level.getSeed(), chunkX, chunkZ);
|
||||
Random random = new Random(seed);
|
||||
Architecture architecture = architecture(level.getSeed(), chunkX, chunkZ);
|
||||
switch (architecture) {
|
||||
case CITY -> city(level, startX, startZ, random);
|
||||
case OFFICES -> offices(level, startX, startZ, random);
|
||||
case PLAINS -> plains(level, startX, startZ, random);
|
||||
case AGGREGATES -> aggregates(level, startX, startZ, random);
|
||||
case ROTATED -> rotated(level, startX, startZ, random);
|
||||
}
|
||||
placeLootLandmarks(level, startX, startZ, random, architecture);
|
||||
placeRareAnomaly(level, startX, startZ, random);
|
||||
return true;
|
||||
}
|
||||
|
||||
public static boolean withinPlayableArea(int x, int z) {
|
||||
return (long) x * x + (long) z * z <= (long) PLAYABLE_RADIUS * PLAYABLE_RADIUS;
|
||||
}
|
||||
|
||||
public static Architecture architecture(long worldSeed, int chunkX, int chunkZ) {
|
||||
int macroX = Math.floorDiv(chunkX, MACRO_CHUNKS);
|
||||
int macroZ = Math.floorDiv(chunkZ, MACRO_CHUNKS);
|
||||
int index = Math.floorMod((int) mix(worldSeed ^ 0x4f1bbcdcL, macroX, macroZ), Architecture.values().length);
|
||||
return Architecture.values()[index];
|
||||
}
|
||||
|
||||
/** Stable fingerprint used to prevent palette selection drift. */
|
||||
public static long fingerprint(long worldSeed, int chunkX, int chunkZ) {
|
||||
return mix(worldSeed ^ architecture(worldSeed, chunkX, chunkZ).ordinal(), chunkX, chunkZ);
|
||||
}
|
||||
|
||||
private static void city(WorldGenLevel level, int x0, int z0, Random random) {
|
||||
int y = 64;
|
||||
for (int x = x0; x < x0 + 16; x++) for (int z = z0; z < z0 + 16; z++) {
|
||||
boolean road = Math.floorMod(x, 32) < 5 || Math.floorMod(z, 32) < 5;
|
||||
set(level, x, y, z, road ? CONCRETE_GRAY : CONCRETE_LIGHT);
|
||||
if (road && (Math.floorMod(x, 8) == 0 || Math.floorMod(z, 8) == 0)) set(level, x, y, z, Blocks.SMOOTH_STONE.defaultBlockState());
|
||||
}
|
||||
if (Math.floorMod(x0, 32) < 5 || Math.floorMod(z0, 32) < 5) return;
|
||||
int minX = x0 + 2, maxX = x0 + 13, minZ = z0 + 2, maxZ = z0 + 13;
|
||||
int height = 18 + random.nextInt(42);
|
||||
for (int floor = 0; floor <= height; floor++) {
|
||||
int py = y + 1 + floor;
|
||||
boolean slab = floor == 0 || floor == height || floor % 5 == 0;
|
||||
for (int x = minX; x <= maxX; x++) for (int z = minZ; z <= maxZ; z++) {
|
||||
boolean edge = x == minX || x == maxX || z == minZ || z == maxZ;
|
||||
if (slab) set(level, x, py, z, CONCRETE_LIGHT);
|
||||
else if (edge) set(level, x, py, z, floor % 5 == 1 ? CONCRETE_WHITE : GLASS_CYAN);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void offices(WorldGenLevel level, int x0, int z0, Random random) {
|
||||
for (int baseY : new int[]{72, 84, 96}) {
|
||||
for (int x = x0; x < x0 + 16; x++) for (int z = z0; z < z0 + 16; z++) {
|
||||
set(level, x, baseY, z, Blocks.OAK_PLANKS.defaultBlockState());
|
||||
set(level, x, baseY + 9, z, CLAY_ORANGE);
|
||||
boolean wall = Math.floorMod(x, 12) == 0 || Math.floorMod(z, 12) == 0;
|
||||
if (wall) for (int y = baseY + 1; y < baseY + 9; y++) {
|
||||
BlockState material = y == baseY + 4 ? Blocks.STAINED_GLASS.pick(DyeColor.BROWN).defaultBlockState() : CLAY_BROWN;
|
||||
set(level, x, y, z, material);
|
||||
}
|
||||
}
|
||||
if (random.nextInt(3) == 0) {
|
||||
int deskX = x0 + 5 + random.nextInt(6), deskZ = z0 + 5 + random.nextInt(6);
|
||||
set(level, deskX, baseY + 1, deskZ, Blocks.OAK_SLAB.defaultBlockState());
|
||||
set(level, deskX + 1, baseY + 1, deskZ, Blocks.BOOKSHELF.defaultBlockState());
|
||||
set(level, deskX, baseY + 1, deskZ + 1, Blocks.CARPET.pick(DyeColor.BROWN).defaultBlockState());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void plains(WorldGenLevel level, int x0, int z0, Random random) {
|
||||
int baseY = 68 + random.nextInt(3);
|
||||
for (int x = x0; x < x0 + 16; x++) for (int z = z0; z < z0 + 16; z++) {
|
||||
set(level, x, baseY - 2, z, Blocks.STONE.defaultBlockState());
|
||||
set(level, x, baseY - 1, z, Blocks.DIRT.defaultBlockState());
|
||||
set(level, x, baseY, z, Blocks.GRASS_BLOCK.defaultBlockState());
|
||||
}
|
||||
if (random.nextInt(3) == 0) tree(level, x0 + 4 + random.nextInt(8), baseY + 1, z0 + 4 + random.nextInt(8));
|
||||
if (random.nextInt(7) == 0) {
|
||||
for (int x = x0 + 3; x <= x0 + 11; x++) for (int z = z0 + 3; z <= z0 + 11; z++) {
|
||||
boolean edge = x == x0 + 3 || x == x0 + 11 || z == z0 + 3 || z == z0 + 11;
|
||||
if (edge) for (int y = baseY + 1; y <= baseY + 4; y++) set(level, x, y, z, Blocks.MUD_BRICKS.defaultBlockState());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void aggregates(WorldGenLevel level, int x0, int z0, Random random) {
|
||||
int baseY = 74 + random.nextInt(10);
|
||||
platform(level, x0, z0, baseY, CONCRETE_LIGHT);
|
||||
boxRoom(level, x0 + 2, baseY + 1, z0 + 2, x0 + 12, baseY + 7, z0 + 12,
|
||||
Blocks.SMOOTH_STONE.defaultBlockState(), Blocks.OAK_PLANKS.defaultBlockState());
|
||||
if (random.nextBoolean()) {
|
||||
int upper = baseY + 20 + random.nextInt(12);
|
||||
platform(level, x0 + 4, z0 + 4, upper, CLAY_ORANGE);
|
||||
for (int y = baseY + 1; y <= upper; y++) set(level, x0 + 8, y, z0 + 8, Blocks.IRON_CHAIN.defaultBlockState());
|
||||
}
|
||||
}
|
||||
|
||||
private static void rotated(WorldGenLevel level, int x0, int z0, Random random) {
|
||||
int y0 = 70 + random.nextInt(12);
|
||||
for (int x = x0; x < x0 + 16; x++) for (int z = z0; z < z0 + 16; z++) set(level, x, y0, z, CONCRETE_LIGHT);
|
||||
boolean alongX = random.nextBoolean();
|
||||
for (int depth = 2; depth <= 13; depth++) for (int y = y0 + 1; y <= y0 + 13; y++) {
|
||||
int x = alongX ? x0 + depth : x0 + 3;
|
||||
int z = alongX ? z0 + 3 : z0 + depth;
|
||||
BlockState state = (y == y0 + 1 || y == y0 + 13 || depth == 2 || depth == 13)
|
||||
? CLAY_BROWN : Blocks.AIR.defaultBlockState();
|
||||
set(level, x, y, z, state);
|
||||
}
|
||||
// Furniture fixed to the vertical "floor" sells the rotated-room illusion without changing gravity.
|
||||
for (int offset = 4; offset <= 10; offset += 3) {
|
||||
int x = alongX ? x0 + offset : x0 + 4;
|
||||
int z = alongX ? z0 + 4 : z0 + offset;
|
||||
set(level, x, y0 + 4, z, Blocks.BOOKSHELF.defaultBlockState());
|
||||
}
|
||||
}
|
||||
|
||||
private static void placeLootLandmarks(WorldGenLevel level, int x0, int z0, Random random, Architecture architecture) {
|
||||
int y = switch (architecture) {
|
||||
case CITY -> 65;
|
||||
case OFFICES -> 73;
|
||||
case PLAINS -> 72;
|
||||
case AGGREGATES -> 83;
|
||||
case ROTATED -> 84;
|
||||
};
|
||||
int x = x0 + 7, z = z0 + 7;
|
||||
while (!level.getBlockState(new BlockPos(x, y, z)).isAir() && y < 180) y++;
|
||||
while (!level.getBlockState(new BlockPos(x, y - 1, z)).isCollisionShapeFullBlock(level, new BlockPos(x, y - 1, z)) && y > 2) y--;
|
||||
if (random.nextInt(5) == 0) set(level, x, y, z, Blocks.CHEST.defaultBlockState());
|
||||
if (random.nextInt(17) == 0) placeBed(level, x0 + 9, y, z0 + 8);
|
||||
}
|
||||
|
||||
private static void placeRareAnomaly(WorldGenLevel level, int x0, int z0, Random random) {
|
||||
if (random.nextInt(96) == 0) {
|
||||
Block anomaly = BackroomMaterialApi.anomalyMaterial();
|
||||
for (int i = 0; i < 5; i++) set(level, x0 + 5 + random.nextInt(6), 66 + random.nextInt(20),
|
||||
z0 + 5 + random.nextInt(6), anomaly.defaultBlockState());
|
||||
}
|
||||
if (random.nextInt(4_096) == 0
|
||||
&& withinPlayableArea(x0 + 7, z0 + 7) && withinPlayableArea(x0 + 9, z0 + 9)) {
|
||||
for (int dx = -1; dx <= 1; dx++) for (int dy = -1; dy <= 1; dy++) for (int dz = -1; dz <= 1; dz++) {
|
||||
set(level, x0 + 8 + dx, 40 + dy, z0 + 8 + dz, Blocks.SMOOTH_STONE.defaultBlockState());
|
||||
}
|
||||
set(level, x0 + 8, 40, z0 + 8, Blocks.DIAMOND_BLOCK.defaultBlockState());
|
||||
}
|
||||
}
|
||||
|
||||
private static void placeBed(WorldGenLevel level, int x, int y, int z) {
|
||||
if (!withinPlayableArea(x, z) || !withinPlayableArea(x + 1, z)) return;
|
||||
Block bed = Blocks.BED.pick(DyeColor.WHITE);
|
||||
BlockState foot = bed.defaultBlockState().setValue(BedBlock.FACING, Direction.EAST).setValue(BedBlock.PART, BedPart.FOOT);
|
||||
BlockState head = foot.setValue(BedBlock.PART, BedPart.HEAD);
|
||||
set(level, x, y, z, foot);
|
||||
set(level, x + 1, y, z, head);
|
||||
}
|
||||
|
||||
private static void platform(WorldGenLevel level, int x0, int z0, int y, BlockState state) {
|
||||
for (int x = x0; x < x0 + 16; x++) for (int z = z0; z < z0 + 16; z++) set(level, x, y, z, state);
|
||||
}
|
||||
|
||||
private static void boxRoom(WorldGenLevel level, int minX, int minY, int minZ,
|
||||
int maxX, int maxY, int maxZ, BlockState shell, BlockState floor) {
|
||||
for (int x = minX; x <= maxX; x++) for (int y = minY; y <= maxY; y++) for (int z = minZ; z <= maxZ; z++) {
|
||||
boolean boundary = x == minX || x == maxX || y == minY || y == maxY || z == minZ || z == maxZ;
|
||||
if (boundary) set(level, x, y, z, y == minY ? floor : shell);
|
||||
}
|
||||
}
|
||||
|
||||
private static void tree(WorldGenLevel level, int x, int y, int z) {
|
||||
for (int dy = 0; dy < 5; dy++) set(level, x, y + dy, z, Blocks.OAK_LOG.defaultBlockState());
|
||||
for (int dx = -2; dx <= 2; dx++) for (int dz = -2; dz <= 2; dz++) for (int dy = 3; dy <= 6; dy++) {
|
||||
if (Math.abs(dx) + Math.abs(dz) + Math.abs(dy - 5) <= 5) set(level, x + dx, y + dy, z + dz, LEAVES);
|
||||
}
|
||||
}
|
||||
|
||||
private static void set(WorldGenLevel level, int x, int y, int z, BlockState state) {
|
||||
if (withinPlayableArea(x, z) && y >= level.getMinY() && y < level.getMaxY()) {
|
||||
level.setBlock(new BlockPos(x, y, z), state, 2);
|
||||
}
|
||||
}
|
||||
|
||||
private static long mix(long seed, int x, int z) {
|
||||
long value = seed ^ (long) x * 0x9E3779B97F4A7C15L ^ (long) z * 0xC2B2AE3D27D4EB4FL;
|
||||
value ^= value >>> 30;
|
||||
value *= 0xBF58476D1CE4E5B9L;
|
||||
value ^= value >>> 27;
|
||||
value *= 0x94D049BB133111EBL;
|
||||
return value ^ value >>> 31;
|
||||
}
|
||||
|
||||
public enum Architecture { CITY, OFFICES, PLAINS, AGGREGATES, ROTATED }
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user