diff --git a/.gitignore b/.gitignore index bac3312..f407000 100644 --- a/.gitignore +++ b/.gitignore @@ -47,3 +47,5 @@ run/schematics run/server run/downloads/log.json run/command_history.txt +run/world/ +run/world*/ diff --git a/AUDIT_NEOFORGE.md b/AUDIT_NEOFORGE.md index 9fa32ba..103bdff 100644 --- a/AUDIT_NEOFORGE.md +++ b/AUDIT_NEOFORGE.md @@ -29,7 +29,7 @@ En revanche, les points ci-dessous sont de **vraies erreurs de logique** (invers | # | Fichier:ligne | Problème | Priorité | |---|---|---|---| -| B1 | `content/multiblock/bluePrintItem/ReactorBluePrintItemScreen.java:56-65,71-73` | Les comptes de barres uranium/graphite sont inversés et un `+3` magique est ajouté lors de l'appel à `sendValueUpdate`, puis `sendValueUpdate` renvoie `countGraphiteRod` deux fois au lieu de `countUraniumRod` pour le dernier paramètre : le comptage des barres envoyé au serveur est corrompu. | 🔴 | +| B1 | `content/multiblock/bluePrintItem/ReactorBluePrintItemScreen.java:56-65,71-73` | Les comptes de barres uranium/graphite sont inversés et un `+3` magique est ajouté lors de l'appel à `sendValueUpdate`, puis `sendValueUpdate` renvoie `countCooledRod` deux fois au lieu de `countFuelRod` pour le dernier paramètre : le comptage des barres envoyé au serveur est corrompu. | 🔴 | | B2 | `content/radiation/capability/RadiationCapability.java:220-223` | Dans `applyEffects`, la branche `radiationLevel3` et la branche `else` utilisent toutes deux `amplifierLevel2.get()` (copier-coller) : il manque un `amplifierLevel3`, l'amplificateur d'effet plafonne au niveau 2 quel que soit le niveau réel de radiation. | 🟠 | | B3 | `content/multiblock/controller/manager/ReactorInputFluidManager.java:129-130,148-149` | `int tank = h.getTanks(); h.getFluidInTank(tank);` utilise le *nombre* de tanks comme *index* (valides : `0..getTanks()-1`) → accès systématiquement hors bornes. | 🟠 | | B4 | `lib/multiblock/manager/MultiBlockCache.java:22-24` | `isCached()` a une sémantique inversée : `return cachedResult == null` renvoie `true` quand **rien n'est en cache**. (Classe par ailleurs non utilisée, cf. §1.) | 🟡 | diff --git a/PARITE_FORGE_NEOFORGE.md b/PARITE_FORGE_NEOFORGE.md new file mode 100644 index 0000000..bdc63fa --- /dev/null +++ b/PARITE_FORGE_NEOFORGE.md @@ -0,0 +1,340 @@ +# Parité Forge → NeoForge — ce qu'il reste à porter + +Ce document liste tout ce qui sépare encore `CreateNuclearNeoForge` de `CreateNuclearForge` +en termes de **features**, et comment porter chaque bloc. + +**Ce fichier ne contient que du travail restant.** Ce qui est fait en sort — l'historique +est dans les commits. + +État au **7 août 2026**, branche `V2-Reacteur`, après le portage de la poudre de neige, du +worldgen piloté par la config et du datagen de recettes. +Pour le domaine réacteur, déjà porté et validé, voir [`PORTAGE_REACTEUR.md`](PORTAGE_REACTEUR.md). + +| | Forge | NeoForge | +|---|---|---| +| Fichiers `.java` | 296 | 301 | +| Ressources | 1 132 | 1 118 | + +--- + +## 0. Comment lire ce document + +Un `diff` brut des arborescences donne **15 fichiers Forge sans équivalent NeoForge**. +Ce chiffre est trompeur : + +- **4 sont remplacés par un équivalent 1.21 meilleur** → §1.1, à ne surtout pas « re-porter » ; +- **2 existent sous un autre nom ou dans un autre package** → §1.2, idem ; +- **9 manquent réellement** → §2, c'est le vrai travail. + +S'y ajoutent des écarts *à l'intérieur* de fichiers présents des deux côtés (§3), invisibles +d'un diff d'arborescence — et c'est là qu'ont été trouvés la moitié des bugs du réacteur. + +> **Le piège récurrent, vérifié deux fois.** Un fichier absent n'est pas le seul symptôme d'une +> feature manquante. À deux reprises, la classe qui « manquait » n'était qu'un **point +> d'enregistrement** : le reste de la feature existait déjà côté NeoForge, complet et correct, +> mais **n'était référencé nulle part** — donc entièrement mort en jeu. +> **Avant de porter quoi que ce soit, chercher les classes NeoForge jamais référencées.** + +--- + +## 1. À ne pas porter — l'équivalent existe déjà + +**Ne pas recréer ces fichiers.** Le `diff` d'arborescence du §7 les listera toujours. + +### 1.1 Volontairement non portés — l'équivalent 1.21 est meilleur + +| Fichier Forge | Remplacé côté NeoForge par | +|---|---| +| `content/radiation/capability/IRadiationCapability` | **Data attachments NeoForge** — `RadiationCapability` expose `AttachmentType RADIATION`, lu via `entity.getData(RADIATION)` | +| `content/radiation/capability/RadiationProvider` | idem — la plomberie `ICapabilityProvider` de Forge n'a pas d'équivalent, ni de raison d'exister | +| `foundation/advancement/CriterionTriggerBase` | ~~**`SimpleCriterionTrigger` vanilla** — `SimpleCreateNuclearTrigger` en hérite directement~~ Suivre Create donc modification du code par rapport au maj de Create | +| `foundation/utility/SimplexNoise` | **`net.minecraft.world.level.levelgen.synth.SimplexNoise`** — `Maths` utilise déjà celui de vanilla | + +### 1.2 Présents sous un autre nom ou dans un autre package + +| Fichier Forge | Équivalent NeoForge | Pourquoi | +|---|---|---| +| `foundation/data/recipe/CNProcessingRecipeGen` | `foundation/data/recipe/CNRecipeProvider` | En 1.21 la classe n'hérite plus de `ProcessingRecipeGen` mais de `RecipeProvider`, et agrège les générateurs. Même rôle, nom aligné sur son parent | +| `api/data/recipe/SnowPowderRecipeGen` | `foundation/data/recipe/SnowPowderRecipeGen` | Suit `EnrichedRecipeGen`, qui vit dans `foundation` côté NeoForge parce qu'il hérite de `StandardProcessingRecipeGen` (introduit en 1.21) | + +> ⚠️ `api/data/recipe/EnrichedRecipeGen` existe encore côté NeoForge mais **n'est référencé +> nulle part** — c'est un vestige de la première tentative de portage, avant +> `StandardProcessingRecipeGen`. Le vivant est celui de `foundation`. Ne pas le prendre pour +> modèle, et le supprimer à l'occasion. + +--- + +## 2. Les 9 fichiers réellement manquants + +Classés par feature, du plus au moins impactant. + +--- + +### 2.1 Vache irradiée + abstraction des animaux — 5 fichiers + +`IrradiatedCow` · `IrradiatedCowModel` · `IrradiatedCowRenderer` · +`IrradiatedAnimal` · `AnimalUtil` + +**Ce qui manque en jeu :** NeoForge a le **chat**, le **poulet** et le **loup** irradiés +(`CNEntityType` : `IRRADIATED_CAT`, `IRRADIATED_CHICKEN`, `IRRADIATED_WOLF`) — **pas la vache**. + +**Le vrai enjeu est l'abstraction.** Forge a factorisé le comportement commun dans +`IrradiatedAnimal` + `AnimalUtil` ; NeoForge ne les a pas, donc **chaque animal duplique la +logique**. C'est ce qui explique les plus gros diffs hors réacteur (`IrradiatedCat` 384 lignes, +`IrradiatedWolf` 333). Porter l'abstraction **en premier**, puis y ramener chat/poulet/loup, +puis ajouter la vache — sinon on ajoute une quatrième copie du même code. + +**Points d'accroche :** +- `CNEntityType` — `EntityEntry`, renderer, `createAttributes` +- `CNModelLayers` — `IRRADIATED_COW` + `registerLayerDefinition(..., IrradiatedCowModel::createBodyLayer)` +- ressources : modèle, texture, lang, table de butin, œuf d'apparition + +**Difficulté : moyenne à élevée.** Le refactor de l'abstraction touche trois entités existantes +qui fonctionnent aujourd'hui — à faire avec un test en jeu de chaque animal. + +--- + +### 2.2 Compat Alex's Caves — 2 fichiers + +`compat/Mods` · `compat/alexscave/AlexscaveCompat` + +**Ce qui manque en jeu :** l'explosion nucléaire n'a aucune intégration avec Alex's Caves. + +**Point d'accroche.** `NuclearExplosionEntity` côté Forge : + +le mod n'existe pas vraiment en 1.21.1 + +```java +if (Mods.ALEXS_CAVE.isLoaded()) { + this.alexscaveHandler = new AlexscaveCompat(); +} +``` + +Le champ est typé `Object` volontairement, pour que la classe se charge même sans Alex's Caves. +Côté NeoForge, `NuclearExplosionEntity` n'en a aucune trace. + +`Mods` est un petit utilitaire générique de détection de mods — utile bien au-delà de cette compat. + +**Difficulté : faible.** `ModList.get().isLoaded(id)` existe à l'identique en NeoForge. + +--- + +### 2.3 Mixins client — 2 fichiers + +`foundation/mixin/client/CameraAccessor` · `foundation/mixin/client/GameRendererMixin` + +**Ce qui manque en jeu :** un effet visuel, très probablement lié au rendu de la radiation ou +de l'explosion (accès à la caméra + hook sur le `GameRenderer`). + +NeoForge a à la place `AntiRadiationArmorTextureMixin`, qui n'existe pas côté Forge : les deux +versions ont donc **divergé sur les mixins client**. Comparer les deux `createnuclear.mixins.json` +avant de porter.: obligatoir etant donnée le fonctionnement de minecraft a changé entre la 1.20.1 et la 1.21.1 + +**Difficulté : moyenne.** Les mixins sont sensibles aux versions et cassent silencieusement. +À porter avec un test visuel explicite. + +--- + +## 3. Écarts internes aux fichiers partagés — non audités + +Ces fichiers existent des deux côtés mais divergent fortement. **Un gros diff n'est pas une +preuve de feature manquante** : l'API 1.21 en explique une grande part. Aucun n'a été audité +ligne à ligne, contrairement au domaine réacteur. + +| Fichier | Lignes divergentes | +|---|---| +| `CNBlocks` | 701 | +| `CNItems` | 457 | +| `foundation/data/recipe/CNStandardRecipeGen` | 395 | +| `foundation/advancement/CNAdvancement` | 394 | +| `content/contraptions/irradiated/cat/IrradiatedCat` | 384 | +| `compat/jei/CreateNuclearJEI` | 344 | +| `content/contraptions/irradiated/wolf/IrradiatedWolf` | 333 | +| `CNCreativeModeTabs` | 249 | +| `content/radiation/capability/RadiationCapability` | 186 | + +> **Ce n'est pas théorique.** Sur les six bugs trouvés en testant le réacteur en jeu, **deux +> étaient cachés dans `CNBlocks`** : la capacité de stress de la sortie à `10240` au lieu de +> `64000` (SU divisé par 6,25) et quatre `addLayer` manquants — dont `reinforced_glass` qui +> rendait donc opaque. Aucun des deux n'était visible dans un diff d'arborescence. +> +> **Méthode qui a marché :** ne pas lire le diff en entier, mais partir d'un symptôme observé en +> jeu et remonter. C'est plus rapide et ça ne trouve que des bugs réels. + +### 3.1 Recettes — diff des JSON générés + +Le diff des `.java` cache mal les recettes manquantes ; celui des JSON générés les montre +directement. Commande dans le §7. + +**Corrigé** (symptôme signalé : le concentré d'azote refroidi ne servait à rien) — toute la +chaîne de l'azote était morte, **cassée aux deux bouts** : + +| Étape | Recette | État avant | +|---|---|---| +| nitrate → concentré d'azote | `smelting` + `blasting` | absente de `CNStandardRecipeGen` | +| concentré → concentré refroidi | `snow_powder` | absente — c'est le §2.1, porté depuis | +| concentré refroidi + glace → azote liquide | `mixing` | absente de `CNMixingRecipeGen` | + +Seul le fluide `LIQUID_NITROGEN` était enregistré — donc obtenable uniquement en créatif. + +**Reste à traiter :** + +| Recette Forge | Absente côté NeoForge | +|---|---| +| `mixing/thorium_fluid` | oui — `CNFluids.THORIUM` est enregistré mais inobtenable, même schéma que l'azote | +| `crafting/thorium_block_from_compacting` · `crafting/thorium_ingot_from_compacting` | oui — `THORIUM_COMPACTING` manque dans `CNStandardRecipeGen` | +| `mechanical_crafting/reactor_alarm` | oui | + +> ⚠️ **Sept recettes partent dans le namespace `create`.** `CNCrushingRecipeGen` (6) et +> `CNWashingRecipeGen` (1) utilisent `create(() -> ingrédient, …)`. En 1.21 cette surcharge +> **prend `Create.ID` comme namespace par défaut**, plus celui du générateur — les JSON +> atterrissent dans `src/generated/resources/data/create/recipe/`. Ce n'est pas un manque : les +> recettes se chargent, mais sous l'id de Create, et **écrasent celles de Create** là où les ids +> coïncident (`crushing/raw_copper`, `crushing/raw_zinc`, `crushing/raw_uranium_block`, +> `splashing/crushed_raw_lead` existent dans le jar de Create). Forge n'a pas le problème : +> l'ancienne surcharge prenait le namespace du générateur. +> **Correctif : `create(CreateNuclear.MOD_ID, () -> ingrédient, …)`.** À faire avec un test JEI, +> les ids de recette changent. + +--- + +## 4. Vérifications en jeu en attente + +Ce n'est pas du portage, mais c'est du travail restant. **Aucun de ces points n'est couvert par +les gametests.** + +| À vérifier | Attendu | Pourquoi c'est en attente | +|---|---|---| +| Comportement de la radiation | seuils, décroissance, persistance à la mort identiques à Forge | `RadiationCapability` diverge de 186 lignes. Le mécanisme (data attachments) est correct, mais rien ne dit que le comportement l'est | +| Tooltip d'une barre d'uranium/graphite | cinq lignes de stats, type en vert (FUEL) ou cyan | livré sans test en jeu | +| `raw_uranium_block` en inventaire | la radiation monte de 27 par item | livré sans test en jeu | +| Tuyau ouvert Create crachant de l'uranium | irradie les entités de la zone | livré sans test en jeu ; la feature était morte avant, donc jamais observée | +| Ventilateur derrière de la neige poudreuse | le concentré d'azote devient du concentré d'azote refroidi ; particules flocon/éternuement ; entités ralenties | §2.1 porté sans test en jeu. Vérifier aussi la catégorie JEI « Cryogenic fan » | +| `enable_world_gen = false` dans la config commune | **monde neuf** sans uranium, plomb, thorium, nitrate ni strates | filtre de placement porté sans test en jeu. Ne se voit que sur un monde généré après coup | +| `enable_world_gen = true` (défaut) | les cinq features génèrent comme avant | le filtre est désormais `createnuclear:config_filter`, plus celui de Create — non-régression à confirmer | +| Chaîne de l'azote de bout en bout | nitrate → *(four)* → concentré → *(ventilateur + neige poudreuse)* → concentré refroidi → *(mixeur + glace)* → 100 mB d'azote liquide | §3.1 corrigé sans test en jeu | + +> **Divergence assumée sur la clé de config.** `CWorldGen` exposait `disableWorldGen` (défaut +> `false`) — un copier-coller littéral de la classe homonyme de Create, **jamais lu par personne**. +> Il est remplacé par `enable_world_gen` (défaut `true`), la clé de Forge. Un `createnuclear-common.toml` +> existant perdra donc son ancienne entrée : sans effet, puisqu'elle n'en avait aucun. + +--- + +## 5. Ordre de travail suggéré + +| # | Chantier | Fichiers | Difficulté | Pourquoi ce rang | +|---|---|---|---|---| +| 1 | **Compat Alex's Caves (§2.2)** | 2 | faible | Isolé, sans risque | +| 2 | **Abstraction animaux + vache (§2.1)** | 5 | moy./élevée | Refactor de 3 entités qui marchent | +| 3 | **Mixins client (§2.3)** | 2 | moyenne | Sensible, à faire avec test visuel | +| 4 | **Audit `CNBlocks` / `CNItems` (§3)** | — | continu | À faire au fil des symptômes, pas d'un bloc | + +Les tests en jeu du §4 passent avant tout ça : trois chantiers viennent d'être livrés sans être +observés une seule fois en jeu. + +--- + +## 6. Méthode de portage — ce qui a marché sur le réacteur + +À reprendre pour chaque chantier ci-dessus. + +1. **Chercher d'abord les classes NeoForge jamais référencées.** Voir l'encadré du §0 : deux + features sur deux se sont révélées présentes mais mortes, faute d'un point d'enregistrement. + Un `grep -rl src` qui ne renvoie que le fichier lui-même est un signal fort. +2. **Porter à l'identique par défaut.** Copier le fichier Forge, ne changer que ce que l'API + 1.21 impose. Sur le réacteur, ~35 fichiers sur 50 sont restés **identiques à l'octet**. +3. **Vérifier chaque fichier par un `diff`** contre son original Forge, et **justifier chaque + ligne qui diffère**. C'est ce qui a permis de garder les deux versions synchronisables. +4. **Compiler après chaque lot**, pas à la fin. +5. **Un commit par lot**, poussé avant d'enchaîner. +6. **Documenter toute divergence assumée** dans le commit, pour qu'une future synchro ne la + « corrige » pas par erreur. +7. **Tester en jeu.** Tout le portage du réacteur compilait et passait les gametests avant que + six bugs bien réels ne sortent au premier test manuel. + +### Pièges 1.20.1 → 1.21 déjà rencontrés + +| Forge 1.20.1 | NeoForge 1.21 | +|---|---| +| `net.minecraftforge.*` | `net.neoforged.neoforge.*` / `net.neoforged.api.*` | +| `ForgeRegistries.X.getKey(v)` | `BuiltInRegistries.X.getKey(v)` | +| `ForgeRegistries.X.getValue(rl)` | `BuiltInRegistries.X.getOptional(rl)` | +| `new ResourceLocation(s)` | `ResourceLocation.parse(s)` | +| `read/write(CompoundTag, boolean)` | `read/write(CompoundTag, HolderLookup.Provider, boolean)` | +| `ItemStack.of(tag)` | `ItemStack.parse(provider, tag)` → `Optional` | +| `stack.serializeNBT()` | `stack.saveOptional(provider)` | +| `stack.getOrCreateTag()` | **DataComponents** — pas d'équivalent mutable | +| `use(...)` → `InteractionResult` | `useItemOn(...)` → `ItemInteractionResult` | +| `NetworkHooks.openScreen(player, be, buf)` | `player.openMenu(be, buf)` | +| `isPathfindable(state, getter, pos, type)` | `isPathfindable(state, type)` | +| `@Mod.EventBusSubscriber(bus = Bus.FORGE)` | `@EventBusSubscriber` — le bus GAME est le défaut, et l'attribut `bus` est déprécié | +| `data//structures/` | `data//structure/` *(singulier)* | +| capabilities `getCapability(...)` | `level.getCapability(Capabilities.X.BLOCK, pos, ctx)` | +| `ForgeCatnipServices.FLUID_RENDERER` | `CatnipServices.FLUID_RENDERER` — typé ``, **cast requis** | +| `ProcessingRecipe` + `RecipeWrapper`/`ItemStackHandler` | `StandardProcessingRecipe` — plus de classe wrapper à écrire, `new SingleRecipeInput(stack)` suffit | +| `Codec.unit(...)` pour un `PlacementModifierType` | `MapCodec.unit(...)` — `PlacementModifierType#codec()` renvoie un `MapCodec` | +| `CatnipServices.REGISTRIES.getKeyOrThrow(...)` | `RegisteredObjectsHelper.getKeyOrThrow(...)` | + +### Deux pièges qui ne cassent pas la compilation + +Ce sont les plus coûteux : le code compile, ne lève rien, et se comporte mal. + +1. **`stack.get(DataComponents.CUSTOM_DATA).copyTag()` renvoie une copie.** Traduire + `stack.getOrCreateTag().putX(...)` littéralement produit une écriture **silencieusement + perdue**. Passer par un composant typé et `stack.set(...)`. + +2. **Un codec persistant qui peut refuser une valeur fait planter la sauvegarde du monde**, + pas l'écriture — donc très loin de la cause. **Valider les invariants en amont, pas dans le + codec.** Rencontré deux fois : + - `ExtraCodecs.POSITIVE_FLOAT` sur la chaleur, qui refusait `0.0`, valeur normale d'un + réacteur à l'arrêt ; + - `ItemStack.CODEC` sur un motif de réacteur, qui **refuse la stack vide** alors que le motif + fait 57 slots presque jamais tous remplis. **Réflexe 1.21 : pour un `ItemStack` qui peut + être vide, c'est `ItemStack.OPTIONAL_CODEC` / `OPTIONAL_STREAM_CODEC`, jamais les variantes + strictes.** + + > Ce second cas ne s'était pas vu aux gametests parce qu'ils **ne rechargent pas le monde** : + > l'erreur ne sortait que dans le log de `runGameTestServer`, à l'arrêt du serveur, pendant + > que « All tests passed » s'affichait juste au-dessus. **Lire le log, pas seulement le + > verdict.** + +### Noms qui masquent une classe vanilla + +Trois classes du mod portent volontairement le nom d'une classe vanilla, pour rester alignées +sur Forge : `ArmorMaterials` (`net.minecraft.world.item.ArmorMaterials`), `CatLieOnBedGoal` et +`CatSitOnBlockGoal` (`net.minecraft.world.entity.ai.goal.*`). Les fichiers concernés importent +le package vanilla en wildcard — **sans danger**, parce qu'en Java une classe du même package +l'emporte sur un import à la demande, et qu'aucun de ces fichiers n'utilise la classe vanilla +homonyme. **Mais si l'un d'eux a un jour besoin de la version vanilla, il faudra la qualifier +complètement** — un `import` simple ne suffira pas. + +--- + +## 7. Vérifier l'état à tout moment + +```bash +# Fichiers Forge sans équivalent NeoForge — doit en lister 15 (4 du §1.1, 2 du §1.2, 9 du §2) +cd ~/Documents/Ynov/Ydays +diff <(cd CreateNuclearForge/src && find . -name '*.java' | sort) \ + <(cd CreateNuclearNeoForge/src && find . -name '*.java' | sort) + +# Recettes présentes côté Forge et absentes côté NeoForge (§3.1) +# Les préfixes diffèrent : `recipes/` en 1.20.1, `recipe/` en 1.21 +diff <(cd CreateNuclearForge/src/generated/resources/data/createnuclear/recipes && find . -name '*.json' | sort) \ + <(cd CreateNuclearNeoForge/src/generated/resources/data/createnuclear/recipe && find . -name '*.json' | sort) + +# Recettes qui fuient dans le namespace de Create — doit être vide hors tags +find CreateNuclearNeoForge/src/generated/resources/data/create -name '*.json' + +# Écart interne d'un fichier partagé +diff CreateNuclearForge/src/main/java/.../X.java \ + CreateNuclearNeoForge/src/main/java/.../X.java + +# Classes NeoForge jamais référencées (le piège du §0) +cd CreateNuclearNeoForge && grep -rl '' src --include=*.java + +# Non-régression du réacteur — doit sortir en 0 des deux côtés +cd CreateNuclearNeoForge && ./gradlew runGameTestServer # 31 tests +cd CreateNuclearForge && ./gradlew runGameTestServer # 28 tests +``` diff --git a/PORTAGE_REACTEUR.md b/PORTAGE_REACTEUR.md new file mode 100644 index 0000000..b0ad09b --- /dev/null +++ b/PORTAGE_REACTEUR.md @@ -0,0 +1,603 @@ +# Portage du réacteur — Forge → NeoForge + +Comparaison `CreateNuclearForge` (branche `V2`) → `CreateNuclearNeoForge` (branche `V2-Reacteur`). +Périmètre : domaine réacteur uniquement (`content/multiblock/**` + dépendances directes). + +> ## ✅ Portage terminé et validé en jeu +> +> Les 8 lots sont faits. Le réacteur s'assemble, produit, chauffe, alarme, fond et explose. +> Validé en jeu par mathi le 6 août 2026 : assemblage, blueprint, fluide, production SU. +> +> **~50 fichiers portés**, dont la majorité identiques à l'octet à l'original Forge. +> `ReactorControllerBlockEntity` : 868 lignes monolithiques → 424 lignes déléguantes, +> divergence vs Forge ramenée de **864 lignes à 76** (toutes des adaptations 1.21). +> +> Gametests : `./gradlew runGameTestServer` → **29 tests, tous verts** (`EXIT=0`). +> Utilisable en CI bloquante. +> +> Ce qui reste hors périmètre est inventorié en **§9**. + +--- + +## 1. Constat central + +**Le réacteur NeoForge n'est pas une version « réorganisée » du réacteur Forge : c'est une génération antérieure du code.** + +C'est le point le plus important du rapport, parce qu'il change la nature du travail. Un portage classique consiste à traduire l'API. Ici, il faut d'abord **remplacer la logique**, puis traduire l'API. + +| | Forge | NeoForge | +|---|---|---| +| `ReactorControllerBlockEntity` | 440 lignes, logique déléguée | **735 lignes monolithiques** | +| Packages `service/`, `reactorLogic/`, `consumable/`, `snapshot/`, `display/` | 31 fichiers | **0 — inexistants** | +| Modèle thermique | équilibre 6:1 fuel/cooler, surchauffe progressive, fluide caloporteur | somme `baseUraniumHeat`/`baseGraphiteHeat` codée en dur | +| Fusion du cœur | compte à rebours 300 ticks + notifications + explosion dimensionnée | **absente** | +| Alarmes | pilotées par coordinateur + advancement | **jamais déclenchées** | +| Consommation des barres | cycles par type de barre, persistés en NBT | timers `tmpUraniumTimer`/`tmpGraphiteTimer` codés en dur | +| Son de fonctionnement | boucle pilotée par blockstate `ACTIVE` | **absent** | +| Tooltip goggles | rendu depuis un snapshot synchronisé | lit `fuelItem`/`coolerItem` locaux | + +Concrètement, le `tick()` NeoForge fait encore : lire l'inventaire d'un unique `ReactorRodInput`, décrémenter deux timers, calculer une chaleur par voisinage uranium/graphite, et faire tourner la sortie. Il n'y a ni surchauffe, ni fusion, ni explosion, ni consommation de fluide. + +**Conséquence sur la méthode :** ne pas essayer de « compléter » le BE NeoForge existant. Il faut le remplacer par la version Forge et porter les 31 fichiers de logique en dessous. Les ~300 lignes actuelles de calcul (`calculateHeat`, `calculateProgress`, `updateTimers`, `getStructureBounds`, `convertePattern`, `formattedPattern`, `rotate`) sont du code mort une fois le portage fait. + +--- + +## 2. Ce qui existe déjà côté NeoForge (bonne nouvelle) + +Toute l'infrastructure dont dépendent les services Forge est **déjà présente** : + +`RodType` · `ReactorFluidType` · `BigFluidStack` · `NotifyUtil` · `CreateNuclearLang` · `BiomeIrradiationService` · `CNBiomes` · `NuclearExplosionEntity` · `CNAdvancement` · `CNAdvancementBehaviour` · `CNConfigs` · `ReactorBluePrintItem.getItemStorage()` · `CNSoundEvents.REACTOR_RUNNING` + +Les 5 managers (`ReactorInputManager`, `ReactorOutputManager`, `ReactorInputFluidManager`, `ReactorAlarmManager`, `AbstractReactorIOManager`) sont eux aussi déjà portés et à jour. + +Le portage ne part donc pas de zéro : il porte sur la couche logique, pas sur les fondations. + +--- + +## 3. Divergences d'API bloquantes — à traiter EN PREMIER + +Ces cinq points doivent être réglés avant de porter le moindre service, sinon rien ne compile. + +### 3.1 `RodType` — modèle différent + +| Forge | NeoForge | +|---|---| +| `Holder item` (une barre par type) | `HolderSet items` (plusieurs barres par type) | +| `baseRodHeat()` → `Supplier`, appelé `.get()` | `baseRodHeat()` → `int` direct | +| `proximityRodHeat()` → `Supplier` | `proximityRodHeat()` → `float` direct | +| `rodTimer()` → `Supplier` | `rodTimer()` → `int` direct | +| **`ratio()` présent** | **`ratio()` absent** | +| `TypeRodPredicate.isFuel(RodType)` / `isCooled(RodType)` | uniquement `IS_FUEL` / `IS_COOLED` sur `ItemStack` | + +**À faire :** ajouter `ratio()` à `RodType` NeoForge (+ builder + codec + config), ajouter les surcharges `isFuel(RodType)` / `isCooled(RodType)`. Dans les services portés, supprimer les `.get()` sur `baseRodHeat`/`proximityRodHeat`/`rodTimer`. + +Le `HolderSet` au lieu du `Holder` n'est pas un problème : les services n'utilisent que `resolveRodType(Item, Level)`, dont la signature est identique des deux côtés. + +### 3.2 `IHeat.HeatLevel` — pas de notion de taille de réacteur + +| Forge | NeoForge | +|---|---| +| `of(int heat, int reactorSize)` | `of(int heat)` | +| `isNotDanger(int heat, int reactorSize)` | **absent** | +| `getFormattedHeatText(int heat, int reactorSize)` | `getFormattedHeatText(int heat)` | +| `getFormattedItemText(ItemStack, Boolean, Level)` | `getFormattedItemText(ItemStack, Boolean)` | + +Côté Forge, les seuils de danger dépendent de la taille du réacteur (5×5 / 7×7 / 9×9). C'est ce qui rend un 9×9 viable à haute chaleur. Côté NeoForge, le seuil est unique. + +**À faire :** porter la version taille-consciente de `IHeat.HeatLevel`, qui dépend du point suivant. + +### 3.3 `CReactorHeat` — fichier de config absent + +```java +public final ConfigInt size5Danger = i(256, 0, 8192, ...); +public final ConfigInt size7Danger = i(1024, 0, 8192, ...); +public final ConfigInt size9Danger = i(4096, 0, 8192, ...); +``` + +**À faire :** créer `infrastructure/config/CReactorHeat.java` et le déclarer dans `CNCServer`/`CNConfigs`. + +### 3.4 `CRods` — config incomplète + +Forge expose 12 valeurs (uranium / graphite / **thorium**, avec `*HeatRatio` et `*ProximityBonus`). NeoForge n'en expose que 3 (`uraniumRodLifetime`, `graphiteRodLifetime`, `maxHeat`) et **ne connaît pas le thorium**. + +**À faire :** aligner `CRods` sur la version Forge. Prérequis du `ratio()` de 3.1. + +### 3.5 `ReactorControllerBlock` — propriété `ACTIVE` absente + +Forge déclare `ASSEMBLED` **et** `ACTIVE`. NeoForge n'a que `ASSEMBLED`. + +`ACTIVE` est recalculée chaque tick serveur et synchronisée automatiquement au client ; c'est elle qui pilote la boucle sonore de fonctionnement. Sans elle, `ReactorRunningSoundInstance` n'a rien à observer. + +**À faire :** ajouter `ACTIVE` au blockstate + au datagen des blockstates JSON. + +--- + +## 4. Fichiers à créer — par ordre de portage + +### Lot 0 — Prérequis (rien ne compile sans) ✅ FAIT +| Fichier | Note | +|---|---| +| `infrastructure/config/CReactorHeat.java` | création | +| `infrastructure/config/CRods.java` | **extension** (thorium + ratios + proximité) | +| `api/multiblock/rods/RodType.java` | **modification** (valeurs en `Supplier`, `ratio()`, prédicats `RodType`) | +| `content/multiblock/IHeat.java` | **modification** (seuils par taille) | +| `content/multiblock/controller/ReactorControllerBlock.java` | **modification** (`ACTIVE`) | +| `content/multiblock/controller/ReactorControllerGenerator.java` | **modification** (textures off/standby/on) | +| `CNItems.java` | **modification** — voir ci-dessous | + +> **Trouvé en cours de route :** seul `THORIUM_ROD` avait un `RodType` enregistré côté +> NeoForge. `URANIUM_ROD` et `GRAPHITE_ROD` n'en avaient aucun, donc `resolveRodType()` +> retombait sur `FALLBACK` (type `NONE`), que `DefaultHeatCalculator` ignore : le modèle +> thermique aurait calculé **zéro chaleur en permanence**, sans erreur visible. Les trois +> barres sont désormais enregistrées et adossées à la config. +> +> ⚠️ Le blockstate du contrôleur passe de 8 à 16 variants (ajout d'`ACTIVE`) : +> `runData` doit être relancé, sinon le bloc affiche un modèle manquant en jeu. + +### Lot 1 — Modèle thermique (`reactorLogic/`, 7 fichiers, ~217 lignes) ✅ FAIT +`IHeatCalculator` · `DefaultHeatCalculator` · `IOverheatController` · `DefaultOverheatController` · `HeatBalance` · `EquilibriumState` · `HeatManager` + +Cœur du modèle : équilibre 6:1 fuel/cooler, surchauffe qui s'accélère (`overFlowLimiter` décrémenté), pénalité de fluide insuffisant ou de dépassement du `maxHeat` du fluide. + +> **Correction d'ordre :** `display/ReactorDisplayState` est une dépendance dure de +> `DefaultHeatCalculator` (paramètre de `computeHeat`). Il a donc été porté avec le lot 1 +> au lieu du lot 4. Ses méthodes NBT prennent un `HolderLookup.Provider` en 1.21, parce que +> `BigFluidStack.write()/read()` l'exigent côté NeoForge. + +### Lot 2 — Lecture de pattern & consommation (`consumable/`, 6 fichiers, ~317 lignes) ✅ FAIT +`PatternReader` · `IConsumable` · `ItemConsumable` · `FluidConsumable` · `ConsumableTimer` · `ConsumptionCycleManager` + +`PatternReader` est utilisé par le lot 1 et le lot 3 — à porter tôt. + +> **Divergence corrigée dans `ReactorBluePrintMenu` :** côté Forge, `pattern` est la grille +> brute du joueur et `patternAll` la **même grille normalisée** — slots vides *et* items +> non-barres remplacés par du `GLASS_PANE`. Côté NeoForge les deux champs pointaient sur +> **le même tableau**, et les items non-barres y étaient conservés. +> +> Sans correction, un item quelconque déposé dans la grille aurait été compté par +> `PatternReader` comme une exigence du pattern. `ReactorHeatUpdateCoordinator.updateHeatOnly` +> exige que *chaque* entrée du pattern soit disponible dans les inputs : un item non-barre +> n'étant jamais fourni, la chaleur serait restée bloquée à 0 sans erreur visible. +> +> `saveData` construit désormais deux tableaux distincts. `getItemStorage()` continue de lire +> `pattern()` (brut), comme la version Forge qui lit l'élément NBT `pattern`. + +#### Lot 2bis — Migration NBT → Data Components du blueprint ✅ FAIT (9 août 2026) + +Le lot 2 ci-dessus portait la lecture du pattern ; ce qui suit va plus loin et retravaille le +**stockage** du blueprint lui-même (`bluePrintItem/`), au-delà d'un simple portage d'API. + +**Point de départ.** À l'ouverture de ce chantier, le blueprint NeoForge était dans un état +intermédiaire : `ReactorBluePrintData`/`PatternData` (les vrais Data Components) coexistaient +avec des restes NBT jamais nettoyés après leur introduction — un composant +`CNDataComponents.PATTERN` qui ne faisait que réemballer une `CompoundTag` (jamais écrit, +toujours lu vide), un paquet réseau (`ReactorBluePrintItemPacket`) hérité du portage Forge dont +**tous les champs étaient write-only** côté client (`heat`, compteurs, `progress`, +`sendUpdate` : aucun n'était relu par un rendu ou une logique quelconque), et quatre Data +Components orphelins (`URANIUM_TIME`, `GRAPHITE_TIME`, `COUNT_GRAPHITE_ROD`, +`COUNT_URANIUM_ROD`) enregistrés mais jamais lus ni écrits. + +**Ce qui a été fait :** +- Suppression complète du paquet réseau et de son enregistrement dans `CNPackets` : ce menu + n'a structurellement besoin d'aucun paquet, dans aucun sens. C'est un menu à un seul joueur + possible (item tenu en main), l'édition du pattern passe déjà par la synchronisation de slots + vanilla, et `saveData()` (`MenuBase#removed`, dans Create) écrit directement sur le + `contentHolder` serveur sans aller-retour réseau. +- Suppression de `CNDataComponents.PATTERN` et des quatre composants orphelins. +- `ReactorBluePrintData` réduit à `(int countCooledRod, int countFuelRod, PatternData[] pattern)` : + `patternAll` n'est plus persisté ni synchronisé, il est dérivé à la demande + (`patternAll(Level)`) depuis `pattern`, ce qui élimine par construction le risque de + divergence entre les deux vues qui avait motivé la correction du lot 2. `graphiteTime`/ + `uraniumTime` sont retirés aussi : ce n'étaient que des constantes de config recopiées sur + chaque blueprint, jamais lues nulle part. +- Ajout d'un sentinel `ReactorBluePrintData.EMPTY`, qui remplace sept réimplémentations + indépendantes de « ce blueprint est-il configuré ? » (`configuredPattern.get(...) == null`, + dupliqué dans `ReactorControllerBlockEntity` ×4, `ReactorControllerBlock`, + `ReactorHeatUpdateCoordinator`, `ConsumptionCycleManager`) par un seul point de vérité, + comparé par identité (`== EMPTY`), sur le même principe que `ItemStack.EMPTY`. + +**Bug trouvé et corrigé, propre à NeoForge :** un blueprint contenant des barres de thorium +perdait silencieusement ces barres à la réouverture du menu (confirmé par un dump NBT +`/setblock` montrant le pattern correctement sauvegardé, mais absent une fois le menu rouvert). +Cause : `THORIUM_ROD` est enregistré `.fuelRodType()` côté `RodType` (`CNItems.java`), mais son +`.tag(...)` n'incluait pas `CNItemTags.FUEL.tag` — contrairement à `URANIUM_ROD`/`GRAPHITE_ROD`, +qui ont bien leur tag. `ReactorBluePrintMenu#initAndReadInventory` filtrait la réaffichage sur +ce tag plutôt que sur `RodType`, donc le thorium passait la sauvegarde mais échouait +silencieusement à la relecture. Corrigé à deux niveaux : le tag manquant ajouté dans +`CNItems.java`, et le filtre de réaffichage réécrit pour utiliser `RodType`/`TypeRodPredicate` +partout (comptage, `pattern`, `patternAll`, réaffichage), la même source de vérité que +`saveData` utilisait déjà pour `patternAll`. + +**Bug de config trouvé au passage, sans lien avec les Data Components :** `CNCCommon` déclarait +sa propre instance de `CRods` (`Type.COMMON`), en plus de celle déjà présente dans `CNCServer` +(`Type.SERVER`) — deux fichiers de config distincts pour la même donnée conceptuelle. Absent +côté Forge, où `CRods` ne vit que dans le config serveur. `ReactorBluePrintMenu` lisait la +copie `COMMON`, `RodType`/`CNItems` la copie `SERVER` : un admin modifiant l'une sans l'autre +aurait désynchronisé silencieusement la durée de vie affichée sur le blueprint de celle +réellement utilisée par le calcul de chaleur. Le champ dupliqué a été retiré de `CNCCommon`, +`ReactorBluePrintMenu` lit désormais `CNConfigs.server().rods`, comme le reste du code. + +> **Différence assumée entre Forge et NeoForge, à ne pas « corriger » lors d'une synchro.** +> Forge stocke le blueprint en NBT brut sur l'`ItemStack` (`getOrCreateTag().put("pattern", ...)`). +> NeoForge le stocke en Data Component typé (`ReactorBluePrintData`, `Codec` + `StreamCodec`). +> Ce n'est pas un écart à réduire : NBT muté en place sur un `ItemStack` casse silencieusement +> en 1.21 (voir §5.1, `copyTag()` renvoie une copie défensive), et les Data Components sont la +> direction que 1.21 impose pour ce genre de donnée structurée. Une synchro Forge → NeoForge sur +> `bluePrintItem/` ne doit **jamais** réintroduire de `CompoundTag`/`getOrCreateTag()` ; elle doit +> traduire chaque nouveau champ NBT Forge en champ du record `ReactorBluePrintData`, avec son +> propre `Codec`/`StreamCodec`, sur le modèle de `PatternData`. +> +> Différence secondaire assumée : le paquet réseau `ReactorBluePrintItemPacket` que Forge +> utilise n'a pas d'équivalent côté NeoForge, par choix, pas par oubli — voir le lot 2bis +> ci-dessus. S'il réapparaît un jour côté Forge avec un vrai usage (affichage de statistiques), +> ne pas le traduire tel quel : passer par la synchronisation de menu vanilla +> (`ContainerData`/`DataSlot`) plutôt que par un paquet personnalisé. + +**Ce qu'il reste, honnêtement :** aucun bug connu à ce stade sur `bluePrintItem/` après cette +passe. Le point réellement ouvert est la **couverture de test** : les 29 gametests ne couvrent +pas le cas qui a produit le bug du thorium (un `RodType` enregistré sans le tag `CNItemTags` +correspondant). Ajouter un cas de ce genre permettrait à une future régression similaire +d'échouer un test plutôt que de n'être trouvée qu'en jeu. + +### Lot 3 — Services (`service/`, 12 fichiers sur 14) ✅ FAIT +`IHeatService` / `DefaultHeatService` · `IExplosionService` / `ReactorMeltdownExecutor` · `IReactorMeltdownMonitor` / `ReactorMeltdownMonitor` · `IReactorAlarmCoordinator` / `ReactorAlarmCoordinator` · `IReactorHeatUpdateCoordinator` / `ReactorHeatUpdateCoordinator` · `IFluidConsumptionRateCalculator` / `FluidConsumptionRateCalculator` + +> **Le piège de §5.1 est neutralisé.** `CNDataComponents.HEAT` (`DataComponentType`) +> **existait déjà** côté NeoForge — inutile d'en créer un. `ReactorHeatUpdateCoordinator` +> écrit désormais via `configuredPattern.set(CNDataComponents.HEAT, (float) heat)`, dans une +> méthode `writeHeat()` dédiée, au lieu de muter `getOrCreateTag()`. Le test de pattern vide +> passe par `isEmptyPattern()`, qui vérifie l'absence de `ReactorBluePrintData`. +> +> Hors javadoc, ce sont les **seules** différences avec l'original Forge : 9 des 10 autres +> fichiers sont identiques à l'octet. + +> **Reporté au lot 6 :** `IPersistenceService` et `DefaultPersistenceService`. Ils appellent +> des accesseurs que le `ReactorControllerBlockEntity` NeoForge actuel n'a pas encore +> (`setMultiblockFacing(Direction)` vs `String`, `getMultiblockPos()` en `BoundingBox` vs +> `int[]`, `serializeInventory`, `get/setDisplayState`). Les porter maintenant obligerait à +> réécrire le BE, c'est-à-dire à faire le lot 6 — ils partiront donc avec lui. + +### Lot 4 — Affichage & snapshot (6 fichiers, ~450 lignes) ✅ FAIT +`snapshot/ReactorInputSnapshot` · `snapshot/ReactorInputSnapshotBuilder` · `display/ReactorDisplayState` (porté au lot 1) · `display/ReactorGoggleTooltipRenderer` · `manager/ReactorFrameDisplayManagerI` · `manager/ReactorFrameDisplayManager` + +`ReactorFrameDisplayManager` remplace les champs `frameFluidCache*` / `frameColumn*` actuellement inlinés dans le BE NeoForge — c'est une extraction, la logique existe déjà et est équivalente. Le BE ne sera débarrassé de ses copies qu'au lot 6. + +> Aucune surprise sur ce lot : `ReactorInputSnapshot` est identique à l'octet, les trois +> autres ne diffèrent que par les imports `minecraftforge` → `neoforged`, plus +> `FluidStack.getDisplayName()` → `getHoverName()` dans le renderer. +> +> Le javadoc d'exemple de `ReactorGoggleTooltipRenderer` a été corrigé au passage : côté +> Forge il montrait un appel à `render()` avec 4 arguments alors que la méthode en prend 5. + +### Lot 5 — Divers contrôleur (2 fichiers, ~100 lignes) ✅ FAIT +`ReactorRunningSoundInstance` · `ReactorDebugDiagnostics` + +> Les deux fichiers sont **identiques à l'octet** — aucune adaptation d'API. `AbstractTickableSoundInstance` +> n'a pas bougé entre 1.20.1 et 1.21, et `ReactorAlarmSoundInstance` (déjà porté) sert de témoin. +> +> En revanche les 8 clés `createnuclear.reactor.debug.*` manquaient dans +> `lang/default/reactor.json` — ajoutées. Les 9 clés `notification.reactor.*` dont dépend le +> `ReactorMeltdownMonitor` du lot 3, elles, existaient déjà. `runData` est nécessaire pour +> répercuter les nouvelles clés dans `en_us.json`. + +### Lot 6 — Réécriture du block entity ✅ FAIT (lot 7 inclus) +`ReactorControllerBlockEntity` passe de **868 lignes monolithiques à 424 lignes** qui délèguent +aux 33 fichiers des lots 0-5. Divergence vs Forge : **864 lignes → 76**, toutes des adaptations +1.21 (`HolderLookup.Provider`, DataComponents, imports `neoforged`). + +Fichiers portés avec : `ReactorControllerBlock` · `ReactorAssembler` · `CNMultiblock` · +`ReactorPattern` · `MultiblockHelpers` · `ReactorOutput` · `ReactorOutputEntity` · +`ReactorOutputManager(I)` · `ReactorFrame` · `ReactorFrameRenderer` · `ReactorCasing` · +`ReactorCooler` · `ReactorCoreEntity` · `ReactorRodInput` · `IPersistenceService` / +`DefaultPersistenceService` · `IMultiblockController` (lot 7). + +> **Features réellement absentes de NeoForge, découvertes en câblant :** +> +> 1. **Seul le réacteur 5×5 existait.** `CNMultiblock` n'enregistrait qu'un multiblock, avec +> entrée et sortie à positions fixes. Forge en enregistre trois (5×5 / 7×7 / 9×9) où les I/O +> se placent librement sur l'enveloppe. Les 7×7 et 9×9 étaient donc **impossibles à assembler**. +> 2. **`ReactorFrameEntity` n'était jamais enregistré.** La classe et son renderer existaient, +> mais sans `BlockEntityEntry` dans `CNBlockEntityTypes` : le bloc frame n'avait aucune block +> entity, donc le rendu du fluide dans les vitres ne pouvait pas fonctionner. Ajouté. +> 3. **La capacité de fluide était multipliée par le nombre d'entrées.** `applyReactorTierCapacity` +> donnait à *chaque* entrée la capacité totale du réacteur ; 4 entrées = 4× la capacité prévue. +> Remplacé par `applyCapacity(int)`, l'assembleur répartissant le total comme sur Forge. +> 4. **`ReactorOutputEntity` utilisait un `KineticScrollValueBehaviour`** (vitesse réglable à la +> molette) au lieu du `generatedSpeed` persisté piloté par le manager. Remplacé, ce qui +> supprime aussi l'interaction `DIR` sur `ReactorOutput` que Forge avait retirée. +> +> **Piège évité :** Forge déclare `int heat = ...` dans `tick()` en variable **locale masquant le +> champ** — donc morte. Une lecture rapide pousse à en faire une affectation du champ, ce qui +> changerait la valeur de `previousHeat` passée au calcul. La sémantique Forge est conservée. + +**Divergences résiduelles assumées** (à ne pas « corriger » lors d'une synchro) : +`CNShapes.REACTOR_INPUT` et `CNBlockEntityTypes.REACTOR_INPUT` gardent leur nom NeoForge +(Forge : `REACTOR_ROD_INPUT`) — les renommer changerait l'identifiant de registre +`createnuclear:reactor_input` et casserait les mondes existants. + +### Lot 9 — Corrections issues du test en jeu ✅ FAIT + +Six défauts trouvés en jouant, tous corrigés. Cinq étaient des divergences NeoForge +préexistantes, un était une erreur introduite par le portage. + +| Symptôme en jeu | Cause | Origine | +|---|---|---| +| Crash à la sauvegarde du monde | `CNDataComponents.HEAT` en `ExtraCodecs.POSITIVE_FLOAT`, qui refuse `0.0` — or un réacteur à l'arrêt écrit 0 à chaque tick | **introduit au lot 3** | +| Fluide quasi noir dans les vitres | `reactor_frame` sans `.noOcclusion()` : bloc traité comme opaque plein, la lumière ne passe plus | préexistant | +| idem | renderer passant un `FluidState` au lieu du `FluidStack`, ce qui perd la teinte | introduit au lot 6 | +| 2 slots dans le rod input | `super(2)` avec uranium/graphite figés, au lieu d'un slot acceptant toute barre via `RodType` | préexistant | +| Explosion malgré un pattern stable | azote liquide **commenté** dans `CNReactorFluidTypes` → pas de `ReactorFluidType` → `efficiency = -1` → `fluidMalus` permanent → surchauffe infinie | préexistant | +| 81 920 SU au lieu de 512 000 | capacité de stress du `REACTOR_OUTPUT` à `10240.0` au lieu de `64000.0` (rapport 6,25 ✓) | préexistant | + +> **Leçon sur les codecs :** un codec **persistant** qui peut refuser une valeur échoue au moment +> de la **sauvegarde**, très loin de l'écriture fautive, et emporte le block entity avec lui. +> Les invariants de domaine se valident en amont, pas dans le codec. +> Le test `heatComponent_atZero_survivesSerialization` verrouille ce cas. + +**Trouvé en vérifiant :** Forge a **5 appels à `addLayer`**, NeoForge n'en avait qu'**un**. +Quatre blocs avaient perdu leur couche de rendu — dont `reinforced_glass` en `translucent`, +qui rendait donc **opaque**. Tous rétablis. + +--- + +## 5. Pièges de traduction 1.20.1 Forge → 1.21 NeoForge + +### 5.1 ⚠️ Le piège majeur : `getConfiguredPatternTag()` renvoie une copie + +Côté Forge, la chaleur est **écrite dans le tag de l'ItemStack** : + +```java +configuredPattern.getOrCreateTag().putDouble("heat", heat); // mute en place +``` + +Côté NeoForge, l'implémentation actuelle est : + +```java +public CompoundTag getConfiguredPatternTag() { + return this.configuredPattern + .getOrDefault(DataComponents.CUSTOM_DATA, CustomData.EMPTY) + .copyTag(); // ⚠️ COPIE +} +``` + +`copyTag()` renvoie une copie défensive. **Tout `put*` dessus est silencieusement perdu.** Traduire `ReactorHeatUpdateCoordinator.updateHeatOnly()` / `calculateAndWriteHeat()` littéralement produirait un réacteur dont la chaleur reste bloquée à 0, sans aucune erreur de compilation ni exception à l'exécution. + +Deux options : + +- **A — pansement** : après modification, réécrire le composant + `stack.set(DataComponents.CUSTOM_DATA, CustomData.of(tag))`. + Rapide, mais conserve la sémantique « NBT brut » que 1.21 cherche à supprimer. +- **B — propre (recommandé)** : introduire un `ReactorHeatData` (ou étendre `ReactorBluePrintData`, qui existe déjà comme record côté NeoForge) et remplacer les accès `tag.getDouble("heat")` par un accès typé. + +Option B est plus de travail mais s'aligne sur la direction déjà prise par le repo NeoForge, qui utilise `CNDataComponents` / `ReactorBluePrintData` / `PatternData`. Mélanger les deux approches est le vrai risque : `PatternReader` (Forge) lit `tag.getCompound("patternAll")`, alors que NeoForge stocke déjà le pattern dans `ReactorBluePrintData.patternAll()` sous forme de `PatternData[]`. + +**→ `PatternReader` ne doit pas être traduit ligne à ligne. Il doit être réécrit pour lire `ReactorBluePrintData`.** + +### 5.2 Correspondances mécaniques + +| Forge 1.20.1 | NeoForge 1.21 | +|---|---| +| `net.minecraftforge.items.IItemHandler` | `net.neoforged.neoforge.items.IItemHandler` | +| `net.minecraftforge.fluids.FluidStack` | `net.neoforged.neoforge.fluids.FluidStack` | +| `net.minecraftforge.api.distmarker.OnlyIn` | `net.neoforged.api.distmarker.OnlyIn` | +| `ForgeRegistries.ITEMS.getKey(item)` | `BuiltInRegistries.ITEM.getKey(item)` | +| `ForgeRegistries.ITEMS.getValue(rl)` | `BuiltInRegistries.ITEM.get(rl)` | +| `new ResourceLocation(s)` | `ResourceLocation.parse(s)` | +| `read/write(CompoundTag, boolean)` | `read/write(CompoundTag, HolderLookup.Provider, boolean)` | +| `handler.deserializeNBT(tag)` | `handler.deserializeNBT(provider, tag)` | +| `ItemStack.of(tag)` | `ItemStack.parse(provider, tag)` → `Optional` | +| `stack.serializeNBT()` | `stack.saveOptional(provider)` | +| `CODEC.encodeStart(..).getOrThrow(false, X::new)` | `CODEC.encodeStart(..).getOrThrow()` | + +`ReactorDisplayState` et `DefaultPersistenceService` concentrent la majorité de ces changements, car ils sérialisent tout. + +### 5.3 `FluidStack` n'est plus nullable de la même façon + +En 1.21, `FluidStack` porte des `DataComponents` et `FluidStack.EMPTY` se compare avec `isEmpty()`, jamais avec `==`. Le code Forge de `ReactorFrameDisplayManager` fait `fluid.isEmpty()` — correct — mais `stack.copy()` conserve désormais les composants, ce qui est le comportement voulu. + +--- + +## 6. Volumétrie + +| Lot | Fichiers | Lignes (Forge) | Difficulté | +|---|---|---|---| +| 0 — Prérequis | 5 (2 créés, 3 modifiés) | ~150 | moyenne | +| 1 — reactorLogic | 7 | 217 | faible (logique pure, peu d'API MC) | +| 2 — consumable | 6 | 317 | **élevée** (`PatternReader` à réécrire) | +| 3 — service | 14 | 536 | moyenne (persistence = élevée) | +| 4 — display/snapshot | 6 | 450 | **élevée** (sérialisation) | +| 5 — divers | 2 | 100 | faible | +| 6 — BlockEntity | 1 | 440 | **élevée** | +| 7 — api multiblock | 1 | ~30 | faible | +| **Total** | **42** | **~2 240** | | + +Les lots 1, 3 et 5 sont essentiellement de la traduction d'imports. Les lots 2, 4 et 6 demandent de vraies décisions d'architecture (DataComponents vs NBT). + +--- + +## 7. Recommandation d'ordre d'exécution + +1. **Lot 0** — sans lui rien ne compile. +2. **Décision DataComponents vs NBT** (§5.1) — elle conditionne les lots 2, 4 et 6. À trancher avant d'écrire une ligne du lot 2. +3. **Lots 1 → 5** dans l'ordre, en compilant après chaque lot. +4. **Lot 6** en dernier : le BE ne compile qu'une fois toutes ses dépendances présentes. +5. **Lot 7** au choix, indépendant. + +Les deux gametests Forge (`DefaultHeatCalculatorGameTest`, `ReactorInputFluidManagerGameTest`) sont à porter avec les lots 1 et 3 respectivement : ils valident précisément la logique la plus risquée. + +### Lot 8 — Gametests ✅ FAIT + +`./gradlew runGameTestServer` — **29 tests, tous verts** (`EXIT=0`). + +Les 4 tests du modèle thermique passent, dont `threeByThreeDiamond` croisé avec le calculateur +du wiki communautaire. C'est la première validation à l'exécution du portage. + +> À l'écriture du lot, la suite comptait 31 tests dont 2 échouant volontairement : les marqueurs +> `*_expectedContract` du bug `extractFluids`, qui échouait identiquement sur Forge. Ce bug est +> corrigé depuis dans les deux repos (§9.4), les marqueurs sont donc passés au vert et les deux +> tests qui figeaient le comportement bogué ont été supprimés. + +**Adaptations du portage :** +- `loadPattern` écrit un `ReactorBluePrintData` typé (57 `PatternData`) au lieu du tag NBT `pattern`. +- Capabilities via `level.getCapability(Capabilities.FluidHandler.BLOCK, pos, null)`. +- `ForgeRegistries.FLUIDS` → `BuiltInRegistries.FLUID`, annotations `net.neoforged.neoforge.gametest`. +- La structure va dans `data/createnuclear/structure/` (**singulier** en 1.21, `structures` en 1.20.1). + +**Bug NeoForge corrigé au passage** (absent de Forge) : `ReactorInputFluidManager` lisait +`getFluidInTank(handler.getTanks())` — un index hors bornes — dans `getInventory()` **et** +`extractFluids()`. Corrigé en `getFluidInTank(0)`. Un `LOGGER.warn` de debug oublié dans +`getBlocksPosition()`, qui spammait à chaque appel, a aussi été retiré. + +--- + +## 8. Hors périmètre de ce rapport + +Le reste du mod présente aussi des écarts. Inventaire complet en §9. + +--- + +## 9. Ce qui n'est PAS porté de Forge vers NeoForge + +> 📄 **Cette section est reprise et approfondie dans [`PARITE_FORGE_NEOFORGE.md`](PARITE_FORGE_NEOFORGE.md)**, +> qui détaille pour chaque feature manquante ses points d'accroche, sa difficulté et l'ordre de +> travail conseillé. Le présent document reste centré sur le réacteur. + +État au 6 août 2026, branche `V2-Reacteur`. Le comptage brut donne **42 fichiers Forge sans +équivalent NeoForge**, mais 6 sont en réalité présents sous un autre nom — le vrai reste est +**36 fichiers**, plus des écarts internes sur des fichiers partagés. + +### 9.1 Faux positifs — présents mais renommés ou déplacés + +Ne pas les reporter comme manquants lors d'une synchro : + +| Forge | NeoForge | +|---|---| +| `content/multiblock/rod/CNRodTypes` | `content/rod/CNRodTypes` | +| `foundation/damageTypes/CNDamageSources` | `foundation/damages**T**ypes/CNDamageSources` | +| `content/equipment/armor/ArmorMaterials` | `CNArmorMaterials` | +| `infrastructure/config/CExplode` | `CExplose` *(faute de frappe côté Neo)* | +| `input/fluid/PlayerInteract**R**eactorFluidInput` | `PlayerInteract**e**ReactorFluidInput` *(faute de frappe)* | +| `irradiated/cat/CatLieOnBedGoal`, `CatSitOnBlockGoal` | `IrradiatedCatLieOnBedGoal`, `IrradiatedCatSitOnBlockGoal` | + +### 9.2 Features entièrement absentes + +**① Jauges DisplayLink du réacteur — 9 fichiers** ← *le manque le plus visible en jeu* + +`CNDisplaySources` · `AbstractReactorStatDisplaySource` · `HeatDisplaySource` · +`FuelDisplaySource` · `CoolerDisplaySource` · `LiquidLevelDisplaySource` · +`ReactorSizeDisplaySource` · `ReactorDisplayConstants` · `ReactorGaugeRenderer` + +NeoForge n'a que `ReactorSummary` + `ReactorSummaryDisplaySource`. Côté Forge, `CNBlocks` +attache en plus au contrôleur `.transform(displaySource(CNDisplaySources.HEAT))`, +`LIQUID_LEVEL`, etc. — **impossible de brancher un Display Link sur une statistique précise +du réacteur** côté NeoForge. Le code réacteur nécessaire (`ReactorDisplayState`, +`getMultiblockSize`, `getInputFluidManager`) est désormais en place, donc c'est un portage +direct sans prérequis. +*NeoForge a en plus `ReactorGaugeOverrides`, qui n'existe pas côté Forge.* + +**② Poudre de neige (Snow Powder) — 4 fichiers** + +`SnowPowderRecipe` · `SnowPowderRecipeGen` · `CNSnowPowderRecipeGen` · `FanSnowPowderCategory` (JEI) + +Recette de ventilateur complète, avec sa catégorie JEI. Rien de tout ça côté NeoForge. + +**③ Vache irradiée + abstraction des animaux — 5 fichiers** + +`IrradiatedCow` · `IrradiatedCowModel` · `IrradiatedCowRenderer` · `IrradiatedAnimal` · `AnimalUtil` + +NeoForge a le chat, le loup et le poulet, mais **pas la vache**. Il lui manque aussi la classe +de base `IrradiatedAnimal` et l'utilitaire `AnimalUtil` : ses animaux dupliquent donc la +logique que Forge a factorisée. C'est le dossier le plus divergent hors réacteur +(`IrradiatedCat` 384 lignes d'écart, `IrradiatedWolf` 333). + +**④ Worldgen piloté par config — 2 fichiers** + +`CNPlacementModifiers` · `ConfigPlacementFilter` + +Filtre de placement lisant la config : sans lui, les options de génération de minerai +(`CWorldGen`) ne sont probablement pas respectées. + +**⑤ Datagen de recettes — 2 fichiers** + +`CNDeployingRecipeGen` · `CNProcessingRecipeGen` — des recettes existent donc côté Forge +et pas côté NeoForge. + +**⑥ Radiation — 2 fichiers** + +`IRadiationCapability` · `RadiationProvider` + +Attendu : NeoForge 1.21 remplace les capabilities Forge par les *data attachments*. +`RadiationCapability` existe et diverge de 186 lignes. **À vérifier fonctionnellement** +plutôt qu'à porter à l'identique. + +**⑦ Compat externe — 2 fichiers** + +`Mods` · `AlexscaveCompat` — intégration Alex's Caves absente. + +**⑧ Mixins client — 2 fichiers** + +`CameraAccessor` · `GameRendererMixin` — effet visuel (probablement lié au shader de radiation). +NeoForge a à la place `AntiRadiationArmorTextureMixin`, qui n'existe pas côté Forge. + +**⑨ Divers — 6 fichiers** + +`CNOpenPipeEffectHandlers` (effets des tuyaux ouverts) · `UraniumOreItem` · +`CriterionTriggerBase` (base des advancements custom) · `RodsTooltipHandler` + +`RodsStats` (tooltips détaillés des barres) · `SimplexNoise` + +**⑩ Extracteur d'irradiation de biome — 1 fichier** + +`BiomeIrradationExtractorItem`. ⚠️ **Déjà porté en amont** : les 4 commits d'`origin/V2` que +cette branche n'a pas encore intégrés l'ajoutent sous le nom `BiomeIrradiationExtractorItem`. +Il apparaîtra automatiquement au merge. + +### 9.3 Portés mais non audités + +Ces fichiers existent des deux côtés mais divergent fortement. Un gros diff n'est **pas** +une preuve de feature manquante — l'API 1.21 en explique une grande part. Aucun n'a été +audité ligne à ligne, contrairement au domaine réacteur. + +| Fichier | Lignes divergentes | +|---|---| +| `CNBlocks` | 701 | +| `CNItems` | 457 | +| `foundation/data/recipe/CNStandardRecipeGen` | 395 | +| `foundation/advancement/CNAdvancement` | 394 | +| `content/contraptions/irradiated/cat/IrradiatedCat` | 384 | +| `compat/jei/CreateNuclearJEI` | 344 | +| `content/contraptions/irradiated/wolf/IrradiatedWolf` | 333 | +| `CNCreativeModeTabs` | 249 | +| `content/radiation/capability/RadiationCapability` | 186 | + +Le lot 9 a montré que ces écarts cachent de vrais bugs : la capacité de stress et les +`addLayer` manquants étaient tous les deux dans `CNBlocks`. + +### 9.4 Bug `extractFluids` ✅ CORRIGÉ dans les deux versions + +`ReactorInputFluidManager.extractFluids` ne décrémentait jamais `fluidNeeded` entre les +handlers : le montant demandé était traité comme un quota **par entrée** au lieu d'un total. +Un réacteur demandant 10 unités avec deux entrées de 10 les vidait toutes les deux, soit 20 +retirées. Plus le joueur posait d'entrées, plus le caloporteur disparaissait vite. + +Un garde `if (toExtract > 1)` avalait aussi les demandes d'exactement 1 unité — précisément +ce que demande `FluidConsumptionRateCalculator` en bas de la courbe de consommation. + +Corrigé à l'identique dans les deux repos (implémentations byte-identical). Les deux tests +`*_expectedContract` passent, les deux tests qui figeaient le comportement bogué sont +supprimés, et le javadoc — qui promettait « true si le montant complet a été extrait » alors +que le code renvoyait true sur toute extraction partielle — documente désormais le vrai +contrat. + +**`runGameTestServer` sort maintenant en code 0 des deux côtés** (Forge 28 tests, +NeoForge 29) : il peut servir de garde-fou CI. + +### 9.5 Ordre suggéré pour la suite + +1. ~~**Merger `origin/V2`**~~ ✅ fait — un seul conflit, l'import prévu dans `CNItems`. +2. **Jauges DisplayLink** — le plus visible, sans prérequis, le réacteur expose déjà tout. +3. ~~**Corriger `extractFluids`**~~ ✅ fait dans les deux repos. +4. **Worldgen config** — silencieux mais fausse la génération de minerai. +5. **Vache irradiée + `IrradiatedAnimal`/`AnimalUtil`** — permet aussi de dédupliquer chat/loup/poulet. +6. Snow Powder, datagen manquant, compat Alex's Caves, mixins client. +7. **Auditer `CNBlocks` et `CNItems`** en dernier, mais ne pas l'oublier : c'est là qu'étaient + cachés deux des six bugs du lot 9. \ No newline at end of file diff --git a/README.md b/README.md index b06cef0..4d9e131 100644 --- a/README.md +++ b/README.md @@ -14,11 +14,11 @@ If you want to help us with translations, we have opened a Crowdin project. Clic # Source Code ## Fabric -https://github.com/valentinlamine/CreateNuclearFabric +https://github.com/Create-Nuclear-Team/CreateNuclearFabric ## Forge -https://github.com/Giovanniricotta2002/CreateNuclearForge +https://github.com/Create-Nuclear-Team/CreateNuclearForge ## NeoForge diff --git a/potion.png b/potion.png deleted file mode 100644 index 624428c..0000000 Binary files a/potion.png and /dev/null differ diff --git a/run/command_history.txt b/run/command_history.txt index fefd5d6..bf4558a 100644 --- a/run/command_history.txt +++ b/run/command_history.txt @@ -29,6 +29,20 @@ /fillbiome ~-10 ~ ~-10 ~10 ~32 ~10 createnuclear:irradiated_land /gamemode survival /fillbiome ~-10 ~ ~-10 ~10 ~32 ~10 createnuclear:irradiated_land +/time set day +/gamemode spectator +/gamemode creative +/effect give Dev createnuclear:iodine +/effect give Dev createnuclear:radiation +/time set day +/effect clear Dev +/effect give Dev createnuclear:radiation +/effect give Dev createnuclear:radiation 30000 +/effect give Dev minecraft:regeneration infinite 255 true +/effect give Dev createnuclear:radiation +/effect give Dev createnuclear:radiation infinite +/effect give Dev minecraft:saturation infinite 255 true +/effect give Dev createnuclear:radiation infinite /effect give Dev createnuclear:radiation /playsound createnuclear:alarm/alarm /playsound createnuclear:alarm/alarm block Dev ~ ~ ~ 300 2 diff --git a/run/server.properties b/run/server.properties index 5a07c65..a1d1855 100644 --- a/run/server.properties +++ b/run/server.properties @@ -1,5 +1,5 @@ #Minecraft server properties -#Wed Oct 15 10:23:51 CEST 2025 +#Fri Aug 07 17:41:43 CEST 2026 accepts-transfers=false allow-flight=false allow-nether=true diff --git a/run/world/session.lock b/run/world/session.lock deleted file mode 100644 index 0d7e5f8..0000000 --- a/run/world/session.lock +++ /dev/null @@ -1 +0,0 @@ -☃ \ No newline at end of file diff --git a/src/generated/resources/.cache/b256105d8411632b0d585496ea8944a751a08034 b/src/generated/resources/.cache/15812fa7bc190aa8f0a8644088c7ff563925e506 similarity index 71% rename from src/generated/resources/.cache/b256105d8411632b0d585496ea8944a751a08034 rename to src/generated/resources/.cache/15812fa7bc190aa8f0a8644088c7ff563925e506 index 089aaff..af79f6e 100644 --- a/src/generated/resources/.cache/b256105d8411632b0d585496ea8944a751a08034 +++ b/src/generated/resources/.cache/15812fa7bc190aa8f0a8644088c7ff563925e506 @@ -1,17 +1,20 @@ -// 1.21.1 2026-08-04T14:18:44.8746949 Create's Processing Recipes +// 1.21.1 2026-08-07T17:41:15.0471152 CreateNuclear's Processing Recipes dda7e53d7d3c598bf66ec80b0b16f3c21ee18951 data/createnuclear/recipe/compacting/thorium_fluid_to_thorium_ingot.json d85b9bde5850d362e8e708e56c0d082f0fc3303f data/createnuclear/recipe/compacting/uranium_fluid_to_yellowcake.json a11a9d69b96e4cd1b748cab33dabd6a2964ad6ac data/createnuclear/recipe/crushing/coal.json 49a7aa4b43af0c778821d656ec594f80c362fab5 data/createnuclear/recipe/crushing/crushed_raw_uranium.json 2fbb42160178800bdce2bb0c4d5af85313475b26 data/createnuclear/recipe/crushing/nitrate.json +f972ded24a5cf2597fa14c4f604743d7e97d23e6 data/createnuclear/recipe/deploying/reactor_fluid_input_from_fluid_pipe_and_reactor_casing.json +f02f00a8c9503a8fdb2472064a279c98db670be7 data/createnuclear/recipe/deploying/reactor_output_from_shaft_and_reactor_casing.json +8401cab3f92d2465b26fe942969e6eb3afd2d551 data/createnuclear/recipe/deploying/reactor_rod_input_from_hopper_and_reactor_casing.json 371ca9bbe8682870942f1b41ec361b8a51be2f44 data/createnuclear/recipe/enriched/enriched_yellowcake.json fdf6d4fce42373c414542289a73f0e206ff438f2 data/createnuclear/recipe/enriched/enriching_campfire.json 0c8d7d7eae585213bb7b3bb8ca4749dabafc27a3 data/createnuclear/recipe/item_application/reactor_casing_from_steel_and_brass_casing.json -0fcfc5ad2396bb96a254b048f5b7103e6f28c4aa data/createnuclear/recipe/item_application/reactor_input_from_hopper_and_reactor_casing.json -be6e4c7d7a82de8b5398ec0e7b0b193568946f05 data/createnuclear/recipe/item_application/reactor_output_from_shaft_and_reactor_casing.json +c077c12bec6544815c584cd6aee851c892d43347 data/createnuclear/recipe/mixing/liquid_nitrogen.json 93bc6d7a40f2ccb89d8d5703c4b0f6c576d5caf8 data/createnuclear/recipe/mixing/steel.json e52ccded5e0955ab39320129fce8f78d50f52250 data/createnuclear/recipe/mixing/uranium_fluid.json 4e362c3fb35c7a4cf946fda91958fe6b84a59e04 data/createnuclear/recipe/pressing/graphene.json +0883773497d77efae10821cad8506662f7331e09 data/createnuclear/recipe/snow_powder/cooled_nitrogen_concentrate.json c1e0e107502e875ee5befc176335cbd1cef7e04d data/create/recipe/crushing/granite.json 724e99e5db2392644021999b8a694a9b20d8ea0e data/create/recipe/crushing/raw_copper.json ede5a70a121245622455815c9ebba7a22e569b55 data/create/recipe/crushing/raw_thorium.json diff --git a/src/generated/resources/.cache/2448df8ffe5180e3e6870fcd493b0e61d5ed7ce8 b/src/generated/resources/.cache/2448df8ffe5180e3e6870fcd493b0e61d5ed7ce8 new file mode 100644 index 0000000..d26aab4 --- /dev/null +++ b/src/generated/resources/.cache/2448df8ffe5180e3e6870fcd493b0e61d5ed7ce8 @@ -0,0 +1,2 @@ +// 1.21.1 2026-08-08T17:51:45.3883913 CreateNuclear Damage Type Tags +be715fe45426c240c8e1dd1b8c5fa096f8453d88 data/minecraft/tags/damage_type/no_knockback.json diff --git a/src/generated/resources/.cache/35ba5b194a3aa18fb27424c32b9e16e01b900a51 b/src/generated/resources/.cache/35ba5b194a3aa18fb27424c32b9e16e01b900a51 index b79e5fc..8a7e7b8 100644 --- a/src/generated/resources/.cache/35ba5b194a3aa18fb27424c32b9e16e01b900a51 +++ b/src/generated/resources/.cache/35ba5b194a3aa18fb27424c32b9e16e01b900a51 @@ -1,4 +1,4 @@ -// 1.21.1 2026-08-09T14:10:08.4533605 Registrate Provider for createnuclear [Registries, Data Maps, Recipes, Advancements, Loot Tables, Tags (blocks), Tags (enchantments), Tags (items), Tags (fluids), Tags (entity_types), generic_server_provider, Blockstates, Item models, Lang (en_us/en_ud), generic_client_provider] +// 1.21.1 2026-08-09T19:21:32.7561765 Registrate Provider for createnuclear [Registries, Data Maps, Recipes, Advancements, Loot Tables, Tags (blocks), Tags (enchantments), Tags (items), Tags (fluids), Tags (entity_types), generic_server_provider, Blockstates, Item models, Lang (en_us/en_ud), generic_client_provider] 0b2c76c9cff305eacfdfb57c4b703f94ca6dc903 assets/createnuclear/blockstates/autunite.json 5d06bc544bcbeb7443178d66955bb5101cf696da assets/createnuclear/blockstates/autunite_pillar.json 5b375444ac5057e63776f4991589c57824574b4a assets/createnuclear/blockstates/cut_autunite.json @@ -30,7 +30,7 @@ f628f8c7aa1e150ad2b5a3ce87dbe7f00b462e74 assets/createnuclear/blockstates/polish 63aaf78c3e0c16c2f37b069c5392e3d1a93ac058 assets/createnuclear/blockstates/raw_uranium_block.json 1c9953bedf6cec725a6db7a199d826dcdfe9dfa7 assets/createnuclear/blockstates/reactor_alarm.json 0fbc9288622efc18fe39b6c8ba89e7e2281dee26 assets/createnuclear/blockstates/reactor_casing.json -60ca260d5b5bcd975c0a3918b461748786cde95e assets/createnuclear/blockstates/reactor_controller.json +0b5bd864f1e3b0c6b0ce868245da96fd8070fb21 assets/createnuclear/blockstates/reactor_controller.json 3c259041f317bb8868fb4f177c457fc12b8d28f5 assets/createnuclear/blockstates/reactor_cooler.json 3cced3f7d35371f1881b0485ee324788adcd2c74 assets/createnuclear/blockstates/reactor_core.json 0d8ab9ccc729b8e78539a2fab7b52b6be0afb24b assets/createnuclear/blockstates/reactor_fluid_input.json @@ -48,6 +48,11 @@ b9c915389ea6cf8338d20c48e908591f8baa2108 assets/createnuclear/blockstates/small_ 643a4d44b491a84d0d26e1a8944dffc40e83ce14 assets/createnuclear/blockstates/thorium_ore.json de2884054a21315d8b167f64b206be331d388420 assets/createnuclear/blockstates/uranium.json 4557f548df100ae1d05e3b5711797225f6a39673 assets/createnuclear/blockstates/uranium_ore.json +<<<<<<< HEAD +33e9dcac40fcb32a46343f546f0aad2e38fe96f0 assets/createnuclear/lang/en_ud.json +28e64c6658311d18a29cf635ef2821b03e53f8a7 assets/createnuclear/lang/en_us.json +======= +>>>>>>> V2 a8495d2cd5e38213683becaf1c84b2852f8b06f5 assets/createnuclear/models/block/autunite_natural_0.json 29394a22ee8655dc22ea51563411505fbcfd2dab assets/createnuclear/models/block/autunite_natural_1.json 9e4f83de1ca7a9ab5363f43fc7c3f7f4ec6164d5 assets/createnuclear/models/block/autunite_natural_2.json @@ -95,6 +100,9 @@ b950ac6ca2df77ffe3abd02e13c38567602e5536 assets/createnuclear/models/block/polis cf8b4d604330762358b21b6e079aaab1eb2987f4 assets/createnuclear/models/block/raw_lead_block.json de9b4dee9580161263fa8020d326ac5fe4e38ed4 assets/createnuclear/models/block/raw_thorium_block.json 561c9f9a4e153fc648a3c7224812c71c8ac2da5c assets/createnuclear/models/block/raw_uranium_block.json +dedbaa09b31d53c446144816edf216d4c20253df assets/createnuclear/models/block/reactor/controller/off.json +988faa4b7af770a8256ea25247e333cbc9d14eca assets/createnuclear/models/block/reactor/controller/on.json +d58fcfe99b438b77b4a2dd8d0dd9e1f35d037bae assets/createnuclear/models/block/reactor/controller/standby.json d5acc57be1c8ccc3e61c40cf11856933d35803b3 assets/createnuclear/models/block/reactor_alarm.json e5cd1c3875c277d3770e723672bd290cb87406b3 assets/createnuclear/models/block/reinforced_glass.json e94d3eb865d17234b18241d89acdec2b10a1d21a assets/createnuclear/models/block/small_autunite_bricks.json @@ -602,6 +610,7 @@ fdace7ee58d9860d4549876326a44645f9df2a10 data/createnuclear/recipe/smithing/defa 15960ecb10eeaba53bc6fe3dcc382853c5795256 data/createnuclear/recipe/smithing/default_anti_radiation_leggings_yellow.json a7d4278d8a39649c8b658ad2f3f1d092cb012bdf data/createnuclear/tags/block/enriching_fire_base_blocks.json 509b5baa813ae0dc3345fd97754e4088132359ec data/createnuclear/tags/block/fan_processing_catalysts/enriched.json +bc8701774f57ab000a8f0f1d390702175a6cdbb8 data/createnuclear/tags/block/fan_processing_catalysts/snow_powder.json 925cb97bc13b3122fe17e59ff3ec2fe4d1eaacd5 data/createnuclear/tags/block/lead_ores.json baae2dfd7e19770072d6261ec68ce157a32be80c data/createnuclear/tags/block/thorium_ores.json bad88bea18fec41c7ad6fa4d396feacc0346a137 data/createnuclear/tags/block/uranium_ores.json @@ -613,7 +622,7 @@ bce34f53cb2d00f7dee84683e8fdd429dabbcc1d data/createnuclear/tags/item/anti_radia 35133e95f1c8fdd7a1c21afcc231fc0bffefb9a8 data/createnuclear/tags/item/anti_radiation_helmet_full_dye.json 2e4d08932c4a84863f0198eb2845f80d8fda1221 data/createnuclear/tags/item/cloth.json 8e6598f14a72a61cbe1e5ed2aaea229fb2dd74e6 data/createnuclear/tags/item/cooler.json -c5587bb72c54f59e9f9a4cd2c3e6bedb78ee198d data/createnuclear/tags/item/fuel.json +d534438c9314e8b014bf1796060dd801f4ef4d4e data/createnuclear/tags/item/fuel.json 925cb97bc13b3122fe17e59ff3ec2fe4d1eaacd5 data/createnuclear/tags/item/lead_ores.json 53e435b1776a11737c64eac604fda6cd48acfc4e data/createnuclear/tags/item/stone_types/autunite.json baae2dfd7e19770072d6261ec68ce157a32be80c data/createnuclear/tags/item/thorium_ores.json diff --git a/src/generated/resources/.cache/9fb1092f32d4fcbf9e061ffd718d4ec689c6c95e b/src/generated/resources/.cache/9fb1092f32d4fcbf9e061ffd718d4ec689c6c95e index b90e23b..de653d5 100644 --- a/src/generated/resources/.cache/9fb1092f32d4fcbf9e061ffd718d4ec689c6c95e +++ b/src/generated/resources/.cache/9fb1092f32d4fcbf9e061ffd718d4ec689c6c95e @@ -1,7 +1,8 @@ -// 1.21.1 2025-11-11T13:47:08.3478244 Recipes +// 1.21.1 2026-08-07T17:41:15.0625726 Recipes 817eb6c9cc23cc6579dfe3793a9b4c82917d0dea data/createnuclear/advancement/recipes/misc/blasting/lead_ingot_for_lead.json 9bdc07b5aa3b5309fd0023b267e4a861adc0f1b3 data/createnuclear/advancement/recipes/misc/blasting/lead_ingot_for_lead_ore.json 531e75db303ab55e6fee923af2088a7fd1ee3123 data/createnuclear/advancement/recipes/misc/blasting/lead_ingot_for_raw_lead.json +f22e3f793d8adb3916bd4c2aa64c63f11c8bb74e data/createnuclear/advancement/recipes/misc/blasting/nitrogen_concentrate_for_nitrogen_concentrate.json 010aef79a85e1e86d5f0ca20f3bc9673c520425a data/createnuclear/advancement/recipes/misc/blasting/raw_uranium_for_uranium_ore.json 2d5a511ce08e8868caef61b8ccba1d47e8c2e8b8 data/createnuclear/advancement/recipes/misc/crafting/crafting/lead_block_from_compacting.json f63c279b2fdac7e2661280105d5a3840c1885b5c data/createnuclear/advancement/recipes/misc/crafting/crafting/lead_ingot_from_compacting.json @@ -16,10 +17,12 @@ c72b09dcbf03429201f475fc848debdba898931a data/createnuclear/advancement/recipes/ 572db9bd6741ae0f5fe035ac53ceec45c869eb05 data/createnuclear/advancement/recipes/misc/smelting/lead_ingot_for_lead.json d468ba59b25ddbffeb5a3a1e298f02e93246bc87 data/createnuclear/advancement/recipes/misc/smelting/lead_ingot_for_lead_ore.json 77cdcf8ebae5a8146f7c1ceb39567a9f83ab4c62 data/createnuclear/advancement/recipes/misc/smelting/lead_ingot_for_raw_lead.json +6c00d3701cf637fa20148b4740856769057c9fd6 data/createnuclear/advancement/recipes/misc/smelting/nitrogen_concentrate_for_nitrogen_concentrate.json 181b98bed073bdbbf3a20bb3d0d886057359da9e data/createnuclear/advancement/recipes/misc/smelting/raw_uranium_for_uranium_ore.json 6b6936bf71a27ec4e5fbad575971f7a87685e03f data/createnuclear/recipe/blasting/lead_ingot_for_lead.json 2e8b61aeb37329a5a1d20a4032cfa0972b3aa35b data/createnuclear/recipe/blasting/lead_ingot_for_lead_ore.json caf2536990c02b52965faf41000479d173ae5ce3 data/createnuclear/recipe/blasting/lead_ingot_for_raw_lead.json +6a46fd45b7623bb32be4be34ce2276cfb05e288c data/createnuclear/recipe/blasting/nitrogen_concentrate_for_nitrogen_concentrate.json 43584c0fb02633ea6ada2b24cea19560ac9e5498 data/createnuclear/recipe/blasting/raw_uranium_for_uranium_ore.json dbca4445a4f024da2c2cc7c88e885639964800d4 data/createnuclear/recipe/crafting/crafting/lead_block_from_compacting.json 9c8258c26e0028851e583b7278122d3e36bcd77f data/createnuclear/recipe/crafting/crafting/lead_ingot_from_compacting.json @@ -34,4 +37,5 @@ fd535efce0c108d879e5a1f13aface5586fc3127 data/createnuclear/recipe/crafting/craf b63c594645138909f08b9d2e587b08534c58b319 data/createnuclear/recipe/smelting/lead_ingot_for_lead.json 8dc067a59870db19ecdac45dca24aedfe2f89eb4 data/createnuclear/recipe/smelting/lead_ingot_for_lead_ore.json 4608b430898214bae0648f7a2383bd9d49aeac8e data/createnuclear/recipe/smelting/lead_ingot_for_raw_lead.json +78a1035e66960224af77af912804f14d15c3cd30 data/createnuclear/recipe/smelting/nitrogen_concentrate_for_nitrogen_concentrate.json e4e201f37ebd694e4cc9a0acc6b76231504d0a4b data/createnuclear/recipe/smelting/raw_uranium_for_uranium_ore.json diff --git a/src/generated/resources/.cache/fed443662143001aeb5341375f6f5ef31d10baea b/src/generated/resources/.cache/fed443662143001aeb5341375f6f5ef31d10baea index 2fe3402..4d8e4b3 100644 --- a/src/generated/resources/.cache/fed443662143001aeb5341375f6f5ef31d10baea +++ b/src/generated/resources/.cache/fed443662143001aeb5341375f6f5ef31d10baea @@ -1,7 +1,8 @@ -// 1.21.1 2026-08-05T14:36:36.8031552 CreateNuclear Generated Registry Entries -aa1897ff27a6aa83e2efbee29e12ad6dab2f6a0a data/createnuclear/createnuclear/fluids/type/fallback.json -afee3d8f15bf8285e175f73befba7b46fdc94b19 data/createnuclear/createnuclear/fluids/type/water.json -706132bf26d87ed4a83f8d5f8793ab69a60b98fe data/createnuclear/createnuclear/rods/type/fallback.json +// 1.21.1 2026-08-08T14:42:18.3436592 CreateNuclear Generated Registry Entries +46e263393c1672a180bc12b99f9e39adf22e66b5 data/createnuclear/createnuclear/fluids/type/fallback.json +2837d5ecbd5735f39c1fac545469486e09c502e0 data/createnuclear/createnuclear/fluids/type/nitrogen.json +152a58adb9f86e1c9fac62fd2095ba8413142568 data/createnuclear/createnuclear/fluids/type/water.json +15403b1cee46d300af2247a08485c04ccd80cfc0 data/createnuclear/createnuclear/rods/type/fallback.json 74de56c37a28ab632a6ffc161475bcec85de68e2 data/createnuclear/damage_type/fan_radiation.json dfb5a22341ff4202e85ca8cdf9b6ec95c6e280d1 data/createnuclear/damage_type/radiation.json 5b42761cb26db47a6ad47ff3c84820f9fd541477 data/createnuclear/neoforge/biome_modifier/lead_ore.json @@ -19,8 +20,8 @@ b0e1ee7314412507fd7346cb48b29d105f6186ad data/createnuclear/worldgen/configured_ 59d61c02881d710b0426a0955f6945b7a12d219f data/createnuclear/worldgen/density_function/irradiated/final_density.json 7f5f110a211b82bc22627b2f901c5f3c6a1265dd data/createnuclear/worldgen/noise/irradiated/erosion.json ea836bafce53bcb743cb2de5ba38271291483865 data/createnuclear/worldgen/noise_settings/irradiated_noise.json -b5a36f76a9c55adc22eaafc5e7de40896777115e data/createnuclear/worldgen/placed_feature/lead_ore.json -32be128c45153cbbbb0b45cb2cc0d45d03864b47 data/createnuclear/worldgen/placed_feature/nitrate_ore.json -f26be417cb34461ecb180e77a9a749eaaf4c9880 data/createnuclear/worldgen/placed_feature/striated_ores_overworld.json -73b8f3c6a9d78055f31e64cad12b02bf25cbd3d1 data/createnuclear/worldgen/placed_feature/thorium_ore.json -e3bd0bd52f43a5df9eef0b23007da524628b17e7 data/createnuclear/worldgen/placed_feature/uranium_ore.json +5a035ad45f9039b81e3e55160da3127fe7079ba9 data/createnuclear/worldgen/placed_feature/lead_ore.json +e69f61cbb4bf2c50a6123ddc87c024c801bb78d1 data/createnuclear/worldgen/placed_feature/nitrate_ore.json +cc85909071cab20eacbe2c34887c6ed6a02dc51e data/createnuclear/worldgen/placed_feature/striated_ores_overworld.json +0762472dada74fb4592de79d598f8d45d955ea2c data/createnuclear/worldgen/placed_feature/thorium_ore.json +a18aa32c139932d0023abe6caa722992b167114b data/createnuclear/worldgen/placed_feature/uranium_ore.json diff --git a/src/generated/resources/assets/createnuclear/blockstates/reactor_controller.json b/src/generated/resources/assets/createnuclear/blockstates/reactor_controller.json index 28a903c..cbd0a7c 100644 --- a/src/generated/resources/assets/createnuclear/blockstates/reactor_controller.json +++ b/src/generated/resources/assets/createnuclear/blockstates/reactor_controller.json @@ -1,33 +1,63 @@ { "variants": { - "assembled=false,facing=east": { - "model": "createnuclear:block/reactor/controller/block", + "active=false,assembled=false,facing=east": { + "model": "createnuclear:block/reactor/controller/off", "y": 270 }, - "assembled=false,facing=north": { - "model": "createnuclear:block/reactor/controller/block", + "active=false,assembled=false,facing=north": { + "model": "createnuclear:block/reactor/controller/off", "y": 180 }, - "assembled=false,facing=south": { - "model": "createnuclear:block/reactor/controller/block" + "active=false,assembled=false,facing=south": { + "model": "createnuclear:block/reactor/controller/off" }, - "assembled=false,facing=west": { - "model": "createnuclear:block/reactor/controller/block", + "active=false,assembled=false,facing=west": { + "model": "createnuclear:block/reactor/controller/off", "y": 90 }, - "assembled=true,facing=east": { - "model": "createnuclear:block/reactor/controller/block", + "active=false,assembled=true,facing=east": { + "model": "createnuclear:block/reactor/controller/standby", "y": 270 }, - "assembled=true,facing=north": { - "model": "createnuclear:block/reactor/controller/block", + "active=false,assembled=true,facing=north": { + "model": "createnuclear:block/reactor/controller/standby", "y": 180 }, - "assembled=true,facing=south": { - "model": "createnuclear:block/reactor/controller/block" + "active=false,assembled=true,facing=south": { + "model": "createnuclear:block/reactor/controller/standby" }, - "assembled=true,facing=west": { - "model": "createnuclear:block/reactor/controller/block", + "active=false,assembled=true,facing=west": { + "model": "createnuclear:block/reactor/controller/standby", + "y": 90 + }, + "active=true,assembled=false,facing=east": { + "model": "createnuclear:block/reactor/controller/off", + "y": 270 + }, + "active=true,assembled=false,facing=north": { + "model": "createnuclear:block/reactor/controller/off", + "y": 180 + }, + "active=true,assembled=false,facing=south": { + "model": "createnuclear:block/reactor/controller/off" + }, + "active=true,assembled=false,facing=west": { + "model": "createnuclear:block/reactor/controller/off", + "y": 90 + }, + "active=true,assembled=true,facing=east": { + "model": "createnuclear:block/reactor/controller/on", + "y": 270 + }, + "active=true,assembled=true,facing=north": { + "model": "createnuclear:block/reactor/controller/on", + "y": 180 + }, + "active=true,assembled=true,facing=south": { + "model": "createnuclear:block/reactor/controller/on" + }, + "active=true,assembled=true,facing=west": { + "model": "createnuclear:block/reactor/controller/on", "y": 90 } } diff --git a/src/generated/resources/assets/createnuclear/lang/en_ud.json b/src/generated/resources/assets/createnuclear/lang/en_ud.json index 61fbb4c..6e48f08 100644 --- a/src/generated/resources/assets/createnuclear/lang/en_ud.json +++ b/src/generated/resources/assets/createnuclear/lang/en_ud.json @@ -250,6 +250,14 @@ "createnuclear.ponder.reactor_t3.text_7": "ʇı ʇɹɐʇs oʇ sǝob ʇuıɹdǝnןq ǝɥʇ ǝɹǝɥʍ ǝɔɐןd ǝɥʇ puɐ 'ɹoʇɔɐǝɹ ǝɥʇ ɟo uıɐɹq :ɹǝןןoɹʇuoƆ", "createnuclear.ponder.reactor_t3.text_8": "ㄥ ɹooןℲ", "createnuclear.ponder.reactor_t3.text_9": "8 ɹooןℲ", + "createnuclear.reactor.debug.label.alarm": "ɯɹɐןⱯ", + "createnuclear.reactor.debug.label.fluid_input": "ʇnduI pınןℲ", + "createnuclear.reactor.debug.label.input": "ʇnduI", + "createnuclear.reactor.debug.label.output": "ʇndʇnO", + "createnuclear.reactor.debug.registered_position": "%s @ pǝɹǝʇsıbǝɹ ", + "createnuclear.reactor.debug.summary": "pıןɐʌ %s 'pǝɹǝʇsıbǝɹ %s :%s", + "createnuclear.reactor.debug.title": "--- sɔıʇsoubɐıp ɹoʇɔɐǝᴚ ---", + "createnuclear.reactor.debug.valid_position": "%s @ pıןɐʌ ", "createnuclear.snow_powder.fan.recipe": "uɐɟ ɔıuǝboʎɹƆ", "createnuclear.subtitle.alarm/alarm": "dooꞀ ɯɹɐןⱯ ɹoʇɔɐǝᴚ", "createnuclear.subtitle.alarm/reactor_alarm": "ɯɹɐןⱯ ɹoʇɔɐǝᴚ", @@ -285,6 +293,7 @@ "createnuclear.tooltip.item.fuel.rod": " :poᴚ ןǝnℲ", "createnuclear.tooltip.item.unknown.rod": " :uʍouʞu∩", "createnuclear.tooltip.proximityRodHeat": "Ὁ ɟᄅ˙% :ʇɐǝH poᴚ ʎʇıɯıxoɹԀ", + "createnuclear.tooltip.ratio": "%d :oıʇɐɹ ɹoɟ ǝnןɐΛ poᴚ", "createnuclear.tooltip.rodTimer": "ʞɔıʇ %d :uoıʇɐɹnᗡ poᴚ", "createnuclear.tooltip.statRod": ":uɹǝʇʇɐԀ ʇɐʇS poᴚ", "createnuclear.tooltip.typeRod": "%s :ǝdʎ⟘ poᴚ", diff --git a/src/generated/resources/assets/createnuclear/lang/en_us.json b/src/generated/resources/assets/createnuclear/lang/en_us.json index 64e96f3..72adb27 100644 --- a/src/generated/resources/assets/createnuclear/lang/en_us.json +++ b/src/generated/resources/assets/createnuclear/lang/en_us.json @@ -250,6 +250,14 @@ "createnuclear.ponder.reactor_t3.text_7": "Controller: brain of the reactor, and the place where the blueprint goes to start it", "createnuclear.ponder.reactor_t3.text_8": "Floor 7", "createnuclear.ponder.reactor_t3.text_9": "Floor 8", + "createnuclear.reactor.debug.label.alarm": "Alarm", + "createnuclear.reactor.debug.label.fluid_input": "Fluid Input", + "createnuclear.reactor.debug.label.input": "Input", + "createnuclear.reactor.debug.label.output": "Output", + "createnuclear.reactor.debug.registered_position": " registered @ %s", + "createnuclear.reactor.debug.summary": "%s: %s registered, %s valid", + "createnuclear.reactor.debug.title": "--- Reactor diagnostics ---", + "createnuclear.reactor.debug.valid_position": " valid @ %s", "createnuclear.snow_powder.fan.recipe": "Cryogenic fan", "createnuclear.subtitle.alarm/alarm": "Reactor Alarm Loop", "createnuclear.subtitle.alarm/reactor_alarm": "Reactor Alarm", @@ -285,6 +293,7 @@ "createnuclear.tooltip.item.fuel.rod": "Fuel Rod: ", "createnuclear.tooltip.item.unknown.rod": "Unknown: ", "createnuclear.tooltip.proximityRodHeat": "Proximity Rod Heat: %.2f Q", + "createnuclear.tooltip.ratio": "Rod Value for ratio: %d", "createnuclear.tooltip.rodTimer": "Rod Duration: %d tick", "createnuclear.tooltip.statRod": "Rod Stat Pattern:", "createnuclear.tooltip.typeRod": "Rod Type: %s", diff --git a/src/generated/resources/assets/createnuclear/models/block/reactor/controller/off.json b/src/generated/resources/assets/createnuclear/models/block/reactor/controller/off.json new file mode 100644 index 0000000..fb38b94 --- /dev/null +++ b/src/generated/resources/assets/createnuclear/models/block/reactor/controller/off.json @@ -0,0 +1,7 @@ +{ + "parent": "createnuclear:block/reactor/controller/block", + "textures": { + "1": "createnuclear:block/reactor/controller/controller_panel_off", + "particle": "createnuclear:block/reactor/controller/controller_panel_off" + } +} \ No newline at end of file diff --git a/src/generated/resources/assets/createnuclear/models/block/reactor/controller/on.json b/src/generated/resources/assets/createnuclear/models/block/reactor/controller/on.json new file mode 100644 index 0000000..423b3f4 --- /dev/null +++ b/src/generated/resources/assets/createnuclear/models/block/reactor/controller/on.json @@ -0,0 +1,7 @@ +{ + "parent": "createnuclear:block/reactor/controller/block", + "textures": { + "1": "createnuclear:block/reactor/controller/controller_panel_on", + "particle": "createnuclear:block/reactor/controller/controller_panel_on" + } +} \ No newline at end of file diff --git a/src/generated/resources/assets/createnuclear/models/block/reactor/controller/standby.json b/src/generated/resources/assets/createnuclear/models/block/reactor/controller/standby.json new file mode 100644 index 0000000..59dc81d --- /dev/null +++ b/src/generated/resources/assets/createnuclear/models/block/reactor/controller/standby.json @@ -0,0 +1,7 @@ +{ + "parent": "createnuclear:block/reactor/controller/block", + "textures": { + "1": "createnuclear:block/reactor/controller/controller_panel_standby", + "particle": "createnuclear:block/reactor/controller/controller_panel_standby" + } +} \ No newline at end of file diff --git a/src/generated/resources/data/createnuclear/advancement/recipes/misc/blasting/nitrogen_concentrate_for_nitrogen_concentrate.json b/src/generated/resources/data/createnuclear/advancement/recipes/misc/blasting/nitrogen_concentrate_for_nitrogen_concentrate.json new file mode 100644 index 0000000..21ae941 --- /dev/null +++ b/src/generated/resources/data/createnuclear/advancement/recipes/misc/blasting/nitrogen_concentrate_for_nitrogen_concentrate.json @@ -0,0 +1,32 @@ +{ + "parent": "minecraft:recipes/root", + "criteria": { + "has_item": { + "conditions": { + "items": [ + { + "items": "createnuclear:nitrate" + } + ] + }, + "trigger": "minecraft:inventory_changed" + }, + "has_the_recipe": { + "conditions": { + "recipe": "createnuclear:blasting/nitrogen_concentrate_for_nitrogen_concentrate" + }, + "trigger": "minecraft:recipe_unlocked" + } + }, + "requirements": [ + [ + "has_the_recipe", + "has_item" + ] + ], + "rewards": { + "recipes": [ + "createnuclear:blasting/nitrogen_concentrate_for_nitrogen_concentrate" + ] + } +} \ No newline at end of file diff --git a/src/generated/resources/data/createnuclear/advancement/recipes/misc/smelting/nitrogen_concentrate_for_nitrogen_concentrate.json b/src/generated/resources/data/createnuclear/advancement/recipes/misc/smelting/nitrogen_concentrate_for_nitrogen_concentrate.json new file mode 100644 index 0000000..e1208f4 --- /dev/null +++ b/src/generated/resources/data/createnuclear/advancement/recipes/misc/smelting/nitrogen_concentrate_for_nitrogen_concentrate.json @@ -0,0 +1,32 @@ +{ + "parent": "minecraft:recipes/root", + "criteria": { + "has_item": { + "conditions": { + "items": [ + { + "items": "createnuclear:nitrate" + } + ] + }, + "trigger": "minecraft:inventory_changed" + }, + "has_the_recipe": { + "conditions": { + "recipe": "createnuclear:smelting/nitrogen_concentrate_for_nitrogen_concentrate" + }, + "trigger": "minecraft:recipe_unlocked" + } + }, + "requirements": [ + [ + "has_the_recipe", + "has_item" + ] + ], + "rewards": { + "recipes": [ + "createnuclear:smelting/nitrogen_concentrate_for_nitrogen_concentrate" + ] + } +} \ No newline at end of file diff --git a/src/generated/resources/data/createnuclear/createnuclear/fluids/type/fallback.json b/src/generated/resources/data/createnuclear/createnuclear/fluids/type/fallback.json index 4c5a3fa..ea208e2 100644 --- a/src/generated/resources/data/createnuclear/createnuclear/fluids/type/fallback.json +++ b/src/generated/resources/data/createnuclear/createnuclear/fluids/type/fallback.json @@ -1,5 +1,5 @@ { "efficiency": -1, - "fluids": [], + "fluid": "minecraft:empty", "maxHeat": -1 } \ No newline at end of file diff --git a/src/generated/resources/data/createnuclear/createnuclear/fluids/type/nitrogen.json b/src/generated/resources/data/createnuclear/createnuclear/fluids/type/nitrogen.json new file mode 100644 index 0000000..62e15f9 --- /dev/null +++ b/src/generated/resources/data/createnuclear/createnuclear/fluids/type/nitrogen.json @@ -0,0 +1,5 @@ +{ + "efficiency": 100, + "fluid": "createnuclear:nitrogen", + "maxHeat": 8196 +} \ No newline at end of file diff --git a/src/generated/resources/data/createnuclear/createnuclear/fluids/type/water.json b/src/generated/resources/data/createnuclear/createnuclear/fluids/type/water.json index b8ae757..5a3ff58 100644 --- a/src/generated/resources/data/createnuclear/createnuclear/fluids/type/water.json +++ b/src/generated/resources/data/createnuclear/createnuclear/fluids/type/water.json @@ -1,5 +1,5 @@ { "efficiency": 1000, - "fluids": "minecraft:water", + "fluid": "minecraft:water", "maxHeat": 2048 } \ No newline at end of file diff --git a/src/generated/resources/data/createnuclear/createnuclear/rods/type/fallback.json b/src/generated/resources/data/createnuclear/createnuclear/rods/type/fallback.json index 4e42f40..57c68b4 100644 --- a/src/generated/resources/data/createnuclear/createnuclear/rods/type/fallback.json +++ b/src/generated/resources/data/createnuclear/createnuclear/rods/type/fallback.json @@ -1,7 +1,7 @@ { "type": "NONE", "baseRodHeat": -1, - "items": [], + "item": "minecraft:air", "proximityRodHeat": -1.0, "rodTimer": -1 } \ No newline at end of file diff --git a/src/generated/resources/data/createnuclear/recipe/blasting/nitrogen_concentrate_for_nitrogen_concentrate.json b/src/generated/resources/data/createnuclear/recipe/blasting/nitrogen_concentrate_for_nitrogen_concentrate.json new file mode 100644 index 0000000..ccf99fe --- /dev/null +++ b/src/generated/resources/data/createnuclear/recipe/blasting/nitrogen_concentrate_for_nitrogen_concentrate.json @@ -0,0 +1,13 @@ +{ + "type": "minecraft:blasting", + "category": "misc", + "cookingtime": 100, + "experience": 0.1, + "ingredient": { + "item": "createnuclear:nitrate" + }, + "result": { + "count": 1, + "id": "createnuclear:nitrogen_concentrate" + } +} \ No newline at end of file diff --git a/src/generated/resources/data/createnuclear/recipe/deploying/reactor_fluid_input_from_fluid_pipe_and_reactor_casing.json b/src/generated/resources/data/createnuclear/recipe/deploying/reactor_fluid_input_from_fluid_pipe_and_reactor_casing.json new file mode 100644 index 0000000..59cf97a --- /dev/null +++ b/src/generated/resources/data/createnuclear/recipe/deploying/reactor_fluid_input_from_fluid_pipe_and_reactor_casing.json @@ -0,0 +1,16 @@ +{ + "type": "create:deploying", + "ingredients": [ + { + "item": "createnuclear:reactor_casing" + }, + { + "item": "create:fluid_pipe" + } + ], + "results": [ + { + "id": "createnuclear:reactor_fluid_input" + } + ] +} \ No newline at end of file diff --git a/src/generated/resources/data/createnuclear/recipe/item_application/reactor_output_from_shaft_and_reactor_casing.json b/src/generated/resources/data/createnuclear/recipe/deploying/reactor_output_from_shaft_and_reactor_casing.json similarity index 84% rename from src/generated/resources/data/createnuclear/recipe/item_application/reactor_output_from_shaft_and_reactor_casing.json rename to src/generated/resources/data/createnuclear/recipe/deploying/reactor_output_from_shaft_and_reactor_casing.json index 8a7ee3f..31a4ae0 100644 --- a/src/generated/resources/data/createnuclear/recipe/item_application/reactor_output_from_shaft_and_reactor_casing.json +++ b/src/generated/resources/data/createnuclear/recipe/deploying/reactor_output_from_shaft_and_reactor_casing.json @@ -1,5 +1,5 @@ { - "type": "create:item_application", + "type": "create:deploying", "ingredients": [ { "item": "createnuclear:reactor_casing" diff --git a/src/generated/resources/data/createnuclear/recipe/item_application/reactor_input_from_hopper_and_reactor_casing.json b/src/generated/resources/data/createnuclear/recipe/deploying/reactor_rod_input_from_hopper_and_reactor_casing.json similarity index 84% rename from src/generated/resources/data/createnuclear/recipe/item_application/reactor_input_from_hopper_and_reactor_casing.json rename to src/generated/resources/data/createnuclear/recipe/deploying/reactor_rod_input_from_hopper_and_reactor_casing.json index a82504d..1c36012 100644 --- a/src/generated/resources/data/createnuclear/recipe/item_application/reactor_input_from_hopper_and_reactor_casing.json +++ b/src/generated/resources/data/createnuclear/recipe/deploying/reactor_rod_input_from_hopper_and_reactor_casing.json @@ -1,5 +1,5 @@ { - "type": "create:item_application", + "type": "create:deploying", "ingredients": [ { "item": "createnuclear:reactor_casing" diff --git a/src/generated/resources/data/createnuclear/recipe/mixing/liquid_nitrogen.json b/src/generated/resources/data/createnuclear/recipe/mixing/liquid_nitrogen.json new file mode 100644 index 0000000..fbd6d3f --- /dev/null +++ b/src/generated/resources/data/createnuclear/recipe/mixing/liquid_nitrogen.json @@ -0,0 +1,18 @@ +{ + "type": "create:mixing", + "ingredients": [ + { + "item": "createnuclear:cooled_nitrogen_concentrate" + }, + { + "item": "minecraft:ice" + } + ], + "processing_time": 20, + "results": [ + { + "amount": 100, + "id": "createnuclear:nitrogen" + } + ] +} \ No newline at end of file diff --git a/src/generated/resources/data/createnuclear/recipe/smelting/nitrogen_concentrate_for_nitrogen_concentrate.json b/src/generated/resources/data/createnuclear/recipe/smelting/nitrogen_concentrate_for_nitrogen_concentrate.json new file mode 100644 index 0000000..7af575d --- /dev/null +++ b/src/generated/resources/data/createnuclear/recipe/smelting/nitrogen_concentrate_for_nitrogen_concentrate.json @@ -0,0 +1,13 @@ +{ + "type": "minecraft:smelting", + "category": "misc", + "cookingtime": 200, + "experience": 0.1, + "ingredient": { + "item": "createnuclear:nitrate" + }, + "result": { + "count": 1, + "id": "createnuclear:nitrogen_concentrate" + } +} \ No newline at end of file diff --git a/src/generated/resources/data/createnuclear/recipe/snow_powder/cooled_nitrogen_concentrate.json b/src/generated/resources/data/createnuclear/recipe/snow_powder/cooled_nitrogen_concentrate.json new file mode 100644 index 0000000..c5cbc83 --- /dev/null +++ b/src/generated/resources/data/createnuclear/recipe/snow_powder/cooled_nitrogen_concentrate.json @@ -0,0 +1,13 @@ +{ + "type": "createnuclear:snow_powder", + "ingredients": [ + { + "item": "createnuclear:nitrogen_concentrate" + } + ], + "results": [ + { + "id": "createnuclear:cooled_nitrogen_concentrate" + } + ] +} \ No newline at end of file diff --git a/src/generated/resources/data/createnuclear/tags/block/fan_processing_catalysts/snow_powder.json b/src/generated/resources/data/createnuclear/tags/block/fan_processing_catalysts/snow_powder.json new file mode 100644 index 0000000..38b8ecd --- /dev/null +++ b/src/generated/resources/data/createnuclear/tags/block/fan_processing_catalysts/snow_powder.json @@ -0,0 +1,5 @@ +{ + "values": [ + "minecraft:powder_snow" + ] +} \ No newline at end of file diff --git a/src/generated/resources/data/createnuclear/tags/item/fuel.json b/src/generated/resources/data/createnuclear/tags/item/fuel.json index 951757d..9efea37 100644 --- a/src/generated/resources/data/createnuclear/tags/item/fuel.json +++ b/src/generated/resources/data/createnuclear/tags/item/fuel.json @@ -1,5 +1,6 @@ { "values": [ - "createnuclear:uranium_rod" + "createnuclear:uranium_rod", + "createnuclear:thorium_rod" ] } \ No newline at end of file diff --git a/src/generated/resources/data/createnuclear/worldgen/placed_feature/lead_ore.json b/src/generated/resources/data/createnuclear/worldgen/placed_feature/lead_ore.json index 7b95187..11ba3bd 100644 --- a/src/generated/resources/data/createnuclear/worldgen/placed_feature/lead_ore.json +++ b/src/generated/resources/data/createnuclear/worldgen/placed_feature/lead_ore.json @@ -21,7 +21,7 @@ } }, { - "type": "create:config_filter" + "type": "createnuclear:config_filter" } ] } \ No newline at end of file diff --git a/src/generated/resources/data/createnuclear/worldgen/placed_feature/nitrate_ore.json b/src/generated/resources/data/createnuclear/worldgen/placed_feature/nitrate_ore.json index 4b98acb..cdfa1f6 100644 --- a/src/generated/resources/data/createnuclear/worldgen/placed_feature/nitrate_ore.json +++ b/src/generated/resources/data/createnuclear/worldgen/placed_feature/nitrate_ore.json @@ -21,7 +21,7 @@ } }, { - "type": "create:config_filter" + "type": "createnuclear:config_filter" } ] } \ No newline at end of file diff --git a/src/generated/resources/data/createnuclear/worldgen/placed_feature/striated_ores_overworld.json b/src/generated/resources/data/createnuclear/worldgen/placed_feature/striated_ores_overworld.json index d5ec73f..7af1397 100644 --- a/src/generated/resources/data/createnuclear/worldgen/placed_feature/striated_ores_overworld.json +++ b/src/generated/resources/data/createnuclear/worldgen/placed_feature/striated_ores_overworld.json @@ -21,7 +21,7 @@ } }, { - "type": "create:config_filter" + "type": "createnuclear:config_filter" } ] } \ No newline at end of file diff --git a/src/generated/resources/data/createnuclear/worldgen/placed_feature/thorium_ore.json b/src/generated/resources/data/createnuclear/worldgen/placed_feature/thorium_ore.json index f1ec761..2b26066 100644 --- a/src/generated/resources/data/createnuclear/worldgen/placed_feature/thorium_ore.json +++ b/src/generated/resources/data/createnuclear/worldgen/placed_feature/thorium_ore.json @@ -21,7 +21,7 @@ } }, { - "type": "create:config_filter" + "type": "createnuclear:config_filter" } ] } \ No newline at end of file diff --git a/src/generated/resources/data/createnuclear/worldgen/placed_feature/uranium_ore.json b/src/generated/resources/data/createnuclear/worldgen/placed_feature/uranium_ore.json index 8f09b17..f5af6eb 100644 --- a/src/generated/resources/data/createnuclear/worldgen/placed_feature/uranium_ore.json +++ b/src/generated/resources/data/createnuclear/worldgen/placed_feature/uranium_ore.json @@ -21,7 +21,7 @@ } }, { - "type": "create:config_filter" + "type": "createnuclear:config_filter" } ] } \ No newline at end of file diff --git a/src/generated/resources/data/minecraft/tags/damage_type/no_knockback.json b/src/generated/resources/data/minecraft/tags/damage_type/no_knockback.json new file mode 100644 index 0000000..b2aff4f --- /dev/null +++ b/src/generated/resources/data/minecraft/tags/damage_type/no_knockback.json @@ -0,0 +1,6 @@ +{ + "values": [ + "createnuclear:radiation", + "createnuclear:fan_radiation" + ] +} \ No newline at end of file diff --git a/src/main/java/net/nuclearteam/createnuclear/CNBlockEntityTypes.java b/src/main/java/net/nuclearteam/createnuclear/CNBlockEntityTypes.java index ca312d4..6c65045 100644 --- a/src/main/java/net/nuclearteam/createnuclear/CNBlockEntityTypes.java +++ b/src/main/java/net/nuclearteam/createnuclear/CNBlockEntityTypes.java @@ -8,6 +8,8 @@ import net.nuclearteam.createnuclear.content.multiblock.casing.ReactorCasingEntity; import net.nuclearteam.createnuclear.content.multiblock.controller.ReactorControllerBlockEntity; import net.nuclearteam.createnuclear.content.multiblock.core.ReactorCoreEntity; +import net.nuclearteam.createnuclear.content.multiblock.frame.ReactorFrameEntity; +import net.nuclearteam.createnuclear.content.multiblock.frame.ReactorFrameRenderer; import net.nuclearteam.createnuclear.content.multiblock.input.fluid.ReactorFluidInputEntity; import net.nuclearteam.createnuclear.content.multiblock.input.item.ReactorRodInputEntity; import net.nuclearteam.createnuclear.content.multiblock.output.ReactorOutputEntity; @@ -29,6 +31,12 @@ public class CNBlockEntityTypes { .validBlocks(CNBlocks.REACTOR_CORE) .register(); + public static final BlockEntityEntry REACTOR_FRAME = + CreateNuclear.REGISTRATE.blockEntity("reactor_frame", ReactorFrameEntity::new) + .validBlocks(CNBlocks.REACTOR_FRAME) + .renderer(() -> ReactorFrameRenderer::new) + .register(); + public static final BlockEntityEntry REACTOR_INPUT = CreateNuclear.REGISTRATE.blockEntity("reactor_input", ReactorRodInputEntity::new) .validBlocks(CNBlocks.REACTOR_ROD_INPUT) diff --git a/src/main/java/net/nuclearteam/createnuclear/CNBlocks.java b/src/main/java/net/nuclearteam/createnuclear/CNBlocks.java index 5fa103a..723d3f6 100644 --- a/src/main/java/net/nuclearteam/createnuclear/CNBlocks.java +++ b/src/main/java/net/nuclearteam/createnuclear/CNBlocks.java @@ -9,6 +9,7 @@ import com.tterrag.registrate.providers.RegistrateRecipeProvider; import com.tterrag.registrate.providers.loot.RegistrateBlockLootTables; import com.tterrag.registrate.util.entry.BlockEntry; +import net.minecraft.client.renderer.RenderType; import net.minecraft.core.Direction; import net.minecraft.core.HolderLookup; import net.minecraft.core.registries.Registries; @@ -55,7 +56,9 @@ import net.nuclearteam.createnuclear.content.multiblock.cooler.ReactorCooler; import net.nuclearteam.createnuclear.content.multiblock.reinforced.ReinforcedGlassBlock; import net.nuclearteam.createnuclear.content.uraniumOre.UraniumOreBlock; +import net.nuclearteam.createnuclear.content.uraniumOre.UraniumOreItem; +import static com.simibubi.create.api.behaviour.display.DisplaySource.displaySource; import static com.simibubi.create.foundation.data.CreateRegistrate.casingConnectivity; import static com.simibubi.create.foundation.data.ModelGen.customItemModel; import static com.simibubi.create.foundation.data.TagGen.axeOrPickaxe; @@ -80,6 +83,12 @@ public class CNBlocks { .tag(BlockTags.NEEDS_DIAMOND_TOOL) .simpleItem() .transform(pickaxeOnly()) + .transform(displaySource(CNDisplaySources.HEAT)) + .transform(displaySource(CNDisplaySources.LIQUID_LEVEL)) + .transform(displaySource(CNDisplaySources.FUEL)) + .transform(displaySource(CNDisplaySources.COOLER)) + .transform(displaySource(CNDisplaySources.REACTOR_SIZE)) + .transform(displaySource(CNDisplaySources.REACTOR_SUMMARY)) .register(); public static final BlockEntry REACTOR_CORE = @@ -102,7 +111,11 @@ public class CNBlocks { public static final BlockEntry REACTOR_FRAME = CreateNuclear.REGISTRATE.block("reactor_frame", ReactorFrame::new) .initialProperties(SharedProperties::stone) - .properties(p -> p.explosionResistance(3F).destroyTime(2F)) + // noOcclusion is what makes the window actually readable: without it the frame counts + // as a full opaque block, light stops propagating through it, and the fluid rendered + // inside the window comes out almost black. + .properties(p -> p.explosionResistance(3F).destroyTime(2F).noOcclusion()) + .addLayer(() -> RenderType::cutoutMipped) .transform(pickaxeOnly()) .tag(BlockTags.NEEDS_DIAMOND_TOOL) .blockstate((c, p) -> @@ -148,6 +161,7 @@ public class CNBlocks { .initialProperties(SharedProperties::stone) .properties(p -> p.explosionResistance(6F)) .properties(p -> p.destroyTime(2F)) + .addLayer(() -> RenderType::cutoutMipped) .transform(pickaxeOnly()) .tag(BlockTags.NEEDS_DIAMOND_TOOL) .blockstate(new ReactorRodInputGenerator()::generate) @@ -160,6 +174,7 @@ public class CNBlocks { .initialProperties(SharedProperties::stone) .properties(p -> p.explosionResistance(6F)) .properties(p -> p.destroyTime(2F)) + .addLayer(() -> RenderType::cutoutMipped) .transform(pickaxeOnly()) .tag(BlockTags.NEEDS_DIAMOND_TOOL) .blockstate(new ReactorFluidInputGenerator()::generate) @@ -175,7 +190,7 @@ public class CNBlocks { .tag(AllTags.AllBlockTags.SAFE_NBT.tag, BlockTags.NEEDS_DIAMOND_TOOL) .transform(pickaxeOnly()) .blockstate(new ReactorOutputGenerator()::generate) - .onRegister(block -> BlockStressValues.CAPACITIES.register(block, () -> 10240.0)) + .onRegister(block -> BlockStressValues.CAPACITIES.register(block, () -> 64000.0)) .item() .transform(customItemModel("reactor", "output", "item")) .register(); @@ -188,6 +203,12 @@ public class CNBlocks { .transform(pickaxeOnly()) .tag(BlockTags.NEEDS_DIAMOND_TOOL) .blockstate(new ReactorControllerGenerator()::generate) + .transform(displaySource(CNDisplaySources.HEAT)) + .transform(displaySource(CNDisplaySources.LIQUID_LEVEL)) + .transform(displaySource(CNDisplaySources.FUEL)) + .transform(displaySource(CNDisplaySources.COOLER)) + .transform(displaySource(CNDisplaySources.REACTOR_SIZE)) + .transform(displaySource(CNDisplaySources.REACTOR_SUMMARY)) .item() .transform(customItemModel("reactor", "controller", "item")) .register(); @@ -204,6 +225,7 @@ public class CNBlocks { .block("reinforced_glass", ReinforcedGlassBlock::new) .initialProperties(() -> Blocks.GLASS) .properties(p -> p.explosionResistance(7.0F).destroyTime(2F)) + .addLayer(() -> RenderType::translucent) .onRegister(CreateRegistrate.connectedTextures(() -> new EncasedCTBehaviour(CNSpriteShifts.REACTOR_GLASS))) .onRegister(casingConnectivity((block,cc) -> cc.makeCasing(block, CNSpriteShifts.REACTOR_GLASS))) .loot(RegistrateBlockLootTables::dropWhenSilkTouch) @@ -242,6 +264,7 @@ public class CNBlocks { .properties(EnrichingFireBlock.getLight()) .tag(CNBlockTags.FAN_PROCESSING_CATALYSTS_ENRICHED.tag) .loot((lt, b) -> lt.add(b, BlockLootSubProvider.noDrop())) + .addLayer(() -> RenderType::cutout) .blockstate((c, p) -> { String baseFolder = "block/enriching/fire/"; ModelFile Floor0 = p.models().getExistingFile(p.modLoc(baseFolder + "floor0")); @@ -403,7 +426,7 @@ public class CNBlocks { .pattern("RRR") .pattern("RRR") .save(p, CreateNuclear.asResource("crafting/" + c.getName()))) - .item() + .item((b, p) -> new UraniumOreItem(b, p, 27)) .tag(CNTags.forgeItemTag("storage_blocks/raw_uranium")) .build() .register(); @@ -473,7 +496,7 @@ public class CNBlocks { CNTags.forgeBlockTag("ores/uranium"), CNBlockTags.URANIUM_ORES.tag ) - .item() + .item((b, p) -> new UraniumOreItem(b, p, 3)) .tag(CNItemTags.URANIUM_ORES.tag, CNTags.forgeItemTag("ores/uranium")) .build() @@ -499,7 +522,7 @@ public class CNBlocks { CNTags.forgeBlockTag("ores/uranium"), CNBlockTags.URANIUM_ORES.tag ) - .item() + .item((b, p) -> new UraniumOreItem(b, p, 3)) .tag(CNItemTags.URANIUM_ORES.tag, CNTags.forgeItemTag("ores/uranium")) .build() diff --git a/src/main/java/net/nuclearteam/createnuclear/CNDataComponents.java b/src/main/java/net/nuclearteam/createnuclear/CNDataComponents.java index aa4a936..533e14c 100644 --- a/src/main/java/net/nuclearteam/createnuclear/CNDataComponents.java +++ b/src/main/java/net/nuclearteam/createnuclear/CNDataComponents.java @@ -23,9 +23,21 @@ public class CNDataComponents { private static final DeferredRegister.DataComponents DATA_COMPONENTS = DeferredRegister.createDataComponents(Registries.DATA_COMPONENT_TYPE, CreateNuclear.MOD_ID); + /** + * Reactor heat stored on the blueprint stack, written by the controller on every tick. + *

+ * Plain {@code Codec.FLOAT}, deliberately not {@code ExtraCodecs.POSITIVE_FLOAT}: zero is + * the normal value for an idle or stopped reactor, and a persistent codec that can reject + * a value crashes the world save rather than the write. That is exactly what happened with + * {@code POSITIVE_FLOAT} ("Value must be positive: 0.0" while serializing the blueprint). + *

+ * The non-negative invariant is enforced upstream instead, where it can't take a save down: + * {@code DefaultHeatCalculator#computeHeat} floors its result at 0. This also matches the + * Forge branch, which stores the value in a plain NBT double. + */ public static final DataComponentType HEAT = register( "heat", - builder -> builder.persistent(ExtraCodecs.POSITIVE_FLOAT).networkSynchronized(ByteBufCodecs.FLOAT) + builder -> builder.persistent(Codec.FLOAT).networkSynchronized(ByteBufCodecs.FLOAT) ); public static final DataComponentType REACTOR_BLUE_PRINT_DATA = register( @@ -33,31 +45,6 @@ public class CNDataComponents { builder -> builder.persistent(ReactorBluePrintData.CODEC).networkSynchronized(ReactorBluePrintData.STREAM_CODEC) ); - public static final DataComponentType PATTERN = register( - "pattern", - builder -> builder.persistent(CompoundTag.CODEC).networkSynchronized(ByteBufCodecs.COMPOUND_TAG) - ); - - public static final DataComponentType URANIUM_TIME = register( - "uranium_time", - builder -> builder.persistent(ExtraCodecs.NON_NEGATIVE_INT).networkSynchronized(ByteBufCodecs.INT) - ); - - public static final DataComponentType GRAPHITE_TIME = register( - "graphite_time", - builder -> builder.persistent(ExtraCodecs.NON_NEGATIVE_INT).networkSynchronized(ByteBufCodecs.INT) - ); - - public static final DataComponentType COUNT_GRAPHITE_ROD = register( - "count_graphite_rod", - builder -> builder.persistent(ExtraCodecs.NON_NEGATIVE_INT).networkSynchronized(ByteBufCodecs.INT) - ); - - public static final DataComponentType COUNT_URANIUM_ROD = register( - "count_uranium_rod", - builder -> builder.persistent(ExtraCodecs.NON_NEGATIVE_INT).networkSynchronized(ByteBufCodecs.INT) - ); - public static final DataComponentType CLOTH_COLOR = register( "cloth_color", b -> b.persistent(StringRepresentable.fromEnum(Cloths::values)).networkSynchronized(NeoForgeStreamCodecs.enumCodec(Cloths.class)) diff --git a/src/main/java/net/nuclearteam/createnuclear/CNDisplaySources.java b/src/main/java/net/nuclearteam/createnuclear/CNDisplaySources.java new file mode 100644 index 0000000..def1c7b --- /dev/null +++ b/src/main/java/net/nuclearteam/createnuclear/CNDisplaySources.java @@ -0,0 +1,31 @@ +package net.nuclearteam.createnuclear; + +import com.simibubi.create.api.behaviour.display.DisplaySource; +import com.simibubi.create.foundation.data.CreateRegistrate; +import com.tterrag.registrate.util.entry.RegistryEntry; +import net.nuclearteam.createnuclear.content.redstone.displayLink.source.*; + +import java.util.function.Supplier; + +public class CNDisplaySources { + private static final CreateRegistrate REGISTRATE = CreateNuclear.REGISTRATE; + + public static final RegistryEntry HEAT = simple("heat", HeatDisplaySource::new); + public static final RegistryEntry LIQUID_LEVEL = simple("liquid_level", LiquidLevelDisplaySource::new); + public static final RegistryEntry REACTOR_SUMMARY = simple("reactor_summary", ReactorSummaryDisplaySource::new); + public static final RegistryEntry FUEL = simple("fuel", FuelDisplaySource::new); + public static final RegistryEntry COOLER = simple("cooler", CoolerDisplaySource::new); + public static final RegistryEntry REACTOR_SIZE = simple("reactor_size", ReactorSizeDisplaySource::new); + + /** + * Divergence assumee vs Forge : en 1.21 Registrate type ses entrees + * {@code RegistryEntry} (type du registre + type de l'entree) la ou 1.20.1 + * n'avait que {@code RegistryEntry}. + */ + private static RegistryEntry simple(String name, Supplier supplier) { + return REGISTRATE.displaySource(name, supplier).register(); + } + + public static void register() { + } +} diff --git a/src/main/java/net/nuclearteam/createnuclear/CNItems.java b/src/main/java/net/nuclearteam/createnuclear/CNItems.java index 4bf9860..426e45e 100644 --- a/src/main/java/net/nuclearteam/createnuclear/CNItems.java +++ b/src/main/java/net/nuclearteam/createnuclear/CNItems.java @@ -21,6 +21,7 @@ import net.nuclearteam.createnuclear.api.data.recipe.SmithingClothRecipeBuilder; import net.nuclearteam.createnuclear.api.multiblock.rods.RodType; import net.nuclearteam.createnuclear.content.biome.BiomeIrradiationExtractorItem; +import net.nuclearteam.createnuclear.infrastructure.config.CNConfigs; import net.nuclearteam.createnuclear.content.equipment.cloth.ClothItem; import net.nuclearteam.createnuclear.content.equipment.cloth.ClothItem.Cloths; import net.nuclearteam.createnuclear.content.multiblock.bluePrintItem.ReactorBluePrintItem; @@ -72,6 +73,12 @@ public class CNItems { URANIUM_ROD = CreateNuclear.REGISTRATE .item("uranium_rod", p -> new RadiationItem(p, 100)) + .onRegister(ItemRodTypesValue.setRodTypeInfos(new RodType.Builder() + .baseRodHeat(() -> CNConfigs.server().rods.uraniumBaseValue.get()) + .proximityRodHeat(() -> (float) CNConfigs.server().rods.uraniumProximityBonus.get()) + .rodTimer(() -> CNConfigs.server().rods.uraniumRodLifetime.get()) + .ratio(() -> CNConfigs.server().rods.uraniumHeatRatio.get()) + .fuelRodType())) .tag(CNTags.forgeItemTag("rods"), CNItemTags.FUEL.tag) .register(); @@ -103,6 +110,12 @@ public class CNItems { GRAPHITE_ROD = CreateNuclear.REGISTRATE .item("graphite_rod", Item::new) + .onRegister(ItemRodTypesValue.setRodTypeInfos(new RodType.Builder() + .baseRodHeat(() -> CNConfigs.server().rods.graphiteBaseValue.get()) + .proximityRodHeat(() -> CNConfigs.server().rods.graphiteProximityMalus.getF()) + .rodTimer(() -> CNConfigs.server().rods.graphiteRodLifetime.get()) + .ratio(() -> CNConfigs.server().rods.graphiteHeatRatio.get()) + .coolerRodType())) .tag(CNTags.forgeItemTag("rods"), CNItemTags.COOLER.tag) .register(), @@ -180,11 +193,12 @@ public class CNItems { THORIUM_ROD = CreateNuclear.REGISTRATE .item("thorium_rod", Item::new) .onRegister(ItemRodTypesValue.setRodTypeInfos(new RodType.Builder() - .rodTimer(3600) - .baseRodHeat(16) - .proximityRodHeat(8) + .baseRodHeat(() -> CNConfigs.server().rods.baseValueThorium.get()) + .proximityRodHeat(() -> (float) CNConfigs.server().rods.thoriumProxyBonus.get()) + .rodTimer(() -> CNConfigs.server().rods.thoriumRodLifetime.get()) + .ratio(() -> CNConfigs.server().rods.thoriumHeatRatio.get()) .fuelRodType())) - .tag(CNTags.forgeItemTag("rods")) + .tag(CNTags.forgeItemTag("rods"), CNItemTags.FUEL.tag) .register(), NITRATE = CreateNuclear.REGISTRATE diff --git a/src/main/java/net/nuclearteam/createnuclear/CNPackets.java b/src/main/java/net/nuclearteam/createnuclear/CNPackets.java index 7543248..1fabb25 100644 --- a/src/main/java/net/nuclearteam/createnuclear/CNPackets.java +++ b/src/main/java/net/nuclearteam/createnuclear/CNPackets.java @@ -6,14 +6,11 @@ import net.minecraft.network.codec.StreamCodec; import net.minecraft.network.protocol.common.custom.CustomPacketPayload; -import net.nuclearteam.createnuclear.content.multiblock.bluePrintItem.ReactorBluePrintItemPacket; import net.nuclearteam.createnuclear.content.multiblock.controller.EventTriggerPacket; import java.util.Locale; public enum CNPackets implements BasePacketPayload.PacketTypeProvider { - CONFIGURE_REACTOR_PATTERN(ReactorBluePrintItemPacket.class, ReactorBluePrintItemPacket.STREAM_CODEC), - // To client TRIGGER_EVENT_TEXT_OVERLAY(EventTriggerPacket.class, EventTriggerPacket.STREAM_CODEC), ; diff --git a/src/main/java/net/nuclearteam/createnuclear/CNRecipeTypes.java b/src/main/java/net/nuclearteam/createnuclear/CNRecipeTypes.java index 2b79d3d..b15e2d2 100644 --- a/src/main/java/net/nuclearteam/createnuclear/CNRecipeTypes.java +++ b/src/main/java/net/nuclearteam/createnuclear/CNRecipeTypes.java @@ -16,6 +16,7 @@ import net.neoforged.neoforge.registries.DeferredHolder; import net.neoforged.neoforge.registries.DeferredRegister; import net.nuclearteam.createnuclear.content.kinetics.fan.processing.EnrichedRecipe; +import net.nuclearteam.createnuclear.content.kinetics.fan.processing.SnowPowderRecipe; import net.minecraft.world.level.Level; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -26,7 +27,8 @@ @SuppressWarnings({"unused", "unchecked"}) public enum CNRecipeTypes implements IRecipeTypeInfo, StringRepresentable { - ENRICHED(EnrichedRecipe::new) + ENRICHED(EnrichedRecipe::new), + SNOW_POWDER(SnowPowderRecipe::new) ; public static final Predicate> CAN_BE_AUTOMATED = r -> !r.id() diff --git a/src/main/java/net/nuclearteam/createnuclear/CNTags.java b/src/main/java/net/nuclearteam/createnuclear/CNTags.java index 29cbea3..fb93d74 100644 --- a/src/main/java/net/nuclearteam/createnuclear/CNTags.java +++ b/src/main/java/net/nuclearteam/createnuclear/CNTags.java @@ -71,6 +71,7 @@ public enum NameSpace { public enum CNBlockTags { FAN_PROCESSING_CATALYSTS_ENRICHED(MOD, "fan_processing_catalysts/enriched"), + FAN_PROCESSING_CATALYSTS_SNOW_POWDER(MOD, "fan_processing_catalysts/snow_powder"), ENRICHING_FIRE_BASE_BLOCKS, THORIUM_ORES, URANIUM_ORES, diff --git a/src/main/java/net/nuclearteam/createnuclear/CreateNuclear.java b/src/main/java/net/nuclearteam/createnuclear/CreateNuclear.java index 5e46619..50636bc 100644 --- a/src/main/java/net/nuclearteam/createnuclear/CreateNuclear.java +++ b/src/main/java/net/nuclearteam/createnuclear/CreateNuclear.java @@ -6,6 +6,7 @@ import com.simibubi.create.foundation.data.CreateRegistrate; import com.simibubi.create.foundation.item.ItemDescription; import com.simibubi.create.foundation.item.KineticStats; +import net.nuclearteam.createnuclear.foundation.item.RodsStats; import com.simibubi.create.foundation.item.TooltipModifier; import net.createmod.catnip.lang.FontHelper; import net.minecraft.core.registries.BuiltInRegistries; @@ -21,7 +22,7 @@ import net.neoforged.neoforge.common.NeoForge; import net.neoforged.neoforge.registries.RegisterEvent; import net.nuclearteam.createnuclear.content.decoration.palettes.CNPaletteBlocks; -import net.nuclearteam.createnuclear.content.equipment.armor.CNArmorMaterials; +import net.nuclearteam.createnuclear.content.equipment.armor.ArmorMaterials; import net.nuclearteam.createnuclear.content.kinetics.fan.processing.CNFanProcessingTypes; import net.nuclearteam.createnuclear.content.radiation.CNRadiationValues; import net.nuclearteam.createnuclear.content.radiation.capability.RadiationCapability; @@ -29,6 +30,7 @@ import net.nuclearteam.createnuclear.foundation.advancement.CNTriggers; import net.nuclearteam.createnuclear.infrastructure.config.CNConfigs; import net.nuclearteam.createnuclear.infrastructure.data.CreateNuclearDatagen; +import net.nuclearteam.createnuclear.infrastructure.worldgen.CNPlacementModifiers; import net.nuclearteam.createnuclear.infrastructure.worldgen.biome.surfacerule.BiomeTagRule; import org.slf4j.Logger; @@ -50,6 +52,7 @@ public class CreateNuclear { .setTooltipModifierFactory(item -> new ItemDescription.Modifier(item, FontHelper.Palette.STANDARD_CREATE) .andThen(TooltipModifier.mapNull(KineticStats.create(item))) + .andThen(RodsStats.create(item)) ); public CreateNuclear(IEventBus eventBus, ModContainer modContainer) { @@ -66,6 +69,7 @@ public static void onCtor(IEventBus modEventBus, ModContainer modContainer) { REGISTRATE.registerEventListeners(modEventBus); CNSoundEvents.prepare(); + CNDisplaySources.register(); CNTags.init(); CNBlocks.register(); CNBlockEntityTypes.register(); @@ -76,7 +80,7 @@ public static void onCtor(IEventBus modEventBus, ModContainer modContainer) { CNEntityType.register(); CNPaletteBlocks.register(); - CNArmorMaterials.register(modEventBus); + ArmorMaterials.register(modEventBus); CNDataComponents.register(modEventBus); CNAttachmentTypes.register(modEventBus); @@ -89,6 +93,7 @@ public static void onCtor(IEventBus modEventBus, ModContainer modContainer) { CNParticleRegistry.DEF_REG.register(modEventBus); CNRecipeTypes.register(modEventBus); CNAttributes.register(modEventBus); + CNPlacementModifiers.register(modEventBus); modEventBus.addListener(CreateNuclear::init); modEventBus.addListener(CreateNuclear::onRegister); @@ -102,6 +107,7 @@ public static void onCtor(IEventBus modEventBus, ModContainer modContainer) { public static void init(final FMLCommonSetupEvent event) { CNFluids.registerFluidInteractions(); + CNOpenPipeEffectHandlers.registerDefaults(); // Runs here rather than at mod construction: the item entries it reads (e.g. Create's // CRUSHED_URANIUM) are only bound once the registry events have finished. CNRadiationValues.register(); diff --git a/src/main/java/net/nuclearteam/createnuclear/api/CreateNuclearRegistries.java b/src/main/java/net/nuclearteam/createnuclear/api/CreateNuclearRegistries.java index 8b2a178..c1c8c61 100644 --- a/src/main/java/net/nuclearteam/createnuclear/api/CreateNuclearRegistries.java +++ b/src/main/java/net/nuclearteam/createnuclear/api/CreateNuclearRegistries.java @@ -6,11 +6,13 @@ import net.nuclearteam.createnuclear.api.multiblock.fluid.ReactorFluidType; import net.nuclearteam.createnuclear.api.multiblock.rods.RodType; - public class CreateNuclearRegistries { public static final ResourceKey> ROD_TYPE = key("rods/type"); public static final ResourceKey> FLUID_TYPE = key("fluids/type"); + public static final ResourceKey FALLBACK_ROD = ResourceKey.create(CreateNuclearRegistries.ROD_TYPE, CreateNuclear.asResource("fallback")); + public static final ResourceKey FALLBACK_FLUID = ResourceKey.create(CreateNuclearRegistries.FLUID_TYPE, CreateNuclear.asResource("fallback")); + private static ResourceKey> key(String name) { return ResourceKey.createRegistryKey(CreateNuclear.asResource(name)); } diff --git a/src/main/java/net/nuclearteam/createnuclear/api/ItemRodTypesValue.java b/src/main/java/net/nuclearteam/createnuclear/api/ItemRodTypesValue.java index 53355a4..e617392 100644 --- a/src/main/java/net/nuclearteam/createnuclear/api/ItemRodTypesValue.java +++ b/src/main/java/net/nuclearteam/createnuclear/api/ItemRodTypesValue.java @@ -3,6 +3,7 @@ import com.simibubi.create.api.registry.SimpleRegistry; import com.tterrag.registrate.util.nullness.NonNullConsumer; import net.minecraft.world.item.Item; +import net.minecraft.world.item.Items; import net.nuclearteam.createnuclear.api.multiblock.rods.RodType; import static net.nuclearteam.createnuclear.api.multiblock.rods.RodType.TypeRod.NONE; @@ -16,7 +17,7 @@ public class ItemRodTypesValue { * Default RodType returned when no mapping exists in the registry. * Built once to avoid allocating a new instance on each lookup. */ - public static final RodType DEFAULT_ROD_TYPE = new RodType(HolderSet.direct(Collections.emptyList()), -1, -1, -1, NONE); + public static final RodType DEFAULT_ROD_TYPE = new RodType(Items.AIR.builtInRegistryHolder(), -1, -1, -1, NONE); /** * Get the {@link RodType} associated with the given {@link Item}. @@ -39,7 +40,7 @@ public static RodType getRodType(Item item) { * @throws NullPointerException if {@code type} is null */ public static NonNullConsumer setRodTypeInfos(RodType.Builder type) { - return item -> ROD_TYPE.register(item, type.addItems(item).build()); + return item -> ROD_TYPE.register(item, type.item(item).build()); } @@ -71,7 +72,7 @@ public static NonNullConsumer setRodTypeInfos(int baseRodHeat, int proximi } return item -> { - RodType built = builder.addItems(item).build(); + RodType built = builder.item(item).build(); ROD_TYPE.register(item, built); }; } diff --git a/src/main/java/net/nuclearteam/createnuclear/api/ReactorFluidTypesValue.java b/src/main/java/net/nuclearteam/createnuclear/api/ReactorFluidTypesValue.java index 67a05e4..d994f80 100644 --- a/src/main/java/net/nuclearteam/createnuclear/api/ReactorFluidTypesValue.java +++ b/src/main/java/net/nuclearteam/createnuclear/api/ReactorFluidTypesValue.java @@ -4,6 +4,7 @@ import com.tterrag.registrate.util.nullness.NonNullConsumer; import net.minecraft.core.HolderSet; import net.minecraft.world.level.material.Fluid; +import net.minecraft.world.level.material.Fluids; import net.nuclearteam.createnuclear.api.multiblock.fluid.ReactorFluidType; import java.util.Collections; @@ -26,7 +27,7 @@ public class ReactorFluidTypesValue { * Default {@link ReactorFluidType} returned when no mapping exists in the * registry. Constructed once to avoid allocating a new instance on each lookup. */ - public static final ReactorFluidType DEFAULT_REACTOR_FLUID_TYPE = new ReactorFluidType(HolderSet.direct(Collections.emptyList()), -1, -1); + public static final ReactorFluidType DEFAULT_REACTOR_FLUID_TYPE = new ReactorFluidType(Fluids.EMPTY.builtInRegistryHolder(), -1, -1); /** * Get the {@link ReactorFluidType} associated with the given {@link Fluid}. diff --git a/src/main/java/net/nuclearteam/createnuclear/api/data/recipe/EnrichedRecipeGen.java b/src/main/java/net/nuclearteam/createnuclear/api/data/recipe/EnrichedRecipeGen.java index 73402df..6b2b031 100644 --- a/src/main/java/net/nuclearteam/createnuclear/api/data/recipe/EnrichedRecipeGen.java +++ b/src/main/java/net/nuclearteam/createnuclear/api/data/recipe/EnrichedRecipeGen.java @@ -1,14 +1,37 @@ package net.nuclearteam.createnuclear.api.data.recipe; import com.simibubi.create.api.data.recipe.ProcessingRecipeGen; +import com.simibubi.create.api.data.recipe.StandardProcessingRecipeGen; import com.simibubi.create.foundation.recipe.IRecipeTypeInfo; +import net.createmod.catnip.registry.RegisteredObjectsHelper; import net.minecraft.core.HolderLookup; import net.minecraft.data.PackOutput; +import net.minecraft.world.item.crafting.Ingredient; +import net.minecraft.world.level.ItemLike; import net.nuclearteam.createnuclear.CNRecipeTypes; +import net.nuclearteam.createnuclear.content.kinetics.fan.processing.EnrichedRecipe; import java.util.concurrent.CompletableFuture; +import java.util.function.Supplier; -public abstract class EnrichedRecipeGen extends ProcessingRecipeGen { +/** + * The base class for Haunting recipe generation. + * Addons should extend this and use the {@link ProcessingRecipeGen#create} methods + * or the helper methods contained in this class to make recipes. + * For an example of how you might do this, see {@link net.nuclearteam.createnuclear.foundation.data.recipe.CNEnrichedRecipeGen}. + * Needs to be added to a registered recipe provider to do anything, see {@link net.nuclearteam.createnuclear.foundation.data.recipe.CNRecipeProvider} + */ +public abstract class EnrichedRecipeGen extends StandardProcessingRecipeGen { + public GeneratedRecipe convert(ItemLike input, ItemLike result) { + return convert(() -> Ingredient.of(input), () -> result); + } + + public GeneratedRecipe convert(Supplier input, Supplier result) { + return create(asResource(RegisteredObjectsHelper.getKeyOrThrow(result.get().asItem()) + .getPath()), + p -> p.withItemIngredients(input.get()) + .output(result.get())); + } public EnrichedRecipeGen(PackOutput generator, CompletableFuture registries, String defaultNamespace) { super(generator, registries, defaultNamespace); diff --git a/src/main/java/net/nuclearteam/createnuclear/foundation/data/recipe/EnrichedRecipeGen.java b/src/main/java/net/nuclearteam/createnuclear/api/data/recipe/SnowPowderRecipeGen.java similarity index 70% rename from src/main/java/net/nuclearteam/createnuclear/foundation/data/recipe/EnrichedRecipeGen.java rename to src/main/java/net/nuclearteam/createnuclear/api/data/recipe/SnowPowderRecipeGen.java index 8600de4..ee1f66e 100644 --- a/src/main/java/net/nuclearteam/createnuclear/foundation/data/recipe/EnrichedRecipeGen.java +++ b/src/main/java/net/nuclearteam/createnuclear/api/data/recipe/SnowPowderRecipeGen.java @@ -1,27 +1,26 @@ -package net.nuclearteam.createnuclear.foundation.data.recipe; +package net.nuclearteam.createnuclear.api.data.recipe; import com.simibubi.create.api.data.recipe.ProcessingRecipeGen; import com.simibubi.create.api.data.recipe.StandardProcessingRecipeGen; -import com.simibubi.create.foundation.recipe.IRecipeTypeInfo; import net.createmod.catnip.registry.RegisteredObjectsHelper; import net.minecraft.core.HolderLookup; import net.minecraft.data.PackOutput; import net.minecraft.world.item.crafting.Ingredient; import net.minecraft.world.level.ItemLike; import net.nuclearteam.createnuclear.CNRecipeTypes; -import net.nuclearteam.createnuclear.content.kinetics.fan.processing.EnrichedRecipe; +import net.nuclearteam.createnuclear.content.kinetics.fan.processing.SnowPowderRecipe; import java.util.concurrent.CompletableFuture; import java.util.function.Supplier; /** - * The base class for Haunting recipe generation. + * The base class for Snow Powder recipe generation. * Addons should extend this and use the {@link ProcessingRecipeGen#create} methods * or the helper methods contained in this class to make recipes. - * For an example of how you might do this, see Create's implementation: {@link net.nuclearteam.createnuclear.foundation.data.recipe.CNEnrichedRecipeGen}. + * For an example of how you might do this, see {@link net.nuclearteam.createnuclear.foundation.data.recipe.CNSnowPowderRecipeGen}. * Needs to be added to a registered recipe provider to do anything, see {@link net.nuclearteam.createnuclear.foundation.data.recipe.CNRecipeProvider} */ -public abstract class EnrichedRecipeGen extends StandardProcessingRecipeGen { +public abstract class SnowPowderRecipeGen extends StandardProcessingRecipeGen { public GeneratedRecipe convert(ItemLike input, ItemLike result) { return convert(() -> Ingredient.of(input), () -> result); } @@ -33,12 +32,12 @@ public GeneratedRecipe convert(Supplier input, Supplier re .output(result.get())); } - public EnrichedRecipeGen(PackOutput output, CompletableFuture registries, String defaultNamespace) { + public SnowPowderRecipeGen(PackOutput output, CompletableFuture registries, String defaultNamespace) { super(output, registries, defaultNamespace); } @Override protected CNRecipeTypes getRecipeType() { - return CNRecipeTypes.ENRICHED; + return CNRecipeTypes.SNOW_POWDER; } -} +} \ No newline at end of file diff --git a/src/main/java/net/nuclearteam/createnuclear/api/multiblock/IMultiblockController.java b/src/main/java/net/nuclearteam/createnuclear/api/multiblock/IMultiblockController.java new file mode 100644 index 0000000..7a9478d --- /dev/null +++ b/src/main/java/net/nuclearteam/createnuclear/api/multiblock/IMultiblockController.java @@ -0,0 +1,8 @@ +package net.nuclearteam.createnuclear.api.multiblock; + +import net.minecraft.core.Direction; + +public interface IMultiblockController { + void setMultiblockFacing(Direction f); + Direction getMultiblockFacing(); +} diff --git a/src/main/java/net/nuclearteam/createnuclear/api/multiblock/MultiBlockManagerBeta.java b/src/main/java/net/nuclearteam/createnuclear/api/multiblock/MultiBlockManagerBeta.java index 7079cd3..d74b16a 100644 --- a/src/main/java/net/nuclearteam/createnuclear/api/multiblock/MultiBlockManagerBeta.java +++ b/src/main/java/net/nuclearteam/createnuclear/api/multiblock/MultiBlockManagerBeta.java @@ -19,7 +19,7 @@ public void register(String id, T data, IMultiBlockPattern blockPattern) { structures.add(new BlockPattern<>(id, data, blockPattern)); } - public BlockPattern findStructure(Level level, BlockPos pos, ReactorControllerBlockEntity entity) { + public BlockPattern findStructure(Level level, BlockPos pos, IMultiblockController entity) { List directions = new ArrayList<>(); directions.add(Direction.NORTH); directions.add(Direction.WEST); @@ -30,7 +30,7 @@ public BlockPattern findStructure(Level level, BlockPos pos, ReactorControlle for (BlockPattern structure : structures) { boolean result = structure.structure().matches(level, pos, direction); if (result){ - entity.setMultiblockFacing(direction.getCounterClockWise().getName()); + entity.setMultiblockFacing(direction.getCounterClockWise()); return structure; } } diff --git a/src/main/java/net/nuclearteam/createnuclear/api/multiblock/fluid/ReactorFluidType.java b/src/main/java/net/nuclearteam/createnuclear/api/multiblock/fluid/ReactorFluidType.java index 97422ea..949378a 100644 --- a/src/main/java/net/nuclearteam/createnuclear/api/multiblock/fluid/ReactorFluidType.java +++ b/src/main/java/net/nuclearteam/createnuclear/api/multiblock/fluid/ReactorFluidType.java @@ -9,9 +9,11 @@ import net.minecraft.core.RegistryCodecs; import net.minecraft.core.registries.BuiltInRegistries; import net.minecraft.core.registries.Registries; +import net.minecraft.resources.RegistryFixedCodec; import net.minecraft.resources.ResourceLocation; import net.minecraft.world.level.Level; import net.minecraft.world.level.material.Fluid; +import net.minecraft.world.level.material.Fluids; import net.neoforged.neoforge.fluids.FluidStack; import net.nuclearteam.createnuclear.api.CreateNuclearRegistries; import net.nuclearteam.createnuclear.api.ReactorFluidTypesValue; @@ -23,21 +25,21 @@ import java.util.Optional; import java.util.stream.Collectors; + /** * Represents a reactor fluid type used by the mod's multiblock. *

- * A {@code ReactorFluidType} holds a set of fluids that identify this - * fluid type, heat-related and efficiency values, and a flag indicating - * whether those values should be read from configuration at runtime. + * A {@code ReactorFluidType} holds the single fluid that identifies this + * fluid type, along with its heat-related and efficiency values. */ -public record ReactorFluidType(HolderSet fluids, int maxHeat, int efficiency, boolean useConfig) { - public ReactorFluidType(HolderSet fluids, int maxHeat, int efficiency) { - this(fluids, maxHeat, efficiency, false); +public record ReactorFluidType(Holder fluid, int maxHeat, int efficiency, boolean useConfig) { + public ReactorFluidType(Holder fluid, int maxHeat, int efficiency) { + this(fluid, maxHeat, efficiency, false); } /** Serialization codec for saving/loading {@link ReactorFluidType} instances. */ public static final Codec CODEC = RecordCodecBuilder.create(i -> i.group( - RegistryCodecs.homogeneousList(Registries.FLUID).fieldOf("fluids").forGetter(ReactorFluidType::fluids), + RegistryFixedCodec.create(Registries.FLUID).fieldOf("fluid").forGetter(ReactorFluidType::fluid), Codec.INT.fieldOf("maxHeat").forGetter(ReactorFluidType::maxHeat), Codec.INT.fieldOf("efficiency").forGetter(ReactorFluidType::efficiency) ).apply(i, ReactorFluidType::new)); @@ -52,13 +54,16 @@ public ReactorFluidType(HolderSet fluids, int maxHeat, int efficiency) { * {@code ReactorFluidType} if found, otherwise {@link Optional#empty()} */ public static Optional> getTypeForFluid(RegistryAccess registryAccess, Fluid fluid) { + ResourceLocation fluidKey = BuiltInRegistries.FLUID.getKey(fluid); + if (fluidKey == null) return Optional.empty(); + return registryAccess.lookupOrThrow(CreateNuclearRegistries.FLUID_TYPE) - .listElements() - .filter(ref -> ref.value().fluids.stream() - .anyMatch(h -> h.unwrapKey() - .map(k -> Optional.ofNullable(BuiltInRegistries.FLUID.getKey(fluid)).map(k.location()::equals).orElse(false)) - .orElse(false))) - .findFirst(); + .listElements() + .filter(ref -> ref.value().fluid.unwrapKey() + .map(k -> k.location().equals(fluidKey)) + .orElse(false) + ) + .findFirst(); } /** @@ -74,26 +79,27 @@ public static Optional> getTypeForFluid(RegistryAcce */ public static ReactorFluidType resolveReactorFluidType(Fluid fluid, Level world) { return ReactorFluidType.getTypeForFluid(world.registryAccess(), fluid) - .map(Reference::value) - .orElseGet(() -> { - ReactorFluidType fromFluid = ReactorFluidTypesValue.getReactorFluidType(fluid); - return fromFluid.isNotEmptyFluid() - ? fromFluid - : world.registryAccess() - .registryOrThrow(CreateNuclearRegistries.FLUID_TYPE) - .getHolderOrThrow(CNReactorFluidTypes.FALLBACK) - .value(); - }); + .map(Reference::value) + .orElseGet(() -> { + ReactorFluidType fromFluid = ReactorFluidTypesValue.getReactorFluidType(fluid); + return fromFluid.isNotEmptyFluid() + ? fromFluid + : world.registryAccess() + .registryOrThrow(CreateNuclearRegistries.FLUID_TYPE) + .getHolderOrThrow(CreateNuclearRegistries.FALLBACK_FLUID) + .value(); + }); } /** - * Returns whether this {@code ReactorFluidType} has at least one associated fluid. + * Returns whether this {@code ReactorFluidType} has an associated fluid + * (i.e. is not the empty/sentinel fluid). * - * @return {@code true} if one or more fluids are defined, {@code false} otherwise + * @return {@code true} if a real fluid is defined, {@code false} otherwise */ public boolean isNotEmptyFluid() { - return this.fluids.size() >= 1; + return this.fluid.value() != Fluids.EMPTY; } /** @@ -103,12 +109,9 @@ public boolean isNotEmptyFluid() { * resulting instance. */ public static class Builder { - private final List> fluids = new ArrayList<>(); + private Holder fluid = null; private int maxHeat = 0; private int efficiency = 0; - private boolean useConfig = false; - - private boolean fluidSet = false; private boolean maxHeatSet = false; private boolean efficiencySet = false; @@ -137,93 +140,59 @@ public Builder efficiency(int efficiency) { } /** - * Adds a fluid to this type from a {@link FluidStack}. + * Sets the fluid identifying this type from a {@link FluidStack}. * * @param fluidStack the fluid stack providing the fluid * @return this builder */ public Builder fluid(FluidStack fluidStack) { - this.fluids.add(fluidStack.getFluid().builtInRegistryHolder()); - this.fluidSet = true; - return this; + return fluid(fluidStack.getFluid()); } /** - * Adds a fluid to this type directly. + * Sets the fluid identifying this type. Calling this again replaces + * the previously set fluid. * - * @param fluid the fluid to add + * @param fluid the fluid to associate with this type * @return this builder */ public Builder fluid(Fluid fluid) { - this.fluids.add(fluid.builtInRegistryHolder()); - this.fluidSet = true; - return this; - } - - /** - * Marks this type to resolve numeric values from configuration at runtime. - * - * @return this builder - */ - public Builder setRodConfig() { - this.useConfig = true; + this.fluid = fluid.builtInRegistryHolder(); return this; } /** * Builds the immutable {@link ReactorFluidType} instance. * - * @throws IllegalStateException if required fields are missing * @return the created instance + * @throws IllegalStateException if required fields are missing */ public ReactorFluidType build() { List missing = new ArrayList<>(); - if (!fluidSet || fluids.isEmpty()) missing.add("fluids"); - - if (!this.useConfig) { - if (!maxHeatSet) missing.add("maxHeat"); - if (!efficiencySet) missing.add("efficiency"); - } + if (fluid == null) missing.add("fluid"); + if (!maxHeatSet) missing.add("maxHeat"); + if (!efficiencySet) missing.add("efficiency"); if (!missing.isEmpty()) - throw new IllegalStateException("Missing required RodType fields: " + String.join(", ", missing)); - - return new ReactorFluidType(HolderSet.direct(fluids), maxHeat, efficiency, useConfig); - } - } - - @Override - public int maxHeat() { - if (!useConfig) return maxHeat; - try { - return 12; //CNConfigs.server().rods.maxHeat.get(); - } catch (IllegalStateException e) { - return maxHeat; - } - } + throw new IllegalStateException("Missing required ReactorFluidType fields: " + String.join(", ", missing)); - @Override - public int efficiency() { - if (!useConfig) return efficiency; - try { - return 12; //CNConfigs.server().rods.maxHeat.get(); - } catch (IllegalStateException e) { - return efficiency; + return new ReactorFluidType(fluid, maxHeat, efficiency); } } @Override public @NotNull String toString() { - String fluidNames = this.fluids.stream() - .map(h -> h.unwrapKey() - .map(k -> k.location().toString()) - .orElseGet(() -> { - ResourceLocation rl = BuiltInRegistries.FLUID.getKey(h.value()); - return rl != null ? rl.toString() : h.value().toString(); - }) - ) - .collect(Collectors.joining(", ")); - - return "ReactorFluidType{fluids=[" + fluidNames + "], maxHeat=" + maxHeat() + ", efficiency=" + efficiency() + "}"; + String fluidNames = this.fluid.unwrapKey() + .map(k -> k.location().toString()) + .orElseGet(() -> { + ResourceLocation rl = BuiltInRegistries.FLUID.getKey(this.fluid.value()); + return rl != null ? rl.toString() : this.fluid.value().toString(); + }); + + return "ReactorFluidType{" + + "fluids=[" + fluidNames + "]," + + "maxHeat=" + maxHeat() + "," + + "efficiency=" + efficiency() + + "}"; } } diff --git a/src/main/java/net/nuclearteam/createnuclear/api/multiblock/rods/RodType.java b/src/main/java/net/nuclearteam/createnuclear/api/multiblock/rods/RodType.java index 2a17dc3..93d74f8 100644 --- a/src/main/java/net/nuclearteam/createnuclear/api/multiblock/rods/RodType.java +++ b/src/main/java/net/nuclearteam/createnuclear/api/multiblock/rods/RodType.java @@ -8,51 +8,66 @@ import net.minecraft.core.HolderSet; import net.minecraft.core.RegistryAccess; import net.minecraft.core.RegistryCodecs; +import net.minecraft.core.registries.BuiltInRegistries; import net.minecraft.core.registries.Registries; +import net.minecraft.resources.RegistryFixedCodec; +import net.minecraft.resources.ResourceLocation; import net.minecraft.util.StringRepresentable; import net.minecraft.world.item.Item; import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; import net.minecraft.world.level.ItemLike; import net.minecraft.world.level.Level; +import net.neoforged.neoforge.registries.NeoForgeRegistries; import net.nuclearteam.createnuclear.CNTags.CNItemTags; import net.nuclearteam.createnuclear.api.CreateNuclearRegistries; import net.nuclearteam.createnuclear.api.ItemRodTypesValue; -import net.nuclearteam.createnuclear.content.rod.CNRodTypes; -import net.nuclearteam.createnuclear.infrastructure.config.CNConfigs; +import net.nuclearteam.createnuclear.content.multiblock.rod.CNRodTypes; import java.util.ArrayList; import java.util.List; import java.util.Objects; import java.util.Optional; import java.util.function.Predicate; +import java.util.function.Supplier; /** * Represents a rod type used by the mod's multiblock. *

- * A {@code RodType} holds a set of items that can represent this rod type, + * A {@code RodType} holds the single item that represents this rod type, * heat-related values, a timing value, and a {@link TypeRod} indicating the * category (fuel, cooler, or mixed). */ @MethodsReturnNonnullByDefault -public record RodType(HolderSet items, - int baseRodHeat, float proximityRodHeat, - int rodTimer, TypeRod type, boolean useConfig) { - - public RodType(HolderSet items, +public record RodType(Holder item, + Supplier baseRodHeat, + Supplier proximityRodHeat, + Supplier rodTimer, + Supplier ratio, + TypeRod type) { + + public RodType(Holder item, int baseRodHeat, float proximityRodHeat, int rodTimer, TypeRod type) { - this(items, baseRodHeat, proximityRodHeat, rodTimer, type, false); + this(item, baseRodHeat, proximityRodHeat, rodTimer, type, 1); + } + + public RodType(Holder item, + int baseRodHeat, float proximityRodHeat, + int rodTimer, TypeRod type, int ratio) { + this(item, () -> baseRodHeat, () -> proximityRodHeat, () -> rodTimer, () -> ratio, type); } /** * Serialization codec for saving/loading {@link RodType} instances. */ public static final Codec CODEC = RecordCodecBuilder.create(i -> i.group( - RegistryCodecs.homogeneousList(Registries.ITEM).fieldOf("items").forGetter(RodType::items), - Codec.INT.fieldOf("baseRodHeat").forGetter(RodType::baseRodHeat), - Codec.FLOAT.fieldOf("proximityRodHeat").forGetter(RodType::proximityRodHeat), - Codec.INT.fieldOf("rodTimer").forGetter(RodType::rodTimer), - StringRepresentable.fromEnum(TypeRod::values).fieldOf("type").forGetter(RodType::type) + RegistryFixedCodec.create(Registries.ITEM).fieldOf("item").forGetter(RodType::item), + Codec.INT.fieldOf("baseRodHeat").forGetter(rt -> rt.baseRodHeat().get()), + Codec.FLOAT.fieldOf("proximityRodHeat").forGetter(rt -> rt.proximityRodHeat().get()), + Codec.INT.fieldOf("rodTimer").forGetter(rt -> rt.rodTimer().get()), + StringRepresentable.fromEnum(TypeRod::values).fieldOf("type").forGetter(RodType::type), + Codec.INT.optionalFieldOf("ratio", 1).forGetter(rt -> rt.ratio().get()) ).apply(i, RodType::new)); /** @@ -67,7 +82,7 @@ public RodType(HolderSet items, public static Optional> getTypeForItem(RegistryAccess registryAccess, Item item) { return registryAccess.lookupOrThrow(CreateNuclearRegistries.ROD_TYPE) .listElements() - .filter(ref -> ref.value().items.contains(item.builtInRegistryHolder())) + .filter(ref -> ref.value().item.equals(item.builtInRegistryHolder())) .findFirst(); } @@ -82,197 +97,223 @@ public static Optional> getTypeForItem(RegistryAccess registr */ public static RodType resolveRodType(Item item, Level world) { return RodType.getTypeForItem(world.registryAccess(), item) - .map(Reference::value) + .map(Holder.Reference::value) .orElseGet(() -> { RodType fromItem = ItemRodTypesValue.getRodType(item); return fromItem.isNotEmptyItem() ? fromItem : world.registryAccess() .registryOrThrow(CreateNuclearRegistries.ROD_TYPE) - .getHolderOrThrow(CNRodTypes.FALLBACK) + .getHolderOrThrow(CreateNuclearRegistries.FALLBACK_ROD) .value(); }); } /** - * Returns whether this {@code RodType} has no associated items. + * Returns whether this {@code RodType} has an associated item (i.e. is + * not the empty/sentinel item). * - * @return {@code true} if no items are defined, {@code false} otherwise + * @return {@code true} if a real item is defined, {@code false} otherwise */ public boolean isNotEmptyItem() { - return this.items.size() >= 1; + return this.item.value() != Items.AIR; } /** * Fluent builder for creating an immutable {@link RodType} instance. *

- * Use the configuration methods and call {@link #build()} to obtain the - * resulting instance. + * Every heat/timing value ({@code baseRodHeat}, {@code proximityRodHeat}, + * {@code rodTimer}, {@code ratio}) can be set either as a fixed + * primitive (evaluated once, at build time) or as a {@link Supplier} + * (re-evaluated on every call to the corresponding {@link RodType} + * accessor). The primitive overloads are pure convenience: they simply + * wrap the given value in a constant supplier, e.g. + * {@code baseRodHeat(5)} is equivalent to {@code baseRodHeat(() -> 5)}. + * Use the {@link Supplier} overloads to back a value with a live source + * such as a config option (see {@code CNItems} for examples), so that + * changes are picked up without rebuilding the {@code RodType}. + *

+ * {@code item}, {@code type}, {@code baseRodHeat}, {@code proximityRodHeat} + * and {@code rodTimer} are required; {@code ratio} defaults to a + * constant {@code 1} and never needs to be set explicitly. Call + * {@link #build()} once all required values are configured. */ public static class Builder { - private final List> items = new ArrayList<>(); - private int baseRodHeat = 0; - private int proximityRodHeat = 0; - private int rodTimer = 0; - private TypeRod type = TypeRod.FUEL; - private boolean useConfig = false; - - private boolean itemsSet = false; - private boolean baseRodHeatSet = false; - private boolean proximityRodHeatSet = false; - private boolean rodTimerSet = false; - private boolean typeSet = false; + private Holder item = null; + private Supplier baseRodHeat = null; + private Supplier proximityRodHeat = null; + private Supplier rodTimer = null; + private Supplier ratio = null; + private TypeRod type = TypeRod.NONE; /** - * Sets the base heat for the rod. + * Sets a fixed base heat value, evaluated once at build time. + * Equivalent to {@code baseRodHeat(() -> baseRodHeat)}. * - * @param baseRodHeat base heat value + * @param baseRodHeat constant base heat value * @return this builder */ public Builder baseRodHeat(int baseRodHeat) { + return baseRodHeat(() -> baseRodHeat); + } + + /** + * Sets a dynamic base heat value, re-evaluated every time + * {@link RodType#baseRodHeat()} is called (e.g. to reflect a live + * config option instead of a value frozen at registration time). + * + * @param baseRodHeat supplier producing the base heat value on demand + * @return this builder + */ + public Builder baseRodHeat(Supplier baseRodHeat) { this.baseRodHeat = baseRodHeat; - this.baseRodHeatSet = true; return this; } /** - * Sets the heat contributed by nearby rods. + * Sets a fixed proximity heat value (heat contributed by nearby rods), + * evaluated once at build time. + * Equivalent to {@code proximityRodHeat(() -> proximityRodHeat)}. * - * @param proximityRodHeat proximity heat value + * @param proximityRodHeat constant proximity heat value * @return this builder */ - public Builder proximityRodHeat(int proximityRodHeat) { + public Builder proximityRodHeat(float proximityRodHeat) { + return proximityRodHeat(() -> proximityRodHeat) ; + } + + /** + * Sets a dynamic proximity heat value, re-evaluated every time + * {@link RodType#proximityRodHeat()} is called. + * + * @param proximityRodHeat supplier producing the proximity heat value on demand + * @return this builder + */ + public Builder proximityRodHeat(Supplier proximityRodHeat) { this.proximityRodHeat = proximityRodHeat; - this.proximityRodHeatSet = true; return this; } /** - * Sets the timer (duration) for the rod's behavior. + * Sets a fixed timer (duration) for the rod's behavior, evaluated + * once at build time. Equivalent to {@code rodTimer(() -> rodTimer)}. * - * @param rodTimer duration in ticks or mod-specific units + * @param rodTimer constant duration in ticks or mod-specific units * @return this builder */ public Builder rodTimer(int rodTimer) { + return rodTimer(() -> rodTimer); + } + + /** + * Sets a dynamic timer (duration) for the rod's behavior, + * re-evaluated every time {@link RodType#rodTimer()} is called. + * + * @param rodTimer supplier producing the duration on demand + * @return this builder + */ + public Builder rodTimer(Supplier rodTimer) { this.rodTimer = rodTimer; - this.rodTimerSet = true; return this; } /** - * Sets the rod type to {@link TypeRod#COOLER}. + * Sets a fixed heat ratio, evaluated once at build time. Optional: + * defaults to a constant {@code 1} if never called. + * Equivalent to {@code ratio(() -> ratio)}. * + * @param ratio constant heat ratio value * @return this builder */ - public Builder coolerRodType() { - this.type = TypeRod.COOLER; - this.typeSet = true; - return this; + public Builder ratio(int ratio) { + return ratio(() -> ratio); } /** - * Sets the rod type to {@link TypeRod#FUEL}. + * Sets a dynamic heat ratio, re-evaluated every time + * {@link RodType#ratio()} is called. Optional: defaults to a + * constant {@code 1} if never called. * + * @param ratio supplier producing the heat ratio on demand * @return this builder */ - public Builder fuelRodType() { - this.type = TypeRod.FUEL; - this.typeSet = true; + public Builder ratio(Supplier ratio) { + this.ratio = ratio; return this; } /** - * Adds one or more items that represent this rod type. + * Sets the rod category directly. Prefer the {@link #coolerRodType()} + * and {@link #fuelRodType()} convenience methods for the common + * cases; use this overload only to set {@link TypeRod#NONE} + * explicitly (e.g. for a fallback/sentinel rod type). * - * @param items array of {@link ItemLike} elements to associate + * @param type the rod category to assign * @return this builder */ - public Builder addItems(ItemLike... items) { - for (ItemLike provider : items) - this.items.add(provider.asItem().builtInRegistryHolder()); - if (items.length > 0) this.itemsSet = true; + public Builder type(TypeRod type) { + this.type = type; return this; } - public Builder setRodConfig() { - this.useConfig = true; + /** + * Sets the rod type to {@link TypeRod#COOLER}. + * + * @return this builder + */ + public Builder coolerRodType() { + return type(TypeRod.COOLER); + } + + /** + * Sets the rod type to {@link TypeRod#FUEL}. + * + * @return this builder + */ + public Builder fuelRodType() { + return type(TypeRod.FUEL); + } + + /** + * Sets the item that represents this rod type. Calling this again + * replaces the previously set item. + * + * @param item the item to associate with this rod type + * @return this builder + */ + public Builder item(ItemLike item) { + this.item = item.asItem().builtInRegistryHolder(); return this; } /** * Builds the immutable {@link RodType} instance. + *

+ * Validates that {@code item} and {@code type}, {@code baseRodHeat}, + * {@code proximityRodHeat} and {@code rodTimer} have all been set + * (either as a fixed value or as a dynamic {@link Supplier}); + * {@code ratio} is exempt since it defaults to a constant + * {@code 1}. * - * @throws IllegalStateException if required fields are missing + * @throws IllegalStateException if one or more required fields are missing, + * naming every missing field in the exception message * @return the created instance */ public RodType build() { List missing = new ArrayList<>(); - if (!itemsSet || items.isEmpty()) missing.add("items"); - if (!typeSet) missing.add("type"); - - if (!this.useConfig) { - if (!baseRodHeatSet) missing.add("baseRodHeat"); - if (!proximityRodHeatSet) missing.add("proximityRodHeat"); - if (!rodTimerSet) missing.add("rodTimer"); - } else { - /* Lazy config resolution: don't access CNConfigs here during registration. - * The actual values will be read at runtime via the resolved* accessors. - * Only MIXTE is considered an error for "useConfig" because there are - * no configuration defaults for the mixed type. - */ -// missing.add("Configuration defaults cannot be applied to the rod type"); - } + if (item == null) missing.add("item"); + if (type == null) missing.add("type"); + if (baseRodHeat == null) missing.add("baseRodHeat"); + if (proximityRodHeat == null) missing.add("proximityRodHeat"); + if (rodTimer == null) missing.add("rodTimer"); if (!missing.isEmpty()) throw new IllegalStateException("Missing required RodType fields: " + String.join(", ", missing)); - return new RodType(HolderSet.direct(items), baseRodHeat, proximityRodHeat, rodTimer, type, useConfig); + return new RodType(item, baseRodHeat, proximityRodHeat, rodTimer, ratio, type); } } - @Override - public int baseRodHeat() { - if (!useConfig) return baseRodHeat; - try { - return switch (type) { - //case FUEL -> CNConfigs.server().rods.baseValueUranium.get(); - //case COOLER -> CNConfigs.server().rods.baseValueGraphite.get(); - default -> baseRodHeat; - }; - } catch (IllegalStateException e) { - return baseRodHeat; - } - } - - @Override - public float proximityRodHeat() { - if (!useConfig) return proximityRodHeat; - try { - return switch (type) { - //case FUEL -> CNConfigs.server().rods.uraniumProxyBonus.get(); - //case COOLER -> CNConfigs.server().rods.graphiteProxyMalus.getF(); - default -> proximityRodHeat; - }; - } catch (IllegalStateException e) { - return proximityRodHeat; - } - } - - @Override - public int rodTimer() { - if (!useConfig) return rodTimer; - try { - return switch (type) { - //case FUEL -> CNConfigs.server().rods.uraniumRodLifetime.get(); - //case COOLER -> CNConfigs.server().rods.graphiteRodLifetime.get(); - default -> rodTimer; - }; - } catch (IllegalStateException e) { - return rodTimer; - } - } - - public enum TypeRod implements StringRepresentable { /** * Fuel rod: generates heat via reaction. @@ -297,31 +338,44 @@ public String getSerializedName() { public static final class TypeRodPredicate { public static final Predicate IS_NOT_NULL = Objects::nonNull; - public static final Predicate IS_FUEL = s -> { - TypeRod type = ItemRodTypesValue.getRodType(s.getItem()).type; - return IS_NOT_NULL.test(s) && (s.is(CNItemTags.FUEL.tag) || type == TypeRod.FUEL); - }; + public static boolean isFuel(ItemStack stack, Level level) { + return IS_NOT_NULL.test(stack) && RodType.resolveRodType(stack.getItem(), level).type == TypeRod.FUEL; + } - public static final Predicate IS_COOLED = s -> { - TypeRod type = ItemRodTypesValue.getRodType(s.getItem()).type; - return IS_NOT_NULL.test(s) && (s.is(CNItemTags.COOLER.tag) || type == TypeRod.COOLER); - }; + public static boolean isFuel(RodType rodType) { + return rodType != null && rodType.type() == TypeRod.FUEL; + } + + public static boolean isCooled(ItemStack stack, Level level) { + return IS_NOT_NULL.test(stack) && RodType.resolveRodType(stack.getItem(), level).type == TypeRod.COOLER; + } + + public static boolean isCooled(RodType rodType) { + return rodType != null && rodType.type() == TypeRod.COOLER; + } - public static String tooltipKey(ItemStack stack) { + public static String tooltipKey(ItemStack stack, Level level) { if (!IS_NOT_NULL.test(stack)) return "unknown"; - if (IS_FUEL.test(stack)) return "fuel"; - if (IS_COOLED.test(stack)) return "cooled"; + if (isFuel(stack, level)) return "fuel"; + if (isCooled(stack, level)) return "cooled"; return "unknown"; } } @Override public String toString() { - return "RodType [items: " + this.items() + - ", baseRodHeat: " + this.baseRodHeat() + - ", proximityRodHeat: " + this.proximityRodHeat() + - ", rodTimer: " + this.rodTimer() + - ", type: " + this.type() + - ", useConfig: " + this.useConfig() + "]"; + String itemName = this.item.unwrapKey() + .map(k -> k.location().toString()) + .orElseGet(() -> { + ResourceLocation rl = BuiltInRegistries.ITEM.getKey(this.item.value()); + return rl != null ? rl.toString() : this.item.value().toString(); + }); + + return "RodType [item: " + itemName + + ", baseRodHeat: " + this.baseRodHeat().get() + + ", proximityRodHeat: " + this.proximityRodHeat().get() + + ", rodTimer: " + this.rodTimer().get() + + ", ratio: " + this.ratio().get() + + ", type: " + this.type() + "]"; } } diff --git a/src/main/java/net/nuclearteam/createnuclear/compat/Mods.java b/src/main/java/net/nuclearteam/createnuclear/compat/Mods.java new file mode 100644 index 0000000..cba6eec --- /dev/null +++ b/src/main/java/net/nuclearteam/createnuclear/compat/Mods.java @@ -0,0 +1,86 @@ +package net.nuclearteam.createnuclear.compat; + +import net.createmod.catnip.lang.Lang; +import net.createmod.catnip.registry.RegisteredObjectsHelper; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.Item; +import net.minecraft.world.level.ItemLike; +import net.minecraft.world.level.block.Block; +import net.neoforged.fml.loading.LoadingModList; + +import java.util.Optional; +import java.util.function.Supplier; + +public enum Mods { + ALEXS_CAVE("alexscaves") + ; + + private final String id; + private final boolean isLoaded; + + Mods(String name) { + id = Lang.asId(name); + isLoaded = LoadingModList.get().getModFileById(id) != null; + } + + Mods() { + id = Lang.asId(name()); + isLoaded = LoadingModList.get().getModFileById(id) != null; + } + + public String id() { + return id; + } + + public ResourceLocation rl(String path) { + return ResourceLocation.fromNamespaceAndPath(id, path); + } + + public Block getBlock(String id) { + return BuiltInRegistries.BLOCK.get(rl(id)); + } + + public Item getItem(String id) { + return BuiltInRegistries.ITEM.get(rl(id)); + } + + public boolean contains(ItemLike entry) { + if (!isLoaded()) + return false; + Item asItem = entry.asItem(); + return asItem != null && RegisteredObjectsHelper.getKeyOrThrow(asItem) + .getNamespace() + .equals(id); + } + + /** + * @return a boolean of whether the mod is loaded or not based on mod id + */ + public boolean isLoaded() { + return isLoaded; + } + + /** + * Simple hook to run code if a mod is installed + * + * @param toRun will be run only if the mod is loaded + * @return Optional.empty() if the mod is not loaded, otherwise an Optional of the return value of the given supplier + */ + public Optional runIfInstalled(Supplier> toRun) { + if (isLoaded()) + return Optional.of(toRun.get().get()); + return Optional.empty(); + } + + /** + * Simple hook to execute code if a mod is installed + * + * @param toExecute will be executed only if the mod is loaded + */ + public void executeIfInstalled(Supplier toExecute) { + if (isLoaded()) { + toExecute.get().run(); + } + } +} diff --git a/src/main/java/net/nuclearteam/createnuclear/compat/alexscave/AlexscaveCompat.java b/src/main/java/net/nuclearteam/createnuclear/compat/alexscave/AlexscaveCompat.java new file mode 100644 index 0000000..3fa1d95 --- /dev/null +++ b/src/main/java/net/nuclearteam/createnuclear/compat/alexscave/AlexscaveCompat.java @@ -0,0 +1,66 @@ +package net.nuclearteam.createnuclear.compat.alexscave; + +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.Explosion; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.state.BlockState; + +/** + * The mod doesn't exist for 1.21.1. + */ +public class AlexscaveCompat { + public AlexscaveCompat() {} + + public boolean MobSpawn(BlockState state, Level level, BlockPos.MutableBlockPos carve, float itemDropModifier, Explosion dummyExplosion){ +// if(state.is(ACBlockRegistry.TREMORZILLA_EGG.get()) && state.getBlock() instanceof TremorzillaEggBlock tremorzillaEggBlock){ +// tremorzillaEggBlock.spawnDinosaurs(level, carve, state); +// return true; +// } +// else if (AlexsCaves.COMMON_CONFIG.nukesSpawnItemDrops.get() && random.nextFloat() < itemDropModifier && state.getFluidState().isEmpty()) { +// level.destroyBlock(carve, true); +// } else { +// state.onBlockExploded(level, carve, dummyExplosion); +// } + return false; + } + +// public boolean isRaycat(LivingEntity entity) { +// return entity instanceof RaycatEntity; +// } +// +// public boolean isTremorzilla(LivingEntity entity){ +// return entity instanceof TremorzillaEntity; +// } +// +// public boolean ACResConfig(){ +// return AlexsCaves.COMMON_CONFIG.nukeMaxBlockExplosionResistance.get() <= 0; +// } + +// public boolean ACDestroyable(BlockState state) { +// return !state.is(ACTagRegistry.NUKE_PROOF) && +// (state.getBlock().getExplosionResistance() < AlexsCaves.COMMON_CONFIG.nukeMaxBlockExplosionResistance.get() +// || state.is(ACBlockRegistry.TREMORZILLA_EGG.get())); +// } + + public void NukeParam() { +// ((ClientProxy) AlexsCaves.PROXY).renderNukeSkyDarkFor = 70; +// ((ClientProxy) AlexsCaves.PROXY).muteNonNukeSoundsFor = 50; + } + +// public SoundEvent[] GetACSounds(){ +// return new SoundEvent[] { +// ACSoundRegistry.LARGE_NUCLEAR_EXPLOSION.get(), +// ACSoundRegistry.NUCLEAR_EXPLOSION.get(), +// ACSoundRegistry.NUCLEAR_EXPLOSION_RINGING.get(), +// ACSoundRegistry.NUCLEAR_EXPLOSION_RUMBLE.get() +// }; +// } + +// public boolean GetACConfig(){ +// return AlexsCaves.CLIENT_CONFIG.nuclearBombFlash.get(); +// } + + public void UpdateACProxy() { +// ((ClientProxy) AlexsCaves.PROXY).renderNukeFlashFor = 16; + } +} diff --git a/src/main/java/net/nuclearteam/createnuclear/compat/jei/CreateNuclearJEI.java b/src/main/java/net/nuclearteam/createnuclear/compat/jei/CreateNuclearJEI.java index fa22835..67eaca8 100644 --- a/src/main/java/net/nuclearteam/createnuclear/compat/jei/CreateNuclearJEI.java +++ b/src/main/java/net/nuclearteam/createnuclear/compat/jei/CreateNuclearJEI.java @@ -32,6 +32,7 @@ import net.minecraft.resources.ResourceLocation; import net.minecraft.world.effect.MobEffect; import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; import net.minecraft.world.item.alchemy.Potion; import net.minecraft.world.item.alchemy.PotionContents; import net.minecraft.world.item.crafting.Recipe; @@ -43,7 +44,9 @@ import net.nuclearteam.createnuclear.CNRecipeTypes; import net.nuclearteam.createnuclear.CreateNuclear; import net.nuclearteam.createnuclear.compat.jei.category.FanEnrichedCategory; +import net.nuclearteam.createnuclear.compat.jei.category.FanSnowPowderCategory; import net.nuclearteam.createnuclear.content.kinetics.fan.processing.EnrichedRecipe; +import net.nuclearteam.createnuclear.content.kinetics.fan.processing.SnowPowderRecipe; import javax.annotation.Nonnull; import javax.annotation.ParametersAreNonnullByDefault; @@ -72,7 +75,13 @@ private void loadCategories() { .catalystStack(ProcessingViaFanCategory.getFan("fan_enriched")) .doubleItemIcon(AllItems.PROPELLER.get(), CNBlocks.ENRICHING_CAMPFIRE.get()) .emptyBackground(178, 72) - .build("fan_enriched", FanEnrichedCategory::new) + .build("fan_enriched", FanEnrichedCategory::new), + snow_powder = builder(SnowPowderRecipe.class) + .addTypedRecipes(CNRecipeTypes.SNOW_POWDER::getType) + .catalystStack(ProcessingViaFanCategory.getFan("fan_snow_powder")) + .doubleItemIcon(AllItems.PROPELLER.get(), Items.POWDER_SNOW_BUCKET) + .emptyBackground(178, 72) + .build("fan_snow_powder", FanSnowPowderCategory::new) ; } diff --git a/src/main/java/net/nuclearteam/createnuclear/compat/jei/category/FanSnowPowderCategory.java b/src/main/java/net/nuclearteam/createnuclear/compat/jei/category/FanSnowPowderCategory.java new file mode 100644 index 0000000..45aa509 --- /dev/null +++ b/src/main/java/net/nuclearteam/createnuclear/compat/jei/category/FanSnowPowderCategory.java @@ -0,0 +1,40 @@ +package net.nuclearteam.createnuclear.compat.jei.category; + +import com.simibubi.create.compat.jei.category.ProcessingViaFanCategory; +import com.simibubi.create.compat.jei.category.animations.AnimatedKinetics; +import com.simibubi.create.foundation.gui.AllGuiTextures; +import net.createmod.catnip.gui.element.GuiGameElement; +import net.minecraft.MethodsReturnNonnullByDefault; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.network.chat.Component; +import net.minecraft.world.level.block.Blocks; +import net.nuclearteam.createnuclear.content.kinetics.fan.processing.SnowPowderRecipe; + +import javax.annotation.ParametersAreNonnullByDefault; + +@MethodsReturnNonnullByDefault +@ParametersAreNonnullByDefault +public class FanSnowPowderCategory extends ProcessingViaFanCategory.MultiOutput { + public FanSnowPowderCategory(Info info) { + super(info); + } + + @Override + protected AllGuiTextures getBlockShadow() { + return AllGuiTextures.JEI_LIGHT; + } + + @Override + protected void renderAttachedBlock(GuiGraphics graphics) { + GuiGameElement.of(Blocks.POWDER_SNOW.defaultBlockState()) + .scale(SCALE) + .atLocal(0, 0, 2) + .lighting(AnimatedKinetics.DEFAULT_LIGHTING) + .render(graphics); + } + + @Override + public Component getTitle() { + return Component.translatable("createnuclear.snow_powder.fan.recipe"); + } +} \ No newline at end of file diff --git a/src/main/java/net/nuclearteam/createnuclear/content/contraptions/irradiated/cat/IrradiatedCatLieOnBedGoal.java b/src/main/java/net/nuclearteam/createnuclear/content/contraptions/irradiated/cat/CatLieOnBedGoal.java similarity index 90% rename from src/main/java/net/nuclearteam/createnuclear/content/contraptions/irradiated/cat/IrradiatedCatLieOnBedGoal.java rename to src/main/java/net/nuclearteam/createnuclear/content/contraptions/irradiated/cat/CatLieOnBedGoal.java index 1c584a7..4e9fe7c 100644 --- a/src/main/java/net/nuclearteam/createnuclear/content/contraptions/irradiated/cat/IrradiatedCatLieOnBedGoal.java +++ b/src/main/java/net/nuclearteam/createnuclear/content/contraptions/irradiated/cat/CatLieOnBedGoal.java @@ -10,10 +10,10 @@ import java.util.EnumSet; @ParametersAreNonnullByDefault -public class IrradiatedCatLieOnBedGoal extends MoveToBlockGoal { +public class CatLieOnBedGoal extends MoveToBlockGoal { private final IrradiatedCat irradiatedCat; - public IrradiatedCatLieOnBedGoal(IrradiatedCat cat, double speedModifier, int searchRange) { + public CatLieOnBedGoal(IrradiatedCat cat, double speedModifier, int searchRange) { super(cat, speedModifier, searchRange, 6); this.irradiatedCat = cat; this.verticalSearchStart = -2; diff --git a/src/main/java/net/nuclearteam/createnuclear/content/contraptions/irradiated/cat/IrradiatedCatSitOnBlockGoal.java b/src/main/java/net/nuclearteam/createnuclear/content/contraptions/irradiated/cat/CatSitOnBlockGoal.java similarity index 92% rename from src/main/java/net/nuclearteam/createnuclear/content/contraptions/irradiated/cat/IrradiatedCatSitOnBlockGoal.java rename to src/main/java/net/nuclearteam/createnuclear/content/contraptions/irradiated/cat/CatSitOnBlockGoal.java index 2d0e460..d43d398 100644 --- a/src/main/java/net/nuclearteam/createnuclear/content/contraptions/irradiated/cat/IrradiatedCatSitOnBlockGoal.java +++ b/src/main/java/net/nuclearteam/createnuclear/content/contraptions/irradiated/cat/CatSitOnBlockGoal.java @@ -11,10 +11,10 @@ import net.minecraft.world.level.block.state.BlockState; import net.minecraft.world.level.block.state.properties.BedPart; -public class IrradiatedCatSitOnBlockGoal extends MoveToBlockGoal { +public class CatSitOnBlockGoal extends MoveToBlockGoal { private final IrradiatedCat irradiatedCat; - public IrradiatedCatSitOnBlockGoal(IrradiatedCat cat, double speedModifier) { + public CatSitOnBlockGoal(IrradiatedCat cat, double speedModifier) { super(cat, speedModifier, 8); this.irradiatedCat = cat; } diff --git a/src/main/java/net/nuclearteam/createnuclear/content/contraptions/irradiated/cat/IrradiatedCat.java b/src/main/java/net/nuclearteam/createnuclear/content/contraptions/irradiated/cat/IrradiatedCat.java index 3472b19..20c8a6f 100644 --- a/src/main/java/net/nuclearteam/createnuclear/content/contraptions/irradiated/cat/IrradiatedCat.java +++ b/src/main/java/net/nuclearteam/createnuclear/content/contraptions/irradiated/cat/IrradiatedCat.java @@ -66,9 +66,9 @@ protected void registerGoals() { this.goalSelector.addGoal(2, new SitWhenOrderedToGoal(this)); this.goalSelector.addGoal(3, new IrradiatedCatRelaxOnOwnerGoal(this)); this.goalSelector.addGoal(4, this.temptGoal); - this.goalSelector.addGoal(5, new IrradiatedCatLieOnBedGoal(this, 1.1, 8)); + this.goalSelector.addGoal(5, new CatLieOnBedGoal(this, 1.1, 8)); this.goalSelector.addGoal(6, new FollowOwnerGoal(this, 1.0F, 10.0F, 5.0F)); - this.goalSelector.addGoal(7, new IrradiatedCatSitOnBlockGoal(this, 0.8)); + this.goalSelector.addGoal(7, new CatSitOnBlockGoal(this, 0.8)); this.goalSelector.addGoal(8, new LeapAtTargetGoal(this, 0.3F)); this.goalSelector.addGoal(9, new OcelotAttackGoal(this)); this.goalSelector.addGoal(10, new BreedGoal(this, 0.8)); diff --git a/src/main/java/net/nuclearteam/createnuclear/content/enriching/campfire/EnrichingCampfireBlock.java b/src/main/java/net/nuclearteam/createnuclear/content/enriching/campfire/EnrichingCampfireBlock.java index b76720a..880fc58 100644 --- a/src/main/java/net/nuclearteam/createnuclear/content/enriching/campfire/EnrichingCampfireBlock.java +++ b/src/main/java/net/nuclearteam/createnuclear/content/enriching/campfire/EnrichingCampfireBlock.java @@ -44,6 +44,7 @@ import net.minecraft.world.phys.shapes.VoxelShape; import net.nuclearteam.createnuclear.CNBlockEntityTypes; import net.nuclearteam.createnuclear.CNEffects; +import net.nuclearteam.createnuclear.content.radiation.capability.RadiationCapability; import org.jetbrains.annotations.Nullable; import javax.annotation.ParametersAreNonnullByDefault; @@ -52,6 +53,8 @@ @ParametersAreNonnullByDefault @SuppressWarnings({"deprecation", "unused"}) public class EnrichingCampfireBlock extends BaseEntityBlock implements SimpleWaterloggedBlock, IBE { + private static final double CAMPFIRE_DOSE = 5.0D; + public static final MapCodec CODEC = RecordCodecBuilder.mapCodec( (enrichChampFireBlock) -> enrichChampFireBlock @@ -84,8 +87,8 @@ protected ItemInteractionResult useItemOn(ItemStack stack, BlockState state, Lev @Override public void entityInside(BlockState state, Level level, BlockPos pos, Entity entity) { - if (state.getValue(LIT) && entity instanceof LivingEntity) { - ((LivingEntity) entity).addEffect(new MobEffectInstance(CNEffects.RADIATION.getDelegate(), 100, 0)); + if (state.getValue(LIT) && entity instanceof LivingEntity livingEntity) { + RadiationCapability.applyContagion(livingEntity, CAMPFIRE_DOSE, 100); } super.entityInside(state, level, pos, entity); } diff --git a/src/main/java/net/nuclearteam/createnuclear/content/equipment/armor/AntiRadiationArmorItem.java b/src/main/java/net/nuclearteam/createnuclear/content/equipment/armor/AntiRadiationArmorItem.java index 9b67964..76061f5 100644 --- a/src/main/java/net/nuclearteam/createnuclear/content/equipment/armor/AntiRadiationArmorItem.java +++ b/src/main/java/net/nuclearteam/createnuclear/content/equipment/armor/AntiRadiationArmorItem.java @@ -60,25 +60,25 @@ public static NonNullUnaryOperator> setColo public static class Helmet extends AntiRadiationArmorItem implements IGoggleHelmet { public Helmet(Properties p) { - super(CNArmorMaterials.ANTI_RADIATION_SUIT, Type.HELMET, p); + super(ArmorMaterials.ANTI_RADIATION_SUIT, Type.HELMET, p); } } public static class Chestplate extends AntiRadiationArmorItem { public Chestplate(Properties p) { - super(CNArmorMaterials.ANTI_RADIATION_SUIT, Type.CHESTPLATE, p); + super(ArmorMaterials.ANTI_RADIATION_SUIT, Type.CHESTPLATE, p); } } public static class Leggings extends AntiRadiationArmorItem { public Leggings(Properties p) { - super(CNArmorMaterials.ANTI_RADIATION_SUIT, Type.LEGGINGS, p); + super(ArmorMaterials.ANTI_RADIATION_SUIT, Type.LEGGINGS, p); } } public static class Boot extends AntiRadiationArmorItem { public Boot(Properties p) { - super(CNArmorMaterials.ANTI_RADIATION_SUIT, Type.BOOTS, p); + super(ArmorMaterials.ANTI_RADIATION_SUIT, Type.BOOTS, p); } } diff --git a/src/main/java/net/nuclearteam/createnuclear/content/equipment/armor/CNArmorMaterials.java b/src/main/java/net/nuclearteam/createnuclear/content/equipment/armor/ArmorMaterials.java similarity index 98% rename from src/main/java/net/nuclearteam/createnuclear/content/equipment/armor/CNArmorMaterials.java rename to src/main/java/net/nuclearteam/createnuclear/content/equipment/armor/ArmorMaterials.java index de77dfe..dc49234 100644 --- a/src/main/java/net/nuclearteam/createnuclear/content/equipment/armor/CNArmorMaterials.java +++ b/src/main/java/net/nuclearteam/createnuclear/content/equipment/armor/ArmorMaterials.java @@ -18,7 +18,7 @@ import java.util.List; import java.util.function.Supplier; -public class CNArmorMaterials { +public class ArmorMaterials { private static final DeferredRegister ARMOR_MATERIALS = DeferredRegister.create(Registries.ARMOR_MATERIAL, CreateNuclear.MOD_ID); public static final Holder ANTI_RADIATION_SUIT = register( diff --git a/src/main/java/net/nuclearteam/createnuclear/content/kinetics/fan/processing/CNFanProcessingTypes.java b/src/main/java/net/nuclearteam/createnuclear/content/kinetics/fan/processing/CNFanProcessingTypes.java index 0f99368..2b3b47d 100644 --- a/src/main/java/net/nuclearteam/createnuclear/content/kinetics/fan/processing/CNFanProcessingTypes.java +++ b/src/main/java/net/nuclearteam/createnuclear/content/kinetics/fan/processing/CNFanProcessingTypes.java @@ -12,6 +12,7 @@ import net.minecraft.core.particles.ParticleTypes; import net.minecraft.util.RandomSource; import net.minecraft.world.effect.MobEffectInstance; +import net.minecraft.world.effect.MobEffects; import net.minecraft.world.entity.Entity; import net.minecraft.world.entity.LivingEntity; import net.minecraft.world.item.ItemStack; @@ -23,6 +24,8 @@ import net.minecraft.world.phys.Vec3; import net.nuclearteam.createnuclear.*; import net.nuclearteam.createnuclear.content.enriching.campfire.EnrichingCampfireBlock; +import net.nuclearteam.createnuclear.content.radiation.capability.RadiationCapability; +import net.nuclearteam.createnuclear.foundation.damageTypes.CNDamageSources; import javax.annotation.Nullable; import java.util.List; @@ -32,12 +35,14 @@ @SuppressWarnings("unused") public class CNFanProcessingTypes { public static final EnrichedType ENRICHED = register("enriched", new EnrichedType()); + public static final SnowPowderType SNOW_POWDER = register("snow_powder", new SnowPowderType()); private static final Map LEGACY_NAME_MAP; static { Object2ReferenceOpenHashMap map = new Object2ReferenceOpenHashMap<>(); map.put("ENRICHED", ENRICHED); + map.put("SNOW_POWDER", SNOW_POWDER); map.trim(); LEGACY_NAME_MAP = map; } @@ -62,6 +67,7 @@ public static FanProcessingType parseLegacy(String str) { } public static class EnrichedType implements FanProcessingType { + private static final double FAN_ENRICHING_DOSE = 5.0D; @Override public boolean isValidAt(Level level, BlockPos pos) { @@ -115,7 +121,64 @@ public void morphAirFlow(AirFlowParticleAccess particleAccess, RandomSource rand @Override public void affectEntity(Entity entity, Level level) { if (entity instanceof LivingEntity livingEntity) { - livingEntity.addEffect(new MobEffectInstance(CNEffects.RADIATION.getDelegate(), 10, 0, true, true)); + RadiationCapability.applyContagion(livingEntity, FAN_ENRICHING_DOSE, 10); + livingEntity.hurt(CNDamageSources.fanRadiation(level), 1); + } + } + } + + public static class SnowPowderType implements FanProcessingType { + + @Override + public boolean isValidAt(Level level, BlockPos pos) { + BlockState state = level.getBlockState(pos); + return CNTags.CNBlockTags.FAN_PROCESSING_CATALYSTS_SNOW_POWDER.matches(state); + } + + @Override + public int getPriority() { + return 301; + } + + @Override + public boolean canProcess(ItemStack stack, Level level) { + Optional>> recipe = CNRecipeTypes.SNOW_POWDER.find(new SingleRecipeInput(stack), level); + return recipe.isPresent(); + } + + @Nullable + @Override + public List process(ItemStack stack, Level level) { + return CNRecipeTypes.SNOW_POWDER.find(new SingleRecipeInput(stack), level) + .map(RecipeHolder::value) + .map(r -> RecipeApplier.applyRecipeOn(level, stack, r, true)) + .orElse(null); + } + + @Override + public void spawnProcessingParticles(Level level, Vec3 pos) { + if (level.random.nextInt(8) != 0) return; + pos = pos.add(VecHelper.offsetRandomly(Vec3.ZERO, level.random, 1) + .multiply(1, 0.5f, 1) + .normalize() + .scale(0.15f) + ); + level.addParticle(ParticleTypes.SNOWFLAKE, pos.x, pos.y + .45f, pos.z, 0.0, 0.0, 0.0); + if (level.random.nextInt(2) != 0) level.addParticle(ParticleTypes.SNEEZE, pos.x, pos.y + .25f, pos.z, 0.0, 0.0, 0.0); + } + + @Override + public void morphAirFlow(AirFlowParticleAccess particleAccess, RandomSource random) { + particleAccess.setColor(Color.mixColors(0x0, 0x49FFFB, random.nextFloat())); + particleAccess.setAlpha(1f); + if (random.nextFloat() < 1 / 128f) particleAccess.spawnExtraParticle(ParticleTypes.SNOWFLAKE, .125f); + if (random.nextFloat() < 1 / 32f) particleAccess.spawnExtraParticle(ParticleTypes.SNEEZE, .125f); + } + + @Override + public void affectEntity(Entity entity, Level level) { + if (entity instanceof LivingEntity livingEntity) { + livingEntity.addEffect(new MobEffectInstance(MobEffects.MOVEMENT_SLOWDOWN, 10, 0, true, true)); } } } diff --git a/src/main/java/net/nuclearteam/createnuclear/content/kinetics/fan/processing/SnowPowderRecipe.java b/src/main/java/net/nuclearteam/createnuclear/content/kinetics/fan/processing/SnowPowderRecipe.java new file mode 100644 index 0000000..8ca555b --- /dev/null +++ b/src/main/java/net/nuclearteam/createnuclear/content/kinetics/fan/processing/SnowPowderRecipe.java @@ -0,0 +1,36 @@ +package net.nuclearteam.createnuclear.content.kinetics.fan.processing; + +import javax.annotation.ParametersAreNonnullByDefault; + +import com.simibubi.create.content.processing.recipe.ProcessingRecipeParams; +import com.simibubi.create.content.processing.recipe.StandardProcessingRecipe; +import net.minecraft.world.item.crafting.SingleRecipeInput; +import net.minecraft.world.level.Level; +import net.nuclearteam.createnuclear.CNRecipeTypes; + +@ParametersAreNonnullByDefault +public class SnowPowderRecipe extends StandardProcessingRecipe { + + public SnowPowderRecipe(ProcessingRecipeParams params) { + super(CNRecipeTypes.SNOW_POWDER, params); + } + + @Override + public boolean matches(SingleRecipeInput inv, Level worldIn) { + if (inv.isEmpty()) + return false; + return ingredients.get(0) + .test(inv.getItem(0)); + } + + @Override + protected int getMaxInputCount() { + return 1; + } + + @Override + protected int getMaxOutputCount() { + return 12; + } + +} \ No newline at end of file diff --git a/src/main/java/net/nuclearteam/createnuclear/content/multiblock/CNMultiblock.java b/src/main/java/net/nuclearteam/createnuclear/content/multiblock/CNMultiblock.java index 60164c4..aa5a500 100644 --- a/src/main/java/net/nuclearteam/createnuclear/content/multiblock/CNMultiblock.java +++ b/src/main/java/net/nuclearteam/createnuclear/content/multiblock/CNMultiblock.java @@ -12,32 +12,76 @@ public class CNMultiblock { public static final MultiBlockManagerBeta REGISTRATE_MULTIBLOCK = new MultiBlockManagerBeta<>(); - public static final String AAAAA = "AAAAA"; - public static final String AABAA = "AABAA"; - public static final String ADADA = "ADADA"; - public static final String BACAB = "BACAB"; - public static final String AAIAA = "AAIAA"; - public static final String AAAA = "AA*AA"; - public static final String AAOAA = "AAOAA"; + + private static final Predicate blockInWorldAPredicate = state -> + stateIs(CNBlocks.REACTOR_CASING.get()).test(state) + || stateIs(CNBlocks.REACTOR_OUTPUT.get()).test(state) + || stateIs(CNBlocks.REACTOR_ROD_INPUT.get()).test(state) + || stateIs(CNBlocks.REACTOR_FLUID_INPUT.get()).test(state) + || stateIs(CNBlocks.REACTOR_ALARM.get()).test(state) + ; static { - REGISTRATE_MULTIBLOCK.register("createnuclear:reactor", + REGISTRATE_MULTIBLOCK.register("createnuclear:reactor5x5", TypeMultiblock.REACTOR_T1, SimpleMultiBlockAislePatternBuilder.start() - .aisle(AAAAA, AAAAA, AAAAA, AAAAA, AAAAA) - .aisle(AABAA, ADADA, BACAB, ADADA, AABAA) - .aisle(AABAA, ADADA, BACAB, ADADA, AABAA) - .aisle(AAIAA, ADADA, BACAB, ADADA, AAAA) - .aisle(AABAA, ADADA, BACAB, ADADA, AABAA) - .aisle(AABAA, ADADA, BACAB, ADADA, AABAA) - .aisle(AAAAA, AAAAA, AAAAA, AAAAA, AAOAA) - .where('A', stateIs(CNBlocks.REACTOR_CASING.get())) + .aisle("OOOOO", "OAAAO", "OAAAO", "OAAAO", "OOOOO") + .aisle("OABAO", "ADDDA", "BDCDB", "ADDDA", "OABAO") + .aisle("OABAO", "ADDDA", "BDCDB", "ADDDA", "OABAO") + .aisle("OABAO", "ADDDA", "BDCDB", "ADDDA", "OA*AO") + .aisle("OABAO", "ADDDA", "BDCDB", "ADDDA", "OABAO") + .aisle("OABAO", "ADDDA", "BDCDB", "ADDDA", "OABAO") + .aisle("OOOOO", "OAAAO", "OAAAO", "OAAAO", "OOOOO") + .where('A', blockInWorldAPredicate) .where('B', stateIs(CNBlocks.REACTOR_FRAME.get())) .where('C', stateIs(CNBlocks.REACTOR_CORE.get())) .where('D', stateIs(CNBlocks.REACTOR_COOLER.get())) .where('*', stateIs(CNBlocks.REACTOR_CONTROLLER.get())) - .where('O', stateIs(CNBlocks.REACTOR_OUTPUT.get())) - .where('I', stateIs(CNBlocks.REACTOR_ROD_INPUT.get())) + .where('O', stateIs(CNBlocks.REACTOR_CASING.get())) + .build() + ); + + REGISTRATE_MULTIBLOCK.register("createnuclear:reactor7x7", + TypeMultiblock.REACTOR_T2, + SimpleMultiBlockAislePatternBuilder.start() + .aisle("OOOOOOO", "OAAAAAO", "OAAAAAO", "OAAAAAO", "OAAAAAO", "OAAAAAO", "OOOOOOO") + .aisle("OABABAO", "ADDDDDA", "BDCDCDB", "ADDDDDA", "BDCDCDB", "ADDDDDA", "OABABAO") + .aisle("OABABAO", "ADDDDDA", "BDCDCDB", "ADDDDDA", "BDCDCDB", "ADDDDDA", "OABABAO") + .aisle("OABABAO", "ADDDDDA", "BDCDCDB", "ADDDDDA", "BDCDCDB", "ADDDDDA", "OABABAO") + .aisle("OABABAO", "ADDDDDA", "BDCDCDB", "ADDDDDA", "BDCDCDB", "ADDDDDA", "OAB*BAO") + .aisle("OABABAO", "ADDDDDA", "BDCDCDB", "ADDDDDA", "BDCDCDB", "ADDDDDA", "OABABAO") + .aisle("OABABAO", "ADDDDDA", "BDCDCDB", "ADDDDDA", "BDCDCDB", "ADDDDDA", "OABABAO") + .aisle("OABABAO", "ADDDDDA", "BDCDCDB", "ADDDDDA", "BDCDCDB", "ADDDDDA", "OABABAO") + .aisle("OOOOOOO", "OAAAAAO", "OAAAAAO", "OAAAAAO", "OAAAAAO", "OAAAAAO", "OOOOOOO") + .where('A', blockInWorldAPredicate) + .where('B', a -> a.getState().is(CNBlocks.REACTOR_FRAME.get())) + .where('C', a -> a.getState().is(CNBlocks.REACTOR_CORE.get())) + .where('D', a -> a.getState().is(CNBlocks.REACTOR_COOLER.get())) + .where('*', a -> a.getState().is(CNBlocks.REACTOR_CONTROLLER.get())) + .where('O', a -> a.getState().is(CNBlocks.REACTOR_CASING.get())) + .build() + ); + + REGISTRATE_MULTIBLOCK.register("createnuclear:reactor9x9", + TypeMultiblock.REACTOR_T3, + SimpleMultiBlockAislePatternBuilder.start() + .aisle("OOOOOOOOO", "OAAAAAAAO", "OAAAAAAAO", "OAAAAAAAO", "OAAAAAAAO", "OAAAAAAAO", "OAAAAAAAO", "OAAAAAAAO", "OOOOOOOOO") + .aisle("OBAABAABO", "BDDDDDDDB", "ADCDCDCDA", "ADDDDDDDA", "BDCDCDCDB", "ADDDDDDDA", "ADCDCDCDA", "BDDDDDDDB", "OBAABAABO") + .aisle("OBAABAABO", "BDDDDDDDB", "ADCDCDCDA", "ADDDDDDDA", "BDCDCDCDB", "ADDDDDDDA", "ADCDCDCDA", "BDDDDDDDB", "OBAABAABO") + .aisle("OBAABAABO", "BDDDDDDDB", "ADCDCDCDA", "ADDDDDDDA", "BDCDCDCDB", "ADDDDDDDA", "ADCDCDCDA", "BDDDDDDDB", "OBAABAABO") + .aisle("OBAABAABO", "BDDDDDDDB", "ADCDCDCDA", "ADDDDDDDA", "BDCDCDCDB", "ADDDDDDDA", "ADCDCDCDA", "BDDDDDDDB", "OBAABAABO") + .aisle("OBAABAABO", "BDDDDDDDB", "ADCDCDCDA", "ADDDDDDDA", "BDCDCDCDB", "ADDDDDDDA", "ADCDCDCDA", "BDDDDDDDB", "OBAA*AABO") + .aisle("OBAABAABO", "BDDDDDDDB", "ADCDCDCDA", "ADDDDDDDA", "BDCDCDCDB", "ADDDDDDDA", "ADCDCDCDA", "BDDDDDDDB", "OBAABAABO") + .aisle("OBAABAABO", "BDDDDDDDB", "ADCDCDCDA", "ADDDDDDDA", "BDCDCDCDB", "ADDDDDDDA", "ADCDCDCDA", "BDDDDDDDB", "OBAABAABO") + .aisle("OBAABAABO", "BDDDDDDDB", "ADCDCDCDA", "ADDDDDDDA", "BDCDCDCDB", "ADDDDDDDA", "ADCDCDCDA", "BDDDDDDDB", "OBAABAABO") + .aisle("OBAABAABO", "BDDDDDDDB", "ADCDCDCDA", "ADDDDDDDA", "BDCDCDCDB", "ADDDDDDDA", "ADCDCDCDA", "BDDDDDDDB", "OBAABAABO") + .aisle("OOOOOOOOO", "OAAAAAAAO", "OAAAAAAAO", "OAAAAAAAO", "OAAAAAAAO", "OAAAAAAAO", "OAAAAAAAO", "OAAAAAAAO", "OOOOOOOOO") + .where('A', blockInWorldAPredicate) + .where('B', a -> a.getState().is(CNBlocks.REACTOR_FRAME.get())) + .where('C', a -> a.getState().is(CNBlocks.REACTOR_CORE.get())) + .where('D', a -> a.getState().is(CNBlocks.REACTOR_COOLER.get())) + .where('*', a -> a.getState().is(CNBlocks.REACTOR_CONTROLLER.get())) + .where('O', a -> a.getState().is(CNBlocks.REACTOR_CASING.get())) .build() ); } diff --git a/src/main/java/net/nuclearteam/createnuclear/content/multiblock/IHeat.java b/src/main/java/net/nuclearteam/createnuclear/content/multiblock/IHeat.java index bf56b3f..fe17625 100644 --- a/src/main/java/net/nuclearteam/createnuclear/content/multiblock/IHeat.java +++ b/src/main/java/net/nuclearteam/createnuclear/content/multiblock/IHeat.java @@ -1,15 +1,19 @@ package net.nuclearteam.createnuclear.content.multiblock; import com.simibubi.create.content.equipment.wrench.IWrenchable; +import com.simibubi.create.content.logistics.BigItemStack; import com.simibubi.create.foundation.item.TooltipHelper; import com.simibubi.create.foundation.utility.CreateLang; import net.createmod.catnip.lang.Lang; import net.createmod.catnip.lang.LangBuilder; import net.minecraft.ChatFormatting; import net.minecraft.world.item.ItemStack; -import net.nuclearteam.createnuclear.CNTags.CNItemTags; +import net.minecraft.world.level.Level; import net.nuclearteam.createnuclear.CreateNuclear; +import net.nuclearteam.createnuclear.api.multiblock.rods.RodType.TypeRodPredicate; import net.nuclearteam.createnuclear.foundation.utility.CreateNuclearLang; +import net.nuclearteam.createnuclear.infrastructure.config.CNConfigs; +import net.nuclearteam.createnuclear.infrastructure.config.CReactorHeat; public interface IHeat extends IWrenchable { enum HeatLevel { @@ -53,21 +57,38 @@ public int getHeatValue() { }; } - public static HeatLevel of(int heat) { + public static HeatLevel of(int heat, int reactorSize) { if (heat < 0) return NONE; heat = Math.abs(heat); - if (heat > 0 && heat < 500) return SAFETY; - if (heat >= 501 && heat <= 800) return CAUTION; - if (heat >= 801 && heat <= 1000) return WARNING; - if (heat >= 1001) return DANGER; + CReactorHeat config = CNConfigs.server().reactorHeat; + int danger; + + switch (reactorSize) { + case 5 -> danger = config.size5Danger.get(); + case 7 -> danger = config.size7Danger.get(); + case 9 -> danger = config.size9Danger.get(); + default -> danger = config.size5Danger.get(); + } + + int caution = (int) (danger * 0.75); + int warning = (int) (danger * 0.90); + + if (heat > 0 && heat < caution) return SAFETY; + if (heat >= caution && heat < warning) return CAUTION; + if (heat >= warning && heat <= danger) return WARNING; + if (heat > danger) return DANGER; return NONE; } - public static LangBuilder getFormattedHeatText(int heat) { - HeatLevel heatLevel = of(heat); + public static boolean isNotDanger(int heat, int reactorSize) { + return of(heat, reactorSize) != DANGER; + } + + public static LangBuilder getFormattedHeatText(int heat, int reactorSize) { + HeatLevel heatLevel = of(heat, reactorSize); LangBuilder builder = CreateLang.builder(CreateNuclear.MOD_ID).text(TooltipHelper.makeProgressBar(5, heatLevel.ordinal()+1)); builder.translate("tooltip.heatLevel." + Lang.asId(heatLevel.name())) @@ -85,28 +106,24 @@ public static LangBuilder getFormattedHeatText(int heat) { return builder; } - public static LangBuilder getFormattedItemText(ItemStack itemRod, Boolean IsEmpty) { + public static LangBuilder getFormattedItemText(ItemStack itemRod, Boolean isEmpty, Level level) { LangBuilder builder = Lang.builder(CreateNuclear.MOD_ID); - String tooltip = "unknown"; - - if (itemRod.is(CNItemTags.FUEL.tag)) { - tooltip = "uranium"; - } - - if (itemRod.is(CNItemTags.COOLER.tag)) { - tooltip = "graphene"; - } + String tooltip = TypeRodPredicate.tooltipKey(itemRod, level); builder.translate("tooltip.item." + tooltip + ".rod") - // when it's empty, we show the number minus one to display zero because we fake the item count as 1 - .add(CreateNuclearLang.number(Math.abs((IsEmpty ? itemRod.getCount() - 1 : itemRod.getCount())))) - .style(ChatFormatting.BLUE) + // when it's empty, we show the number minus one to display zero because we fake the item count as 1 + .add(CreateNuclearLang.number(Math.abs((isEmpty ? itemRod.getCount() - 1 : itemRod.getCount())))) + .style(ChatFormatting.BLUE) ; return builder; } + public static LangBuilder getFormattedItemText(BigItemStack itemRod, Boolean isEmpty, Level level) { + return getFormattedItemText(new ItemStack(itemRod.stack.getItem(), itemRod.count), isEmpty, level); + } + public static LangBuilder getName(String name) { LangBuilder builder = CreateNuclearLang.builder(CreateNuclear.MOD_ID); builder.translate("gui." + name + ".info_header.title"); @@ -114,4 +131,4 @@ public static LangBuilder getName(String name) { return builder; } } -} +} \ No newline at end of file diff --git a/src/main/java/net/nuclearteam/createnuclear/content/multiblock/MultiblockHelpers.java b/src/main/java/net/nuclearteam/createnuclear/content/multiblock/MultiblockHelpers.java index 08e05d1..f168e39 100644 --- a/src/main/java/net/nuclearteam/createnuclear/content/multiblock/MultiblockHelpers.java +++ b/src/main/java/net/nuclearteam/createnuclear/content/multiblock/MultiblockHelpers.java @@ -1,13 +1,13 @@ package net.nuclearteam.createnuclear.content.multiblock; -import net.minecraft.core.BlockPos; import net.minecraft.world.entity.LivingEntity; -import net.minecraft.world.level.Level; import net.nuclearteam.createnuclear.content.multiblock.controller.ReactorControllerBlockEntity; import net.nuclearteam.createnuclear.content.multiblock.pattern.ReactorPattern; +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.Level; import net.nuclearteam.createnuclear.foundation.advancement.CNAdvancementBehaviour; +import org.jetbrains.annotations.Nullable; -import javax.annotation.Nullable; import java.util.function.BiConsumer; public class MultiblockHelpers { @@ -19,9 +19,7 @@ public class MultiblockHelpers { * invoke provided callbacks to register or remove parts. */ public static void handleOnPlace(BlockPos pos, Level level, BiConsumer register) { - handleOnPlace(pos, level, true); - - BlockPos controllerPos = pattern.findControllerPos(pos, level); + BlockPos controllerPos = pattern.findControllerPos(pos, level, true); if (controllerPos != null) { ReactorControllerBlockEntity controllerBlockEntity = (ReactorControllerBlockEntity) level.getBlockEntity(controllerPos); if (controllerBlockEntity != null) { @@ -30,18 +28,8 @@ public static void handleOnPlace(BlockPos pos, Level level, BiConsumer remover) { - handleOnPlace(pos, level, false); - - BlockPos controllerPos = pattern.findControllerPos(pos, level); + BlockPos controllerPos = pattern.findControllerPos(pos, level, false); if (controllerPos != null) { ReactorControllerBlockEntity controllerBlockEntity = (ReactorControllerBlockEntity) level.getBlockEntity(controllerPos); if (controllerBlockEntity != null) { diff --git a/src/main/java/net/nuclearteam/createnuclear/content/multiblock/ReactorAssembler.java b/src/main/java/net/nuclearteam/createnuclear/content/multiblock/ReactorAssembler.java index 38bb598..a591c5f 100644 --- a/src/main/java/net/nuclearteam/createnuclear/content/multiblock/ReactorAssembler.java +++ b/src/main/java/net/nuclearteam/createnuclear/content/multiblock/ReactorAssembler.java @@ -3,13 +3,14 @@ import net.createmod.catnip.lang.LangBuilder; import net.minecraft.ChatFormatting; import net.minecraft.core.BlockPos; -import net.minecraft.world.entity.player.Player; +import net.minecraft.core.Direction; +import net.minecraft.sounds.SoundSource; import net.minecraft.world.level.Level; import net.minecraft.world.level.block.Block; import net.minecraft.world.level.block.state.BlockState; -import net.minecraft.world.phys.AABB; +import net.minecraft.world.level.levelgen.structure.BoundingBox; import net.nuclearteam.createnuclear.CNBlocks; -import net.nuclearteam.createnuclear.CreateNuclear; +import net.nuclearteam.createnuclear.CNSoundEvents; import net.nuclearteam.createnuclear.api.multiblock.BlockPattern; import net.nuclearteam.createnuclear.api.multiblock.TypeMultiblock; import net.nuclearteam.createnuclear.content.multiblock.controller.ReactorControllerBlockEntity; @@ -20,6 +21,7 @@ import net.nuclearteam.createnuclear.foundation.utility.NotifyUtil; import net.nuclearteam.createnuclear.infrastructure.config.CNConfigs; +import java.util.ArrayList; import java.util.List; public final class ReactorAssembler { @@ -30,16 +32,25 @@ public final class ReactorAssembler { private ReactorAssembler() {} public static void assemble(BlockPos pos, Level level) { + if (level.isClientSide) return; + ReactorControllerBlockEntity entity = getBlockEntity(level, pos); if (entity == null) return; BlockPattern result = CNMultiblock.REGISTRATE_MULTIBLOCK.findStructure(level, pos, entity); if (result == null) return; - CreateNuclear.LOGGER.warn("ReactorAssembler#assemble id: {}, size: {}, name: {}", - result.id(), result.data().getSize(), result.data().getName()); + // Captured before setAssembled(true) below: assemble() is also reached from + // ReactorPattern.findControllerPos on an already-assembled reactor, and only the + // actual not-assembled -> assembled transition should notify and play the cue. + boolean wasAssembled = entity.isAssembled(); + + sendMessageToPlayer(level, pos, CreateNuclearLang.translate("notification.reactor.assembled"), !wasAssembled); - sendMessageToPlayer(level, pos, CreateNuclearLang.translate("notification.reactor.assembled"), !entity.isAssembled()); + if (!wasAssembled) { + // Server-side (null player) so the one-shot broadcasts to nearby clients. + level.playSound(null, pos, CNSoundEvents.MOTOR_ASSEMBLE.getMainEvent(), SoundSource.BLOCKS, 1.0f, 1.0f); + } switch (result.data()) { case REACTOR_T1 -> entity.getAdvancement().awardPlayer(CNAdvancement.T1_REACTOR); @@ -49,12 +60,14 @@ public static void assemble(BlockPos pos, Level level) { entity.setMultiblockSize(result.data().getSize()); entity.setAssembled(true); - entity.setMultiblockStructure(entity.getStructureBounds(pos, entity.getMultiblockSize(), entity.getMultiblockFacing())); + entity.setMultiblockStructure(ReactorAssembler.getStructureBound(pos, entity.getMultiblockSize(), entity.getMultiblockFacing())); findAndRegisterSpecialBlocks(entity.getMultiblockPos(), entity, level); } public static void disassemble(BlockPos pos, Level level) { + if (level.isClientSide) return; + ReactorControllerBlockEntity entity = getBlockEntity(level, pos); if (entity == null || !entity.isAssembled()) return; @@ -63,19 +76,23 @@ public static void disassemble(BlockPos pos, Level level) { sendMessageToPlayer(level, pos, CreateNuclearLang.translate("notification.reactor.disassembled"), true); + // Both early returns above already guarantee this is a real assembled -> broken + // transition, so no extra guard is needed here. + level.playSound(null, pos, CNSoundEvents.MOTOR_DISASSEMBLE.getMainEvent(), SoundSource.BLOCKS, 1.0f, 1.0f); + entity.setAssembled(false); entity.removeIOAll(); } - public static void findAndRegisterSpecialBlocks(int[] reactorPos, ReactorControllerBlockEntity entity, Level level) { - int xMin = reactorPos[0], xMax = reactorPos[1]; - int yMin = reactorPos[2], yMax = reactorPos[3]; - int zMin = reactorPos[4], zMax = reactorPos[5]; + public static void findAndRegisterSpecialBlocks(BoundingBox reactorPos, ReactorControllerBlockEntity entity, Level level) { + int xMin = reactorPos.minX(), xMax = reactorPos.maxX(); + int yMin = reactorPos.minY(), yMax = reactorPos.maxY(); + int zMin = reactorPos.minZ(), zMax = reactorPos.maxZ(); final Block reactorOutputBlock = CNBlocks.REACTOR_OUTPUT.get(); - final Block reactorInputBlock = CNBlocks.REACTOR_ROD_INPUT.get(); + final Block reactorRodInputBlock = CNBlocks.REACTOR_ROD_INPUT.get(); final Block reactorInputFluidBlock = CNBlocks.REACTOR_FLUID_INPUT.get(); - //final Block reactorAlarmBlock = CNBlocks.REACTOR_ALARM.get(); + final Block reactorAlarmBlock = CNBlocks.REACTOR_ALARM.get(); final Block reactorFrameBlock = CNBlocks.REACTOR_FRAME.get(); BlockPos.MutableBlockPos mutablePos = new BlockPos.MutableBlockPos(); @@ -83,6 +100,10 @@ public static void findAndRegisterSpecialBlocks(int[] reactorPos, ReactorControl int frameMinY = Integer.MAX_VALUE; int frameMaxY = Integer.MIN_VALUE; + // Collected here and assigned their capacity after the scan: the reactor-size capacity is a + // single TOTAL shared across every fluid input, so we split it once we know how many there are. + List fluidInputs = new ArrayList<>(); + for (int y = yMin; y <= yMax; y++) { boolean isYBoundary = (y == yMin || y == yMax); for (int x = xMin; x <= xMax; x++) { @@ -97,15 +118,15 @@ public static void findAndRegisterSpecialBlocks(int[] reactorPos, ReactorControl if (blockState.is(reactorOutputBlock)) { entity.addOutput(mutablePos.immutable()); - } else if (blockState.is(reactorInputBlock)) { + } else if (blockState.is(reactorRodInputBlock)) { entity.addInput(mutablePos.immutable()); } else if (blockState.is(reactorInputFluidBlock)) { entity.addInputFluid(mutablePos.immutable()); if (level.getBlockEntity(mutablePos) instanceof ReactorFluidInputEntity fluidInput) { - fluidInput.applyReactorTierCapacity(entity.getMultiblockSize()); + fluidInputs.add(fluidInput); } - //} else if (blockState.is(reactorAlarmBlock)) { - // entity.addAlarm(mutablePos.immutable()); + } else if (blockState.is(reactorAlarmBlock)) { + entity.addAlarm(mutablePos.immutable()); } else if (blockState.is(reactorFrameBlock)) { if (level.getBlockEntity(mutablePos) instanceof ReactorFrameEntity frame) { frame.setController(entity.getBlockPos()); @@ -118,7 +139,27 @@ public static void findAndRegisterSpecialBlocks(int[] reactorPos, ReactorControl } if (frameMinY != Integer.MAX_VALUE) { - entity.setFrameColumn(frameMinY, frameMaxY); + entity.getFrameDisplayManager().setFrameColumn(frameMinY, frameMaxY, entity::notifyUpdate); + } + + distributeFluidInputCapacity(fluidInputs, entity.getMultiblockSize()); + } + + /** + * Splits the reactor-size total fluid capacity evenly across all fluid inputs so the combined + * max capacity always equals the configured value, regardless of how many inputs were placed. + * The integer remainder is spread one unit at a time over the first inputs so the sum stays exact. + */ + private static void distributeFluidInputCapacity(List fluidInputs, int reactorSize) { + int count = fluidInputs.size(); + if (count == 0) return; + + int total = ReactorFluidInputEntity.getCapacityForReactorSize(reactorSize); + int base = total / count; + int remainder = total % count; + + for (int i = 0; i < count; i++) { + fluidInputs.get(i).applyCapacity(base + (i < remainder ? 1 : 0)); } } @@ -129,12 +170,6 @@ private static ReactorControllerBlockEntity getBlockEntity(Level level, BlockPos return null; } - @Deprecated - private static List getPlayersInRadius(Level level, BlockPos center, double radius) { - AABB box = new AABB(center).inflate(radius); - return level.getEntitiesOfClass(Player.class, box); - } - private static void sendMessageToPlayer(Level level, BlockPos pos, LangBuilder component, boolean condition) { if (!condition) return; @@ -143,4 +178,22 @@ private static void sendMessageToPlayer(Level level, BlockPos pos, LangBuilder c ChatFormatting.GOLD, configRadius, configWarnAll ); } + + public static BoundingBox getStructureBound(BlockPos center, int size, Direction facing) { + int radius = (size - 1) / 2; + int height = radius + 1; + int depth = size - 1; + Direction into = facing.getOpposite(); + + int axisX = into.getStepX() * depth; + int axisZ = into.getStepZ() * depth; + + int minX = center.getX() + Math.min(0, axisX) - (axisX == 0 ? radius : 0); + int maxX = center.getX() + Math.max(0, axisX) + (axisX == 0 ? radius : 0); + int minZ = center.getZ() + Math.min(0, axisZ) - (axisZ == 0 ? radius : 0); + int maxZ = center.getZ() + Math.max(0, axisZ) + (axisZ == 0 ? radius : 0); + + return new BoundingBox(minX, center.getY() - height, minZ, + maxX, center.getY() + height, maxZ); + } } diff --git a/src/main/java/net/nuclearteam/createnuclear/content/multiblock/bluePrintItem/PatternData.java b/src/main/java/net/nuclearteam/createnuclear/content/multiblock/bluePrintItem/PatternData.java index aefbe4c..9aa7e6e 100644 --- a/src/main/java/net/nuclearteam/createnuclear/content/multiblock/bluePrintItem/PatternData.java +++ b/src/main/java/net/nuclearteam/createnuclear/content/multiblock/bluePrintItem/PatternData.java @@ -1,36 +1,41 @@ package net.nuclearteam.createnuclear.content.multiblock.bluePrintItem; import com.mojang.serialization.Codec; -import com.mojang.serialization.DataResult; import com.mojang.serialization.codecs.RecordCodecBuilder; import net.minecraft.network.RegistryFriendlyByteBuf; import net.minecraft.network.codec.ByteBufCodecs; import net.minecraft.network.codec.StreamCodec; import net.minecraft.world.item.ItemStack; -import net.nuclearteam.createnuclear.CNTags.CNItemTags; public record PatternData(int slot, ItemStack stack) { private static final ItemStack DEFAULT_STACK = ItemStack.EMPTY; + + /** + * Uses {@link ItemStack#OPTIONAL_CODEC}, not {@link ItemStack#CODEC}: a reactor pattern is 57 + * slots and is almost never full, and {@code ItemStack.CODEC} refuses the empty stack + * ("Item must not be minecraft:air" / "Value must be within range [1;99]: 0"). With the strict + * codec, any blueprint sitting in a controller made the block entity throw at chunk-save time, + * so the controller silently lost its blueprint on every world reload. + *

+ * The encoding of non-empty stacks is identical between the two codecs: only the empty stack + * changes, from "rejected" to {@code {}}, so blueprints already on disk still load. + */ public static final Codec CODEC = RecordCodecBuilder.create(i -> i.group( - Codec.INT.fieldOf("slot").forGetter(PatternData::slot), - ItemStack.CODEC - .flatXmap(stack -> { - if (!stack.isEmpty() || stack.is(CNItemTags.FUEL.tag) || stack.is(CNItemTags.COOLER.tag)) return DataResult.success(stack); - return DataResult.success(DEFAULT_STACK); - }, - DataResult::success - ) - .fieldOf("Stack") - .forGetter(PatternData::stack) - ).apply(i, PatternData::new) - ); + Codec.INT.fieldOf("slot").forGetter(PatternData::slot), + ItemStack.OPTIONAL_CODEC.fieldOf("Stack").forGetter(PatternData::stack) + ).apply(i, PatternData::new)); - public static final StreamCodec STREAM_CODEC = - StreamCodec.composite( - ByteBufCodecs.INT, PatternData::slot, - ItemStack.STREAM_CODEC, PatternData::stack, - PatternData::new - ); + /** + * Uses {@link ItemStack#OPTIONAL_STREAM_CODEC} for the same reason as {@link #CODEC}: + * {@code ItemStack.STREAM_CODEC} throws {@code EncoderException("Empty ItemStack not allowed")} + * on the empty stack, and this component is {@code networkSynchronized}, so an unfilled pattern + * failed to encode as soon as the blueprint was sent to a client. + */ + public static final StreamCodec STREAM_CODEC = StreamCodec.composite( + ByteBufCodecs.INT, PatternData::slot, + ItemStack.OPTIONAL_STREAM_CODEC, PatternData::stack, + PatternData::new + ); public static ItemStack getDefaultStack() { return DEFAULT_STACK; diff --git a/src/main/java/net/nuclearteam/createnuclear/content/multiblock/bluePrintItem/ReactorBluePrintData.java b/src/main/java/net/nuclearteam/createnuclear/content/multiblock/bluePrintItem/ReactorBluePrintData.java index a1b39c5..d51e37e 100644 --- a/src/main/java/net/nuclearteam/createnuclear/content/multiblock/bluePrintItem/ReactorBluePrintData.java +++ b/src/main/java/net/nuclearteam/createnuclear/content/multiblock/bluePrintItem/ReactorBluePrintData.java @@ -6,12 +6,18 @@ import net.minecraft.network.RegistryFriendlyByteBuf; import net.minecraft.network.codec.ByteBufCodecs; import net.minecraft.network.codec.StreamCodec; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.level.Level; +import net.nuclearteam.createnuclear.api.multiblock.rods.RodType.TypeRodPredicate; import java.util.Arrays; import java.util.List; import java.util.Objects; -public record ReactorBluePrintData(int countGraphiteRod, int countUraniumRod, int graphiteTime, int uraniumTime, PatternData[] pattern, PatternData[] patternAll) { +public record ReactorBluePrintData(int countCooledRod, int countFuelRod, PatternData[] pattern) { + public static final ReactorBluePrintData EMPTY = new ReactorBluePrintData(0, 0, new PatternData[0]); + private static final Codec PATTERN_ARRAY_CODEC = PatternData.CODEC.listOf().xmap( list -> list.toArray(PatternData[]::new), List::of @@ -20,44 +26,51 @@ public record ReactorBluePrintData(int countGraphiteRod, int countUraniumRod, in private static final StreamCodec STREAM_PATTERN_ARRAY_CODEC = CatnipStreamCodecBuilders.array(PatternData.STREAM_CODEC, PatternData.class); public static final Codec CODEC = RecordCodecBuilder.create(instance -> - instance.group( - Codec.INT.fieldOf("countGraphiteRod").forGetter(ReactorBluePrintData::countGraphiteRod), - Codec.INT.fieldOf("countUraniumRod").forGetter(ReactorBluePrintData::countUraniumRod), - Codec.INT.fieldOf("graphiteTime").forGetter(ReactorBluePrintData::graphiteTime), - Codec.INT.fieldOf("uraniumTime").forGetter(ReactorBluePrintData::uraniumTime), - PATTERN_ARRAY_CODEC.fieldOf("pattern").forGetter(ReactorBluePrintData::pattern), - PATTERN_ARRAY_CODEC.fieldOf("patternAll").forGetter(ReactorBluePrintData::patternAll) - ).apply(instance, ReactorBluePrintData::new) + instance.group( + Codec.INT.fieldOf("countCooledRod").forGetter(ReactorBluePrintData::countCooledRod), + Codec.INT.fieldOf("countFuelRod").forGetter(ReactorBluePrintData::countFuelRod), + PATTERN_ARRAY_CODEC.fieldOf("pattern").forGetter(ReactorBluePrintData::pattern) + ).apply(instance, ReactorBluePrintData::new) ); public static final StreamCodec STREAM_CODEC = StreamCodec.composite( - ByteBufCodecs.INT, ReactorBluePrintData::countGraphiteRod, - ByteBufCodecs.INT, ReactorBluePrintData::countUraniumRod, - ByteBufCodecs.INT, ReactorBluePrintData::graphiteTime, - ByteBufCodecs.INT, ReactorBluePrintData::uraniumTime, + ByteBufCodecs.INT, ReactorBluePrintData::countCooledRod, + ByteBufCodecs.INT, ReactorBluePrintData::countFuelRod, STREAM_PATTERN_ARRAY_CODEC, ReactorBluePrintData::pattern, - STREAM_PATTERN_ARRAY_CODEC, ReactorBluePrintData::patternAll, ReactorBluePrintData::new ); + /** + * Normalized view of {@link #pattern()}. Recomputed on demand, never stored. + */ + public PatternData[] patternAll(Level level) { + PatternData[] result = new PatternData[pattern().length]; + ItemStack glassPane = new ItemStack(Items.GLASS_PANE); + + for (int i = 0; i < pattern().length; i++) { + ItemStack stack = pattern()[i].stack(); + boolean isRods = TypeRodPredicate.isFuel(stack, level) || TypeRodPredicate.isCooled(stack, level); + + result[i] = new PatternData(i, isRods ? stack : glassPane); + } + + return result; + } + @Override public boolean equals(Object o) { if (this == o) return true; - if (!(o instanceof ReactorBluePrintData that)) return false; - - return countGraphiteRod == that.countGraphiteRod - && countUraniumRod == that.countUraniumRod - && graphiteTime == that.graphiteTime - && uraniumTime == that.uraniumTime - && Arrays.equals(pattern, that.pattern) - && Arrays.equals(patternAll, that.patternAll); + if (!(o instanceof ReactorBluePrintData(int cooledRod, int fuelRod, PatternData[] pattern1))) return false; + + return countCooledRod == cooledRod + && countFuelRod == fuelRod + && Arrays.equals(pattern, pattern1); } @Override public int hashCode() { - int result = Objects.hash(countGraphiteRod, countUraniumRod, graphiteTime, uraniumTime); + int result = Objects.hash(countCooledRod, countFuelRod); result = 31 * result + Arrays.hashCode(pattern); - result = 31 * result + Arrays.hashCode(patternAll); return result; } } diff --git a/src/main/java/net/nuclearteam/createnuclear/content/multiblock/bluePrintItem/ReactorBluePrintItem.java b/src/main/java/net/nuclearteam/createnuclear/content/multiblock/bluePrintItem/ReactorBluePrintItem.java index 956f447..38debf8 100644 --- a/src/main/java/net/nuclearteam/createnuclear/content/multiblock/bluePrintItem/ReactorBluePrintItem.java +++ b/src/main/java/net/nuclearteam/createnuclear/content/multiblock/bluePrintItem/ReactorBluePrintItem.java @@ -1,10 +1,10 @@ package net.nuclearteam.createnuclear.content.multiblock.bluePrintItem; -import com.simibubi.create.AllDataComponents; -import com.simibubi.create.foundation.item.ItemHelper; +import net.minecraft.ChatFormatting; import net.minecraft.MethodsReturnNonnullByDefault; -import net.minecraft.nbt.CompoundTag; +import net.minecraft.network.chat.ClickEvent; import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.MutableComponent; import net.minecraft.server.level.ServerPlayer; import net.minecraft.world.InteractionHand; import net.minecraft.world.InteractionResult; @@ -15,16 +15,15 @@ import net.minecraft.world.inventory.AbstractContainerMenu; import net.minecraft.world.item.Item; import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.TooltipFlag; import net.minecraft.world.item.context.UseOnContext; import net.minecraft.world.level.Level; import net.neoforged.neoforge.items.ItemStackHandler; -import net.nuclearteam.createnuclear.CNDamageTypes; import net.nuclearteam.createnuclear.CNDataComponents; import net.nuclearteam.createnuclear.CNItems; import org.jetbrains.annotations.Nullable; import javax.annotation.ParametersAreNonnullByDefault; -import java.util.Arrays; import java.util.List; @ParametersAreNonnullByDefault @@ -35,6 +34,19 @@ public ReactorBluePrintItem(Properties properties) { super(properties); } + + @Override + public void appendHoverText(ItemStack stack, TooltipContext context, List tooltipComponents, TooltipFlag tooltipFlag) { + super.appendHoverText(stack, context, tooltipComponents, tooltipFlag); + + tooltipComponents.add(Component.translatable("item.createnuclear.reactor_blueprint.tooltip") + .withStyle(ChatFormatting.GRAY)); + + // Adjust the tooltip text to hint at the available action + tooltipComponents.add(Component.translatable("item.createnuclear.reactor_blueprint.tooltip_hint") + .withStyle(ChatFormatting.DARK_GRAY)); + } + @Override public Component getDisplayName() { return Component.translatable("reactor.item.gui.name"); @@ -57,18 +69,26 @@ public InteractionResult useOn(UseOnContext context) { public InteractionResultHolder use(Level world, Player player, InteractionHand hand) { ItemStack heldItem = player.getItemInHand(hand); + // Plain right-click -> Opens the Blueprint screen if (!player.isShiftKeyDown() && hand == InteractionHand.MAIN_HAND) { if (!world.isClientSide && player instanceof ServerPlayer) - player.openMenu(this, buf -> { - ItemStack.STREAM_CODEC.encode(buf, heldItem); - }); + player.openMenu(this, buf -> ItemStack.STREAM_CODEC.encode(buf, heldItem)); return InteractionResultHolder.success(heldItem); } + // Shift + right-click -> Sends a clickable link in chat! else if (player.isShiftKeyDown() && hand == InteractionHand.MAIN_HAND) { - if (!world.isClientSide && player instanceof ServerPlayer) { - player.openMenu(this, buf -> { - ItemStack.STREAM_CODEC.encode(buf, heldItem); - }); + if (!world.isClientSide) { + MutableComponent message = Component.translatable("item.createnuclear.reactor_blueprint.chat_info") + .withStyle(ChatFormatting.GREEN) + .append(" ") + .append(Component.translatable("item.createnuclear.reactor_blueprint.wiki_link") + .withStyle(style -> style + .withColor(ChatFormatting.AQUA) + .withUnderlined(true) + .withClickEvent(new ClickEvent(ClickEvent.Action.OPEN_URL, "https://wiki.createnuclear.net/")) + )); + + player.sendSystemMessage(message); } return InteractionResultHolder.success(heldItem); } diff --git a/src/main/java/net/nuclearteam/createnuclear/content/multiblock/bluePrintItem/ReactorBluePrintItemPacket.java b/src/main/java/net/nuclearteam/createnuclear/content/multiblock/bluePrintItem/ReactorBluePrintItemPacket.java deleted file mode 100644 index 7e163d7..0000000 --- a/src/main/java/net/nuclearteam/createnuclear/content/multiblock/bluePrintItem/ReactorBluePrintItemPacket.java +++ /dev/null @@ -1,62 +0,0 @@ -package net.nuclearteam.createnuclear.content.multiblock.bluePrintItem; - -import io.netty.buffer.ByteBuf; -import net.createmod.catnip.codecs.stream.CatnipStreamCodecBuilders; -import net.createmod.catnip.codecs.stream.CatnipStreamCodecs; -import net.createmod.catnip.net.base.ServerboundPacketPayload; -import net.minecraft.nbt.CompoundTag; -import net.minecraft.network.codec.ByteBufCodecs; -import net.minecraft.network.codec.StreamCodec; -import net.minecraft.server.level.ServerPlayer; -import net.nuclearteam.createnuclear.CNPackets; -import net.nuclearteam.createnuclear.infrastructure.config.CNConfigs; - -public record ReactorBluePrintItemPacket(CompoundTag tag, float heat, int graphiteTime, int uraniumTime, int countGraphiteRod, int countUraniumRod) implements ServerboundPacketPayload { - - public static final StreamCodec STREAM_CODEC = StreamCodec.composite( - ByteBufCodecs.COMPOUND_TAG, ReactorBluePrintItemPacket::tag, - ByteBufCodecs.FLOAT, ReactorBluePrintItemPacket::heat, - ByteBufCodecs.INT, ReactorBluePrintItemPacket::graphiteTime, - ByteBufCodecs.INT, ReactorBluePrintItemPacket::uraniumTime, - ByteBufCodecs.INT, ReactorBluePrintItemPacket::countGraphiteRod, - ByteBufCodecs.INT, ReactorBluePrintItemPacket::countUraniumRod, - ReactorBluePrintItemPacket::new - ); - - @Override - public PacketTypeProvider getTypeProvider() { - return CNPackets.CONFIGURE_REACTOR_PATTERN; - } - - @Override - public void handle(ServerPlayer player) { - if (player.containerMenu instanceof ReactorBluePrintMenu c) { - // On crée directement le state et on l’applique - this.applyState(new ReactorState( - this.countUraniumRod, - this.countGraphiteRod, - this.graphiteTime, - this.uraniumTime, - this.heat - ), c); - } - } - - public record ReactorState( - int countUraniumRod, - int countGraphiteRod, - int graphiteTime, - int uraniumTime, - float heat - ) {} - - public void applyState(ReactorState state, ReactorBluePrintMenu c) { - c.countUraniumRod = state.countUraniumRod(); - c.countGraphiteRod = state.countGraphiteRod(); - c.graphiteTime = state.graphiteTime(); - c.uraniumTime = state.uraniumTime(); - c.heat = state.heat(); - c.sendUpdate = true; - } - -} diff --git a/src/main/java/net/nuclearteam/createnuclear/content/multiblock/bluePrintItem/ReactorBluePrintItemScreen.java b/src/main/java/net/nuclearteam/createnuclear/content/multiblock/bluePrintItem/ReactorBluePrintItemScreen.java index 47e4960..0ad678c 100644 --- a/src/main/java/net/nuclearteam/createnuclear/content/multiblock/bluePrintItem/ReactorBluePrintItemScreen.java +++ b/src/main/java/net/nuclearteam/createnuclear/content/multiblock/bluePrintItem/ReactorBluePrintItemScreen.java @@ -52,23 +52,5 @@ protected void containerTick() { if (!ItemStack.matches(menu.player.getMainHandItem(), menu.contentHolder)) { menu.player.closeContainer(); } - - float coef = 0.1F; - - CompoundTag tag = menu.contentHolder.getOrDefault(CNDataComponents.PATTERN, new CompoundTag()); - - sendValueUpdate(tag, coef, - tag.getInt("graphiteTime"), - tag.getInt("uraniumTime"), - tag.getInt("countUraniumRod"), - tag.getInt("countGraphiteRod")+3 - ); - - - } - - - private static void sendValueUpdate(CompoundTag tag, float heat, int graphiteTime, int uraniumTime, int countGraphiteRod, int countUraniumRod) { - CatnipServices.NETWORK.sendToServer(new ReactorBluePrintItemPacket(tag, heat, graphiteTime, uraniumTime, countGraphiteRod, countGraphiteRod)); } } \ No newline at end of file diff --git a/src/main/java/net/nuclearteam/createnuclear/content/multiblock/bluePrintItem/ReactorBluePrintMenu.java b/src/main/java/net/nuclearteam/createnuclear/content/multiblock/bluePrintItem/ReactorBluePrintMenu.java index 810981a..d07f1f3 100644 --- a/src/main/java/net/nuclearteam/createnuclear/content/multiblock/bluePrintItem/ReactorBluePrintMenu.java +++ b/src/main/java/net/nuclearteam/createnuclear/content/multiblock/bluePrintItem/ReactorBluePrintMenu.java @@ -8,26 +8,35 @@ import net.minecraft.world.inventory.MenuType; import net.minecraft.world.item.ItemStack; import net.minecraft.world.item.Items; +import net.minecraft.world.level.Level; import net.neoforged.api.distmarker.Dist; import net.neoforged.api.distmarker.OnlyIn; import net.neoforged.neoforge.items.ItemStackHandler; import net.neoforged.neoforge.items.SlotItemHandler; import net.nuclearteam.createnuclear.*; -import net.nuclearteam.createnuclear.CNTags.CNItemTags; -import net.nuclearteam.createnuclear.infrastructure.config.CNConfigs; +import net.nuclearteam.createnuclear.api.multiblock.rods.RodType.TypeRodPredicate; import static net.nuclearteam.createnuclear.content.multiblock.bluePrintItem.ReactorBluePrintItem.getItemStorage; public class ReactorBluePrintMenu extends GhostItemMenu { - public float heat = 0F; - public int graphiteTime = 5000; - public int uraniumTime = 3600; - public int countGraphiteRod = 0; - public int countUraniumRod = 0; - public double progress = 0; - - public boolean sendUpdate = false; + /** + * Grid position of each of the 57 pattern slots, index-matched to {@link PatternData#slot()} + * and to {@code DefaultHeatCalculator#FORMATTED_PATTERN}: the reactor's proximity-heat + * calculation resolves neighbours by this exact index, so the array must never be reordered + * or resized independently of that grid. + */ + private static final int[][] POSITIONS = { + {3, 0}, {4, 0}, {5, 0}, + {2, 1}, {3, 1}, {4, 1}, {5, 1}, {6, 1}, + {1, 2}, {2, 2}, {3, 2}, {4, 2}, {5, 2}, {6, 2}, {7, 2}, + {0, 3}, {1, 3}, {2, 3}, {3, 3}, {4, 3}, {5, 3}, {6, 3}, {7, 3}, {8, 3}, + {0, 4}, {1, 4}, {2, 4}, {3, 4}, {4, 4}, {5, 4}, {6, 4}, {7, 4}, {8, 4}, + {0, 5}, {1, 5}, {2, 5}, {3, 5}, {4, 5}, {5, 5}, {6, 5}, {7, 5}, {8, 5}, + {1, 6}, {2, 6}, {3, 6}, {4, 6}, {5, 6}, {6, 6}, {7, 6}, + {2, 7}, {3, 7}, {4, 7}, {5, 7}, {6, 7}, + {3, 8}, {4, 8}, {5, 8} + }; private ReactorBluePrintData reactorBluePrintData; @@ -43,6 +52,10 @@ public static ReactorBluePrintMenu create(int id, Inventory inv, ItemStack stack return new ReactorBluePrintMenu(CNMenus.REACTOR_BLUEPRINT_MENU.get(), id, inv, stack); } + public ReactorBluePrintData getReactorBluePrintData() { + return reactorBluePrintData; + } + @Override protected boolean allowRepeats() { return false; @@ -51,35 +64,26 @@ protected boolean allowRepeats() { @Override protected void initAndReadInventory(ItemStack contentHolder) { super.initAndReadInventory(contentHolder); - int[][] positions = { - {3, 0}, {4, 0}, {5, 0}, - {2, 1}, {3, 1}, {4, 1}, {5, 1}, {6, 1}, - {1, 2}, {2, 2}, {3, 2}, {4, 2}, {5, 2}, {6, 2}, {7, 2}, - {0, 3}, {1, 3}, {2, 3}, {3, 3}, {4, 3}, {5, 3}, {6, 3}, {7, 3}, {8, 3}, - {0, 4}, {1, 4}, {2, 4}, {3, 4}, {4, 4}, {5, 4}, {6, 4}, {7, 4}, {8, 4}, - {0, 5}, {1, 5}, {2, 5}, {3, 5}, {4, 5}, {5, 5}, {6, 5}, {7, 5}, {8, 5}, - {1, 6}, {2, 6}, {3, 6}, {4, 6}, {5, 6}, {6, 6}, {7, 6}, - {2, 7}, {3, 7}, {4, 7}, {5, 7}, {6, 7}, - {3, 8}, {4, 8}, {5, 8} - }; - - - PatternData[] patternData = new PatternData[positions.length]; - ItemStack defaultStack = new ItemStack(Items.GLASS_PANE); - - for (int i = 0; i < positions.length; i++) { - patternData[i] = new PatternData(i, defaultStack); - } - - ReactorBluePrintData defaultPattern = new ReactorBluePrintData(0, 0, CNConfigs.common().rods.graphiteRodLifetime.get(), CNConfigs.common().rods.uraniumRodLifetime.get(), patternData, patternData); - reactorBluePrintData = contentHolder.getOrDefault(CNDataComponents.REACTOR_BLUE_PRINT_DATA, defaultPattern); + ItemStack glassPane = new ItemStack(Items.GLASS_PANE); + PatternData[] emptyPattern = new PatternData[POSITIONS.length]; + for (int i = 0; i < emptyPattern.length; i++) { + emptyPattern[i] = new PatternData(i, glassPane); + } - for (int i = 0; i < positions.length; i++) { - ItemStack stack = reactorBluePrintData.pattern()[i].stack().is(CNItemTags.COOLER.tag) || reactorBluePrintData.pattern()[i].stack().is(CNItemTags.FUEL.tag) - ? reactorBluePrintData.pattern()[i].stack() - : ItemStack.EMPTY; - ghostInventory.setStackInSlot(i, stack); + ReactorBluePrintData defaultData = new ReactorBluePrintData(0, 0, emptyPattern); + reactorBluePrintData = contentHolder.getOrDefault(CNDataComponents.REACTOR_BLUE_PRINT_DATA, defaultData); + + // Classified via RodType, not via the CNItemTags.COOLER/FUEL item tags: a rod can be + // registered as fuel/cooler through RodType.Builder without also carrying the matching + // item tag (e.g. the thorium rod), which made saved rods vanish on reopen when this used + // to filter on the tags instead. + Level level = playerInventory.player.level(); + PatternData[] pattern = reactorBluePrintData.pattern(); + for (int i = 0; i < POSITIONS.length; i++) { + ItemStack stack = pattern[i].stack(); + boolean isRod = TypeRodPredicate.isFuel(stack, level) || TypeRodPredicate.isCooled(stack, level); + ghostInventory.setStackInSlot(i, isRod ? stack : ItemStack.EMPTY); } } @@ -101,77 +105,48 @@ protected void addSlots() { } private void addPatternSlots() { - int startWidth = 8+23; + int startWidth = 8 + 23; int startHeight = 45; int incr = 18; - int i = 0; - int[][] positions = { - {3, 0}, {4, 0}, {5, 0}, - {2, 1}, {3, 1}, {4, 1}, {5, 1}, {6, 1}, - {1, 2}, {2, 2}, {3, 2}, {4, 2}, {5, 2}, {6, 2}, {7, 2}, - {0, 3}, {1, 3}, {2, 3}, {3, 3}, {4, 3}, {5, 3}, {6, 3}, {7, 3}, {8, 3}, - {0, 4}, {1, 4}, {2, 4}, {3, 4}, {4, 4}, {5, 4}, {6, 4}, {7, 4}, {8, 4}, - {0, 5}, {1, 5}, {2, 5}, {3, 5}, {4, 5}, {5, 5}, {6, 5}, {7, 5}, {8, 5}, - {1, 6}, {2, 6}, {3, 6}, {4, 6}, {5, 6}, {6, 6}, {7, 6}, - {2, 7}, {3, 7}, {4, 7}, {5, 7}, {6, 7}, - {3, 8}, {4, 8}, {5, 8} - }; - - for (int[] pos : positions) {// up and down not middle - this.addSlot(new SlotItemHandler(ghostInventory,i, startWidth + incr * pos[0], startHeight + incr * pos[1])); - i++; + + for (int i = 0; i < POSITIONS.length; i++) { + int[] pos = POSITIONS[i]; + this.addSlot(new SlotItemHandler(ghostInventory, i, startWidth + incr * pos[0], startHeight + incr * pos[1])); } } @Override protected void saveData(ItemStack contentHolder) { - int[][] positions = { - {3, 0}, {4, 0}, {5, 0}, - {2, 1}, {3, 1}, {4, 1}, {5, 1}, {6, 1}, - {1, 2}, {2, 2}, {3, 2}, {4, 2}, {5, 2}, {6, 2}, {7, 2}, - {0, 3}, {1, 3}, {2, 3}, {3, 3}, {4, 3}, {5, 3}, {6, 3}, {7, 3}, {8, 3}, - {0, 4}, {1, 4}, {2, 4}, {3, 4}, {4, 4}, {5, 4}, {6, 4}, {7, 4}, {8, 4}, - {0, 5}, {1, 5}, {2, 5}, {3, 5}, {4, 5}, {5, 5}, {6, 5}, {7, 5}, {8, 5}, - {1, 6}, {2, 6}, {3, 6}, {4, 6}, {5, 6}, {6, 6}, {7, 6}, - {2, 7}, {3, 7}, {4, 7}, {5, 7}, {6, 7}, - {3, 8}, {4, 8}, {5, 8} - }; - - PatternData[] patternData = new PatternData[positions.length]; + Level level = playerInventory.player.level(); + + // `pattern` keeps the grid exactly as the player laid it out. The normalized + // `patternAll` view (empty slots and non-rod items replaced by glass panes) is + // no longer built or stored here: ReactorBluePrintData#patternAll(Level) derives + // it from `pattern` on every call instead. See PatternReader. + PatternData[] pattern = new PatternData[POSITIONS.length]; + ItemStack glassPane = new ItemStack(Items.GLASS_PANE); int countGraphiteRod = 0; int countUraniumRod = 0; - for (int i = 0; i < positions.length; i++) { + for (int i = 0; i < POSITIONS.length; i++) { ItemStack stack = ghostInventory.getStackInSlot(i); - if (!stack.isEmpty() && stack.getCount() >= 1 && stack.getCount() <= 99) { - if (stack.is(CNItemTags.COOLER.tag)) { - countGraphiteRod++; - } - if (stack.is(CNItemTags.FUEL.tag)) { - countUraniumRod++; - } - patternData[i] = new PatternData(i, stack); - } else { - patternData[i] = new PatternData(i, new ItemStack(Items.GLASS_PANE)); + if (stack.isEmpty() || stack.getCount() < 1 || stack.getCount() > 99) { + pattern[i] = new PatternData(i, glassPane); + continue; } - } - - /*CreateNuclear.LOGGER.warn("PatternData slots:\n{}\n\n Count: {}", - Arrays.stream(patternData) - .map(pd -> "Slot " + pd.slot() + " => " + pd.stack()) - .collect(Collectors.joining("\n")), countGraphiteRod - );*/ - ReactorBluePrintData reactorBluePrintData = new ReactorBluePrintData(countGraphiteRod, countUraniumRod, CNConfigs.common().rods.graphiteRodLifetime.get(), CNConfigs.common().rods.uraniumRodLifetime.get(), patternData, patternData); - contentHolder.set(CNDataComponents.REACTOR_BLUE_PRINT_DATA, reactorBluePrintData); + boolean isFuel = TypeRodPredicate.isFuel(stack, level); + boolean isCooler = TypeRodPredicate.isCooled(stack, level); - ReactorBluePrintData newData = new ReactorBluePrintData(countGraphiteRod, countUraniumRod, CNConfigs.common().rods.graphiteRodLifetime.get(), CNConfigs.common().rods.uraniumRodLifetime.get(), patternData, patternData); + if (isCooler) countGraphiteRod++; + if (isFuel) countUraniumRod++; - if (!newData.equals(reactorBluePrintData)) { - contentHolder.set(CNDataComponents.REACTOR_BLUE_PRINT_DATA, newData); + pattern[i] = new PatternData(i, stack); } + ReactorBluePrintData data = new ReactorBluePrintData(countGraphiteRod, countUraniumRod, pattern); + contentHolder.set(CNDataComponents.REACTOR_BLUE_PRINT_DATA, data); } protected int getPlayerInventoryXOffset() { @@ -190,7 +165,7 @@ public boolean stillValid(Player player) { @Override public void clicked(int slotId, int dragType, ClickType clickTypeIn, Player player) { if (clickTypeIn == ClickType.THROW) { - if ( slotId >= 0 && slotId < 9) { + if (slotId >= 0 && slotId < 9) { clickTypeIn = ClickType.PICKUP; super.clicked(slotId, dragType, clickTypeIn, player); } @@ -198,4 +173,4 @@ public void clicked(int slotId, int dragType, ClickType clickTypeIn, Player play } super.clicked(slotId, dragType, clickTypeIn, player); } -} \ No newline at end of file +} diff --git a/src/main/java/net/nuclearteam/createnuclear/content/multiblock/bluePrintItem/test.txt b/src/main/java/net/nuclearteam/createnuclear/content/multiblock/bluePrintItem/test.txt deleted file mode 100644 index e32c721..0000000 --- a/src/main/java/net/nuclearteam/createnuclear/content/multiblock/bluePrintItem/test.txt +++ /dev/null @@ -1,16 +0,0 @@ -contentHolder: - ReactorBluePrintData[ - countGraphiteRod=0, - countUraniumRod=0, - graphiteTime=0, - uraniumTime=0, - pattern=[Lnet.nuclearteam.createnuclear.content.multiblock.bluePrintItem.PatternData;@341d34dd, - patternAll=[Lnet.nuclearteam.createnuclear.content.multiblock.bluePrintItem.PatternData;@341d34dd], - [ - PatternData[slot=0, stack=1 minecraft:glass_pane], PatternData[slot=1, stack=1 minecraft:glass_pane], PatternData[slot=2, stack=1 minecraft:glass_pane], - PatternData[slot=3, stack=1 minecraft:glass_pane], PatternData[slot=4, stack=1 minecraft:glass_pane], PatternData[slot=5, stack=1 minecraft:glass_pane], - PatternData[slot=6, stack=1 minecraft:glass_pane], PatternData[slot=7, stack=1 minecraft:glass_pane], PatternData[slot=8, stack=1 minecraft:glass_pane], - PatternData[slot=9, stack=1 minecraft:glass_pane], PatternData[slot=10, stack=1 minecraft:glass_pane], PatternData[slot=11, stack=1 minecraft:glass_pane], - PatternData[slot=12, stack=1 minecraft:glass_pane], PatternData[slot=13, stack=1 minecraft:glass_pane], PatternData[slot=14, stack=1 minecraft:glass_pane], - PatternData[slot=15, stack=1 minecraft:glass_pane], PatternData[slot=16, stack=1 minecraft:glass_pane], PatternData[slot=17, stack=1 minecraft:glass_pane], PatternData[slot=18, stack=1 minecraft:glass_pane], PatternData[slot=19, stack=1 minecraft:glass_pane], PatternData[slot=20, stack=1 minecraft:glass_pane], PatternData[slot=21, stack=1 minecraft:glass_pane], PatternData[slot=22, stack=1 minecraft:glass_pane], PatternData[slot=23, stack=1 minecraft:glass_pane], PatternData[slot=24, stack=1 minecraft:glass_pane], PatternData[slot=25, stack=1 minecraft:glass_pane], PatternData[slot=26, stack=1 minecraft:glass_pane], PatternData[slot=27, stack=1 minecraft:glass_pane], PatternData[slot=28, stack=1 minecraft:glass_pane], PatternData[slot=29, stack=1 minecraft:glass_pane], PatternData[slot=30, stack=1 minecraft:glass_pane], PatternData[slot=31, stack=1 minecraft:glass_pane], PatternData[slot=32, stack=1 minecraft:glass_pane], PatternData[slot=33, stack=1 minecraft:glass_pane], PatternData[slot=34, stack=1 minecraft:glass_pane], PatternData[slot=35, stack=1 minecraft:glass_pane], PatternData[slot=36, stack=1 minecraft:glass_pane], PatternData[slot=37, stack=1 minecraft:glass_pane], PatternData[slot=38, stack=1 minecraft:glass_pane], PatternData[slot=39, stack=1 minecraft:glass_pane], PatternData[slot=40, stack=1 minecraft:glass_pane], PatternData[slot=41, stack=1 minecraft:glass_pane], PatternData[slot=42, stack=1 minecraft:glass_pane], PatternData[slot=43, stack=1 minecraft:glass_pane], PatternData[slot=44, stack=1 minecraft:glass_pane], PatternData[slot=45, stack=1 minecraft:glass_pane], PatternData[slot=46, stack=1 minecraft:glass_pane], PatternData[slot=47, stack=1 minecraft:glass_pane], PatternData[slot=48, stack=1 minecraft:glass_pane], PatternData[slot=49, stack=1 minecraft:glass_pane], PatternData[slot=50, stack=1 minecraft:glass_pane], PatternData[slot=51, stack=1 minecraft:glass_pane], PatternData[slot=52, stack=1 minecraft:glass_pane], PatternData[slot=53, stack=1 minecraft:glass_pane], -PatternData[slot=54, stack=1 minecraft:glass_pane], PatternData[slot=55, stack=1 minecraft:glass_pane], PatternData[slot=56, stack=1 minecraft:glass_pane]] \ No newline at end of file diff --git a/src/main/java/net/nuclearteam/createnuclear/content/multiblock/casing/ReactorCasing.java b/src/main/java/net/nuclearteam/createnuclear/content/multiblock/casing/ReactorCasing.java index 460a1b1..57331ed 100644 --- a/src/main/java/net/nuclearteam/createnuclear/content/multiblock/casing/ReactorCasing.java +++ b/src/main/java/net/nuclearteam/createnuclear/content/multiblock/casing/ReactorCasing.java @@ -4,36 +4,35 @@ import com.simibubi.create.foundation.block.IBE; import net.minecraft.MethodsReturnNonnullByDefault; import net.minecraft.core.BlockPos; -import net.minecraft.core.Vec3i; import net.minecraft.network.chat.Component; import net.minecraft.util.StringRepresentable; import net.minecraft.world.InteractionHand; import net.minecraft.world.InteractionResult; +import net.minecraft.world.entity.LivingEntity; import net.minecraft.world.entity.player.Player; import net.minecraft.world.item.ItemStack; import net.minecraft.world.item.context.UseOnContext; import net.minecraft.world.level.Level; import net.minecraft.world.level.block.Block; import net.minecraft.world.level.block.Blocks; -import net.minecraft.world.level.block.entity.BlockEntity; import net.minecraft.world.level.block.entity.BlockEntityType; import net.minecraft.world.level.block.state.BlockState; import net.nuclearteam.createnuclear.CNBlockEntityTypes; import net.nuclearteam.createnuclear.CNBlocks; -import net.nuclearteam.createnuclear.content.multiblock.controller.ReactorControllerBlock; -import net.nuclearteam.createnuclear.content.multiblock.controller.ReactorControllerBlockEntity; +import net.nuclearteam.createnuclear.content.multiblock.MultiblockHelpers; import net.nuclearteam.createnuclear.content.multiblock.input.item.ReactorRodInput; +import net.nuclearteam.createnuclear.content.multiblock.pattern.ReactorPattern; import net.nuclearteam.createnuclear.foundation.utility.CreateNuclearLang; import javax.annotation.Nullable; import javax.annotation.ParametersAreNonnullByDefault; -import java.util.List; @ParametersAreNonnullByDefault @MethodsReturnNonnullByDefault @SuppressWarnings({"deprecation", "unused"}) public class ReactorCasing extends Block implements IWrenchable, IBE { private final TypeBlock typeBlock; + protected ReactorPattern pattern = new ReactorPattern(); public ReactorCasing(Properties properties, TypeBlock tBlock) { super(properties); @@ -44,22 +43,20 @@ public ReactorCasing(Properties properties, TypeBlock tBlock) { @Override // Called when the block is placed on the world public void onPlace(BlockState state, Level level, BlockPos pos, BlockState oldState, boolean movedByPiston) { super.onPlace(state, level, pos, oldState, movedByPiston); - List players = level.players(); - FindController(pos, level, players, true); + // change nearby chunks' biomes to the mod biome (radius in blocks) + pattern.findController(pos, level, true); } - @Override // called when the player destroys the block, with or without a tool - public void playerDestroy(Level level, Player player, BlockPos pos, BlockState state, @Nullable BlockEntity blockEntity, ItemStack tool) { - super.playerDestroy(level, player, pos, state, blockEntity, tool); - List players = level.players(); - FindController(pos, level, players, false); + @Override + public void setPlacedBy(Level level, BlockPos pos, BlockState state, @Nullable LivingEntity pPlacer, ItemStack stack) { + super.setPlacedBy(level, pos, state, pPlacer, stack); + MultiblockHelpers.handleAdvancedPlacedBy(pos, level, pPlacer); } @Override public void onRemove(BlockState state, Level level, BlockPos pos, BlockState newState, boolean movedByPiston) { super.onRemove(state, level, pos, newState, movedByPiston); - List players = level.players(); - FindController(pos, level, players, false); + pattern.findController(pos, level, false); } @Override @@ -75,28 +72,6 @@ public InteractionResult onWrenched(BlockState state, UseOnContext context) { return InteractionResult.SUCCESS; } - // En attendant le controller pour verifier le pattern - public ReactorControllerBlock FindController(BlockPos blockPos, Level level, List players, boolean first){ // Function that checks the surrounding blocks in order - BlockPos newBlock; // to find the controller and verify the pattern - Vec3i pos = new Vec3i(blockPos.getX(), blockPos.getY(), blockPos.getZ()); - for (int y = pos.getY()-3; y != pos.getY()+4; y+=1) { - for (int x = pos.getX()-5; x != pos.getX()+5; x+=1) { - for (int z = pos.getZ()-5; z != pos.getZ()+5; z+=1) { - newBlock = new BlockPos(x, y, z); - if (level.getBlockState(newBlock).is(CNBlocks.REACTOR_CONTROLLER.get())) { // verifying the pattern - ReactorControllerBlock controller = (ReactorControllerBlock) level.getBlockState(newBlock).getBlock(); - controller.Verify(level.getBlockState(newBlock), newBlock, level, players, first); - ReactorControllerBlockEntity entity = controller.getBlockEntity(level, newBlock); - if (entity.created) { - return controller; - } - } - } - } - } - return null; - } - @Override public Class getBlockEntityClass() { return ReactorCasingEntity.class; diff --git a/src/main/java/net/nuclearteam/createnuclear/content/multiblock/controller/ReactorControllerBlock.java b/src/main/java/net/nuclearteam/createnuclear/content/multiblock/controller/ReactorControllerBlock.java index 0569b00..b7c13e7 100644 --- a/src/main/java/net/nuclearteam/createnuclear/content/multiblock/controller/ReactorControllerBlock.java +++ b/src/main/java/net/nuclearteam/createnuclear/content/multiblock/controller/ReactorControllerBlock.java @@ -6,12 +6,16 @@ import net.minecraft.ChatFormatting; import net.minecraft.MethodsReturnNonnullByDefault; import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; import net.minecraft.network.chat.Component; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.sounds.SoundSource; import net.minecraft.world.InteractionHand; -import net.minecraft.world.InteractionResult; import net.minecraft.world.ItemInteractionResult; +import net.minecraft.world.entity.LivingEntity; import net.minecraft.world.entity.player.Player; import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; import net.minecraft.world.item.context.BlockPlaceContext; import net.minecraft.world.level.Level; import net.minecraft.world.level.block.Block; @@ -22,28 +26,44 @@ import net.minecraft.world.level.block.state.properties.BooleanProperty; import net.minecraft.world.phys.BlockHitResult; import net.nuclearteam.createnuclear.*; -import net.nuclearteam.createnuclear.content.multiblock.CNMultiblock; -import net.nuclearteam.createnuclear.content.multiblock.output.ReactorOutput; -import net.nuclearteam.createnuclear.content.multiblock.output.ReactorOutputEntity; +import net.nuclearteam.createnuclear.content.multiblock.IHeat; +import net.nuclearteam.createnuclear.content.multiblock.MultiblockHelpers; +import net.nuclearteam.createnuclear.content.multiblock.ReactorAssembler; +import net.nuclearteam.createnuclear.content.multiblock.bluePrintItem.ReactorBluePrintData; +import net.nuclearteam.createnuclear.content.multiblock.input.fluid.PersistentFluidLocks; +import net.nuclearteam.createnuclear.foundation.advancement.CNAdvancement; import net.nuclearteam.createnuclear.foundation.block.HorizontalDirectionalReactorBlock; +import net.nuclearteam.createnuclear.foundation.utility.CreateNuclearLang; +import net.nuclearteam.createnuclear.foundation.utility.NotifyUtil; +import net.nuclearteam.createnuclear.infrastructure.config.CNConfigs; import javax.annotation.Nullable; import javax.annotation.ParametersAreNonnullByDefault; -import java.util.List; @MethodsReturnNonnullByDefault @ParametersAreNonnullByDefault @SuppressWarnings("deprecation") public class ReactorControllerBlock extends HorizontalDirectionalReactorBlock implements IWrenchable, IBE { public static final BooleanProperty ASSEMBLED = BooleanProperty.create("assembled"); + /** + * Whether the reactor is currently producing energy. Recomputed server-side on + * every tick by the controller block entity and synced to the client by vanilla, + * which is what drives the running-sound loop and the lit controller texture. + */ + public static final BooleanProperty ACTIVE = BooleanProperty.create("active"); public ReactorControllerBlock(Properties properties) { super(properties); + this.registerDefaultState(this.stateDefinition.any() + .setValue(FACING, Direction.NORTH) + .setValue(ASSEMBLED, false) + .setValue(ACTIVE, false) // Inactive by default + ); } @Override protected void createBlockStateDefinition(StateDefinition.Builder builder) { - builder.add(FACING).add(ASSEMBLED); + builder.add(FACING).add(ASSEMBLED).add(ACTIVE); super.createBlockStateDefinition(builder); } @@ -51,12 +71,22 @@ protected void createBlockStateDefinition(StateDefinition.Builder be.created = false); + if (worldIn.isClientSide) + return; + // A direct neighbor of the controller has changed. Do NOT blindly disassemble: + // only do so if the structure is actually broken. Otherwise, placing/replacing a + // block adjacent to the controller (frame above/below, input/alarm on the sides, + // cooler behind) would invalidate the assembly that was just validated. + // disassemble() checks findStructure and does nothing if the structure is complete. + if (state.getValue(ASSEMBLED)) + ReactorAssembler.disassemble(pos, worldIn); } @Override @@ -68,35 +98,50 @@ protected ItemInteractionResult useItemOn(ItemStack stack, BlockState state, Lev if (!(blockEntity instanceof ReactorControllerBlockEntity controllerBlockEntity)) return ItemInteractionResult.PASS_TO_DEFAULT_BLOCK_INTERACTION; ItemStack heldItem = player.getItemInHand(hand); + if (heldItem.is(Items.DEBUG_STICK)) { + withBlockEntityDo(level, pos, be -> be.logReactorConnections(player)); + } if (!state.getValue(ASSEMBLED)) { - player.sendSystemMessage(Component.translatable("reactor.info.assembled.none").withStyle(ChatFormatting.RED)); } else { - if (heldItem.is(CNItems.REACTOR_BLUEPRINT.get()) && controllerBlockEntity.inventory.getItem(0).isEmpty()){ + if (heldItem.is(CNItems.REACTOR_BLUEPRINT.get()) && controllerBlockEntity.getInventoryObject().getItem(0).isEmpty() + && heldItem.getOrDefault(CNDataComponents.REACTOR_BLUE_PRINT_DATA, ReactorBluePrintData.EMPTY) != ReactorBluePrintData.EMPTY){ withBlockEntityDo(level, pos, be -> { - be.inventory.setStackInSlot(0, heldItem); - be.configuredPattern = heldItem; + be.getInventoryObject().setStackInSlot(0, heldItem); + be.setConfiguredPattern(heldItem); player.setItemInHand(hand, ItemStack.EMPTY); }); + // Inserting the blueprint is what starts energy production: this is the activation + // cue, not the multiblock assembly one (that lives in ReactorAssembler). + // One-shot played server-side (null player) so it broadcasts to nearby clients. + level.playSound(null, pos, CNSoundEvents.REACTOR_ACTIVATION.getMainEvent(), SoundSource.BLOCKS, 1.0f, 1.0f); return ItemInteractionResult.SUCCESS; } - else if (heldItem.isEmpty() && !controllerBlockEntity.inventory.getItem(0).isEmpty()) { + else if (heldItem.isEmpty() && !controllerBlockEntity.getInventoryObject().getItem(0).isEmpty()) { withBlockEntityDo(level, pos, be -> { - player.setItemInHand(hand, be.inventory.getItem(0)); - be.inventory.setStackInSlot(0, ItemStack.EMPTY); - be.configuredPattern = ItemStack.EMPTY; - be.total = 0.0; - be.rotate(be.getBlockState(), new BlockPos(be.getBlockPos().getX(), be.getBlockPos().getY() + (-3), be.getBlockPos().getZ()), be.getLevel(), 0, false); + ItemStack blueprint = be.getInventoryObject().getItem(0); + int storedHeat = Math.round(blueprint.getOrDefault(CNDataComponents.HEAT, 0f)); + if (IHeat.HeatLevel.of(storedHeat, be.getMultiblockSize()) == IHeat.HeatLevel.DANGER) { + be.getAdvancement().setPlayer(player.getUUID()); + be.getAdvancement().awardPlayer(CNAdvancement.NO_TIME_TO_DIE); + } + player.setItemInHand(hand, blueprint); + be.getInventoryObject().setStackInSlot(0, ItemStack.EMPTY); + be.setConfiguredPattern(ItemStack.EMPTY); + //be.clearTimers(); // uncomment if the timer should reset when the reactor stops + be.getOutputManager().rotateOutputs(be.getLevel(), be.getAssembled(), 0); be.notifyUpdate(); }); + // Blueprint removed: the multiblock stays assembled, it just stops producing. + level.playSound(null, pos, CNSoundEvents.REACTOR_SHUT_OFF.getMainEvent(), SoundSource.BLOCKS, 1.0f, 1.0f); state.setValue(ASSEMBLED, false); return ItemInteractionResult.SUCCESS; } - else if (!heldItem.isEmpty() && !controllerBlockEntity.inventory.getItem(0).isEmpty()) { + else if (!heldItem.isEmpty() && !controllerBlockEntity.getInventoryObject().getItem(0).isEmpty()) { return ItemInteractionResult.PASS_TO_DEFAULT_BLOCK_INTERACTION; } } @@ -108,90 +153,42 @@ public void onRemove(BlockState state, Level worldIn, BlockPos pos, BlockState n if (!state.hasBlockEntity() || state.getBlock() == newState.getBlock()) return; - withBlockEntityDo(worldIn, pos, be -> ItemHelper.dropContents(worldIn, pos, be.inventory)); + withBlockEntityDo(worldIn, pos, be -> ItemHelper.dropContents(worldIn, pos, be.getInventoryObject())); worldIn.removeBlockEntity(pos); - ReactorControllerBlock controller = (ReactorControllerBlock) state.getBlock(); - - controller.Rotate(state, pos.below(3), worldIn, 0); - List players = worldIn.players(); - for (Player p : players) { - p.sendSystemMessage(Component.translatable("reactor.info.assembled.destroyer")); + if (worldIn instanceof ServerLevel serverLevel) { + PersistentFluidLocks.get(serverLevel).clearLock(pos); } + + if (!state.getValue(ASSEMBLED)) + return; + + int configRadius = CNConfigs.server().notify.warningDistance.get(); + boolean configWarnAll = CNConfigs.server().notify.warnAllPlayers.get(); + NotifyUtil.sendActionBar(worldIn, pos, + CreateNuclearLang.translate("notification.reactor.disassembled"), + ChatFormatting.GOLD, configRadius, configWarnAll + ); } @Override - public void onPlace(BlockState state, Level level, BlockPos pos, BlockState oldState, boolean movedByPiston) { - super.onPlace(state, level, pos, oldState, movedByPiston); + public void setPlacedBy(Level level, BlockPos pos, BlockState state, @Nullable LivingEntity pPlacer, ItemStack stack) { + super.setPlacedBy(level, pos, state, pPlacer, stack); + MultiblockHelpers.handleAdvancedPlacedBy(pos, level, pPlacer); if (state.getValue(ASSEMBLED)) return; - List players = level.players(); - ReactorControllerBlock controller = (ReactorControllerBlock) state.getBlock(); - controller.Verify(state, pos, level, players, true); + ReactorAssembler.assemble(pos, level); } @Override public void playerDestroy(Level level, Player player, BlockPos pos, BlockState state, @Nullable BlockEntity blockEntity, ItemStack tool) { super.playerDestroy(level, player, pos, state, blockEntity, tool); - ReactorControllerBlock controller = (ReactorControllerBlock) state.getBlock(); - ReactorControllerBlockEntity entity = controller.getBlockEntity(level, pos); - if (!entity.created) - return; - controller.Rotate(state, pos.below(3), level, 0); - List players = level.players(); - for (Player p : players) { + if (!(blockEntity instanceof ReactorControllerBlockEntity entity)) return; + if (!entity.isAssembled()) return; + for (Player p : level.players()) { p.sendSystemMessage(Component.translatable("reactor.info.assembled.creator")); } - } - - // this is the Function that verifies if the pattern is correct (as a test, we added the energy output) - public void Verify(BlockState state, BlockPos pos, Level level, List players, boolean create){ - ReactorControllerBlock controller = (ReactorControllerBlock) level.getBlockState(pos).getBlock(); - ReactorControllerBlockEntity entity = controller.getBlockEntity(level, pos); - var result = CNMultiblock.REGISTRATE_MULTIBLOCK.findStructure(level, pos, this.getBlockEntity(level, pos)); // control the pattern - if (result != null) { // the pattern is correct - - for (Player player : players) { - if (create && !entity.created) { - player.sendSystemMessage(Component.translatable("reactor.info.assembled.creator")); - level.setBlockAndUpdate(pos, state.setValue(ASSEMBLED, true)); - entity.created = true; - entity.destroyed = false; - } - } - return; - } - - // the pattern is incorrect - for (Player player : players) { - if (!create && !entity.destroyed) - { - player.sendSystemMessage(Component.translatable("reactor.info.assembled.destroyer")); - level.setBlockAndUpdate(pos, state.setValue(ASSEMBLED, false)); - entity.created = false; - entity.destroyed = true; - Rotate(state, pos.below(3), level, 0); - } - } - } - public void Rotate(BlockState state, BlockPos pos, Level level, int rotation) { - if (level.getBlockState(pos).is(CNBlocks.REACTOR_OUTPUT.get())) { - ReactorOutput block = (ReactorOutput) level.getBlockState(pos).getBlock(); - ReactorOutputEntity entity = block.getBlockEntityType().getBlockEntity(level, pos); - - if (state.getValue(ASSEMBLED) && rotation != 0) { // Starting the energy - entity.speed = rotation; - entity.setSpeed(Math.abs(entity.speed)); - } else { // stopping the energy - - entity.setSpeed(0); - entity.speed = 0; - } - entity.updateSpeed = true; - entity.updateGeneratedRotation(); - - entity.setSpeed(rotation); - } +// entity.removeIOAll(); } @Override @@ -203,5 +200,4 @@ public Class getBlockEntityClass() { public BlockEntityType getBlockEntityType() { return CNBlockEntityTypes.REACTOR_CONTROLLER.get(); } - } diff --git a/src/main/java/net/nuclearteam/createnuclear/content/multiblock/controller/ReactorControllerBlockEntity.java b/src/main/java/net/nuclearteam/createnuclear/content/multiblock/controller/ReactorControllerBlockEntity.java index c5ceb7e..6a18511 100644 --- a/src/main/java/net/nuclearteam/createnuclear/content/multiblock/controller/ReactorControllerBlockEntity.java +++ b/src/main/java/net/nuclearteam/createnuclear/content/multiblock/controller/ReactorControllerBlockEntity.java @@ -1,11 +1,11 @@ package net.nuclearteam.createnuclear.content.multiblock.controller; import com.simibubi.create.api.equipment.goggles.IHaveGoggleInformation; -import com.simibubi.create.content.logistics.BigItemStack; import com.simibubi.create.foundation.blockEntity.SmartBlockEntity; import com.simibubi.create.foundation.blockEntity.behaviour.BlockEntityBehaviour; -import com.simibubi.create.foundation.utility.CreateLang; import com.simibubi.create.foundation.utility.IInteractionChecker; +import net.minecraft.client.Minecraft; +import net.minecraft.core.*; import lib.multiblock.SimpleMultiBlockAislePatternBuilder; import net.createmod.catnip.platform.CatnipServices; import net.minecraft.ChatFormatting; @@ -14,130 +14,83 @@ import net.minecraft.core.HolderLookup; import net.minecraft.core.component.DataComponents; import net.minecraft.nbt.CompoundTag; -import net.minecraft.nbt.ListTag; -import net.minecraft.nbt.Tag; import net.minecraft.network.chat.Component; import net.minecraft.server.level.ServerLevel; -import net.minecraft.world.item.Item; +import net.minecraft.world.entity.player.Player; import net.minecraft.world.item.ItemStack; import net.minecraft.world.item.component.CustomData; import net.minecraft.world.level.Level; import net.minecraft.world.level.block.entity.BlockEntity; import net.minecraft.world.level.block.entity.BlockEntityType; import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.levelgen.structure.BoundingBox; import net.minecraft.world.level.material.Fluid; -import net.neoforged.neoforge.capabilities.Capabilities; +import net.neoforged.api.distmarker.Dist; +import net.neoforged.api.distmarker.OnlyIn; import net.neoforged.neoforge.fluids.FluidStack; -import net.neoforged.neoforge.fluids.capability.IFluidHandler; import net.neoforged.neoforge.items.IItemHandler; -import net.neoforged.neoforge.items.wrapper.EmptyItemHandler; -import net.nuclearteam.createnuclear.*; + +import net.nuclearteam.createnuclear.CNDataComponents; +import net.nuclearteam.createnuclear.CNSoundEvents; +import net.nuclearteam.createnuclear.CreateNuclear; +import net.nuclearteam.createnuclear.api.multiblock.IMultiblockController; import net.nuclearteam.createnuclear.content.logistics.BigFluidStack; -import net.nuclearteam.createnuclear.content.multiblock.CNMultiblock; -import net.nuclearteam.createnuclear.content.multiblock.IHeat; -import net.nuclearteam.createnuclear.content.multiblock.bluePrintItem.PatternData; import net.nuclearteam.createnuclear.content.multiblock.bluePrintItem.ReactorBluePrintData; -import net.nuclearteam.createnuclear.content.multiblock.controller.manager.*; -import net.nuclearteam.createnuclear.content.multiblock.input.fluid.FluidLockManager; -import net.nuclearteam.createnuclear.content.multiblock.input.fluid.PersistentFluidLocks; -import net.nuclearteam.createnuclear.content.multiblock.input.fluid.ReactorFluidInputEntity; -import net.nuclearteam.createnuclear.content.multiblock.input.item.ReactorRodInputEntity; -import net.nuclearteam.createnuclear.content.multiblock.output.ReactorOutput; -import net.nuclearteam.createnuclear.content.multiblock.output.ReactorOutputEntity; -import net.nuclearteam.createnuclear.content.multiblock.pattern.ReactorPattern; +import net.nuclearteam.createnuclear.content.multiblock.controller.display.ReactorDisplayState; +import net.nuclearteam.createnuclear.content.multiblock.controller.display.ReactorGoggleTooltipRenderer; +import net.nuclearteam.createnuclear.content.multiblock.controller.service.*; +import net.nuclearteam.createnuclear.content.multiblock.controller.snapshot.ReactorInputSnapshot; +import net.nuclearteam.createnuclear.content.multiblock.controller.snapshot.ReactorInputSnapshotBuilder; +import net.nuclearteam.createnuclear.content.multiblock.IHeat; +import net.nuclearteam.createnuclear.content.multiblock.reactorLogic.HeatBalance; +import net.nuclearteam.createnuclear.foundation.advancement.CNAdvancement; import net.nuclearteam.createnuclear.foundation.advancement.CNAdvancementBehaviour; +import net.nuclearteam.createnuclear.content.multiblock.controller.consumable.ConsumptionCycleManager; import java.util.ArrayList; -import java.util.HashMap; import java.util.List; -import java.util.Map; -import static net.nuclearteam.createnuclear.content.multiblock.CNMultiblock.*; +import net.nuclearteam.createnuclear.content.multiblock.input.fluid.PersistentFluidLocks; +import net.nuclearteam.createnuclear.content.multiblock.controller.manager.*; +import net.nuclearteam.createnuclear.content.multiblock.pattern.ReactorPattern; +import net.nuclearteam.createnuclear.content.multiblock.reactorLogic.HeatManager; + import static net.nuclearteam.createnuclear.content.multiblock.controller.ReactorControllerBlock.ASSEMBLED; -@SuppressWarnings({"unused"}) -public class ReactorControllerBlockEntity extends SmartBlockEntity implements IInteractionChecker, IHaveGoggleInformation { - public boolean destroyed = false; - public boolean created = false; - public boolean test = true; - public int speed = 16; // This is the result speed of the reactor, change this to change the total capacity - - public boolean sendUpdate; - - public ReactorControllerBlock controller; - - public ReactorControllerInventory inventory; - - - //private boolean powered; - public State powered = State.OFF; - public float reactorPower; - public float lastReactorPower; - int overFlowHeatTimer = 0; - int overFlowLimiter = 30; - double overHeat = 0; - public int baseUraniumHeat = 25; - public int baseGraphiteHeat = -10; - public int proximityUraniumHeat = 5; - public int proximityGraphiteHeat = -5; - public int maxUraniumPerGraphite = 3; - public int graphiteTimer = 3600; - public int uraniumTimer = 3600; - public int tmpGraphiteTimer = graphiteTimer; - public int tmpUraniumTimer = uraniumTimer; - public int countUraniumRod; - public int countGraphiteRod; - public int heat; - public double total; - private boolean isTotal = false; - public CompoundTag screen_pattern = new CompoundTag(); - public ItemStack configuredPattern; - - private ItemStack fuelItem; - private ItemStack coolerItem; - - private final int[][] formattedPattern = new int[][]{ - {99,99,99,0,1,2,99,99,99}, - {99,99,3,4,5,6,7,99,99}, - {99,8,9,10,11,12,13,14,99}, - {15,16,17,18,19,20,21,22,23}, - {24,25,26,27,28,29,30,31,32}, - {33,34,35,36,37,38,39,40,41}, - {99,42,43,44,45,46,47,48,99}, - {99,99,49,50,51,52,53,99,99}, - {99,99,99,54,55,56,99,99,99} - }; - private final int[][] offsets = { {1, 0}, {-1, 0}, {0, 1}, {0, -1} }; - - /* - FORGE ARGUMENTS PART +@SuppressWarnings({ "unused" }) +public class ReactorControllerBlockEntity extends SmartBlockEntity + implements IInteractionChecker, IHaveGoggleInformation, IMultiblockController { + /** + * The assembled state is stored in the block state + * (`ReactorControllerBlock.ASSEMBLED`). + * Use the helper accessors below to query or toggle it to keep + * entity/blockstate consistent. */ - private final ReactorPattern pattern = new ReactorPattern(); - private int explosionCountdown = 0; + private final ReactorControllerInventory inventory; + private int countFuelRod; + private int countCoolerRod; + private HeatBalance heatBalance; + private int heat; + private int lastAppliedOutputHeat; private boolean isExploding = false; - //private final ConsumptionCycleManager cycleManager = new ConsumptionCycleManager(); + /** Client-only looping "running" sound instance; never accessed server-side. */ + @OnlyIn(Dist.CLIENT) + private ReactorRunningSoundInstance runningSound; + + private final ConsumptionCycleManager cycleManager = new ConsumptionCycleManager(); private double liquidLife; + private ItemStack configuredPattern; - private BigItemStack bigFuelItem; - private BigItemStack bigCoolerItem; private List bigFluidStack; private int reactorSize = 0; - private String reactorFacing = "null"; + private Direction reactorFacing = null; // les pos sont [xMin, xMax, yMin, yMax, zMin, zMax] - private int[] reactorPos; - // Vertical span (block Y) of the reactor frame columns, used to map the - // fluid fill ratio onto the liquid actually drawn in the frame windows. - private int frameColumnMinY = Integer.MAX_VALUE; - private int frameColumnMaxY = Integer.MIN_VALUE; - // Per-tick cache for the fluid drawn in the frame windows. Recomputed from the - // live (synced) input tanks so the displayed fluid and its fill ratio always - // come from the same source and stay consistent for every fluid type. - private long frameFluidCacheTick = -1; - private FluidStack frameFluidCache = FluidStack.EMPTY; - private float frameFluidFillRatioCache = 0f; + private BoundingBox reactorPos; + + private boolean needsToResolveEntities = false; private double fluidBuffer = 0.0; @@ -147,15 +100,20 @@ public class ReactorControllerBlockEntity extends SmartBlockEntity implements II private final ReactorOutputManagerI outputManager; private final ReactorInputFluidManagerI inputFluidManager; private final ReactorAlarmManagerI alarmManager; + private final ReactorFrameDisplayManagerI frameDisplayManager; - // Client Display Data (Synced via NBT) - private Map clientDisplayItems = new HashMap<>(); - private List clientDisplayFluids = new ArrayList<>(); - private long clientMaxFluidCapacity = 0; + private ReactorDisplayState displayState = ReactorDisplayState.EMPTY; // services (dependencies) - abstracted behind interfaces to follow DIP - //private final IHeatService heatService; - //private final IPersistenceService persistenceService; + private final IHeatService heatService; + private final IPersistenceService persistenceService; + private final IExplosionService meltdownExecutor; + private final IReactorHeatUpdateCoordinator heatCoordinator; + private final IFluidConsumptionRateCalculator fluidRateCalculator; + /** Counts down to the next explosion once in danger; keeps the original 1-tick lag against the heat recalculated later this tick. */ + private final IReactorMeltdownMonitor meltdownMonitor; + /** Drives the alarm blocks based on the danger flag; built in {@link #addBehaviours} since it depends on {@link #advancement}. */ + private IReactorAlarmCoordinator alarmCoordinator; // service fields are injected; implementations live in separate classes @@ -164,13 +122,13 @@ public ReactorControllerInventory getInventoryObject() { return this.inventory; } - /*public void deserializeInventory(CompoundTag tag) { - this.inventory.deserializeNBT(tag); + public void deserializeInventory(HolderLookup.Provider registries, CompoundTag tag) { + this.inventory.deserializeNBT(registries, tag); } - public CompoundTag serializeInventory() { - return this.inventory.serializeNBT(); - }*/ + public CompoundTag serializeInventory(HolderLookup.Provider registries) { + return this.inventory.serializeNBT(registries); + } public ItemStack getConfiguredPattern() { return this.configuredPattern; @@ -180,28 +138,29 @@ public void setConfiguredPattern(ItemStack stack) { this.configuredPattern = stack; } - public CompoundTag getConfiguredPatternTag() { - return this.configuredPattern.getOrDefault(DataComponents.CUSTOM_DATA, CustomData.EMPTY).copyTag(); - } - - public int[] getMultiblockPos() { - return this.reactorPos; - } - - public BigItemStack getBigFuelItem() { - return this.bigFuelItem; + /** + * Current heat stored on the blueprint stack. + *

+ * Replaces the Forge {@code getConfiguredPatternTag().getDouble("heat")}: the value now + * lives in the {@link CNDataComponents#HEAT} data component, because reading the stack's + * NBT tag back on NeoForge yields a defensive copy. + */ + public int getConfiguredPatternHeat() { + if (this.configuredPattern.isEmpty()) return 0; + return Math.round(this.configuredPattern.getOrDefault(CNDataComponents.HEAT, 0f)); } - public void setBigFuelItem(BigItemStack b) { - this.bigFuelItem = b; + public CompoundTag getConfiguredPatternTag() { + return this.configuredPattern.getOrDefault(DataComponents.CUSTOM_DATA, CustomData.EMPTY).copyTag(); } - public BigItemStack getBigCoolerItem() { - return this.bigCoolerItem; + private ReactorBluePrintData getConfiguredPatternData() { + return getConfiguredPattern().getOrDefault(CNDataComponents.REACTOR_BLUE_PRINT_DATA, ReactorBluePrintData.EMPTY); } - public void setBigCoolerItem(BigItemStack b) { - this.bigCoolerItem = b; + /** @return the first loaded fluid, or {@code null} if no fluid is present. */ + private BigFluidStack currentFluidStack() { + return bigFluidStack.isEmpty() ? null : bigFluidStack.get(0); } public List getBigFluidStack() { @@ -212,96 +171,63 @@ public void setBigFluidStack(List b) { this.bigFluidStack = b; } - private void refreshFrameFluidCache() { - if (level == null) return; - long now = level.getGameTime(); - if (now == frameFluidCacheTick) return; - frameFluidCacheTick = now; - - long amount = 0; - long capacity = 0; - FluidStack fluid = FluidStack.EMPTY; - for (IFluidHandler handler : inputFluidManager.getFuildHandlers(level)) { - int tanks = handler.getTanks(); - for (int t = 0; t < tanks; t++) { - FluidStack stack = handler.getFluidInTank(t); - capacity += handler.getTankCapacity(t); - amount += stack.getAmount(); - if (fluid.isEmpty() && !stack.isEmpty()) fluid = stack.copy(); - } - } - - frameFluidCache = fluid; - frameFluidFillRatioCache = capacity > 0 ? Math.min(1f, (float) amount / (float) capacity) : 0f; - } - - public FluidStack getDisplayedFluid() { - refreshFrameFluidCache(); - return frameFluidCache; + public int getMultiblockSize() { + return this.reactorSize; } - /** - * How full the reactor's fluid input is, in the range {@code [0, 1]}, used by - * the frame renderer to size the visible liquid column. - */ - public float getDisplayedFluidFillRatio() { - refreshFrameFluidCache(); - return frameFluidFillRatioCache; + public void setMultiblockSize(int s) { + this.reactorSize = s; } - /** Records the lowest and highest frame block-Y of the assembled reactor. */ - public void setFrameColumn(int minY, int maxY) { - if (this.frameColumnMinY == minY && this.frameColumnMaxY == maxY) return; - this.frameColumnMinY = minY; - this.frameColumnMaxY = maxY; - notifyUpdate(); + @Override + public Direction getMultiblockFacing() { + return this.reactorFacing; } - /** @return the lowest frame block-Y, or {@link Integer#MAX_VALUE} if unknown. */ - public int getFrameColumnMinY() { - return frameColumnMinY; + @Override + public void setMultiblockFacing(Direction f) { + this.reactorFacing = f; } - /** @return the highest frame block-Y, or {@link Integer#MIN_VALUE} if unknown. */ - public int getFrameColumnMaxY() { - return frameColumnMaxY; + public CNAdvancementBehaviour getAdvancement() { + return this.advancement; } - public boolean hasFrameColumn() { - return frameColumnMinY != Integer.MAX_VALUE && frameColumnMaxY != Integer.MIN_VALUE - && frameColumnMaxY >= frameColumnMinY; + /** Main constructor allowing dependency injection for testability and DIP compliance. */ + public BoundingBox getMultiblockPos() { + return this.reactorPos; } - public int getMultiblockSize() { - return this.reactorSize; + public void setMultiblockStructure(BoundingBox p) { + this.reactorPos = p; } - public void setMultiblockSize(int s) { - this.reactorSize = s; + public void clearTimers() { + this.cycleManager.clear(); } - public String getMultiblockFacing() { - return this.reactorFacing; + public ReactorFrameDisplayManagerI getFrameDisplayManager() { + return this.frameDisplayManager; } - public void setMultiblockFacing(String f) { - this.reactorFacing = f; + public ReactorInputFluidManagerI getInputFluidManager() { + return this.inputFluidManager; } - public CNAdvancementBehaviour getAdvancement() { - return this.advancement; + public ReactorOutputManagerI getOutputManager() { + return this.outputManager; } - public void setMultiblockStructure(int[] p) { - this.reactorPos = p; + public void setDisplayState(ReactorDisplayState state) { + this.displayState = state; } - public double getLiquidLife() { - return this.liquidLife; + public ReactorDisplayState getDisplayState() { + return this.displayState; } - public void setLiquidLife(double l) { - this.liquidLife = l; + private List allManagers() { + return List.of(inputManager, inputFluidManager, outputManager, alarmManager); } /** @@ -312,95 +238,79 @@ public ReactorControllerBlockEntity(BlockEntityType type, BlockPos pos, Block super(type, pos, state); this.inventory = new ReactorControllerInventory(this); this.configuredPattern = ItemStack.EMPTY; + this.heatBalance = new HeatBalance(0, 0); this.inputManager = new ReactorInputManager(); this.outputManager = new ReactorOutputManager(); this.inputFluidManager = new ReactorInputFluidManager(); this.alarmManager = new ReactorAlarmManager(); + this.frameDisplayManager = new ReactorFrameDisplayManager(); - this.bigFuelItem = new BigItemStack(ItemStack.EMPTY); - this.bigCoolerItem = new BigItemStack(ItemStack.EMPTY); this.bigFluidStack = new ArrayList<>(); - //this.heatService = new DefaultHeatService(new HeatManager()); - //this.persistenceService = new DefaultPersistenceService(); + this.heatService = new DefaultHeatService(new HeatManager()); + this.persistenceService = new DefaultPersistenceService(); + this.meltdownExecutor = new ReactorMeltdownExecutor(); + this.heatCoordinator = new ReactorHeatUpdateCoordinator(this.heatService); + this.fluidRateCalculator = new FluidConsumptionRateCalculator(this.heatService); + this.meltdownMonitor = new ReactorMeltdownMonitor(); } @Override public void addBehaviours(List behaviours) { - + behaviours.add(advancement = new CNAdvancementBehaviour(this, CNAdvancement.T1_REACTOR, CNAdvancement.T2_REACTOR, CNAdvancement.T3_REACTOR, CNAdvancement.NO_TIME_TO_DIE, CNAdvancement.SILENCE_THE_CORE)); + this.alarmCoordinator = new ReactorAlarmCoordinator(advancement); } - public boolean getAssembled() { // permet de savoir si le réacteur est formé ou pas. + public boolean getAssembled() { // returns whether the reactor structure is currently assembled BlockState state = getBlockState(); return state.getValue(ASSEMBLED); } @Override public boolean addToGoggleTooltip(List tooltip, boolean isPlayerSneaking) { - if(!configuredPattern.isEmpty()) { - CreateLang.translate("gui.gauge.info_header").style(ChatFormatting.GRAY).forGoggles(tooltip); - IHeat.HeatLevel.getName("reactor_controller").style(ChatFormatting.GRAY).forGoggles(tooltip); - - IHeat.HeatLevel.getFormattedHeatText(heat).forGoggles(tooltip); - - if (fuelItem.isEmpty()) { - // if rod empty we initialize it at 1 (and display it as 0) to avoid having air item displayed instead of the rod - IHeat.HeatLevel.getFormattedItemText(new ItemStack(CNItems.URANIUM_ROD.asItem(), 1), true).forGoggles(tooltip); - } else { - IHeat.HeatLevel.getFormattedItemText(fuelItem, false).forGoggles(tooltip); - } - - if (fuelItem.isEmpty()) { - // if rod empty we initialize it at 1 (and display it as 0) to avoid having air item displayed instead of the rod - IHeat.HeatLevel.getFormattedItemText(new ItemStack(CNItems.GRAPHITE_ROD.asItem(), 1), true).forGoggles(tooltip); - } else { - IHeat.HeatLevel.getFormattedItemText(coolerItem, false).forGoggles(tooltip); - } + if (getConfiguredPatternData() == ReactorBluePrintData.EMPTY) { + return false; } + ReactorGoggleTooltipRenderer.render(tooltip, displayState, getConfiguredPatternHeat(), isPlayerSneaking, reactorSize); + return true; } - //(Si les methode read et write ne sont pas implémenté alors lorsque l'on relance le monde minecraft les items dans le composant auront disparu !) + // (If read/write are not implemented, the items stored in this block entity's + // inventory will be lost when the world is reloaded!) @Override - protected void read(CompoundTag tag, HolderLookup.Provider registries, boolean clientPacket) { //Permet de stocker les item 1/2 - if (!clientPacket) { - inventory.deserializeNBT(registries, tag.getCompound("inventory")); + protected void read(CompoundTag compound, HolderLookup.Provider registries, boolean clientPacket) { + super.read(compound, registries, clientPacket); // Always call first to restore the base coordinates + // delegate managers and persistence + this.inputManager.read(compound); + this.outputManager.read(compound); + this.inputFluidManager.read(compound); + this.alarmManager.read(compound); + this.frameDisplayManager.read(compound); + + this.persistenceService.readBasicState(this, compound, registries, clientPacket); + this.needsToResolveEntities = true; + + this.cycleManager.clear(); + if (compound.contains("cycleManager")) { + this.cycleManager.deserializeNBT(compound.getCompound("cycleManager")); } - configuredPattern = ItemStack.EMPTY; - if (tag.contains("configuredPattern")) { - ItemStack.parse(registries, tag.getCompound("configuredPattern")).ifPresent(i -> configuredPattern = i); - } - - coolerItem = ItemStack.EMPTY; - if (tag.contains("coolerItem")) { - ItemStack.parse(registries, tag.getCompound("coolerItem")).ifPresent(i -> coolerItem = i); - } - - fuelItem = ItemStack.EMPTY; - if (tag.contains("fuelItem")) { - ItemStack.parse(registries, tag.getCompound("fuelItem")).ifPresent(i -> fuelItem = i); - } - - total = tag.getDouble("total"); - heat = tag.getInt("heat"); - super.read(tag, registries, clientPacket); } @Override - protected void write(CompoundTag compound, HolderLookup.Provider registries, boolean clientPacket) { //Permet de stocker les item 2/2 - if (!clientPacket) { - compound.put("inventory", inventory.serializeNBT(registries)); - } + protected void write(CompoundTag compound, HolderLookup.Provider registries, boolean clientPacket) { + super.write(compound, registries, clientPacket); + this.inputManager.write(compound); + this.outputManager.write(compound); + this.inputFluidManager.write(compound); + this.alarmManager.write(compound); + this.frameDisplayManager.write(compound); - if (configuredPattern != null) compound.put("configuredPattern", configuredPattern.saveOptional(registries)); - if (coolerItem != null) compound.put("coolerItem", coolerItem.saveOptional(registries)); - if (fuelItem != null) compound.put("fuelItem", fuelItem.saveOptional(registries)); + this.persistenceService.writeBasicState(this, compound, registries, clientPacket); - compound.putDouble("total", Double.isNaN(total) ? total : calculateProgress()); - compound.putInt("heat", heat); - super.write(compound, registries, clientPacket); + compound.put("cycleManager", cycleManager.serializeNBT()); } public boolean isAssembled() { @@ -420,337 +330,158 @@ public void setAssembled(boolean assembled) { this.setChanged(); } - public enum State { - ON, OFF + public void logReactorConnections(Player player) { + ReactorDebugDiagnostics.sendReactorConnectionsTo(player, level, inputManager, inputFluidManager, outputManager, alarmManager); + } + + private void updateReactorStateVisibility() { + if (level == null || level.isClientSide) return; + + BlockState state = getBlockState(); + if (!(state.getBlock() instanceof ReactorControllerBlock)) return; + + boolean currentActive = state.getValue(ReactorControllerBlock.ACTIVE); + + // The reactor is "ACTIVE" (ON) only if it is assembled AND has the resources required to run + boolean targetActive = isAssembled() && heatCoordinator.canRun(getConfiguredPattern(), getDisplayState(), getInputFluidManager(), level, getAssembled()); + + if (currentActive != targetActive) { + level.setBlock(worldPosition, state.setValue(ReactorControllerBlock.ACTIVE, targetActive), 3); + } } @Override public void tick() { super.tick(); - if (level.isClientSide) + if (level.isClientSide) { + tickRunningSound(); + return; + } + if (isExploding) return; + // Heat value written by the previous tick's handleAssembledState(); this tick's recalculated + // heat is not visible here yet, so the alarm/meltdown danger flag is intentionally 1 tick behind. + int currentHeat = getConfiguredPatternHeat(); + boolean isDanger = alarmCoordinator.computeDanger(currentHeat, this.getMultiblockSize()); - if (isEmptyConfiguredPattern()) { - ReactorBluePrintData data = getReactorBluePrintData(); - countGraphiteRod = data.countGraphiteRod(); - countUraniumRod = data.countUraniumRod(); + alarmCoordinator.update(level, alarmManager, isDanger); + + IReactorMeltdownMonitor.MeltdownState meltdownState = meltdownMonitor.tick(level, getBlockPos(), isDanger); + + if (meltdownState == IReactorMeltdownMonitor.MeltdownState.EXPLODE) { + if (level instanceof ServerLevel serverLevel) { + this.meltdownExecutor.triggerExplosion(serverLevel, getBlockPos(), reactorSize, countFuelRod); - if (!isTotal) { - total = calculateProgress(); - isTotal = true; - } - BlockEntity blockEntity = level.getBlockEntity(getBlockPosForReactor('I')); - - if (blockEntity instanceof ReactorRodInputEntity be) { - fuelItem = be.inventory.getStackInSlot(0); - coolerItem = be.inventory.getStackInSlot(1); - - IItemHandler capability = level.getCapability(Capabilities.ItemHandler.BLOCK, be.getBlockPos(), Direction.NORTH.getOpposite()); - if (capability == null) - capability = EmptyItemHandler.INSTANCE; - if (tmpUraniumTimer >= 0) { - tmpUraniumTimer -= 1 * countUraniumRod; - } else { - ItemStack extractItem1 = capability.extractItem(0, 1, false); - tmpUraniumTimer = uraniumTimer; - } - if (tmpGraphiteTimer >= 0) { - tmpGraphiteTimer -= 1 * countGraphiteRod; - } else { - ItemStack extractItem2 = capability.extractItem(1, 1, false); - tmpGraphiteTimer = graphiteTimer; - } - - if (!fuelItem.isEmpty() && !coolerItem.isEmpty()) { - heat = (int) calculateHeat(inventory.getItem(0)); - if (updateTimers()) { - - if (IHeat.HeatLevel.of(heat) == IHeat.HeatLevel.SAFETY || IHeat.HeatLevel.of(heat) == IHeat.HeatLevel.CAUTION || IHeat.HeatLevel.of(heat) == IHeat.HeatLevel.WARNING) { - this.rotate(getBlockState(), new BlockPos(getBlockPos().getX(), getBlockPos().getY() + FindController('O').getY(), getBlockPos().getZ()), getLevel(), heat/4, true); - return; - } else { - EventTriggerPacket packet = new EventTriggerPacket(600); - CreateNuclear.LOGGER.warn("hum EventTriggerBlock ? {}", packet); - CatnipServices.NETWORK.sendToClientsAround((ServerLevel) level, getBlockPos(), 32, packet); - - this.rotate(getBlockState(), new BlockPos(getBlockPos().getX(), getBlockPos().getY() + FindController('O').getY(), getBlockPos().getZ()), getLevel(), 0, false); - isTotal = false; - return; - } - } else { - this.rotate(getBlockState(), new BlockPos(getBlockPos().getX(), getBlockPos().getY() + FindController('O').getY(), getBlockPos().getZ()), getLevel(), 0, false); - isTotal = false; - return; - } - } - this.notifyUpdate(); } + isExploding = true; + return; } - } - private boolean isEmptyConfiguredPattern() { - return !configuredPattern.isEmpty();// || !configuredPattern.getOrCreateTag().isEmpty(); - } + if (!isEmptyConfiguredPattern()) { + // Deliberately a local, shadowing the field, exactly as on Forge: the field `heat` + // must keep the value handleAssembledState() computed, and is only refreshed there. + int heat = getConfiguredPatternHeat(); + countCoolerRod = getConfiguredPatternCoolerRodCount(); + countFuelRod = getConfiguredPatternFuelRodCount(); + heatBalance = heatCoordinator.calculateHeatBalance(configuredPattern, displayState, level); + } + resolveEntitiesIfNeeded(); - private boolean updateTimers() { - total -= 1; - return total >= 0;//(total/constTotal) >= 0; - } + ReactorInputSnapshot snapshot = ReactorInputSnapshotBuilder.build(level, inputManager, inputFluidManager); + this.displayState = new ReactorDisplayState(snapshot.items(), snapshot.fluids(), snapshot.maxFluidCapacity()); + this.bigFluidStack = snapshot.fluids(); - private ReactorBluePrintData getDefaultReactorBluePrintData() { - return new ReactorBluePrintData( - 0, 0, 0, 0, - new PatternData[0], new PatternData[0] - ); - } - private ReactorBluePrintData getReactorBluePrintData() { - return configuredPattern.getOrDefault(CNDataComponents.REACTOR_BLUE_PRINT_DATA, getDefaultReactorBluePrintData()); + updateReactorStateVisibility(); + handleAssembledState(); } - private double calculateProgress() { - ReactorBluePrintData data = getReactorBluePrintData(); - countGraphiteRod = data.countGraphiteRod(); - countUraniumRod = data.countUraniumRod(); - graphiteTimer = data.graphiteTime(); - uraniumTimer = data.uraniumTime(); - - double progressGraphite = countGraphiteRod > 0 - ? (double) graphiteTimer / countGraphiteRod - : 0.0; - double progressUranium = countUraniumRod > 0 - ? (double) uraniumTimer / countUraniumRod - : 0.0; - - double tmp = progressGraphite + progressUranium; - - return progressGraphite + progressUranium; + /** + * Rod counts required by the configured pattern. + *

+ * Forge reads {@code countCoolerRod}/{@code countFuelRod} off the blueprint's NBT tag; + * on NeoForge they live in the {@code ReactorBluePrintData} component written by + * {@code ReactorBluePrintMenu#saveData}. + */ + private int getConfiguredPatternCoolerRodCount() { + return getConfiguredPatternData().countCooledRod(); } - private double calculateHeat(ItemStack input) { - ReactorBluePrintData data = input.getOrDefault(CNDataComponents.REACTOR_BLUE_PRINT_DATA, getDefaultReactorBluePrintData()); + private int getConfiguredPatternFuelRodCount() { + return getConfiguredPatternData().countFuelRod(); + } - countGraphiteRod = data.countGraphiteRod(); - countUraniumRod = data.countUraniumRod(); - heat = 0; + /** + * Client-side: keep the looping "running" sound alive while the reactor is producing energy. + * Driven by the {@code ACTIVE} blockstate property, which the server recomputes every tick in + * {@link #updateReactorStateVisibility()} and vanilla syncs to the client automatically. + */ + @OnlyIn(Dist.CLIENT) + private void tickRunningSound() { + BlockState state = getBlockState(); + boolean active = state.hasProperty(ReactorControllerBlock.ACTIVE) + && state.getValue(ReactorControllerBlock.ACTIVE); - // if more than maxUraniumPerGraphite of the rods are uranium, the reactor will overheat - if (countUraniumRod > countGraphiteRod * maxUraniumPerGraphite) { - overFlowHeatTimer++; - if (overFlowHeatTimer >= overFlowLimiter) { - overHeat++; - overFlowHeatTimer = 0; - if (overFlowLimiter > 2) { - overFlowLimiter--; - } + if (!active) { + if (runningSound != null) { + runningSound.stopSound(); + runningSound = null; } - } else { - overFlowHeatTimer = 0; - overFlowLimiter = 30; - overHeat = Math.max(0, overHeat - 2); + return; } - PatternData[] patternDataAll = data.patternAll(); - for (PatternData pd : patternDataAll) { - char currentRod = '\0'; - ItemStack stack = pd.stack(); - - if (stack.is(CNItems.URANIUM_ROD)) { - heat += baseUraniumHeat; - currentRod = 'u'; - } else if (stack.is(CNItems.GRAPHITE_ROD)) { - heat += baseGraphiteHeat; - currentRod = 'g'; - } - - if (currentRod != '\0') { - pattern: - for (int i = 0; i < formattedPattern.length; i++) { - for (int j = 0; j < formattedPattern[i].length; j++) { - if (formattedPattern[i][j] == pd.slot()) { - // the offsets for the four directions (down, up, right, left) is int[][] offsets = { {1, 0}, {-1, 0}, {0, 1}, {0, -1} }; (defined at the top of the class) - for (int[] offset : offsets) { - int ni = i + offset[0], nj = j + offset[1]; - if (ni < 0 || ni >= formattedPattern.length || nj < 0 || nj >= formattedPattern[i].length) continue; - - int neighborSlot = formattedPattern[ni][nj]; - for (PatternData pd2 : patternDataAll) { - if (pd2.slot() == neighborSlot) { - ItemStack stack2 = pd.stack(); - if (currentRod == 'u') { - heat += stack2.is(CNItems.URANIUM_ROD) ? proximityUraniumHeat : proximityGraphiteHeat; - } - break; - } - } - } - break pattern; - } - } - } - } + if (runningSound == null || runningSound.isStopped()) { + runningSound = new ReactorRunningSoundInstance(level, worldPosition, CNSoundEvents.REACTOR_RUNNING.getMainEvent()); + Minecraft.getInstance().getSoundManager().play(runningSound); } - - return heat + overHeat; } - private BlockPos getBlockPosForReactor(char character) { - BlockPos pos = FindController(character); - BlockPos posController = getBlockPos(); - BlockPos posInput = new BlockPos(posController.getX(), posController.getY(), posController.getZ()); - - int[][] directions = { - {0,0, pos.getX()}, // NORTH - {0,0, -pos.getX()}, // SOUTH - {-pos.getX(),0,0}, // EAST - {pos.getX(),0,0} // WEST - }; - + // --- extracted sub-steps to keep single responsibility per method --- + private void resolveEntitiesIfNeeded() { + if (!needsToResolveEntities) + return; + List handlers = inputManager.getItemHandlers(level); + CreateNuclear.LOGGER.warn("Resolving inputs after load, handlers found: {}", handlers.size()); + needsToResolveEntities = false; + this.setChanged(); + } - for (int[] direction : directions) { - BlockPos newPos = posController.offset(direction[0], direction[1], direction[2]); - if (level.getBlockState(newPos).is(CNBlocks.REACTOR_ROD_INPUT.get())) { - posInput = newPos; - break; + /** + * Tick logic applied while the reactor is assembled: if it doesn't have + * enough fuel ({@link IReactorHeatUpdateCoordinator#canRun}), only the + * displayed heat is updated (and outputs stopped); otherwise the actual + * heat is calculated, fluid consumed, the item consumption cycle advanced, + * and outputs rotated according to the heat. + */ + private void handleAssembledState() { + if (!heatCoordinator.canRun(configuredPattern, displayState, inputFluidManager, level, isAssembled())) { + heatCoordinator.updateHeatOnly(configuredPattern, displayState, currentFluidStack(), heatBalance, heat, inventory, level, isAssembled()); + if (!outputManager.getBlocksPosition(getLevel()).isEmpty()) { + outputManager.rotateOutputs(getLevel(), getAssembled(), 0); } + + setChanged(); + notifyUpdate(); + return; } - return posInput; - } - - private CompoundTag convertePattern(CompoundTag compoundTag) { - ListTag pattern = compoundTag.getList("Items", Tag.TAG_COMPOUND); - - int[][] list = new int[][]{ - {99,99,99,0,1,2,99,99,99}, - {99,99,3,4,5,6,7,99,99}, - {99,8,9,10,11,12,13,14,99}, - {15,16,17,18,19,20,21,22,23}, - {24,25,26,27,28,29,30,31,32}, - {33,34,35,36,37,38,39,40,41}, - {99,42,43,44,45,46,47,48,99}, - {99,99,49,50,51,52,53,99,99}, - {99,99,99,54,55,56,99,99,99} - }; - - - return null; - } - - private static BlockPos FindController(char character) { - return SimpleMultiBlockAislePatternBuilder.start() - .aisle(AAAAA, AAAAA, AAAAA, AAAAA, AAAAA) - .aisle(AABAA, ADADA, BACAB, ADADA, AABAA) - .aisle(AABAA, ADADA, BACAB, ADADA, AABAA) - .aisle(AAIAA, ADADA, BACAB, ADADA, AAAA) - .aisle(AABAA, ADADA, BACAB, ADADA, AABAA) - .aisle(AABAA, ADADA, BACAB, ADADA, AABAA) - .aisle(AAAAA, AAAAA, AAAAA, AAAAA, AAOAA) - .where('A', a -> a.getState().is(CNBlocks.REACTOR_CASING.get())) - .where('B', a -> a.getState().is(CNBlocks.REACTOR_FRAME.get())) - .where('C', a -> a.getState().is(CNBlocks.REACTOR_CORE.get())) - .where('D', a -> a.getState().is(CNBlocks.REACTOR_COOLER.get())) - .where('*', a -> a.getState().is(CNBlocks.REACTOR_CONTROLLER.get())) - .where('O', a -> a.getState().is(CNBlocks.REACTOR_OUTPUT.get())) - .where('I', a -> a.getState().is(CNBlocks.REACTOR_ROD_INPUT.get())) - .getDistanceController(character); - } - - public void rotate(BlockState state, BlockPos pos, Level level, int rotation, boolean isActif) { - rotation = rotation > 0 ? rotation : heat/4; - if (level.getBlockState(pos).is(CNBlocks.REACTOR_OUTPUT.get()) && rotation > 0 && isActif) { - if (level.getBlockState(pos).getBlock() instanceof ReactorOutput block) { - ReactorOutputEntity entity = block.getBlockEntityType().getBlockEntity(level, pos); - if (state.getValue(ASSEMBLED)) { // Starting the energy - entity.speed = rotation; - entity.heat = rotation; - } else { // stopping the energy - entity.speed = 0; - entity.heat = 0; - } - entity.updateSpeed = true; - entity.updateGeneratedRotation(); - entity.setSpeed(rotation); + setChanged(); + notifyUpdate(); - } - } - else { - if (level.getBlockState(pos).getBlock() instanceof ReactorOutput block) { - ReactorOutputEntity entity = block.getBlockEntityType().getBlockEntity(level, pos); - entity.setSpeed(0); - entity.heat = 0; - entity.updateSpeed = true; - entity.updateGeneratedRotation(); - } - } - } + BigFluidStack fluidStack = currentFluidStack(); + heat = heatCoordinator.calculateAndWriteHeat(configuredPattern, fluidStack, heatBalance, heat, inventory, level, displayState); + fluidBuffer = fluidRateCalculator.tick(fluidStack, reactorSize, level, inputFluidManager, fluidBuffer); + cycleManager.update(configuredPattern, level, inputManager, level.getGameTime() % 20 == 0); - @Deprecated - public int[] getStructureBounds(BlockPos startPos, int structureSize, String facing) { - int[] northOffsets5x5 = new int[] { -2, 2, -3, 3, 0, 4 }; - int[] northOffsets7x7 = new int[] { -3, 3, -4, 4, 0, 6 }; - int[] northOffsets9x9 = new int[] { -4, 4, -5, 5, 0, 8 }; - - int[] eastOffsets5x5 = new int[] { -4, 0, -3, 3, -2, 2 }; - int[] eastOffsets7x7 = new int[] { -6, 0, -4, 4, -3, 3 }; - int[] eastOffsets9x9 = new int[] { -8, 0, -5, 5, -4, 4 }; - - int[] southOffsets5x5 = new int[] { -2, 2, -3, 3, -4, 0 }; - int[] southOffsets7x7 = new int[] { -3, 3, -4, 4, -6, 0 }; - int[] southOffsets9x9 = new int[] { -4, 4, -5, 5, -8, 0 }; - - int[] westOffsets5x5 = new int[] { 0, 4, -3, 3, -2, 2 }; - int[] westOffsets7x7 = new int[] { 0, 6, -4, 4, -3, 3 }; - int[] westOffsets9x9 = new int[] { 0, 8, -5, 5, -4, 4 }; - - switch (facing) { - case "north": - switch (structureSize) { - case 5: - return applyOffset(startPos, northOffsets5x5); - case 7: - return applyOffset(startPos, northOffsets7x7); - case 9: - return applyOffset(startPos, northOffsets9x9); - } - case "east": - switch (structureSize) { - case 5: - return applyOffset(startPos, eastOffsets5x5); - case 7: - return applyOffset(startPos, eastOffsets7x7); - case 9: - return applyOffset(startPos, eastOffsets9x9); - } - case "south": - switch (structureSize) { - case 5: - return applyOffset(startPos, southOffsets5x5); - case 7: - return applyOffset(startPos, southOffsets7x7); - case 9: - return applyOffset(startPos, southOffsets9x9); - } - case "west": - switch (structureSize) { - case 5: - return applyOffset(startPos, westOffsets5x5); - case 7: - return applyOffset(startPos, westOffsets7x7); - case 9: - return applyOffset(startPos, westOffsets9x9); - } - default: - return new int[] { 0, 0, 0, 0, 0, 0 }; + if (IHeat.HeatLevel.isNotDanger(heat, getMultiblockSize()) && !outputManager.getBlocksPosition(level).isEmpty()) { + if (Math.abs(heat - lastAppliedOutputHeat) >= ReactorOutputManager.RPM_DIVIDER / 2) { + lastAppliedOutputHeat = heat; + } + outputManager.rotateOutputs(getLevel(), getAssembled(), lastAppliedOutputHeat); } } - @Deprecated - private int[] applyOffset(BlockPos pos, int[] offset) { - int x = pos.getX(); - int y = pos.getY(); - int z = pos.getZ(); - - return new int[] { x + offset[0], x + offset[1], y + offset[2], y + offset[3], z + offset[4], z + offset[5] }; + private boolean isEmptyConfiguredPattern() { + return getConfiguredPatternData() == ReactorBluePrintData.EMPTY; } public void addInput(BlockPos inputPos) { @@ -781,6 +512,10 @@ public void addInputFluid(BlockPos outputPos) { public void removeInputFluid(BlockPos outputPos) { this.inputFluidManager.removeBlock(outputPos); this.setChanged(); + // Breaking a fluid input discards its tank contents along with the block entity. + // Re-evaluate the fluid lock so a different fluid can be accepted once no remaining + // input still holds liquid — otherwise the controller stays locked to the old fluid. + clearLockIfAllInputsEmpty(); } public void addAlarm(BlockPos alarmPos) { @@ -794,77 +529,52 @@ public void removeAlarm(BlockPos alarmPos) { } public void removeIOAll() { - this.inputManager.clearInvalid(level); - this.outputManager.clearInvalid(level); - this.inputFluidManager.clearInvalid(level); - this.alarmManager.clearInvalid(level); + allManagers().forEach(m -> m.clearInvalid(level)); this.setChanged(); } /** Try to lock this controller to the given Fluid. Returns true if allowed. */ public boolean tryLockFluid(Fluid fluid) { - // server-persistent approach (preferred): use PersistentFluidLocks when on - // server - if (level != null && !level.isClientSide && level instanceof ServerLevel serverLevel) { + // Fluid locks are server-authoritative and persisted per-level via PersistentFluidLocks. + // On the client there is no lock to enforce, so stay permissive. + if (level instanceof ServerLevel serverLevel) { return PersistentFluidLocks.get(serverLevel).tryLock(getBlockPos(), fluid); } - // fallback to in-memory manager (single-server-run) - return FluidLockManager.tryLock(getBlockPos(), fluid); + return true; } /** Returns whether the given FluidStack is acceptable for this controller. */ public boolean canAcceptFluid(FluidStack stack) { if (stack == null || stack.isEmpty()) return true; - if (level != null && !level.isClientSide && level instanceof ServerLevel serverLevel) { + if (level instanceof ServerLevel serverLevel) { return PersistentFluidLocks.get(serverLevel).canAccept(getBlockPos(), stack.getFluid()); } - return FluidLockManager.canAccept(getBlockPos(), stack); + return true; } /** Force-clear the lock on this controller. */ public void clearLock() { - if (level != null && !level.isClientSide && level instanceof ServerLevel serverLevel) { + if (level instanceof ServerLevel serverLevel) { PersistentFluidLocks.get(serverLevel).clearLock(getBlockPos()); - } else { - FluidLockManager.clearLock(getBlockPos()); + setChanged(); + sendData(); } - setChanged(); - sendData(); } public void clearLockIfAllInputsEmpty() { if (level == null || level.isClientSide) return; - final int SCAN_RADIUS = CNMultiblock.REGISTRATE_MULTIBLOCK.findStructure(level, getBlockPos(), this).data() - .getSize(); // adapte selon la taille max du multiblock - BlockPos center = getBlockPos(); - boolean anyNonEmpty = false; - - for (int dx = -SCAN_RADIUS; dx <= SCAN_RADIUS && !anyNonEmpty; dx++) { - for (int dy = -SCAN_RADIUS; dy <= SCAN_RADIUS && !anyNonEmpty; dy++) { - for (int dz = -SCAN_RADIUS; dz <= SCAN_RADIUS && !anyNonEmpty; dz++) { - BlockPos p = center.offset(dx, dy, dz); - BlockEntity be = level.getBlockEntity(p); - if (!(be instanceof ReactorFluidInputEntity)) - continue; - - IFluidHandler handler = level.getCapability(Capabilities.FluidHandler.BLOCK, p, null); - if (handler == null) - continue; - - for (int t = 0; t < handler.getTanks(); t++) { - if (!handler.getFluidInTank(t).isEmpty()) { - anyNonEmpty = true; - break; - } - } - } - } - } + boolean anyNonEmpty = getInputFluidManager().getFuildHandlers(level).stream() + .anyMatch(handler -> { + for (int t = 0; t < handler.getTanks(); t++) + if (!handler.getFluidInTank(t).isEmpty()) + return true; + return false; + }); if (!anyNonEmpty) clearLock(); } -} \ No newline at end of file +} diff --git a/src/main/java/net/nuclearteam/createnuclear/content/multiblock/controller/ReactorControllerGenerator.java b/src/main/java/net/nuclearteam/createnuclear/content/multiblock/controller/ReactorControllerGenerator.java index 48ac214..618ff0d 100644 --- a/src/main/java/net/nuclearteam/createnuclear/content/multiblock/controller/ReactorControllerGenerator.java +++ b/src/main/java/net/nuclearteam/createnuclear/content/multiblock/controller/ReactorControllerGenerator.java @@ -8,6 +8,7 @@ import net.neoforged.neoforge.client.model.generators.ModelFile; public class ReactorControllerGenerator extends SpecialBlockStateGen { + @Override protected int getXRotation(BlockState state) { return 0; @@ -15,11 +16,52 @@ protected int getXRotation(BlockState state) { @Override protected int getYRotation(BlockState state) { + if (state == null || !state.hasProperty(ReactorControllerBlock.FACING)) { + return 0; + } return horizontalAngle(state.getValue(ReactorControllerBlock.FACING)); } @Override public ModelFile getModel(DataGenContext ctx, RegistrateBlockstateProvider prov, BlockState state) { - return prov.models().getExistingFile(prov.modLoc("block/reactor/controller/block")); + final String controllerPath = "block/reactor/controller/controller_panel_"; + + // Safety fallback if the state is invalid at the start of Minecraft's block scan + if (!state.hasProperty(ReactorControllerBlock.ASSEMBLED) || !state.hasProperty(ReactorControllerBlock.ACTIVE)) { + return prov.models() + .withExistingParent("block/reactor/controller/" + ControllerVisualState.OFF.getSuffix(), prov.modLoc("block/reactor/controller/block")) + .texture("1", prov.modLoc(controllerPath + ControllerVisualState.OFF.getSuffix())) + .texture("particle", prov.modLoc(controllerPath + ControllerVisualState.OFF.getSuffix())); + } + + boolean isAssembled = state.getValue(ReactorControllerBlock.ASSEMBLED); + boolean isActive = state.getValue(ReactorControllerBlock.ACTIVE); + + ControllerVisualState controllerState = ControllerVisualState.OFF; + if (isAssembled) { + controllerState = isActive ? ControllerVisualState.ON : ControllerVisualState.STANDBY; + } + + return prov.models() + .withExistingParent("block/reactor/controller/" + controllerState.getSuffix(), prov.modLoc("block/reactor/controller/block")) + .texture("1", prov.modLoc(controllerPath + controllerState.getSuffix())) + .texture("particle", prov.modLoc(controllerPath + controllerState.getSuffix())); + } + + enum ControllerVisualState { + OFF("off"), + ON("on"), + STANDBY("standby") + ; + + private final String suffix; + + ControllerVisualState(String suffix) { + this.suffix = suffix; + } + + public String getSuffix() { + return this.suffix; + } } -} \ No newline at end of file +} diff --git a/src/main/java/net/nuclearteam/createnuclear/content/multiblock/controller/ReactorDebugDiagnostics.java b/src/main/java/net/nuclearteam/createnuclear/content/multiblock/controller/ReactorDebugDiagnostics.java new file mode 100644 index 0000000..e1e7b74 --- /dev/null +++ b/src/main/java/net/nuclearteam/createnuclear/content/multiblock/controller/ReactorDebugDiagnostics.java @@ -0,0 +1,57 @@ +package net.nuclearteam.createnuclear.content.multiblock.controller; + +import net.minecraft.ChatFormatting; +import net.minecraft.core.BlockPos; +import net.minecraft.network.chat.Component; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.level.Level; +import net.nuclearteam.createnuclear.content.multiblock.controller.manager.*; +import net.nuclearteam.createnuclear.foundation.utility.CreateNuclearLang; + +import java.util.Collection; + +public class ReactorDebugDiagnostics { + private static final String TITLE = "reactor.debug.title"; + private static final String SUMMARY = "reactor.debug.summary"; + private static final String REGISTERED_POSITION = "reactor.debug.registered_position"; + private static final String VALID_POSITION = "reactor.debug.valid_position"; + + private static final String INPUT = "reactor.debug.label.input"; + private static final String INPUT_FLUID = "reactor.debug.label.fluid_input"; + private static final String OUTPUT = "reactor.debug.label.output"; + private static final String ALARM = "reactor.debug.label.alarm"; + + private ReactorDebugDiagnostics() {} + + public static void sendReactorConnectionsTo(Player player, Level level, + ReactorInputManagerI inputManager, ReactorInputFluidManagerI inputFluidManager, + ReactorOutputManagerI outputManager, + ReactorAlarmManagerI alarmManager + ) { + player.sendSystemMessage(CreateNuclearLang.translate(TITLE).style(ChatFormatting.GOLD).component()); + + sendManagerSummary(player, CreateNuclearLang.translate(INPUT).component(), + inputManager.size(), inputManager.getBlocksPosition(), inputManager.getBlocksPosition(level)); + + sendManagerSummary(player, CreateNuclearLang.translate(INPUT_FLUID).component(), + inputFluidManager.size(), inputFluidManager.getBlocksPosition(), inputFluidManager.getBlocksPosition(level)); + + sendManagerSummary(player, CreateNuclearLang.translate(OUTPUT).component(), + outputManager.size(), outputManager.getBlocksPosition(), outputManager.getBlocksPosition(level)); + + sendManagerSummary(player, CreateNuclearLang.translate(ALARM).component(), + alarmManager.size(), alarmManager.getBlocksPosition(), alarmManager.getBlocksPosition(level)); + } + + private static void sendManagerSummary(Player player, Component label, int registeredCount, + Collection registered, Collection valid) { + player.sendSystemMessage(CreateNuclearLang.translate(SUMMARY, label, registeredCount, valid.size()).style(ChatFormatting.YELLOW).component()); + + for (BlockPos pos : registered) { + player.sendSystemMessage(CreateNuclearLang.translate(REGISTERED_POSITION, pos.toShortString()).component()); + } + for (BlockPos pos : valid) { + player.sendSystemMessage(CreateNuclearLang.translate(VALID_POSITION, pos.toShortString()).component()); + } + } +} diff --git a/src/main/java/net/nuclearteam/createnuclear/content/multiblock/controller/ReactorRunningSoundInstance.java b/src/main/java/net/nuclearteam/createnuclear/content/multiblock/controller/ReactorRunningSoundInstance.java new file mode 100644 index 0000000..52f39cb --- /dev/null +++ b/src/main/java/net/nuclearteam/createnuclear/content/multiblock/controller/ReactorRunningSoundInstance.java @@ -0,0 +1,61 @@ +package net.nuclearteam.createnuclear.content.multiblock.controller; + +import net.minecraft.client.resources.sounds.AbstractTickableSoundInstance; +import net.minecraft.core.BlockPos; +import net.minecraft.sounds.SoundEvent; +import net.minecraft.sounds.SoundSource; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.state.BlockState; +import net.nuclearteam.createnuclear.CreateNuclear; + +/** + * Client-side looping sound played while a reactor is actively producing energy. + *

+ * Mirrors {@code ReactorAlarmSoundInstance}: the instance stops itself once the block at + * {@code pos} is no longer a running reactor controller (the {@code ACTIVE} blockstate property + * is synced from the server), so it survives the controller block being broken or the reactor + * shutting down even if the owning block entity's tick stops driving it. + */ +public class ReactorRunningSoundInstance extends AbstractTickableSoundInstance { + private final BlockPos pos; + private final Level level; + + public ReactorRunningSoundInstance(Level level, BlockPos pos, SoundEvent sound) { + super(sound, SoundSource.BLOCKS, level != null ? level.random : null); + this.level = level; + this.pos = pos; + if (pos != null) { + this.x = pos.getX(); + this.y = pos.getY(); + this.z = pos.getZ(); + } + this.looping = true; + this.delay = 0; + this.volume = 1.0F; + } + + @Override + public void tick() { + try { + if (level == null || pos == null) { + stop(); + return; + } + + BlockState state = level.getBlockState(pos); + if (!(state.getBlock() instanceof ReactorControllerBlock) + || !state.hasProperty(ReactorControllerBlock.ACTIVE) + || !state.getValue(ReactorControllerBlock.ACTIVE)) { + stop(); + } + } catch (Exception e) { + stop(); + CreateNuclear.LOGGER.warn(e.getMessage()); + } + } + + /** Public entry point so the owning block entity can stop the loop ({@link #stop()} is protected). */ + public void stopSound() { + stop(); + } +} diff --git a/src/main/java/net/nuclearteam/createnuclear/content/multiblock/controller/consumable/ConsumableTimer.java b/src/main/java/net/nuclearteam/createnuclear/content/multiblock/controller/consumable/ConsumableTimer.java new file mode 100644 index 0000000..f668341 --- /dev/null +++ b/src/main/java/net/nuclearteam/createnuclear/content/multiblock/controller/consumable/ConsumableTimer.java @@ -0,0 +1,62 @@ +package net.nuclearteam.createnuclear.content.multiblock.controller.consumable; + +import net.minecraft.nbt.CompoundTag; + +public class ConsumableTimer { + private final IConsumable consumable; + private int remainingTicks; + private int maxTicks; + private int countInPattern; + + public ConsumableTimer(IConsumable consumable, int maxTicks, int count) { + this.consumable = consumable; + this.maxTicks = maxTicks; + this.remainingTicks = maxTicks; + this.countInPattern = count; + } + + public boolean tick() { + if (remainingTicks > 0) + remainingTicks--; + return remainingTicks <= 0; + } + + public void reset(int newMaxTicks, int newCount) { + this.maxTicks = newMaxTicks; + this.remainingTicks = newMaxTicks; + this.countInPattern = newCount; + } + + public IConsumable getConsumable() { + return consumable; + } + + public String getId() { + return consumable.getId(); + } + + public int getCountInPattern() { + return countInPattern; + } + + public int getMaxTicks() { + return maxTicks; + } + + public CompoundTag serializeNBT() { + CompoundTag tag = new CompoundTag(); + tag.put("consumable", consumable.serializeNBT()); + tag.putInt("remainingTicks", remainingTicks); + tag.putInt("maxTicks", maxTicks); + tag.putInt("countInPattern", countInPattern); + return tag; + } + + public static ConsumableTimer deserializeNBT(CompoundTag tag) { + IConsumable consumable = IConsumable.deserializeNBT(tag.getCompound("consumable")); + ConsumableTimer t = new ConsumableTimer( + consumable, tag.getInt("maxTicks"), tag.getInt("countInPattern")); + t.remainingTicks = tag.getInt("remainingTicks"); + return t; + } +} diff --git a/src/main/java/net/nuclearteam/createnuclear/content/multiblock/controller/consumable/ConsumptionCycleManager.java b/src/main/java/net/nuclearteam/createnuclear/content/multiblock/controller/consumable/ConsumptionCycleManager.java new file mode 100644 index 0000000..bfe124a --- /dev/null +++ b/src/main/java/net/nuclearteam/createnuclear/content/multiblock/controller/consumable/ConsumptionCycleManager.java @@ -0,0 +1,163 @@ +package net.nuclearteam.createnuclear.content.multiblock.controller.consumable; + +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.ListTag; +import net.minecraft.nbt.Tag; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.Level; +import net.nuclearteam.createnuclear.CNDataComponents; +import net.nuclearteam.createnuclear.api.multiblock.rods.RodType; +import net.nuclearteam.createnuclear.content.multiblock.bluePrintItem.ReactorBluePrintData; +import net.nuclearteam.createnuclear.content.multiblock.controller.manager.ReactorInputManagerI; + +import java.util.*; + +public class ConsumptionCycleManager { + + private final List timers = new ArrayList<>(); + private Map patternSnapshot = new HashMap<>(); + + public void startCycle(ItemStack pattern, Level level) { + timers.clear(); + buildTimers(pattern, level); + takeSnapshot(pattern, level); + } + + public void resetCycle(ItemStack pattern, Level level, ReactorInputManagerI manager) { + Map counts = PatternReader.readItemCounts(pattern, level); + List updated = new ArrayList<>(); + + for (Map.Entry entry : counts.entrySet()) { + RodType rodType = RodType.resolveRodType(entry.getKey(), level); + if (!rodType.isNotEmptyItem()) + continue; + + String id = BuiltInRegistries.ITEM.getKey(entry.getKey()).getPath(); + int count = entry.getValue(); + int ticks = new ItemConsumable(id, rodType).computeTimer(count); + + timers.stream() + .filter(t -> t.getId().equals(id)) + .findFirst() + .ifPresentOrElse( + existing -> { + existing.reset(ticks, count); + updated.add(existing); + }, + () -> updated.add(new ConsumableTimer( + new ItemConsumable(id, rodType), ticks, count))); + } + + timers.clear(); + timers.addAll(updated); + takeSnapshot(pattern, level); + } + + public boolean tick(ReactorInputManagerI manager, Level level) { + boolean consumed = false; + Iterator it = timers.iterator(); + + while (it.hasNext()) { + ConsumableTimer timer = it.next(); + if (timer.tick()) { + timer.getConsumable().consume(manager, level); + consumed = true; + + if (timer.getCountInPattern() > 1) { + timer.reset(timer.getMaxTicks(), timer.getCountInPattern()); + } else { + it.remove(); + } + } + } + return consumed; + } + + public boolean hasPatternChanged(ItemStack pattern, Level level) { + Map current = readItemIds(pattern, level); + if (current.size() != patternSnapshot.size()) + return true; + for (Map.Entry e : current.entrySet()) + if (!e.getValue().equals(patternSnapshot.get(e.getKey()))) + return true; + return false; + } + + public boolean isEmpty() { + return timers.isEmpty(); + } + + public void clear() { + timers.clear(); + patternSnapshot.clear(); + } + + public CompoundTag serializeNBT() { + ListTag list = new ListTag(); + timers.forEach(t -> list.add(t.serializeNBT())); + CompoundTag tag = new CompoundTag(); + tag.put("timers", list); + CompoundTag snap = new CompoundTag(); + patternSnapshot.forEach(snap::putInt); + tag.put("snapshot", snap); + return tag; + } + + public void deserializeNBT(CompoundTag tag) { + timers.clear(); + tag.getList("timers", Tag.TAG_COMPOUND) + .forEach(t -> timers.add(ConsumableTimer.deserializeNBT((CompoundTag) t))); + CompoundTag snap = tag.getCompound("snapshot"); + snap.getAllKeys().forEach(k -> patternSnapshot.put(k, snap.getInt(k))); + } + + private void buildTimers(ItemStack pattern, Level level) { + PatternReader.readItemCounts(pattern, level).forEach((item, count) -> { + RodType rodType = RodType.resolveRodType(item, level); + if (!rodType.isNotEmptyItem()) + return; + String id = BuiltInRegistries.ITEM.getKey(item).getPath(); + int ticks = new ItemConsumable(id, rodType).computeTimer(count); + timers.add(new ConsumableTimer(new ItemConsumable(id, rodType), ticks, count)); + }); + } + + private void takeSnapshot(ItemStack pattern, Level level) { + patternSnapshot = readItemIds(pattern, level); + } + + private Map readItemIds(ItemStack pattern, Level level) { + Map result = new HashMap<>(); + PatternReader.readItemCounts(pattern, level).forEach((item, count) -> { + RodType rt = RodType.resolveRodType(item, level); + if (rt.isNotEmptyItem()) + result.put(BuiltInRegistries.ITEM.getKey(item).getPath(), count); + }); + return result; + } + + /** + * Advances the consumption cycle by one tick: starts a cycle if none is + * running and the pattern isn't empty, resets it if the pattern changed + * (only checked when {@code checkPatternChange} is true), then ticks the + * current cycle. + * + * @param checkPatternChange if {@code false}, the pattern change isn't + * checked this tick (used to throttle the check's frequency) + */ + public void update(ItemStack pattern, Level level, ReactorInputManagerI inputManager, boolean checkPatternChange) { + boolean emptyPattern = pattern.getOrDefault(CNDataComponents.REACTOR_BLUE_PRINT_DATA, ReactorBluePrintData.EMPTY) == ReactorBluePrintData.EMPTY; + if (isEmpty() && !emptyPattern) { + startCycle(pattern, level); + } + + if (!isEmpty()) { + if (checkPatternChange && hasPatternChanged(pattern, level)) { + resetCycle(pattern, level, inputManager); + } + tick(inputManager, level); + } + } +} diff --git a/src/main/java/net/nuclearteam/createnuclear/content/multiblock/controller/consumable/FluidConsumable.java b/src/main/java/net/nuclearteam/createnuclear/content/multiblock/controller/consumable/FluidConsumable.java new file mode 100644 index 0000000..be34dc9 --- /dev/null +++ b/src/main/java/net/nuclearteam/createnuclear/content/multiblock/controller/consumable/FluidConsumable.java @@ -0,0 +1,49 @@ +package net.nuclearteam.createnuclear.content.multiblock.controller.consumable; + +import net.minecraft.nbt.CompoundTag; +import net.minecraft.world.level.Level; +import net.nuclearteam.createnuclear.content.multiblock.controller.manager.ReactorInputManagerI; + +public class FluidConsumable implements IConsumable { + private final String fluidName; + private final int mbPerCycle; + private final int baseTicks; + + public FluidConsumable(String fluidName, int mbPerCycle, int baseTicks) { + this.fluidName = fluidName; + this.mbPerCycle = mbPerCycle; + this.baseTicks = baseTicks; + } + + @Override + public String getId() { + return fluidName; + } + + @Override + public int computeTimer(int count) { + return Math.max(1, baseTicks); + } + + @Override + public boolean consume(ReactorInputManagerI manager, Level level) { + return false; + } + + @Override + public CompoundTag serializeNBT() { + CompoundTag tag = new CompoundTag(); + tag.putString("type", "fluid"); + tag.putString("fluidName", fluidName); + tag.putInt("mbPerCycle", mbPerCycle); + tag.putInt("baseTicks", baseTicks); + return tag; + } + + public static FluidConsumable deserializeNBT(CompoundTag tag) { + return new FluidConsumable( + tag.getString("fluidName"), + tag.getInt("mbPerCycle"), + tag.getInt("baseTicks")); + } +} diff --git a/src/main/java/net/nuclearteam/createnuclear/content/multiblock/controller/consumable/IConsumable.java b/src/main/java/net/nuclearteam/createnuclear/content/multiblock/controller/consumable/IConsumable.java new file mode 100644 index 0000000..21d0ee5 --- /dev/null +++ b/src/main/java/net/nuclearteam/createnuclear/content/multiblock/controller/consumable/IConsumable.java @@ -0,0 +1,24 @@ +package net.nuclearteam.createnuclear.content.multiblock.controller.consumable; + +import net.minecraft.nbt.CompoundTag; +import net.minecraft.world.level.Level; +import net.nuclearteam.createnuclear.content.multiblock.controller.manager.ReactorInputManagerI; + +public interface IConsumable { + String getId(); + + int computeTimer(int countInPattern); + + boolean consume(ReactorInputManagerI manager, Level level); + + CompoundTag serializeNBT(); + + static IConsumable deserializeNBT(CompoundTag tag) { + return switch (tag.getString("type")) { + case "item" -> ItemConsumable.deserializeNBT(tag); + case "fluid" -> FluidConsumable.deserializeNBT(tag); + default -> throw new IllegalArgumentException( + "Unknown consumable type: " + tag.getString("type")); + }; + } +} diff --git a/src/main/java/net/nuclearteam/createnuclear/content/multiblock/controller/consumable/ItemConsumable.java b/src/main/java/net/nuclearteam/createnuclear/content/multiblock/controller/consumable/ItemConsumable.java new file mode 100644 index 0000000..556b554 --- /dev/null +++ b/src/main/java/net/nuclearteam/createnuclear/content/multiblock/controller/consumable/ItemConsumable.java @@ -0,0 +1,45 @@ +package net.nuclearteam.createnuclear.content.multiblock.controller.consumable; + +import net.minecraft.nbt.CompoundTag; +import net.minecraft.world.level.Level; +import net.nuclearteam.createnuclear.api.multiblock.rods.RodType; +import net.nuclearteam.createnuclear.content.multiblock.controller.manager.ReactorInputManagerI; + +public class ItemConsumable implements IConsumable { + private final String itemName; + private final RodType rodType; + + public ItemConsumable(String itemName, RodType rodType) { + this.itemName = itemName; + this.rodType = rodType; + } + + @Override + public String getId() { + return itemName; + } + + @Override + public int computeTimer(int count) { + int base = Math.max(1, rodType.rodTimer().get()); + return Math.max(1, (int) (base / Math.max(1, count))); + } + + @Override + public boolean consume(ReactorInputManagerI manager, Level level) { + return manager.extractItemByName(level, itemName); + } + + @Override + public CompoundTag serializeNBT() { + CompoundTag tag = new CompoundTag(); + tag.putString("type", "item"); + tag.putString("itemName", itemName); + return tag; + } + + public static ItemConsumable deserializeNBT(CompoundTag tag) { + String itemName = tag.getString("itemName"); + return new ItemConsumable(itemName, null); + } +} diff --git a/src/main/java/net/nuclearteam/createnuclear/content/multiblock/controller/consumable/PatternReader.java b/src/main/java/net/nuclearteam/createnuclear/content/multiblock/controller/consumable/PatternReader.java new file mode 100644 index 0000000..eae30dc --- /dev/null +++ b/src/main/java/net/nuclearteam/createnuclear/content/multiblock/controller/consumable/PatternReader.java @@ -0,0 +1,57 @@ +package net.nuclearteam.createnuclear.content.multiblock.controller.consumable; + +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.level.Level; +import net.nuclearteam.createnuclear.CNDataComponents; +import net.nuclearteam.createnuclear.content.multiblock.bluePrintItem.PatternData; +import net.nuclearteam.createnuclear.content.multiblock.bluePrintItem.ReactorBluePrintData; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +/** + * Reads the rods a configured blueprint asks for, as a count per item. + *

+ * The Forge implementation parsed the raw {@code patternAll}/{@code pattern} NBT + * elements off the blueprint stack. On NeoForge the blueprint carries a typed + * {@link ReactorBluePrintData} data component instead, so the parsing is replaced + * by a read of {@link ReactorBluePrintData#patternAll(Level)}. + *

+ * {@code patternAll} is the normalized view: every empty slot and every non-rod + * item becomes a glass pane, so consumers can assume it only ever holds real rods, + * which is why glass panes are the only value skipped here. Unlike the earlier NBT + * port, it is not stored. {@link ReactorBluePrintData} only persists {@code pattern}, + * the raw grid, and derives {@code patternAll} from it on every call, so the two + * views can no longer diverge on disk. + */ +public final class PatternReader { + + private PatternReader() { + } + + public static Map readItemCounts(ItemStack configuredPattern, Level level) { + if (configuredPattern == null || configuredPattern.isEmpty()) + return Collections.emptyMap(); + + ReactorBluePrintData data = configuredPattern.get(CNDataComponents.REACTOR_BLUE_PRINT_DATA); + if (data == null) + return Collections.emptyMap(); + + PatternData[] slots = data.patternAll(level); + if (slots.length == 0) + return Collections.emptyMap(); + + Map counts = new HashMap<>(); + for (PatternData slot : slots) { + if (slot == null) + continue; + ItemStack stack = slot.stack(); + if (stack != null && !stack.isEmpty() && !stack.is(Items.GLASS_PANE)) + counts.merge(stack.getItem(), 1, Integer::sum); + } + return counts; + } +} diff --git a/src/main/java/net/nuclearteam/createnuclear/content/multiblock/controller/display/ReactorDisplayState.java b/src/main/java/net/nuclearteam/createnuclear/content/multiblock/controller/display/ReactorDisplayState.java new file mode 100644 index 0000000..ae81082 --- /dev/null +++ b/src/main/java/net/nuclearteam/createnuclear/content/multiblock/controller/display/ReactorDisplayState.java @@ -0,0 +1,141 @@ +package net.nuclearteam.createnuclear.content.multiblock.controller.display; + +import net.minecraft.core.HolderLookup; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.ListTag; +import net.minecraft.nbt.Tag; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.Item; +import net.nuclearteam.createnuclear.content.logistics.BigFluidStack; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; + +/** + * Immutable snapshot of the data the reactor controller exposes for client-side + * display (e.g. the Goggles tooltip rendered by {@code ReactorGoggleTooltipRenderer}). + *

+ * Instances are produced server-side once per tick from the live input inventories + * and tanks, then synced to the client through the block entity's {@code clientPacket} + * NBT via {@link #serializeNBT(HolderLookup.Provider)} / + * {@link #deserializeNBT(HolderLookup.Provider, CompoundTag)}. This data is transient + * sync data only: it is not part of the persisted save NBT, so its layout can evolve + * freely without migration concerns. + *

+ * Usage: + *

{@code
+ * // Server side, e.g. in tick():
+ * this.displayState = new ReactorDisplayState(items, fluids, maxFluidCapacity);
+ *
+ * // Writing the client packet:
+ * compound.put("displayState", displayState.serializeNBT(registries));
+ *
+ * // Reading the client packet:
+ * displayState = compound.contains("displayState")
+ *         ? ReactorDisplayState.deserializeNBT(registries, compound.getCompound("displayState"))
+ *         : ReactorDisplayState.EMPTY;
+ * }
+ * + * @param items item rods currently loaded, mapped to their stacked count + * @param fluids fluids currently loaded in the input tanks + * @param maxFluidCapacity combined capacity (in millibuckets) of all input fluid tanks + */ +public record ReactorDisplayState(Map items, List fluids, long maxFluidCapacity) { + static final String COMPONENT_ITEM = "Item"; + static final String COMPONENT_COUNT = "Count"; + static final String COMPONENT_CLIENT_DISPLAY_ITEMS = "clientDisplayItems"; + static final String COMPONENT_CLIENT_DISPLAY_FLUIDS = "clientDisplayFluids"; + static final String COMPONENT_CLIENT_MAX_FLUIDS_CAPACITY = "clientMaxFluidCapacity"; + + /** + * Shared empty instance, used before the first tick has populated any display + * data and as the fallback when the client packet doesn't carry a display state. + */ + public static final ReactorDisplayState EMPTY = new ReactorDisplayState(Map.of(), List.of(), 0); + + /** + * Defensively copies the given collections so the record stays immutable + * regardless of how the caller's collections are mutated afterwards. + */ + public ReactorDisplayState { + items = Map.copyOf(items); + fluids = List.copyOf(fluids); + } + + /** + * Serializes this state into NBT for inclusion in the block entity's + * {@code clientPacket} data. Pair with + * {@link #deserializeNBT(HolderLookup.Provider, CompoundTag)} to restore an + * equivalent instance on the client. + * + * @param registries registry lookup used to write the contained fluid stacks + * @return a new {@link CompoundTag} containing the serialized item map, + * fluid list and max fluid capacity + */ + public CompoundTag serializeNBT(HolderLookup.Provider registries) { + CompoundTag compound = new CompoundTag(); + + ListTag displayItemsTag = new ListTag(); + for (Entry entry : items().entrySet()) { + CompoundTag tag = new CompoundTag(); + tag.putString(COMPONENT_ITEM, BuiltInRegistries.ITEM.getKey(entry.getKey()).toString()); + tag.putInt(COMPONENT_COUNT, entry.getValue()); + displayItemsTag.add(tag); + } + compound.put(COMPONENT_CLIENT_DISPLAY_ITEMS, displayItemsTag); + + ListTag displayFluidsTag = new ListTag(); + for (BigFluidStack stack : fluids()) { + displayFluidsTag.add(stack.write(registries)); + } + compound.put(COMPONENT_CLIENT_DISPLAY_FLUIDS, displayFluidsTag); + + compound.putLong(COMPONENT_CLIENT_MAX_FLUIDS_CAPACITY, maxFluidCapacity()); + + return compound; + } + + /** + * Reconstructs a {@link ReactorDisplayState} from NBT previously produced by + * {@link #serializeNBT(HolderLookup.Provider)}. Acts as a static factory since + * records cannot be mutated in place. + *

+ * Missing keys are treated as empty/zero, so a compound that contains none of + * the expected entries yields an instance equivalent to {@link #EMPTY}. + * + * @param registries registry lookup used to read the contained fluid stacks + * @param compound the {@code displayState} compound from the client packet + * @return a new immutable {@link ReactorDisplayState} built from the given NBT + */ + public static ReactorDisplayState deserializeNBT(HolderLookup.Provider registries, CompoundTag compound) { + Map items = new HashMap<>(); + if (compound.contains(COMPONENT_CLIENT_DISPLAY_ITEMS)) { + ListTag list = compound.getList(COMPONENT_CLIENT_DISPLAY_ITEMS, Tag.TAG_COMPOUND); + for (int i = 0; i < list.size(); i++) { + CompoundTag tag = list.getCompound(i); + ResourceLocation id = ResourceLocation.tryParse(tag.getString(COMPONENT_ITEM)); + if (id == null) continue; + Item item = BuiltInRegistries.ITEM.getOptional(id).orElse(null); + if (item != null) { + items.put(item, tag.getInt(COMPONENT_COUNT)); + } + } + } + + List fluids = new ArrayList<>(); + if (compound.contains(COMPONENT_CLIENT_DISPLAY_FLUIDS)) { + ListTag list = compound.getList(COMPONENT_CLIENT_DISPLAY_FLUIDS, Tag.TAG_COMPOUND); + for (int i = 0; i < list.size(); i++) { + fluids.add(BigFluidStack.read(registries, list.getCompound(i))); + } + } + + long maxFluidCapacity = compound.getLong(COMPONENT_CLIENT_MAX_FLUIDS_CAPACITY); + + return new ReactorDisplayState(items, fluids, maxFluidCapacity); + } +} diff --git a/src/main/java/net/nuclearteam/createnuclear/content/multiblock/controller/display/ReactorGoggleTooltipRenderer.java b/src/main/java/net/nuclearteam/createnuclear/content/multiblock/controller/display/ReactorGoggleTooltipRenderer.java new file mode 100644 index 0000000..5b35b06 --- /dev/null +++ b/src/main/java/net/nuclearteam/createnuclear/content/multiblock/controller/display/ReactorGoggleTooltipRenderer.java @@ -0,0 +1,139 @@ +package net.nuclearteam.createnuclear.content.multiblock.controller.display; + +import com.simibubi.create.foundation.item.TooltipHelper; +import com.simibubi.create.foundation.utility.CreateLang; +import net.minecraft.ChatFormatting; +import net.minecraft.network.chat.Component; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.nuclearteam.createnuclear.content.logistics.BigFluidStack; +import net.nuclearteam.createnuclear.content.multiblock.IHeat; +import net.nuclearteam.createnuclear.foundation.utility.CreateNuclearLang; + +import java.util.List; +import java.util.Map; + +/** + * Builds the Goggles tooltip shown when looking at an assembled reactor controller. + *

+ * This is a stateless presentation helper: it only reads from a + * {@link ReactorDisplayState} snapshot (plus the current heat level) and appends the + * resulting lines to the tooltip. It performs no NBT access, inventory lookups or + * other block-entity-specific logic, keeping the rendering separate from the block + * entity's data/state management. + *

+ * Typical usage from {@code ReactorControllerBlockEntity#addToGoggleTooltip}: + *

{@code
+ * @Override
+ * public boolean addToGoggleTooltip(List tooltip, boolean isPlayerSneaking) {
+ *     ReactorGoggleTooltipRenderer.render(tooltip, displayState, heat, isPlayerSneaking, reactorSize);
+ *     return true;
+ * }
+ * }
+ */ +public final class ReactorGoggleTooltipRenderer { + private ReactorGoggleTooltipRenderer() {} + + /** + * Appends the reactor controller's Goggles tooltip lines to {@code tooltip}. + *

+ * Renders, in order: the gauge info header, the reactor heat level, the loaded + * item rods (or a placeholder if none), and the loaded fluids with their fill + * ratio as a progress bar (or a placeholder if none). + * + * @param tooltip the tooltip line list to append to + * @param state the current display snapshot (items, fluids, max fluid capacity) + * @param heat the reactor's current heat value, used to render the heat tooltip line + * @param isPlayerSneaking whether the viewing player is sneaking (currently unused, + * kept for signature parity with other goggle tooltip providers) + * @param reactorSize the reactor's multiblock size, used to resolve the heat thresholds + */ + public static void render(List tooltip, ReactorDisplayState state, int heat, boolean isPlayerSneaking, int reactorSize) { + renderHeaderAndHeat(tooltip, heat, reactorSize); + renderItemRods(tooltip, state); + renderFluidTanks(tooltip, state); + } + + /** + * Appends the gauge info header and the reactor's current heat level line. + * + * @param tooltip the tooltip line list to append to + * @param heat the reactor's current heat value + */ + private static void renderHeaderAndHeat(List tooltip, int heat, int reactorSize) { + CreateLang.translate("gui.gauge.info_header") + .style(ChatFormatting.GRAY) + .forGoggles(tooltip); + IHeat.HeatLevel.getName("reactor_controller").style(ChatFormatting.GRAY).forGoggles(tooltip); + + IHeat.HeatLevel.getFormattedHeatText(heat, reactorSize).forGoggles(tooltip); + } + + /** + * Appends one line per loaded item rod with its stacked count, or a placeholder + * line showing a count of 0 if no rods are loaded. + * + * @param tooltip the tooltip line list to append to + * @param state the current display snapshot + */ + private static void renderItemRods(List tooltip, ReactorDisplayState state) { + if (state.items().isEmpty()) { + CreateNuclearLang.builder() + .add(Component.translatable("createnuclear.tooltip.item.empty.rod").withStyle(ChatFormatting.GRAY)) + .add(CreateNuclearLang.number(0).style(ChatFormatting.GOLD)) + .forGoggles(tooltip); + } else { + for (Map.Entry entry : state.items().entrySet()) { + ItemStack stack = new ItemStack(entry.getKey()); + int count = entry.getValue(); + CreateNuclearLang.itemName(stack) + .style(ChatFormatting.GRAY) + .text(": ") + .add(CreateNuclearLang.number(count).style(ChatFormatting.GOLD)) + .forGoggles(tooltip); + } + } + } + + /** + * Appends one line per loaded fluid with a fill-ratio progress bar relative to + * {@link ReactorDisplayState#maxFluidCapacity()}, or a "no fluid" placeholder + * with an empty progress bar if no fluids are loaded. + * + * @param tooltip the tooltip line list to append to + * @param state the current display snapshot + */ + private static void renderFluidTanks(List tooltip, ReactorDisplayState state) { + if (state.fluids().isEmpty()) { + CreateNuclearLang.translate("tooltip.fluid.none") + .style(ChatFormatting.GRAY) + .forGoggles(tooltip); + + CreateNuclearLang.builder() + .text(TooltipHelper.makeProgressBar(5, 0)) + .style(ChatFormatting.BLUE) + .space() + .text("0%") + .forGoggles(tooltip); + } else { + long maxCapacity = state.maxFluidCapacity(); + for (BigFluidStack stack : state.fluids()) { + float fillRatio = maxCapacity > 0 ? (float) stack.amount / maxCapacity : 0; + if (fillRatio >= 0.99f) + fillRatio = 1.0f; + int filledBars = Math.round(fillRatio * 5); + + CreateNuclearLang.translate("tooltip.fluid", stack.stack.getHoverName()) + .style(ChatFormatting.GRAY) + .forGoggles(tooltip); + + CreateNuclearLang.builder() + .text(TooltipHelper.makeProgressBar(5, filledBars)) + .style(ChatFormatting.BLUE) + .space() + .text(Math.round(fillRatio * 100) + "%") + .forGoggles(tooltip); + } + } + } +} diff --git a/src/main/java/net/nuclearteam/createnuclear/content/multiblock/controller/manager/ReactorFrameDisplayManager.java b/src/main/java/net/nuclearteam/createnuclear/content/multiblock/controller/manager/ReactorFrameDisplayManager.java new file mode 100644 index 0000000..1ec4369 --- /dev/null +++ b/src/main/java/net/nuclearteam/createnuclear/content/multiblock/controller/manager/ReactorFrameDisplayManager.java @@ -0,0 +1,135 @@ +package net.nuclearteam.createnuclear.content.multiblock.controller.manager; + +import net.minecraft.nbt.CompoundTag; +import net.minecraft.world.level.Level; +import net.neoforged.neoforge.fluids.FluidStack; +import net.neoforged.neoforge.fluids.capability.IFluidHandler; +import org.jetbrains.annotations.Nullable; + +public class ReactorFrameDisplayManager implements ReactorFrameDisplayManagerI { + public static final String COMPONENT_FRAME_COLUMN_MIN_Y = "frameColumnMinY"; + public static final String COMPONENT_FRAME_COLUMN_MAX_Y = "frameColumnMaxY"; + + // Vertical span (block Y) of the reactor frame columns, used to map the + // fluid fill ratio onto the liquid actually drawn in the frame windows. + private int frameColumnMinY = Integer.MAX_VALUE; + private int frameColumnMaxY = Integer.MIN_VALUE; + + // Per-tick cache for the fluid drawn in the frame windows. Recomputed from the + // live (synced) input tanks so the displayed fluid and its fill ratio always + // come from the same source and stay consistent for every fluid type. + private long frameFluidCacheTick = -1; + private float frameFluidFillRatioCache = 0f; + + private FluidStack frameFluidCache = FluidStack.EMPTY; + + /** + * Returns the fluid currently held by the reactor, used by the frame + * renderer to draw the matching liquid in the window. On the client this + * reads the synced ; on the server it reads the + * aggregated . Returns {@link FluidStack#EMPTY} when + * the reactor holds no fluid. + *

+ * Recomputes, at most once per game tick, the fluid shown in the frame + * windows and how full the input is. Both values are read from the same live + * input tanks (whose contents are synced to the client), so they always agree + * regardless of the fluid type. + */ + private void refreshFrameFluidCache(@Nullable Level level, ReactorInputFluidManagerI handlers) { + if (level == null) return; + long now = level.getGameTime(); + + if (now == frameFluidCacheTick) return; + frameFluidCacheTick = now; + + long amount = 0, capacity = 0; + FluidStack fluid = FluidStack.EMPTY; + + for (IFluidHandler handler : handlers.getFuildHandlers(level)) { + int tanks = handler.getTanks(); + for (int t = 0; t < tanks; t++) { + FluidStack stack = handler.getFluidInTank(t); + capacity += handler.getTankCapacity(t); + amount += stack.getAmount(); + + if (fluid.isEmpty() && !stack.isEmpty()) fluid = stack.copy(); + } + } + + frameFluidCache = fluid; + frameFluidFillRatioCache = capacity > 0 ? Math.min(1f, (float) amount / (float) capacity) : 0f; + } + + /** + * The fluid currently shown in the reactor frame windows (may be empty). + */ + @Override + public FluidStack getDisplayedFluid(Level level, ReactorInputFluidManagerI handlers) { + refreshFrameFluidCache(level, handlers); + + return frameFluidCache; + } + + /** + * How full the reactor's fluid input is, in the range {@code [0, 1]}, used by + * the frame renderer to size the visible liquid column. + */ + @Override + public float getDisplayedFluidFillRatio(Level level, ReactorInputFluidManagerI handlers) { + refreshFrameFluidCache(level, handlers); + + return frameFluidFillRatioCache; + } + + /** + * Records the lowest and highest frame block-Y of the assembled reactor. + */ + @Override + public void setFrameColumn(int minY, int maxY, Runnable onChange) { + if (this.frameColumnMinY == minY && this.frameColumnMaxY == maxY) return; + this.frameColumnMinY = minY; + this.frameColumnMaxY = maxY; + onChange.run(); + } + + /** + * @return the lowest frame block-Y, or {@link Integer#MAX_VALUE} if unknown. + */ + @Override + public int getFrameColumnMinY() { + return frameColumnMinY; + } + + /** + * @return the highest frame block-Y, or {@link Integer#MIN_VALUE} if unknown. + */ + @Override + public int getFrameColumnMaxY() { + return frameColumnMaxY; + } + + @Override + public boolean hasFrameColumn() { + return frameColumnMinY != Integer.MAX_VALUE && frameColumnMaxY != Integer.MIN_VALUE + && frameColumnMaxY >= frameColumnMinY; + } + + @Override + public void read(CompoundTag compound) { + if (compound.contains(COMPONENT_FRAME_COLUMN_MIN_Y)) { + frameColumnMinY = compound.getInt(COMPONENT_FRAME_COLUMN_MIN_Y); + } + + if (compound.contains(COMPONENT_FRAME_COLUMN_MAX_Y)) { + frameColumnMaxY = compound.getInt(COMPONENT_FRAME_COLUMN_MAX_Y); + } + } + + @Override + public void write(CompoundTag compound) { + if (hasFrameColumn()) { + compound.putInt(COMPONENT_FRAME_COLUMN_MIN_Y, frameColumnMinY); + compound.putInt(COMPONENT_FRAME_COLUMN_MAX_Y, frameColumnMaxY); + } + } +} diff --git a/src/main/java/net/nuclearteam/createnuclear/content/multiblock/controller/manager/ReactorFrameDisplayManagerI.java b/src/main/java/net/nuclearteam/createnuclear/content/multiblock/controller/manager/ReactorFrameDisplayManagerI.java new file mode 100644 index 0000000..49bfdcf --- /dev/null +++ b/src/main/java/net/nuclearteam/createnuclear/content/multiblock/controller/manager/ReactorFrameDisplayManagerI.java @@ -0,0 +1,47 @@ +package net.nuclearteam.createnuclear.content.multiblock.controller.manager; + +import net.minecraft.nbt.CompoundTag; +import net.minecraft.world.level.Level; +import net.neoforged.neoforge.fluids.FluidStack; + +/** + * Manages the display state of the reactor frame windows: the fluid shown, + * its fill ratio, and the vertical column bounds of the assembled frame. + */ +public interface ReactorFrameDisplayManagerI { + /** + * Returns the fluid currently shown in the reactor frame windows (may be + * empty). Recomputes the underlying cache at most once per game tick from + * the given fluid handlers. + */ + FluidStack getDisplayedFluid(Level level, ReactorInputFluidManagerI handlers); + + /** + * Returns how full the reactor's fluid input is, in the range {@code [0, 1]}. + * Recomputes the underlying cache at most once per game tick from the + * given fluid handlers. + */ + float getDisplayedFluidFillRatio(Level level, ReactorInputFluidManagerI handlers); + + /** + * Records the lowest and highest frame block-Y of the assembled reactor. + * If the values changed, {@code onChange} is invoked (e.g. to trigger a + * client sync via {@code notifyUpdate()}). + */ + void setFrameColumn(int minY, int maxY, Runnable onChange); + + /** @return the lowest frame block-Y, or {@link Integer#MAX_VALUE} if unknown. */ + int getFrameColumnMinY(); + + /** @return the highest frame block-Y, or {@link Integer#MIN_VALUE} if unknown. */ + int getFrameColumnMaxY(); + + /** @return true if both frame column bounds are known and consistent. */ + boolean hasFrameColumn(); + + /** Reads state from an NBT tag (deserialization). */ + void read(CompoundTag compound); + + /** Writes state to an NBT tag (serialization). */ + void write(CompoundTag compound); +} diff --git a/src/main/java/net/nuclearteam/createnuclear/content/multiblock/controller/manager/ReactorInputFluidManager.java b/src/main/java/net/nuclearteam/createnuclear/content/multiblock/controller/manager/ReactorInputFluidManager.java index 9963fa3..5e37e97 100644 --- a/src/main/java/net/nuclearteam/createnuclear/content/multiblock/controller/manager/ReactorInputFluidManager.java +++ b/src/main/java/net/nuclearteam/createnuclear/content/multiblock/controller/manager/ReactorInputFluidManager.java @@ -91,7 +91,6 @@ public List getBlocksPosition(Level level) { List positions = new ArrayList<>(); for (BlockPos p : this.getBlocksPosition()) { - CreateNuclear.LOGGER.warn("getBlocksPosition: {} {}", level.getBlockEntity(p), level.getBlockEntity(p) instanceof ReactorFluidInputEntity); if (level.getBlockEntity(p) instanceof ReactorFluidInputEntity) positions.add(p); } return List.copyOf(positions); @@ -126,36 +125,45 @@ public VirtualReactorInputFluid getInventory(Level level) { if (handlers.isEmpty()) return new VirtualReactorInputFluid(); for (IFluidHandler h : handlers) { - int tank = h.getTanks(); - virtualReactorInputFluid.addFluid(h.getFluidInTank(tank)); + virtualReactorInputFluid.addFluid(h.getFluidInTank(0)); } return virtualReactorInputFluid; } - @Override /** - * Attempt to extract up to {@code fluidNeeded} units of fluid from tracked handlers. - * Returns true if the full amount was extracted. + * Extracts up to {@code fluidNeeded} units of fluid, spread across the tracked handlers. + *

+ * The requested amount is a TOTAL across every input, not a per-input quota: each handler + * only ever drains what is still missing, so a request of 10 units against two inputs + * holding 10 each removes 10 in total, not 20. + * + * @return {@code true} if at least one unit was actually drained. A partial extraction still + * returns {@code true}: the reactor consumes whatever coolant it can reach rather + * than refusing to run. */ + @Override public boolean extractFluids(Level level, int fluidNeeded) { - if (level == null) return false; - boolean isExtracted = false; + if (level == null || fluidNeeded <= 0) return false; List handlers = getFuildHandlers(level); if (handlers.isEmpty()) return false; + int remaining = fluidNeeded; + for (IFluidHandler handler : handlers) { - int tank = handler.getTanks(); - FluidStack stack = handler.getFluidInTank(tank); + if (remaining <= 0) break; + + FluidStack stack = handler.getFluidInTank(0); if (stack.isEmpty()) continue; - int toExtract = Math.min(fluidNeeded, stack.getAmount()); - if (toExtract > 1) { - handler.drain(toExtract, FluidAction.EXECUTE); - isExtracted = true; - } - if (fluidNeeded <= 0) break; + + int toExtract = Math.min(remaining, stack.getAmount()); + if (toExtract <= 0) continue; + + // Subtract what was actually drained, not what was asked for: a handler is free to + // hand back less than requested. + remaining -= handler.drain(toExtract, FluidAction.EXECUTE).getAmount(); } - return isExtracted; + return remaining < fluidNeeded; } } diff --git a/src/main/java/net/nuclearteam/createnuclear/content/multiblock/controller/manager/ReactorInputManager.java b/src/main/java/net/nuclearteam/createnuclear/content/multiblock/controller/manager/ReactorInputManager.java index 653340f..75f52d3 100644 --- a/src/main/java/net/nuclearteam/createnuclear/content/multiblock/controller/manager/ReactorInputManager.java +++ b/src/main/java/net/nuclearteam/createnuclear/content/multiblock/controller/manager/ReactorInputManager.java @@ -83,8 +83,8 @@ public VirtualReactorInputsItem getInventory(Level level) { for (int s = 0; s < slots; s++) { ItemStack st = h.getStackInSlot(s); - if (TypeRodPredicate.IS_FUEL.test(st)) totalFuel += st.getCount(); - else if (TypeRodPredicate.IS_COOLED.test(st)) totalCooler += st.getCount(); + if (TypeRodPredicate.isFuel(st, level)) totalFuel += st.getCount(); + else if (TypeRodPredicate.isCooled(st, level)) totalCooler += st.getCount(); } } @@ -107,11 +107,11 @@ public boolean extractItems(Level level, int fuelNeeded, int coolerNeeded) { if (stack.isEmpty()) continue; - if (fuelRemaining > 0 && TypeRodPredicate.IS_FUEL.test(stack)) { + if (fuelRemaining > 0 && TypeRodPredicate.isFuel(stack, level)) { int toExtract = Math.min(fuelRemaining, stack.getCount()); handler.extractItem(s, toExtract, false); fuelRemaining -= toExtract; - } else if (coolerRemaining > 0 && TypeRodPredicate.IS_COOLED.test(stack)) { + } else if (coolerRemaining > 0 && TypeRodPredicate.isCooled(stack, level)) { int toExtract = Math.min(coolerRemaining, stack.getCount()); handler.extractItem(s, toExtract, false); coolerRemaining -= toExtract; diff --git a/src/main/java/net/nuclearteam/createnuclear/content/multiblock/controller/manager/ReactorOutputManager.java b/src/main/java/net/nuclearteam/createnuclear/content/multiblock/controller/manager/ReactorOutputManager.java index 34e9f03..cb6616a 100644 --- a/src/main/java/net/nuclearteam/createnuclear/content/multiblock/controller/manager/ReactorOutputManager.java +++ b/src/main/java/net/nuclearteam/createnuclear/content/multiblock/controller/manager/ReactorOutputManager.java @@ -5,19 +5,19 @@ import net.minecraft.nbt.ListTag; import net.minecraft.world.level.Level; import net.minecraft.world.level.block.entity.BlockEntity; +import net.nuclearteam.createnuclear.content.multiblock.output.ReactorOutput; import net.nuclearteam.createnuclear.content.multiblock.output.ReactorOutputEntity; import java.util.ArrayList; import java.util.List; -import java.util.function.BiFunction; /** * Manager for a reactor's outputs (`ReactorOutput`). - * Handles serialization of output positions and distribution - * of energy via `distributeSU`. + * Handles serialization of output positions. */ public class ReactorOutputManager extends AbstractReactorIOManager implements ReactorOutputManagerI { private static final String NBT_KEY = "ReactorOutputs"; + public static final int RPM_DIVIDER = 32; @Override public void write(CompoundTag compound) { @@ -42,79 +42,10 @@ public void read(CompoundTag compound) { } /** - * Example usage: - * double totalSUToDistribute = this.reactorPower; // or another value depending on logic - * if (!reactorOutputEntityList.isEmpty() && totalSUToDistribute > 0) { - * double remaining = outputManager.distributeSU(totalSUToDistribute, reactorOutputEntityList, (outEntity, amount) -> { - * // Inserter: apply `amount` to the output and return the amount not accepted. - * // Here we accept everything and apply the corresponding speed (rounded) - * int speed = (int) Math.round(amount); - * try { - * outEntity.setSpeed(speed); - * outEntity.heat = speed; // adjust heat if needed - * outEntity.updateSpeed = true; - * outEntity.updateGeneratedRotation(); - * return 0.0; // all accepted - * } catch (Exception e) { - * return amount; // nothing accepted on error - * } - * }); - * // `remaining` contains the SU that couldn't be inserted — you can store or drop it - * this.reactorPower = (float) remaining; // example: keep the remainder - * } - * @param totalSU total SU to distribute - * @param level world level (may be null) - * @param inserter function that applies an amount to an output and returns not accepted amount - * @return amount of SU not inserted + * {@inheritDoc} + * A tracked position is dropped if its chunk isn't loaded or the block + * entity at that position is no longer a {@link ReactorOutputEntity}. */ - public double distributeSU(double totalSU, Level level, BiFunction inserter) { - if (totalSU <= 0 || inserter == null) return totalSU; - List validOutputs = new ArrayList<>(); - for (BlockPos p : new ArrayList<>(positions)) { - if (level == null || !level.isLoaded(p)) continue; - BlockEntity be = level.getBlockEntity(p); - if (be instanceof ReactorOutputEntity out) validOutputs.add(out); - } - if (validOutputs.isEmpty()) return totalSU; - - double remaining = totalSU; - List active = new ArrayList<>(validOutputs); - final double MIN_ACCEPTED_SU = 1e-6; - - // Distribute evenly and reallocate the remainder among outputs still able to accept - boolean progress = true; - while (remaining > MIN_ACCEPTED_SU && !active.isEmpty() && progress) { - progress = false; - double share = remaining / active.size(); - List stillActive = new ArrayList<>(); - - for (ReactorOutputEntity out : active) { - double toInsert = share; - double notAccepted; - try { - notAccepted = inserter.apply(out, toInsert); - } catch (Exception e) { - notAccepted = toInsert; - } - if (Double.isNaN(notAccepted) || notAccepted < 0) notAccepted = 0; - if (notAccepted > toInsert) notAccepted = toInsert; - double inserted = toInsert - notAccepted; - - if (inserted > MIN_ACCEPTED_SU) { - progress = true; - remaining -= inserted; - // keep this output active; it may accept more in the next round - stillActive.add(out); - } - // if inserted == 0, remove it (do not attempt to send more) - } - - active = stillActive; - } - - return remaining; - } - @Override public void clearInvalid(Level level) { List toRemove = new ArrayList<>(); @@ -129,6 +60,11 @@ public void clearInvalid(Level level) { positions.removeAll(toRemove); } + /** + * {@inheritDoc} + * Filters the tracked positions down to those currently backed by a + * loaded {@link ReactorOutputEntity}. + */ @Override public List getBlocksPosition(Level level) { List positions = new ArrayList<>(); @@ -138,4 +74,41 @@ public List getBlocksPosition(Level level) { } return List.copyOf(positions); } + + /** + * {@inheritDoc} + * The total rotation (in RPM, divided down by {@link #RPM_DIVIDER}) is + * split as evenly as possible across the tracked outputs, with the + * remainder distributed to the first outputs. Each output is set to its + * share of the rotation speed if the reactor is assembled, otherwise + * stopped. + */ + @Override + public void rotateOutputs(Level level, boolean assembled, int rotation) { + if (positions.isEmpty()) return; + + int totalRpm = rotation / RPM_DIVIDER; + int size = positions.size(); + int remainingRotation = totalRpm % size; + + for (int i = 0; i < size; i++) { + int dividedRotation = (totalRpm / size) + (i < remainingRotation ? 1 : 0); + BlockPos pos = positions.get(i); + + if (!(level.getBlockState(pos).getBlock() instanceof ReactorOutput block)) continue; + ReactorOutputEntity entity = block.getBlockEntityType().getBlockEntity(level, pos); + if (entity == null) continue; + + if (dividedRotation > 0) { + entity.speed = assembled ? dividedRotation : 0; + entity.updateSpeed = true; + entity.setSpeedAndUpdate(dividedRotation); + entity.updateGeneratedRotation(); + } else { + entity.setSpeedAndUpdate(0); + entity.updateSpeed = true; + entity.updateGeneratedRotation(); + } + } + } } diff --git a/src/main/java/net/nuclearteam/createnuclear/content/multiblock/controller/manager/ReactorOutputManagerI.java b/src/main/java/net/nuclearteam/createnuclear/content/multiblock/controller/manager/ReactorOutputManagerI.java index 45ef40a..ddc8b4b 100644 --- a/src/main/java/net/nuclearteam/createnuclear/content/multiblock/controller/manager/ReactorOutputManagerI.java +++ b/src/main/java/net/nuclearteam/createnuclear/content/multiblock/controller/manager/ReactorOutputManagerI.java @@ -2,26 +2,19 @@ import net.minecraft.core.BlockPos; import net.minecraft.world.level.Level; -import net.nuclearteam.createnuclear.content.multiblock.output.ReactorOutputEntity; import java.util.List; -import java.util.function.BiFunction; /** * Interface exposing operations specific to reactor outputs. - * The main responsibility is to distribute an amount of energy (SU) - * among valid outputs and use an `inserter` to apply quantities to each - * `ReactorOutputEntity`. */ public interface ReactorOutputManagerI extends ReactorIOManager { - /** - * Distributes `totalSU` among valid outputs in `level`. - * The `inserter` receives the target entity and the proposed amount and - * must return the amount not accepted (>= 0). - * Returns the remaining amount that was not inserted. - */ - double distributeSU(double totalSU, Level level, BiFunction inserter); - /** Returns an immutable copy of tracked positions. */ List getBlocksPosition(Level level); + + /** + * Distributes the total rotation across the tracked outputs and applies + * the corresponding speed/stop to each {@code ReactorOutputEntity}. + */ + void rotateOutputs(Level level, boolean assembled, int rotation); } diff --git a/src/main/java/net/nuclearteam/createnuclear/content/multiblock/controller/service/DefaultHeatService.java b/src/main/java/net/nuclearteam/createnuclear/content/multiblock/controller/service/DefaultHeatService.java new file mode 100644 index 0000000..d066346 --- /dev/null +++ b/src/main/java/net/nuclearteam/createnuclear/content/multiblock/controller/service/DefaultHeatService.java @@ -0,0 +1,26 @@ +package net.nuclearteam.createnuclear.content.multiblock.controller.service; + +import net.minecraft.world.level.Level; +import net.nuclearteam.createnuclear.content.logistics.BigFluidStack; +import net.nuclearteam.createnuclear.content.multiblock.controller.ReactorControllerInventory; +import net.nuclearteam.createnuclear.content.multiblock.controller.display.ReactorDisplayState; +import net.nuclearteam.createnuclear.content.multiblock.reactorLogic.HeatBalance; +import net.nuclearteam.createnuclear.content.multiblock.reactorLogic.HeatManager; + +public class DefaultHeatService implements IHeatService { + private final HeatManager impl; + + public DefaultHeatService(HeatManager impl) { + this.impl = impl; + } + + @Override + public int getLiquidTimer() { + return impl.getLiquidTimer(); + } + + @Override + public double calculateHeat(BigFluidStack bigFluidStack, HeatBalance heatBalance, int previousHeat, ReactorControllerInventory inventory, Level level, ReactorDisplayState displayState) { + return impl.calculateHeat(bigFluidStack, heatBalance, previousHeat, inventory, level, displayState); + } +} diff --git a/src/main/java/net/nuclearteam/createnuclear/content/multiblock/controller/service/DefaultPersistenceService.java b/src/main/java/net/nuclearteam/createnuclear/content/multiblock/controller/service/DefaultPersistenceService.java new file mode 100644 index 0000000..106672a --- /dev/null +++ b/src/main/java/net/nuclearteam/createnuclear/content/multiblock/controller/service/DefaultPersistenceService.java @@ -0,0 +1,51 @@ +package net.nuclearteam.createnuclear.content.multiblock.controller.service; + +import net.minecraft.core.Direction; +import net.minecraft.core.HolderLookup; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.NbtOps; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.levelgen.structure.BoundingBox; +import net.nuclearteam.createnuclear.content.multiblock.controller.ReactorControllerBlockEntity; +import net.nuclearteam.createnuclear.content.multiblock.controller.display.ReactorDisplayState; + +public class DefaultPersistenceService implements IPersistenceService { + @Override + public void readBasicState(ReactorControllerBlockEntity owner, CompoundTag compound, HolderLookup.Provider registries, boolean clientPacket) { + owner.setMultiblockSize(compound.getInt("reactorSize")); + owner.setMultiblockFacing(Direction.byName(compound.getString("reactorFacing"))); + + owner.setMultiblockStructure(compound.contains("reactorPose") + ? BoundingBox.CODEC.parse(NbtOps.INSTANCE, compound.get("reactorPose")).result().orElse(null) + : null + ); + + if (!clientPacket) { + owner.deserializeInventory(registries, compound.getCompound("pattern")); + } else { + owner.setDisplayState(compound.contains("displayState") + ? ReactorDisplayState.deserializeNBT(registries, compound.getCompound("displayState")) + : ReactorDisplayState.EMPTY + ); + } + owner.setConfiguredPattern(ItemStack.parse(registries, compound.getCompound("items")).orElse(ItemStack.EMPTY)); + + } + + @Override + public void writeBasicState(ReactorControllerBlockEntity owner, CompoundTag compound, HolderLookup.Provider registries, boolean clientPacket) { + compound.putInt("reactorSize", owner.getMultiblockSize()); + compound.putString("reactorFacing", owner.getMultiblockFacing() != null ? owner.getMultiblockFacing().getSerializedName() : ""); + if (owner.getMultiblockPos() != null) { + compound.put("reactorPose", BoundingBox.CODEC.encodeStart(NbtOps.INSTANCE, owner.getMultiblockPos()).getOrThrow()); + } + + if (!clientPacket) { + compound.put("pattern", owner.serializeInventory(registries)); + } else { + compound.put("displayState", owner.getDisplayState().serializeNBT(registries)); + } + compound.put("items", owner.getConfiguredPattern().saveOptional(registries)); + + } +} diff --git a/src/main/java/net/nuclearteam/createnuclear/content/multiblock/controller/service/FluidConsumptionRateCalculator.java b/src/main/java/net/nuclearteam/createnuclear/content/multiblock/controller/service/FluidConsumptionRateCalculator.java new file mode 100644 index 0000000..57c48cf --- /dev/null +++ b/src/main/java/net/nuclearteam/createnuclear/content/multiblock/controller/service/FluidConsumptionRateCalculator.java @@ -0,0 +1,52 @@ +package net.nuclearteam.createnuclear.content.multiblock.controller.service; + +import net.minecraft.world.level.Level; +import net.nuclearteam.createnuclear.content.logistics.BigFluidStack; +import net.nuclearteam.createnuclear.content.multiblock.controller.manager.ReactorInputFluidManagerI; + +/** + * Default implementation of {@link IFluidConsumptionRateCalculator}. + * The rate per cycle depends on the fluid's efficiency and the liquid timer + * exposed by {@link IHeatService}, which itself depends on the reactor size. + */ +public class FluidConsumptionRateCalculator implements IFluidConsumptionRateCalculator { + private final IHeatService heatService; + + public FluidConsumptionRateCalculator(IHeatService heatService) { + this.heatService = heatService; + } + + /** + * {@inheritDoc} + * Does nothing if the fluid stack is {@code null} or holds a negligible + * amount (≤ 1). + */ + @Override + public double tick(BigFluidStack fluidStack, int reactorSize, Level level, + ReactorInputFluidManagerI inputFluidManager, double fluidBuffer) { + if (fluidStack == null || fluidStack.amount <= 1) { + return fluidBuffer; + } + + double amountPerCycle = fluidStack.getFluidtype(level).efficiency(); + switch (reactorSize) { + case 5 -> amountPerCycle /= (double) heatService.getLiquidTimer() / 40; + case 7 -> amountPerCycle /= (double) heatService.getLiquidTimer() / 147; + case 9 -> amountPerCycle /= (double) heatService.getLiquidTimer() / 360; + } + + fluidBuffer += amountPerCycle; + + if (fluidBuffer >= 1.0) { + int toExtract = (int) Math.floor(fluidBuffer); + // Known limitation in ReactorInputFluidManager#extractFluids: fluidNeeded is not + // decremented across multiple tracked input handlers, so each handler may drain up + // to toExtract independently (possible over-extraction with several inputs). + // Intentionally left untouched here; out of scope for this calculator. + if (inputFluidManager.extractFluids(level, toExtract)) { + fluidBuffer -= toExtract; + } + } + return fluidBuffer; + } +} diff --git a/src/main/java/net/nuclearteam/createnuclear/content/multiblock/controller/service/IExplosionService.java b/src/main/java/net/nuclearteam/createnuclear/content/multiblock/controller/service/IExplosionService.java new file mode 100644 index 0000000..13fa188 --- /dev/null +++ b/src/main/java/net/nuclearteam/createnuclear/content/multiblock/controller/service/IExplosionService.java @@ -0,0 +1,17 @@ +package net.nuclearteam.createnuclear.content.multiblock.controller.service; + +import net.minecraft.core.BlockPos; +import net.minecraft.server.level.ServerLevel; +import net.nuclearteam.createnuclear.infrastructure.config.CNConfigs; + +public interface IExplosionService { + void triggerExplosion(ServerLevel level, BlockPos controllerPos, int reactorSize, int countFuelRod, + int notifyRadius, boolean notifyWarnAll); + + default void triggerExplosion(ServerLevel level, BlockPos controllerPos, int reactorSize, int countFuelRod) { + triggerExplosion(level, controllerPos, reactorSize, + countFuelRod, CNConfigs.server().notify.warningDistance.get(), + CNConfigs.server().notify.warnAllPlayers.get() + ); + } +} diff --git a/src/main/java/net/nuclearteam/createnuclear/content/multiblock/controller/service/IFluidConsumptionRateCalculator.java b/src/main/java/net/nuclearteam/createnuclear/content/multiblock/controller/service/IFluidConsumptionRateCalculator.java new file mode 100644 index 0000000..9a9830f --- /dev/null +++ b/src/main/java/net/nuclearteam/createnuclear/content/multiblock/controller/service/IFluidConsumptionRateCalculator.java @@ -0,0 +1,25 @@ +package net.nuclearteam.createnuclear.content.multiblock.controller.service; + +import net.minecraft.world.level.Level; +import net.nuclearteam.createnuclear.content.logistics.BigFluidStack; +import net.nuclearteam.createnuclear.content.multiblock.controller.manager.ReactorInputFluidManagerI; + +/** + * Computes how much fluid should be consumed per tick and triggers extraction + * once the accumulated amount reaches a whole unit. + */ +public interface IFluidConsumptionRateCalculator { + + /** + * Advances the consumption calculation by one tick. + * + * @param fluidStack fluid currently loaded, or {@code null} if none + * @param reactorSize reactor size, used to determine the consumption rate + * @param level current level, used to resolve the fluid type + * @param inputFluidManager input manager responsible for extracting the consumed fluid + * @param fluidBuffer fractional remainder accumulated since the previous tick + * @return the new fractional remainder to carry over to the next tick + */ + double tick(BigFluidStack fluidStack, int reactorSize, Level level, + ReactorInputFluidManagerI inputFluidManager, double fluidBuffer); +} diff --git a/src/main/java/net/nuclearteam/createnuclear/content/multiblock/controller/service/IHeatService.java b/src/main/java/net/nuclearteam/createnuclear/content/multiblock/controller/service/IHeatService.java new file mode 100644 index 0000000..ed396bd --- /dev/null +++ b/src/main/java/net/nuclearteam/createnuclear/content/multiblock/controller/service/IHeatService.java @@ -0,0 +1,13 @@ +package net.nuclearteam.createnuclear.content.multiblock.controller.service; + +import net.minecraft.world.level.Level; +import net.nuclearteam.createnuclear.content.logistics.BigFluidStack; +import net.nuclearteam.createnuclear.content.multiblock.controller.ReactorControllerInventory; +import net.nuclearteam.createnuclear.content.multiblock.controller.display.ReactorDisplayState; +import net.nuclearteam.createnuclear.content.multiblock.reactorLogic.HeatBalance; + +public interface IHeatService { + int getLiquidTimer(); + + double calculateHeat(BigFluidStack bigFluidStack, HeatBalance heatBalance, int previousHeat, ReactorControllerInventory inventory, Level level, ReactorDisplayState displayState); +} diff --git a/src/main/java/net/nuclearteam/createnuclear/content/multiblock/controller/service/IPersistenceService.java b/src/main/java/net/nuclearteam/createnuclear/content/multiblock/controller/service/IPersistenceService.java new file mode 100644 index 0000000..c43f400 --- /dev/null +++ b/src/main/java/net/nuclearteam/createnuclear/content/multiblock/controller/service/IPersistenceService.java @@ -0,0 +1,18 @@ +package net.nuclearteam.createnuclear.content.multiblock.controller.service; + +import net.minecraft.core.HolderLookup; +import net.minecraft.nbt.CompoundTag; +import net.nuclearteam.createnuclear.content.multiblock.controller.ReactorControllerBlockEntity; + +/** + * Reads and writes the controller's own state (multiblock size/facing/bounds, + * blueprint stack, and either the inventory or the client display snapshot). + *

+ * Both methods take a {@link HolderLookup.Provider} on top of the Forge + * signature: from 1.21 onwards, serializing an {@code ItemStack} or a + * {@code FluidStack} requires registry access. + */ +public interface IPersistenceService { + void readBasicState(ReactorControllerBlockEntity owner, CompoundTag compound, HolderLookup.Provider registries, boolean clientPacket); + void writeBasicState(ReactorControllerBlockEntity owner, CompoundTag compound, HolderLookup.Provider registries, boolean clientPacket); +} diff --git a/src/main/java/net/nuclearteam/createnuclear/content/multiblock/controller/service/IReactorAlarmCoordinator.java b/src/main/java/net/nuclearteam/createnuclear/content/multiblock/controller/service/IReactorAlarmCoordinator.java new file mode 100644 index 0000000..f83fe12 --- /dev/null +++ b/src/main/java/net/nuclearteam/createnuclear/content/multiblock/controller/service/IReactorAlarmCoordinator.java @@ -0,0 +1,29 @@ +package net.nuclearteam.createnuclear.content.multiblock.controller.service; + +import net.minecraft.world.level.Level; +import net.nuclearteam.createnuclear.content.multiblock.controller.manager.ReactorAlarmManagerI; + +/** + * Classifies the reactor's heat into a danger flag and drives the + * {@link net.nuclearteam.createnuclear.content.multiblock.alarm.ReactorAlarm} blocks + * registered in a {@link ReactorAlarmManagerI} accordingly. + */ +public interface IReactorAlarmCoordinator { + + /** + * @param heat the heat value to classify + * @param multiblockSize the reactor's multiblock size, used to resolve the danger threshold + * @return {@code true} if {@code heat} falls in the DANGER heat-level range + */ + boolean computeDanger(int heat, int multiblockSize); + + /** + * Powers on/off every alarm block known to {@code alarmManager} to match {@code isDanger}, + * and awards the SILENCE_THE_CORE advancement on the rising edge (alarm turning on). + * + * @param level the reactor's level + * @param alarmManager the manager holding the alarm block positions + * @param isDanger whether the alarms should be powered on + */ + void update(Level level, ReactorAlarmManagerI alarmManager, boolean isDanger); +} diff --git a/src/main/java/net/nuclearteam/createnuclear/content/multiblock/controller/service/IReactorHeatUpdateCoordinator.java b/src/main/java/net/nuclearteam/createnuclear/content/multiblock/controller/service/IReactorHeatUpdateCoordinator.java new file mode 100644 index 0000000..789dd78 --- /dev/null +++ b/src/main/java/net/nuclearteam/createnuclear/content/multiblock/controller/service/IReactorHeatUpdateCoordinator.java @@ -0,0 +1,87 @@ +package net.nuclearteam.createnuclear.content.multiblock.controller.service; + +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.Level; +import net.nuclearteam.createnuclear.content.logistics.BigFluidStack; +import net.nuclearteam.createnuclear.content.multiblock.controller.ReactorControllerInventory; +import net.nuclearteam.createnuclear.content.multiblock.controller.display.ReactorDisplayState; +import net.nuclearteam.createnuclear.content.multiblock.controller.manager.ReactorInputFluidManagerI; +import net.nuclearteam.createnuclear.content.multiblock.reactorLogic.HeatBalance; + +/** + * Centralizes the reactor's heat calculation logic and the decision of + * whether it has enough fuel to run. + *

+ * The {@link HeatBalance} consumed by {@link #updateHeatOnly} and + * {@link #calculateAndWriteHeat} is produced upstream by + * {@link #calculateHeatBalance}; callers resolve it once per tick and pass + * the result into both methods below. + *

+ * Both write methods store the resulting heat on the blueprint stack through + * the {@code CNDataComponents.HEAT} data component. The Forge implementation + * mutated the stack's NBT tag in place; that has no NeoForge equivalent, + * because reading the tag back yields a defensive copy. + */ +public interface IReactorHeatUpdateCoordinator { + + /** + * Computes the pattern's fuel/cooler heat balance (see {@link HeatBalance}), + * used to determine whether the reactor is within the wiki-reference 6:1 + * fuel:cooler equilibrium. + * + * @param configuredPattern the blueprint's configured pattern (required items) + * @param displayState snapshot of items/fluids currently available to the reactor, may be {@code null} + * @param level the level the reactor is in, used to resolve rod types + * @return the resolved heat balance for the pattern + */ + HeatBalance calculateHeatBalance(ItemStack configuredPattern, ReactorDisplayState displayState, Level level); + + /** + * Determines whether the reactor has enough fuel (FUEL-type rods) to start, + * on top of being assembled and having a fluid input. + * + * @param configuredPattern the blueprint's configured pattern (required items) + * @param displayState snapshot of items/fluids currently available to the reactor, may be {@code null} + * @param inputFluidManager the reactor's fluid input manager, used to check that at least one input exists + * @param level the level the reactor is in, used to resolve rod types + * @param assembled whether the multiblock is currently assembled + * @return {@code true} if every fuel rod required by the pattern is + * available and at least one fuel rod is required + */ + boolean canRun(ItemStack configuredPattern, ReactorDisplayState displayState, + ReactorInputFluidManagerI inputFluidManager, Level level, boolean assembled); + + /** + * Updates the heat without running the reactor (case where it isn't ready). + * Heat is only calculated if the full pattern (every required item, not + * just fuel) is available; otherwise it drops back to zero. + * + * @param configuredPattern the blueprint's configured pattern (required items) + * @param displayState snapshot of items/fluids currently available to the reactor, may be {@code null} + * @param fluidStack the reactor's current coolant fluid stack, may be {@code null} + * @param heatBalance the pattern's fuel/cooler heat balance for this tick + * @param previousHeat the heat value calculated on the previous tick + * @param inventory the reactor controller's inventory, used to read the configured pattern items + * @param level the level the reactor is in + * @param assembled whether the multiblock is currently assembled + * @return the calculated heat, also written to the configured pattern's heat component + */ + int updateHeatOnly(ItemStack configuredPattern, ReactorDisplayState displayState, BigFluidStack fluidStack, + HeatBalance heatBalance, int previousHeat, ReactorControllerInventory inventory, Level level, boolean assembled); + + /** + * Calculates the reactor's heat during normal operation and writes it to + * the configured pattern's heat component. + * + * @param configuredPattern the blueprint's configured pattern (required items) + * @param fluidStack the reactor's current coolant fluid stack, may be {@code null} + * @param heatBalance the pattern's fuel/cooler heat balance for this tick + * @param previousHeat the heat value calculated on the previous tick + * @param inventory the reactor controller's inventory, used to read the configured pattern items + * @param level the level the reactor is in + * @param displayState snapshot of items/fluids currently available to the reactor, may be {@code null} + * @return the calculated heat + */ + int calculateAndWriteHeat(ItemStack configuredPattern, BigFluidStack fluidStack, HeatBalance heatBalance, int previousHeat, + ReactorControllerInventory inventory, Level level, ReactorDisplayState displayState); +} diff --git a/src/main/java/net/nuclearteam/createnuclear/content/multiblock/controller/service/IReactorMeltdownMonitor.java b/src/main/java/net/nuclearteam/createnuclear/content/multiblock/controller/service/IReactorMeltdownMonitor.java new file mode 100644 index 0000000..a326391 --- /dev/null +++ b/src/main/java/net/nuclearteam/createnuclear/content/multiblock/controller/service/IReactorMeltdownMonitor.java @@ -0,0 +1,31 @@ +package net.nuclearteam.createnuclear.content.multiblock.controller.service; + +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.Level; + +/** + * Tracks the reactor's meltdown countdown and the player notifications tied to it. + * Consumes the danger flag computed by {@link IReactorAlarmCoordinator#computeDanger} + * instead of recomputing it, so the heat-level classification stays in a single place. + */ +public interface IReactorMeltdownMonitor { + + /** + * Outcome of a single countdown tick. + * {@code NONE} - not in danger, countdown reset to zero. + * {@code CRITICAL} - in danger, countdown running, explosion threshold not reached yet. + * {@code EXPLODE} - countdown threshold reached, the caller must trigger the explosion now. + */ + enum MeltdownState { NONE, CRITICAL, EXPLODE } + + /** + * Advances the countdown by one tick and sends the matching player notifications + * (stabilized / overheating / meltdown flash / critical failure). + * + * @param level the reactor's level, used for notifications and game time + * @param pos the controller's position, used as the notification origin + * @param isDanger whether the reactor is currently at heat level DANGER + * @return the resulting {@link MeltdownState} for this tick + */ + MeltdownState tick (Level level, BlockPos pos, boolean isDanger); +} diff --git a/src/main/java/net/nuclearteam/createnuclear/content/multiblock/controller/service/ReactorAlarmCoordinator.java b/src/main/java/net/nuclearteam/createnuclear/content/multiblock/controller/service/ReactorAlarmCoordinator.java new file mode 100644 index 0000000..4b36543 --- /dev/null +++ b/src/main/java/net/nuclearteam/createnuclear/content/multiblock/controller/service/ReactorAlarmCoordinator.java @@ -0,0 +1,44 @@ +package net.nuclearteam.createnuclear.content.multiblock.controller.service; + +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.state.BlockState; +import net.nuclearteam.createnuclear.content.multiblock.IHeat; +import net.nuclearteam.createnuclear.content.multiblock.alarm.ReactorAlarm; +import net.nuclearteam.createnuclear.content.multiblock.controller.manager.ReactorAlarmManagerI; +import net.nuclearteam.createnuclear.foundation.advancement.CNAdvancement; +import net.nuclearteam.createnuclear.foundation.advancement.CNAdvancementBehaviour; + +/** + * Default {@link IReactorAlarmCoordinator}. Extracted from + * {@code ReactorControllerBlockEntity.activateAlarms(boolean)}: the alarm wiring and + * advancement award are unchanged from the original implementation. + */ +public class ReactorAlarmCoordinator implements IReactorAlarmCoordinator { + private final CNAdvancementBehaviour advancement; + + public ReactorAlarmCoordinator(CNAdvancementBehaviour advancement) { + this.advancement = advancement; + } + + @Override + public boolean computeDanger(int heat, int multiblockSize) { + return IHeat.HeatLevel.of(heat, multiblockSize) == IHeat.HeatLevel.DANGER; + } + + @Override + public void update(Level level, ReactorAlarmManagerI alarmManager, boolean isDanger) { + if (alarmManager == null) return; + + for (BlockPos pos : alarmManager.getBlocksPosition(level)) { + if (!level.isLoaded(pos)) continue; + + BlockState state = level.getBlockState(pos); + + if (state.getBlock() instanceof ReactorAlarm && state.getValue(ReactorAlarm.POWERED) != isDanger) { + level.setBlock(pos, state.setValue(ReactorAlarm.POWERED, isDanger), 3); + if (isDanger) advancement.awardPlayer(CNAdvancement.SILENCE_THE_CORE); + } + } + } +} diff --git a/src/main/java/net/nuclearteam/createnuclear/content/multiblock/controller/service/ReactorHeatUpdateCoordinator.java b/src/main/java/net/nuclearteam/createnuclear/content/multiblock/controller/service/ReactorHeatUpdateCoordinator.java new file mode 100644 index 0000000..505ef7b --- /dev/null +++ b/src/main/java/net/nuclearteam/createnuclear/content/multiblock/controller/service/ReactorHeatUpdateCoordinator.java @@ -0,0 +1,153 @@ +package net.nuclearteam.createnuclear.content.multiblock.controller.service; + +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.Level; +import net.nuclearteam.createnuclear.CNDataComponents; +import net.nuclearteam.createnuclear.api.multiblock.rods.RodType; +import net.nuclearteam.createnuclear.content.logistics.BigFluidStack; +import net.nuclearteam.createnuclear.content.multiblock.bluePrintItem.ReactorBluePrintData; +import net.nuclearteam.createnuclear.content.multiblock.controller.ReactorControllerInventory; +import net.nuclearteam.createnuclear.content.multiblock.controller.consumable.PatternReader; +import net.nuclearteam.createnuclear.content.multiblock.controller.display.ReactorDisplayState; +import net.nuclearteam.createnuclear.content.multiblock.controller.manager.ReactorInputFluidManagerI; +import net.nuclearteam.createnuclear.content.multiblock.reactorLogic.HeatBalance; + +import java.util.Collections; +import java.util.Map; + +/** + * Default implementation of {@link IReactorHeatUpdateCoordinator}. + * Delegates raw heat calculation to {@link IHeatService} and only adds the + * pattern-availability logic (fuel-only vs. full pattern). + */ +public class ReactorHeatUpdateCoordinator implements IReactorHeatUpdateCoordinator { + private final IHeatService heatService; + + public ReactorHeatUpdateCoordinator(IHeatService heatService) { + this.heatService = heatService; + } + + /** + * A blueprint counts as unconfigured when it carries no + * {@link ReactorBluePrintData}. Replaces the Forge check on an empty NBT tag. + */ + private static boolean isEmptyPattern(ItemStack configuredPattern) { + return configuredPattern.getOrDefault(CNDataComponents.REACTOR_BLUE_PRINT_DATA, ReactorBluePrintData.EMPTY) == ReactorBluePrintData.EMPTY; + } + + /** + * Stores the computed heat on the blueprint stack. + *

+ * The Forge original wrote into {@code configuredPattern.getOrCreateTag()}, + * mutating the stack's tag in place. On NeoForge the equivalent read returns a + * defensive copy, so the write has to go through the typed data component or it + * would be silently discarded and the heat would stay pinned at zero. + */ + private static void writeHeat(ItemStack configuredPattern, int heat) { + if (configuredPattern.isEmpty()) return; + configuredPattern.set(CNDataComponents.HEAT, (float) heat); + } + + @Override + public HeatBalance calculateHeatBalance(ItemStack configuredPattern, ReactorDisplayState displayState, Level level) { + Map patternCounts = PatternReader.readItemCounts(configuredPattern, level); + Map currentItems = displayState != null && displayState.items() != null + ? displayState.items() : Collections.emptyMap(); + + int heatPoints = 0; + int coolingPoints = 0; + + for (Map.Entry entry : patternCounts.entrySet()) { + Item item = entry.getKey(); + int requiredCount = entry.getValue(); + int availableCount = currentItems.getOrDefault(item, 0); + int countToUse = Math.min(requiredCount, availableCount); + if (countToUse <= 0) continue; + + RodType rodType = RodType.resolveRodType(item, level); + if (rodType == null || !rodType.isNotEmptyItem()) continue; + + int weighted = rodType.ratio().get() * countToUse; + if (rodType.type() == RodType.TypeRod.FUEL) heatPoints += weighted; + else if (rodType.type() == RodType.TypeRod.COOLER) coolingPoints += weighted; + } + + return new HeatBalance(heatPoints, coolingPoints); + } + + /** Predicate applied to each pattern entry when checking availability. */ + @FunctionalInterface + private interface PatternEntryCheck { + boolean test(Item item, int requiredCount, int availableCount); + } + + /** + * Iterates over the items required by the configured pattern and applies + * {@code check} to each entry, stopping as soon as one entry fails. + * + * @return {@code true} if {@code check} succeeded for every pattern entry + */ + private static boolean checkPatternAvailability(ItemStack configuredPattern, ReactorDisplayState displayState, Level level, + PatternEntryCheck check) { + Map patternCounts = PatternReader.readItemCounts(configuredPattern, level); + Map currentItems = displayState != null && displayState.items() != null + ? displayState.items() : Collections.emptyMap(); + + for (Map.Entry entry : patternCounts.entrySet()) { + Item item = entry.getKey(); + int requiredCount = entry.getValue(); + int availableCount = currentItems.getOrDefault(item, 0); + if (!check.test(item, requiredCount, availableCount)) { + return false; + } + } + return true; + } + + @Override + public boolean canRun(ItemStack configuredPattern, ReactorDisplayState displayState, + ReactorInputFluidManagerI inputFluidManager, Level level, boolean assembled) { + if (isEmptyPattern(configuredPattern) || inputFluidManager.size() == 0 || !assembled) { + return false; + } + + boolean[] hasAnyFuel = {false}; + boolean allFuelAvailable = checkPatternAvailability(configuredPattern, displayState, level, + (item, requiredCount, availableCount) -> { + RodType rodType = RodType.resolveRodType(item, level); + if (rodType.isNotEmptyItem() && rodType.type() == RodType.TypeRod.FUEL) { + hasAnyFuel[0] = true; + return availableCount > 0; + } + return true; // non-fuel entries don't block this gate + } + ); + + return allFuelAvailable && hasAnyFuel[0]; + } + + @Override + public int updateHeatOnly(ItemStack configuredPattern, ReactorDisplayState displayState, BigFluidStack fluidStack, + HeatBalance heatBalance, int previousHeat, ReactorControllerInventory inventory, Level level, boolean assembled) { + int heat = 0; + boolean emptyPattern = isEmptyPattern(configuredPattern); + if (!emptyPattern && assembled) { + boolean allAvailable = checkPatternAvailability(configuredPattern, displayState, level, + (item, requiredCount, availableCount) -> availableCount >= requiredCount); + if (allAvailable) { + heat = (int) heatService.calculateHeat(fluidStack, heatBalance, previousHeat, inventory, level, displayState); + } + } + writeHeat(configuredPattern, heat); + return heat; + } + + @Override + public int calculateAndWriteHeat(ItemStack configuredPattern, BigFluidStack fluidStack, HeatBalance heatBalance, int previousHeat, + ReactorControllerInventory inventory, Level level, ReactorDisplayState displayState) { + int heat = (int) heatService.calculateHeat(fluidStack, heatBalance, previousHeat, inventory, level, displayState); + writeHeat(configuredPattern, heat); + return heat; + } +} diff --git a/src/main/java/net/nuclearteam/createnuclear/content/multiblock/controller/service/ReactorMeltdownExecutor.java b/src/main/java/net/nuclearteam/createnuclear/content/multiblock/controller/service/ReactorMeltdownExecutor.java new file mode 100644 index 0000000..c957b42 --- /dev/null +++ b/src/main/java/net/nuclearteam/createnuclear/content/multiblock/controller/service/ReactorMeltdownExecutor.java @@ -0,0 +1,51 @@ +package net.nuclearteam.createnuclear.content.multiblock.controller.service; + +import net.minecraft.ChatFormatting; +import net.minecraft.core.BlockPos; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.util.Mth; +import net.minecraft.world.level.GameRules; +import net.nuclearteam.createnuclear.CNEntityType; +import net.nuclearteam.createnuclear.content.explosion.NuclearExplosionEntity; +import net.nuclearteam.createnuclear.foundation.utility.CreateNuclearLang; +import net.nuclearteam.createnuclear.foundation.utility.NotifyUtil; +import net.nuclearteam.createnuclear.infrastructure.worldgen.biome.BiomeIrradiationService; +import net.nuclearteam.createnuclear.infrastructure.worldgen.biome.CNBiomes; + +public class ReactorMeltdownExecutor implements IExplosionService { + @Override + public void triggerExplosion(ServerLevel level, BlockPos controllerPos, int reactorSize, int countFuelRod, int notifyRadius, boolean notifyWarnAll) { + BlockPos explosionPos = controllerPos.above(5); + + NotifyUtil.sendTitle(level, controllerPos, + CreateNuclearLang.translate("notification.reactor.destroyed"), + CreateNuclearLang.translate("notification.reactor.meltdown_finished"), + ChatFormatting.DARK_RED, notifyRadius, notifyWarnAll, 10, 60, 20 + ); + + float size = computeExplosionSize(reactorSize, countFuelRod); + + NuclearExplosionEntity explosion = new NuclearExplosionEntity(CNEntityType.NUCLEAR_EXPLOSION.get(), level); + explosion.setPos(explosionPos.getX() + 0.5D, explosionPos.getY() + 10.0D, explosionPos.getZ() - 2.0D); + explosion.setSize(size); + explosion.setNoGriefing(!level.getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING)); + level.addFreshEntity(explosion); + + level.destroyBlock(controllerPos, false); + + BiomeIrradiationService.circularArea(level, explosionPos, CNBiomes.Irradiated.PLAIN, (int) (size * 30)); + + } + + private static float computeExplosionSize(int reactorSize, int countFuelRod) { + // Scales with reactor footprint: 5x5 -> 1.0, 7x7 -> 1.4, 9x9 -> 1.8 + float structureFactor = reactorSize / 5f; + + // Square root gives diminishing returns: each extra uranium rod adds less + // to the radius than the previous one. + float fuelImpact = Mth.sqrt(countFuelRod) * .3f; + + // Final size: constant base + (fuel impact scaled by structure factor) + return Mth.clamp(1.5f + (fuelImpact * structureFactor), 1f, 10f); + } +} diff --git a/src/main/java/net/nuclearteam/createnuclear/content/multiblock/controller/service/ReactorMeltdownMonitor.java b/src/main/java/net/nuclearteam/createnuclear/content/multiblock/controller/service/ReactorMeltdownMonitor.java new file mode 100644 index 0000000..bc75570 --- /dev/null +++ b/src/main/java/net/nuclearteam/createnuclear/content/multiblock/controller/service/ReactorMeltdownMonitor.java @@ -0,0 +1,67 @@ +package net.nuclearteam.createnuclear.content.multiblock.controller.service; + +import net.minecraft.ChatFormatting; +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.Level; +import net.nuclearteam.createnuclear.foundation.utility.CreateNuclearLang; +import net.nuclearteam.createnuclear.foundation.utility.NotifyUtil; +import net.nuclearteam.createnuclear.infrastructure.config.CNConfigs; + +/** + * Default {@link IReactorMeltdownMonitor}. Extracted from + * {@code ReactorControllerBlockEntity.tick()}: the countdown logic and its notifications + * are unchanged from the original implementation, only moved into their own class. + */ +public class ReactorMeltdownMonitor implements IReactorMeltdownMonitor { + /** Ticks of continuous danger before the reactor explodes (15s at 20 TPS). */ + private static final int EXPLOSION_THRESHOLD_TICKS = 300; + /** Last seconds of the countdown during which the flashing "meltdown in" message is shown. */ + private static final int CRITICAL_WINDOW_SECONDS = 10; + + private int explosionCountDown = 0; + + @Override + public MeltdownState tick(Level level, BlockPos pos, boolean isDanger) { + int configRadius = CNConfigs.server().notify.warningDistance.get(); + boolean configWarnAll = CNConfigs.server().notify.warnAllPlayers.get(); + + if (!isDanger) { + boolean wasCountingDown = explosionCountDown > 0; + explosionCountDown = 0; + if (wasCountingDown) { + NotifyUtil.sendActionBar(level, pos, + CreateNuclearLang.translate("notification.reactor.stabilized"), + ChatFormatting.GREEN, configRadius, configWarnAll); + } + return MeltdownState.NONE; + } + + explosionCountDown++; + int secondsLeft = (EXPLOSION_THRESHOLD_TICKS - explosionCountDown) / 20; + + if (secondsLeft <= CRITICAL_WINDOW_SECONDS && secondsLeft > 0) { + boolean isWhite = (level.getGameTime() / 5) % 2 == 0; + ChatFormatting flashColor = isWhite ? ChatFormatting.WHITE : ChatFormatting.RED; + + NotifyUtil.sendActionBar(level, pos, + CreateNuclearLang.translate("notification.reactor.meltdown_in") + .add(CreateNuclearLang.number(secondsLeft)) + .add(CreateNuclearLang.translate("generic.unit.seconds")), + flashColor, configRadius, configWarnAll); + } else if (secondsLeft > CRITICAL_WINDOW_SECONDS && explosionCountDown % 20 == 0) { + NotifyUtil.sendActionBar(level, pos, + CreateNuclearLang.translate("notification.reactor.overheating"), + ChatFormatting.DARK_RED, configRadius, configWarnAll); + } + + if (explosionCountDown >= EXPLOSION_THRESHOLD_TICKS) { + NotifyUtil.sendTitle(level, pos, + CreateNuclearLang.translate("notification.reactor.critical_failure"), + CreateNuclearLang.translate("notification.reactor.imminent_explosion"), + ChatFormatting.DARK_RED, configRadius, configWarnAll, 0, 40, 10); + return MeltdownState.EXPLODE; + } + + return MeltdownState.CRITICAL; + } +} diff --git a/src/main/java/net/nuclearteam/createnuclear/content/multiblock/controller/snapshot/ReactorInputSnapshot.java b/src/main/java/net/nuclearteam/createnuclear/content/multiblock/controller/snapshot/ReactorInputSnapshot.java new file mode 100644 index 0000000..b9e5575 --- /dev/null +++ b/src/main/java/net/nuclearteam/createnuclear/content/multiblock/controller/snapshot/ReactorInputSnapshot.java @@ -0,0 +1,32 @@ +package net.nuclearteam.createnuclear.content.multiblock.controller.snapshot; + +import net.minecraft.world.item.Item; +import net.nuclearteam.createnuclear.content.logistics.BigFluidStack; + +import java.util.List; +import java.util.Map; + +/** + * Snapshot of the reactor's inputs, collected once per tick. + *

+ * {@code items} reflects the items actually present (for tooltip display). + * {@code bigFuelItem}/{@code bigCoolerItem} are derived from + * {@link net.nuclearteam.createnuclear.content.multiblock.input.item.VirtualReactorInputsItem} and always represent, respectively, + * Uranium Rod / Graphite Rod (generic tag-based count, fixed identity) — + * do not use these fields to infer the actual item type loaded. + *

+ * The elements of {@code fluids} ({@link BigFluidStack}) remain mutable + * (public fields); this snapshot only guarantees immutability of the + * list/map structure, not of their contents. + */ + +public record ReactorInputSnapshot(Map items, List fluids, long maxFluidCapacity) { + public static final ReactorInputSnapshot EMPTY = new ReactorInputSnapshot( + Map.of(), List.of(), 0 + ); + + public ReactorInputSnapshot { + items = Map.copyOf(items); + fluids = List.copyOf(fluids); + } +} diff --git a/src/main/java/net/nuclearteam/createnuclear/content/multiblock/controller/snapshot/ReactorInputSnapshotBuilder.java b/src/main/java/net/nuclearteam/createnuclear/content/multiblock/controller/snapshot/ReactorInputSnapshotBuilder.java new file mode 100644 index 0000000..9396e27 --- /dev/null +++ b/src/main/java/net/nuclearteam/createnuclear/content/multiblock/controller/snapshot/ReactorInputSnapshotBuilder.java @@ -0,0 +1,49 @@ +package net.nuclearteam.createnuclear.content.multiblock.controller.snapshot; + +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.Level; +import net.neoforged.neoforge.fluids.capability.IFluidHandler; +import net.neoforged.neoforge.items.IItemHandler; +import net.nuclearteam.createnuclear.content.logistics.BigFluidStack; +import net.nuclearteam.createnuclear.content.multiblock.controller.manager.ReactorInputFluidManagerI; +import net.nuclearteam.createnuclear.content.multiblock.controller.manager.ReactorInputManagerI; +import net.nuclearteam.createnuclear.content.multiblock.input.fluid.VirtualReactorInputFluid; +import net.nuclearteam.createnuclear.content.multiblock.input.item.VirtualReactorInputsItem; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class ReactorInputSnapshotBuilder { + private ReactorInputSnapshotBuilder() {} + + public static ReactorInputSnapshot build(Level level, ReactorInputManagerI inputManager, ReactorInputFluidManagerI inputFluidManager) { + // Populate display fields for client sync + Map items = new HashMap<>(); + List itemHandlers = inputManager.getItemHandlers(level); + for (IItemHandler h : itemHandlers) { + for (int s = 0; s < h.getSlots(); s++) { + ItemStack st = h.getStackInSlot(s); + if (!st.isEmpty()) { + items.merge(st.getItem(), st.getCount(), Integer::sum); + } + } + } + + long maxFluidCapacity = 0; + for (IFluidHandler h : inputFluidManager.getFuildHandlers(level)) { + if (h.getTanks() > 0) { + maxFluidCapacity += h.getTankCapacity(0); + } + } + + VirtualReactorInputsItem virtualItems = inputManager.getInventory(level); + VirtualReactorInputFluid virtualFluid = inputFluidManager.getInventory(level); + List fluids = VirtualReactorInputFluid.toBigList(virtualFluid.fluids()); + + + + return new ReactorInputSnapshot(items, fluids, maxFluidCapacity); + } +} diff --git a/src/main/java/net/nuclearteam/createnuclear/content/multiblock/cooler/ReactorCooler.java b/src/main/java/net/nuclearteam/createnuclear/content/multiblock/cooler/ReactorCooler.java index 2c66f9b..d046d12 100644 --- a/src/main/java/net/nuclearteam/createnuclear/content/multiblock/cooler/ReactorCooler.java +++ b/src/main/java/net/nuclearteam/createnuclear/content/multiblock/cooler/ReactorCooler.java @@ -2,65 +2,38 @@ import com.simibubi.create.content.equipment.wrench.IWrenchable; import net.minecraft.core.BlockPos; -import net.minecraft.core.Vec3i; -import net.minecraft.world.entity.player.Player; +import net.minecraft.world.entity.LivingEntity; import net.minecraft.world.item.ItemStack; import net.minecraft.world.level.Level; import net.minecraft.world.level.block.Block; -import net.minecraft.world.level.block.entity.BlockEntity; import net.minecraft.world.level.block.state.BlockState; -import net.nuclearteam.createnuclear.CNBlocks; -import net.nuclearteam.createnuclear.content.multiblock.controller.ReactorControllerBlock; -import net.nuclearteam.createnuclear.content.multiblock.controller.ReactorControllerBlockEntity; +import net.nuclearteam.createnuclear.content.multiblock.MultiblockHelpers; +import net.nuclearteam.createnuclear.content.multiblock.pattern.ReactorPattern; import javax.annotation.Nullable; -import java.util.List; public class ReactorCooler extends Block implements IWrenchable { public ReactorCooler(Properties properties) { super(properties); } + protected ReactorPattern pattern = new ReactorPattern(); @Override // Called when the block is placed on the world public void onPlace(BlockState state, Level level, BlockPos pos, BlockState oldState, boolean movedByPiston) { super.onPlace(state, level, pos, oldState, movedByPiston); - List players = level.players(); - FindController(pos, level, players, true); + pattern.findController(pos, level, true); } - @Override // called when the player destroys the block, with or without a tool - public void playerDestroy(Level level, Player player, BlockPos pos, BlockState state, @Nullable BlockEntity blockEntity, ItemStack tool) { - super.playerDestroy(level, player, pos, state, blockEntity, tool); - List players = level.players(); - FindController(pos, level, players, false); + @Override + public void setPlacedBy(Level level, BlockPos pos, BlockState state, @Nullable LivingEntity pPlacer, ItemStack stack) { + super.setPlacedBy(level, pos, state, pPlacer, stack); + MultiblockHelpers.handleAdvancedPlacedBy(pos, level, pPlacer); } @Override public void onRemove(BlockState state, Level level, BlockPos pos, BlockState newState, boolean movedByPiston) { super.onRemove(state, level, pos, newState, movedByPiston); - List players = level.players(); - FindController(pos, level, players, false); - } - - public ReactorControllerBlock FindController(BlockPos blockPos, Level level, List players, boolean first){ // Function that checks the surrounding blocks in order - BlockPos newBlock; // to find the controller and verify the pattern - Vec3i pos = new Vec3i(blockPos.getX(), blockPos.getY(), blockPos.getZ()); - for (int y = pos.getY()-3; y != pos.getY()+4; y+=1) { - for (int x = pos.getX()-5; x != pos.getX()+5; x+=1) { - for (int z = pos.getZ()-5; z != pos.getZ()+5; z+=1) { - newBlock = new BlockPos(x, y, z); - if (level.getBlockState(newBlock).is(CNBlocks.REACTOR_CONTROLLER.get())) { // verifying the pattern - ReactorControllerBlock controller = (ReactorControllerBlock) level.getBlockState(newBlock).getBlock(); - controller.Verify(level.getBlockState(newBlock), newBlock, level, players, first); - ReactorControllerBlockEntity entity = controller.getBlockEntity(level, newBlock); - if (entity.created) { - return controller; - } - } - } - } - } - return null; + pattern.findController(pos, level, false); } } diff --git a/src/main/java/net/nuclearteam/createnuclear/content/multiblock/core/ReactorCoreEntity.java b/src/main/java/net/nuclearteam/createnuclear/content/multiblock/core/ReactorCoreEntity.java index 1eb1704..9c38de1 100644 --- a/src/main/java/net/nuclearteam/createnuclear/content/multiblock/core/ReactorCoreEntity.java +++ b/src/main/java/net/nuclearteam/createnuclear/content/multiblock/core/ReactorCoreEntity.java @@ -1,21 +1,15 @@ package net.nuclearteam.createnuclear.content.multiblock.core; -import lib.multiblock.SimpleMultiBlockAislePatternBuilder; import net.minecraft.core.BlockPos; -import net.minecraft.world.level.Level; import net.minecraft.world.level.block.entity.BlockEntityType; import net.minecraft.world.level.block.state.BlockState; -import net.nuclearteam.createnuclear.CNBlocks; -import net.nuclearteam.createnuclear.content.multiblock.IHeat; import net.nuclearteam.createnuclear.content.multiblock.casing.ReactorCasingEntity; -import net.nuclearteam.createnuclear.content.multiblock.controller.ReactorControllerBlockEntity; -import net.nuclearteam.createnuclear.infrastructure.config.CNConfigs; - -import static net.nuclearteam.createnuclear.content.multiblock.CNMultiblock.*; @SuppressWarnings({"unused"}) public class ReactorCoreEntity extends ReactorCasingEntity { + private int countdownTicks = 0; + private boolean hasExploded = false; public ReactorCoreEntity(BlockEntityType type, BlockPos pos, BlockState state) { super(type, pos, state); @@ -25,69 +19,6 @@ public ReactorCoreEntity(BlockEntityType type, BlockPos pos, BlockState state public void tick() { super.tick(); - if (level.isClientSide()) return; - - BlockPos controllerPos = getBlockPosForReactor(); - if (level.getBlockEntity(controllerPos) instanceof ReactorControllerBlockEntity reactorController) { - int heat = reactorController.heat; - if (IHeat.HeatLevel.of(heat) == IHeat.HeatLevel.DANGER) { - if (countdownTicks >= CNConfigs.common().explode.time.get()) { // 300 ticks = 15 secondes - explodeReactorCore(level, getBlockPos()); - } else { - countdownTicks++; - } - } else { - countdownTicks = 0; // Reset the countdown if the heat level is not in danger - } - } - } - - private void explodeReactorCore(Level world, BlockPos pos) { - level.explode(null, pos.getX(), pos.getY(), pos.getZ(), 20F, Level.ExplosionInteraction.BLOCK); - } - - private static BlockPos FindController(char character) { - return SimpleMultiBlockAislePatternBuilder.start() - .aisle(AAAAA, AAAAA, AAAAA, AAAAA, AAAAA) - .aisle(AABAA, ADADA, BACAB, ADADA, AABAA) - .aisle(AABAA, ADADA, BACAB, ADADA, AABAA) - .aisle(AAIAA, ADADA, BACAB, ADADA, AAAA) - .aisle(AABAA, ADADA, BACAB, ADADA, AABAA) - .aisle(AABAA, ADADA, BACAB, ADADA, AABAA) - .aisle(AAAAA, AAAAA, AAAAA, AAAAA, AAOAA) - .where('A', a -> a.getState().is(CNBlocks.REACTOR_CASING.get())) - .where('B', a -> a.getState().is(CNBlocks.REACTOR_FRAME.get())) - .where('C', a -> a.getState().is(CNBlocks.REACTOR_CORE.get())) - .where('D', a -> a.getState().is(CNBlocks.REACTOR_COOLER.get())) - .where('*', a -> a.getState().is(CNBlocks.REACTOR_CONTROLLER.get())) - .where('O', a -> a.getState().is(CNBlocks.REACTOR_OUTPUT.get())) - .where('I', a -> a.getState().is(CNBlocks.REACTOR_ROD_INPUT.get())) - .getDistanceController(character); - } - - private BlockPos getBlockPosForReactor() { - BlockPos posController = getBlockPos(); - BlockPos posInput = new BlockPos(posController.getX(), posController.getY(), posController.getZ()); - - int[][][] directions = { - {{0, 2, 2}, {0, 1, 2}, {0, 0, 2}, {0, -1, 2}, {0, -2, 2}}, // NORTH - {{0, 2, -2}, {0, 1, -2}, {0, 0, -2}, {0, -1, -2}, {0, -2, -2}}, // SOUTH - - {{2, 2, 0}, {2, 1, 0}, {2, 0, 0}, {2, -1, 0}, {2, -2, 0}}, // EAST - {{-2, 2, 0}, {-2, 1, 0}, {-2, 0, 0}, {-2, -1, 0}, {-2, -2, 0}} // WEST - }; - - - for (int[][] direction : directions) { - for (int[] dir : direction) { - BlockPos newPos = posController.offset(dir[0], dir[1], dir[2]); - if (level.getBlockState(newPos).is(CNBlocks.REACTOR_CONTROLLER.get())) { - posInput = newPos; - break; - } - } - } - - return posInput; + if (level.isClientSide() || hasExploded) return; } } diff --git a/src/main/java/net/nuclearteam/createnuclear/content/multiblock/fluid/CNReactorFluidTypes.java b/src/main/java/net/nuclearteam/createnuclear/content/multiblock/fluid/CNReactorFluidTypes.java index b75a8ed..6dac564 100644 --- a/src/main/java/net/nuclearteam/createnuclear/content/multiblock/fluid/CNReactorFluidTypes.java +++ b/src/main/java/net/nuclearteam/createnuclear/content/multiblock/fluid/CNReactorFluidTypes.java @@ -11,8 +11,6 @@ import static net.nuclearteam.createnuclear.api.ReactorFluidTypesValue.DEFAULT_REACTOR_FLUID_TYPE; public class CNReactorFluidTypes { - public static final ResourceKey FALLBACK = ResourceKey.create(CreateNuclearRegistries.FLUID_TYPE, CreateNuclear.asResource("fallback")); - /** * Static registry of reactor fluid types ({@link ReactorFluidType}) for the CreateNuclear mod. * @@ -42,12 +40,12 @@ public static void bootstrap(BootstrapContext ctx) { .efficiency(1000) .build() ); - /* register(ctx, "nitrogen", new ReactorFluidType.Builder() + register(ctx, "nitrogen", new ReactorFluidType.Builder() .fluid(CNFluids.LIQUID_NITROGEN.getSource()) .maxHeat(8196) .efficiency(100) .build() - );*/ + ); } private static void register(BootstrapContext ctx, String name, ReactorFluidType type) { diff --git a/src/main/java/net/nuclearteam/createnuclear/content/multiblock/frame/ReactorFrame.java b/src/main/java/net/nuclearteam/createnuclear/content/multiblock/frame/ReactorFrame.java index d9c1ad3..803ada5 100644 --- a/src/main/java/net/nuclearteam/createnuclear/content/multiblock/frame/ReactorFrame.java +++ b/src/main/java/net/nuclearteam/createnuclear/content/multiblock/frame/ReactorFrame.java @@ -1,11 +1,11 @@ package net.nuclearteam.createnuclear.content.multiblock.frame; import com.simibubi.create.content.equipment.wrench.IWrenchable; +import com.simibubi.create.foundation.block.IBE; import net.minecraft.core.BlockPos; import net.minecraft.core.Direction; -import net.minecraft.core.Vec3i; import net.minecraft.util.StringRepresentable; -import net.minecraft.world.entity.player.Player; +import net.minecraft.world.entity.LivingEntity; import net.minecraft.world.item.ItemStack; import net.minecraft.world.item.context.BlockPlaceContext; import net.minecraft.world.level.Level; @@ -13,22 +13,21 @@ import net.minecraft.world.level.block.Block; import net.minecraft.world.level.block.Mirror; import net.minecraft.world.level.block.Rotation; -import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.entity.BlockEntityType; import net.minecraft.world.level.block.state.BlockState; import net.minecraft.world.level.block.state.StateDefinition; import net.minecraft.world.level.block.state.properties.EnumProperty; import net.minecraft.world.level.block.state.properties.Property; -import net.nuclearteam.createnuclear.CNBlocks; -import net.nuclearteam.createnuclear.content.multiblock.controller.ReactorControllerBlock; +import net.nuclearteam.createnuclear.CNBlockEntityTypes; +import net.nuclearteam.createnuclear.content.multiblock.MultiblockHelpers; +import net.nuclearteam.createnuclear.content.multiblock.pattern.ReactorPattern; import net.nuclearteam.createnuclear.foundation.utility.CreateNuclearLang; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import java.util.List; - -public class ReactorFrame extends Block implements IWrenchable { +public class ReactorFrame extends Block implements IWrenchable, IBE { public static final Property PART = EnumProperty.create("part", Part.class); - + protected ReactorPattern pattern = new ReactorPattern(); public ReactorFrame(Properties properties) { super(properties); } @@ -108,32 +107,33 @@ public BlockState mirror(BlockState state, Mirror mirror) { @Override public void onPlace(BlockState state, Level level, BlockPos pos, BlockState oldState, boolean movedByPiston) { super.onPlace(state, level, pos, oldState, movedByPiston); - List players = level.players(); - FindController(pos, level, players, true); + pattern.findController(pos, level, true); } - @Override // called when the player destroys the block, with or without a tool - public void playerDestroy(Level level, Player player, BlockPos pos, BlockState state, @Nullable BlockEntity blockEntity, ItemStack tool) { - super.playerDestroy(level, player, pos, state, blockEntity, tool); - List players = level.players(); - FindController(pos, level, players, false); + + @Override + public void setPlacedBy(Level level, BlockPos pos, BlockState state, @javax.annotation.Nullable LivingEntity pPlacer, ItemStack stack) { + super.setPlacedBy(level, pos, state, pPlacer, stack); + MultiblockHelpers.handleAdvancedPlacedBy(pos, level, pPlacer); } - public ReactorControllerBlock FindController(BlockPos blockPos, Level level, List players, boolean first){ // Function that checks the surrounding blocks in order - BlockPos newBlock; // to find the controller and verify the pattern - Vec3i pos = new Vec3i(blockPos.getX(), blockPos.getY(), blockPos.getZ()); - for (int y = pos.getY()-3; y != pos.getY()+4; y+=1) { - for (int x = pos.getX()-5; x != pos.getX()+5; x+=1) { - for (int z = pos.getZ()-5; z != pos.getZ()+5; z+=1) { - newBlock = new BlockPos(x, y, z); - if (level.getBlockState(newBlock).is(CNBlocks.REACTOR_CONTROLLER.get())) { // verifying the pattern - ReactorControllerBlock controller = (ReactorControllerBlock) level.getBlockState(newBlock).getBlock(); - controller.Verify(level.getBlockState(newBlock), newBlock, level, players, first); - return controller; - } - } - } + @Override + public void onRemove(BlockState state, Level level, BlockPos pos, BlockState newState, boolean movedByPiston) { + super.onRemove(state, level, pos, newState, movedByPiston); + // playerDestroy is NOT called in creative mode, so the structure must also be + // re-evaluated here. Only react to an actual block removal/replacement (different + // block type), not to a simple PART property change (same block, via setBlock). + if (!state.is(newState.getBlock())) { + pattern.findController(pos, level, false); } - return null; } + @Override + public Class getBlockEntityClass() { + return ReactorFrameEntity.class; + } + + @Override + public BlockEntityType getBlockEntityType() { + return CNBlockEntityTypes.REACTOR_FRAME.get(); + } } diff --git a/src/main/java/net/nuclearteam/createnuclear/content/multiblock/frame/ReactorFrameRenderer.java b/src/main/java/net/nuclearteam/createnuclear/content/multiblock/frame/ReactorFrameRenderer.java index 154340c..ce46c08 100644 --- a/src/main/java/net/nuclearteam/createnuclear/content/multiblock/frame/ReactorFrameRenderer.java +++ b/src/main/java/net/nuclearteam/createnuclear/content/multiblock/frame/ReactorFrameRenderer.java @@ -3,10 +3,12 @@ import com.mojang.blaze3d.vertex.PoseStack; import com.simibubi.create.foundation.blockEntity.renderer.SafeBlockEntityRenderer; import net.createmod.catnip.platform.CatnipServices; +import net.createmod.catnip.render.FluidRenderHelper; import net.minecraft.client.renderer.MultiBufferSource; import net.minecraft.client.renderer.blockentity.BlockEntityRendererProvider; import net.neoforged.neoforge.fluids.FluidStack; import net.nuclearteam.createnuclear.content.multiblock.controller.ReactorControllerBlockEntity; +import net.nuclearteam.createnuclear.content.multiblock.controller.manager.ReactorFrameDisplayManagerI; /** * Renders the reactor fluid dynamically inside the {@link ReactorFrame} window. @@ -30,7 +32,9 @@ protected void renderSafe(ReactorFrameEntity be, float partialTicks, PoseStack m ReactorControllerBlockEntity controller = be.getControllerEntity(); if (controller == null) return; - FluidStack fluid = controller.getDisplayedFluid(); + ReactorFrameDisplayManagerI frameDisplay = controller.getFrameDisplayManager(); + + FluidStack fluid = frameDisplay.getDisplayedFluid(controller.getLevel(), controller.getInputFluidManager()); if (fluid == null || fluid.isEmpty()) return; // Local vertical bounds of the liquid volume inside this block, matching @@ -51,10 +55,10 @@ protected void renderSafe(ReactorFrameEntity be, float partialTicks, PoseStack m // from the bottom frame's lip (frameMinY + 4/16) to the top frame's cap // (frameMaxY + 9/16), so even a nearly-empty reactor still shows a sliver. float yMax = boxYMax; - if (controller.hasFrameColumn()) { - float ratio = controller.getDisplayedFluidFillRatio(); - double liquidBottomWorldY = controller.getFrameColumnMinY() + 4.0 / 16.0; - double liquidTopWorldY = controller.getFrameColumnMaxY() + 9.0 / 16.0; + if (frameDisplay.hasFrameColumn()) { + float ratio = frameDisplay.getDisplayedFluidFillRatio(controller.getLevel(), controller.getInputFluidManager()); + double liquidBottomWorldY = frameDisplay.getFrameColumnMinY() + 4.0 / 16.0; + double liquidTopWorldY = frameDisplay.getFrameColumnMaxY() + 9.0 / 16.0; double surfaceWorldY = liquidBottomWorldY + ratio * (liquidTopWorldY - liquidBottomWorldY); double localSurface = surfaceWorldY - be.getBlockPos().getY(); if (localSurface <= boxYMin) return; // liquid level is below this block @@ -64,10 +68,16 @@ protected void renderSafe(ReactorFrameEntity be, float partialTicks, PoseStack m // Last arg (invertGasses) must be false: liquid nitrogen has density 0, so // it counts as "lighter than air" and would otherwise be flipped 180°, // hiding the top surface. We always fill bottom-to-top here. - CatnipServices.FLUID_RENDERER.renderFluidBox( - fluid.getFluid().defaultFluidState(), // Conversion de FluidStack en FluidState + // Must pass the FluidStack, not fluid.getFluid().defaultFluidState(): the FluidState + // overload resolves the texture and tint from the fluid's block state, which loses the + // stack's tint and renders water almost black in the frame windows. + // CatnipServices.FLUID_RENDERER is declared as FluidRenderHelper, so the platform + // type has to be reintroduced by hand — on NeoForge it is neoforge's FluidStack. + @SuppressWarnings("unchecked") + FluidRenderHelper fluidRenderer = (FluidRenderHelper) CatnipServices.FLUID_RENDERER; + + fluidRenderer.renderFluidBox(fluid, X_MIN, boxYMin, Z_MIN, X_MAX, yMax, Z_MAX, - buffer, ms, light, false, false - ); + buffer, ms, light, false, false); } } diff --git a/src/main/java/net/nuclearteam/createnuclear/content/multiblock/input/fluid/PlayerInteracteReactorFluidInput.java b/src/main/java/net/nuclearteam/createnuclear/content/multiblock/input/fluid/PlayerInteractReactorFluidInput.java similarity index 99% rename from src/main/java/net/nuclearteam/createnuclear/content/multiblock/input/fluid/PlayerInteracteReactorFluidInput.java rename to src/main/java/net/nuclearteam/createnuclear/content/multiblock/input/fluid/PlayerInteractReactorFluidInput.java index 5cb663f..ca1db27 100644 --- a/src/main/java/net/nuclearteam/createnuclear/content/multiblock/input/fluid/PlayerInteracteReactorFluidInput.java +++ b/src/main/java/net/nuclearteam/createnuclear/content/multiblock/input/fluid/PlayerInteractReactorFluidInput.java @@ -24,7 +24,7 @@ import net.neoforged.neoforge.fluids.FluidStack; import net.neoforged.neoforge.fluids.capability.IFluidHandler; -public class PlayerInteracteReactorFluidInput { +public class PlayerInteractReactorFluidInput { public static InteractionResult interact(Level level, BlockPos pos, Player player, InteractionHand hand, ItemStack stack, boolean onClient, BlockHitResult ray) { FluidExchange exchange = null; ReactorFluidInputEntity be = (ReactorFluidInputEntity) level.getBlockEntity(pos); diff --git a/src/main/java/net/nuclearteam/createnuclear/content/multiblock/input/fluid/ReactorFluidInput.java b/src/main/java/net/nuclearteam/createnuclear/content/multiblock/input/fluid/ReactorFluidInput.java index 00c6713..d70af53 100644 --- a/src/main/java/net/nuclearteam/createnuclear/content/multiblock/input/fluid/ReactorFluidInput.java +++ b/src/main/java/net/nuclearteam/createnuclear/content/multiblock/input/fluid/ReactorFluidInput.java @@ -86,7 +86,7 @@ public BlockState getStateForPlacement(BlockPlaceContext context) { if (!player.isCreative()) return ItemInteractionResult.PASS_TO_DEFAULT_BLOCK_INTERACTION; - InteractionResult result = PlayerInteracteReactorFluidInput.interact(world, pos, player, hand, heldItem, onClient, ray); + InteractionResult result = PlayerInteractReactorFluidInput.interact(world, pos, player, hand, heldItem, onClient, ray); // Convertit le vieux InteractionResult en ItemInteractionResult si nécessaire pour NeoForge return result.consumesAction() ? ItemInteractionResult.SUCCESS : ItemInteractionResult.PASS_TO_DEFAULT_BLOCK_INTERACTION; diff --git a/src/main/java/net/nuclearteam/createnuclear/content/multiblock/input/fluid/ReactorFluidInputEntity.java b/src/main/java/net/nuclearteam/createnuclear/content/multiblock/input/fluid/ReactorFluidInputEntity.java index 7f5ce31..462d6c0 100644 --- a/src/main/java/net/nuclearteam/createnuclear/content/multiblock/input/fluid/ReactorFluidInputEntity.java +++ b/src/main/java/net/nuclearteam/createnuclear/content/multiblock/input/fluid/ReactorFluidInputEntity.java @@ -64,9 +64,15 @@ public static int getCapacityForReactorSize(int reactorSize) { }; } - /** Applique la capacité correspondant à la taille du réacteur à laquelle cet input appartient. */ - public void applyReactorTierCapacity(int reactorSize) { - int capacity = getCapacityForReactorSize(reactorSize); + /** + * Applies an explicit tank capacity to this input. + *

+ * The per-reactor-size capacity ({@link #getCapacityForReactorSize(int)}) is the TOTAL the + * reactor should hold, split across all fluid inputs by {@code ReactorAssembler}, so the sum + * of every input's capacity stays equal to the configured value no matter how many inputs the + * player places. + */ + public void applyCapacity(int capacity) { if (internalTank.getCapacity() == capacity) return; internalTank.setCapacity(capacity); if (level != null && !level.isClientSide) { diff --git a/src/main/java/net/nuclearteam/createnuclear/content/multiblock/input/item/ReactorRodInput.java b/src/main/java/net/nuclearteam/createnuclear/content/multiblock/input/item/ReactorRodInput.java index 3f0a4ec..50a297d 100644 --- a/src/main/java/net/nuclearteam/createnuclear/content/multiblock/input/item/ReactorRodInput.java +++ b/src/main/java/net/nuclearteam/createnuclear/content/multiblock/input/item/ReactorRodInput.java @@ -4,9 +4,9 @@ import com.simibubi.create.foundation.block.IBE; import com.simibubi.create.foundation.item.ItemHelper; import net.minecraft.core.BlockPos; -import net.minecraft.core.Vec3i; import net.minecraft.world.InteractionHand; import net.minecraft.world.ItemInteractionResult; + import net.minecraft.world.entity.LivingEntity; import net.minecraft.world.entity.player.Player; import net.minecraft.world.item.BlockItem; @@ -15,26 +15,26 @@ import net.minecraft.world.level.BlockGetter; import net.minecraft.world.level.Level; import net.minecraft.world.level.block.Block; -import net.minecraft.world.level.block.entity.BlockEntity; import net.minecraft.world.level.block.entity.BlockEntityType; import net.minecraft.world.level.block.state.BlockState; import net.minecraft.world.level.block.state.StateDefinition; + import net.minecraft.world.phys.BlockHitResult; import net.minecraft.world.phys.shapes.CollisionContext; import net.minecraft.world.phys.shapes.VoxelShape; + import net.nuclearteam.createnuclear.CNBlockEntityTypes; -import net.nuclearteam.createnuclear.CNBlocks; import net.nuclearteam.createnuclear.CNShapes; -import net.nuclearteam.createnuclear.content.multiblock.MultiblockHelpers; -import net.nuclearteam.createnuclear.content.multiblock.controller.ReactorControllerBlock; import net.nuclearteam.createnuclear.content.multiblock.controller.ReactorControllerBlockEntity; -import net.nuclearteam.createnuclear.foundation.block.HorizontalDirectionalReactorBlock; +import net.nuclearteam.createnuclear.content.multiblock.MultiblockHelpers; import net.nuclearteam.createnuclear.foundation.block.MultiDirectionalReactorBlock; import org.jetbrains.annotations.NotNull; import javax.annotation.Nullable; -import java.util.List; +import javax.annotation.ParametersAreNonnullByDefault; + +@ParametersAreNonnullByDefault public class ReactorRodInput extends MultiDirectionalReactorBlock implements IWrenchable, IBE { public ReactorRodInput(Properties properties) { @@ -59,7 +59,7 @@ public ItemInteractionResult useItemOn(ItemStack stack, BlockState state, Level if (level.isClientSide()) {return ItemInteractionResult.SUCCESS;} withBlockEntityDo(level, pos, be -> player.openMenu(be, be::sendToMenu)); - return ItemInteractionResult.PASS_TO_DEFAULT_BLOCK_INTERACTION; + return ItemInteractionResult.SUCCESS; } @Override @@ -74,54 +74,18 @@ public void setPlacedBy(Level level, BlockPos pos, BlockState state, @Nullable L MultiblockHelpers.handleAdvancedPlacedBy(pos, level, pPlacer); } - // @Override // ! may be useless - // public void setPlacedBy(Level worldIn, BlockPos pos, BlockState state, LivingEntity placer, ItemStack stack) { - // super.setPlacedBy(worldIn, pos, state, placer, stack); - // if (worldIn.isClientSide) - // return; - // if (stack == null) - // return; - // withBlockEntityDo(worldIn, pos, be -> { - // CompoundTag orCreateTag = stack.getOrCreateTag(); - // be.readInventory(orCreateTag.getCompound("Inventory")); - // }); - // } - - @Override - public void playerDestroy(Level level, Player player, BlockPos pos, BlockState state, @Nullable BlockEntity blockEntity, ItemStack tool) { - super.playerDestroy(level, player, pos, state, blockEntity, tool); - List players = level.players(); - FindController(pos, level, players, false); - } @Override public void onRemove(BlockState pState, Level pLevel, BlockPos pPos, BlockState pNewState, boolean pIsMoving) { - super.onRemove(pState, pLevel, pPos, pNewState, pIsMoving); - - withBlockEntityDo(pLevel, pPos, be -> ItemHelper.dropContents(pLevel, pPos, be.inventory)); - pLevel.removeBlockEntity(pPos); - - List players = pLevel.players(); - FindController(pPos, pLevel, players, false); - } - - public ReactorControllerBlock FindController(BlockPos blockPos, Level level, List players, boolean first){ // Function that checks the surrounding blocks in order - BlockPos newBlock; // to find the controller and verify the pattern - Vec3i pos = new Vec3i(blockPos.getX(), blockPos.getY(), blockPos.getZ()); - for (int x = pos.getX()-5; x != pos.getX()+5; x+=1) { - for (int z = pos.getZ()-5; z != pos.getZ()+5; z+=1) { - newBlock = new BlockPos(x, pos.getY(), z); - if (level.getBlockState(newBlock).is(CNBlocks.REACTOR_CONTROLLER.get())) { // verifying the pattern - ReactorControllerBlock controller = (ReactorControllerBlock) level.getBlockState(newBlock).getBlock(); - controller.Verify(level.getBlockState(newBlock), newBlock, level, players, first); - ReactorControllerBlockEntity entity = controller.getBlockEntity(level, newBlock); - if (entity.created) { - return controller; - } - } - } + // Only act when the block is actually removed/replaced, not on a mere state change + // (e.g. a FACING update) — otherwise rotating the block would spill its rods. + if (!pState.is(pNewState.getBlock())) { + // Drop the contents BEFORE super removes the block entity: super.onRemove() + // calls level.removeBlockEntity(pos), after which the inventory is gone. + withBlockEntityDo(pLevel, pPos, be -> ItemHelper.dropContents(pLevel, pPos, be.inventory)); + MultiblockHelpers.handleRemoval(pPos, pLevel, ReactorControllerBlockEntity::removeInput); } - return null; + super.onRemove(pState, pLevel, pPos, pNewState, pIsMoving); } @Override @@ -129,7 +93,6 @@ public BlockState getStateForPlacement(BlockPlaceContext context) { return this.defaultBlockState() .setValue(FACING, context.getNearestLookingDirection().getOpposite()); } - @Override public @NotNull VoxelShape getShape(BlockState state, BlockGetter worldIn, BlockPos pos, CollisionContext context) { return CNShapes.REACTOR_INPUT.get(state.getValue(FACING)); @@ -144,4 +107,4 @@ public Class getBlockEntityClass() { public BlockEntityType getBlockEntityType() { return CNBlockEntityTypes.REACTOR_INPUT.get(); } -} \ No newline at end of file +} diff --git a/src/main/java/net/nuclearteam/createnuclear/content/multiblock/input/item/ReactorRodInputEntity.java b/src/main/java/net/nuclearteam/createnuclear/content/multiblock/input/item/ReactorRodInputEntity.java index 671ede8..b7819c8 100644 --- a/src/main/java/net/nuclearteam/createnuclear/content/multiblock/input/item/ReactorRodInputEntity.java +++ b/src/main/java/net/nuclearteam/createnuclear/content/multiblock/input/item/ReactorRodInputEntity.java @@ -2,7 +2,6 @@ import com.simibubi.create.foundation.blockEntity.SmartBlockEntity; import com.simibubi.create.foundation.blockEntity.behaviour.BlockEntityBehaviour; -import lib.multiblock.SimpleMultiBlockAislePatternBuilder; import net.minecraft.core.BlockPos; import net.minecraft.core.HolderLookup; import net.minecraft.nbt.CompoundTag; @@ -17,13 +16,10 @@ import net.neoforged.neoforge.capabilities.Capabilities; import net.neoforged.neoforge.capabilities.RegisterCapabilitiesEvent; import net.nuclearteam.createnuclear.CNBlockEntityTypes; -import net.nuclearteam.createnuclear.CNBlocks; import javax.annotation.Nullable; import java.util.List; -import static net.nuclearteam.createnuclear.content.multiblock.CNMultiblock.*; - public class ReactorRodInputEntity extends SmartBlockEntity implements MenuProvider { protected BlockPos block; @@ -70,25 +66,6 @@ public Level getLevel() { } - private static BlockPos FindController(char character) { - return SimpleMultiBlockAislePatternBuilder.start() - .aisle(AAAAA, AAAAA, AAAAA, AAAAA, AAAAA) - .aisle(AABAA, ADADA, BACAB, ADADA, AABAA) - .aisle(AABAA, ADADA, BACAB, ADADA, AABAA) - .aisle(AAIAA, ADADA, BACAB, ADADA, AAAA) - .aisle(AABAA, ADADA, BACAB, ADADA, AABAA) - .aisle(AABAA, ADADA, BACAB, ADADA, AABAA) - .aisle(AAAAA, AAAAA, AAAAA, AAAAA, AAOAA) - .where('A', a -> a.getState().is(CNBlocks.REACTOR_CASING.get())) - .where('B', a -> a.getState().is(CNBlocks.REACTOR_FRAME.get())) - .where('C', a -> a.getState().is(CNBlocks.REACTOR_CORE.get())) - .where('D', a -> a.getState().is(CNBlocks.REACTOR_COOLER.get())) - .where('*', a -> a.getState().is(CNBlocks.REACTOR_CONTROLLER.get())) - .where('O', a -> a.getState().is(CNBlocks.REACTOR_OUTPUT.get())) - .where('I', a -> a.getState().is(CNBlocks.REACTOR_ROD_INPUT.get())) - .getDistanceController(character); - } - @Override public Component getDisplayName() { return Component.translatable("gui.createnuclear.reactor_input.title"); diff --git a/src/main/java/net/nuclearteam/createnuclear/content/multiblock/input/item/ReactorRodInputInventory.java b/src/main/java/net/nuclearteam/createnuclear/content/multiblock/input/item/ReactorRodInputInventory.java index 351dfa8..117abfb 100644 --- a/src/main/java/net/nuclearteam/createnuclear/content/multiblock/input/item/ReactorRodInputInventory.java +++ b/src/main/java/net/nuclearteam/createnuclear/content/multiblock/input/item/ReactorRodInputInventory.java @@ -2,15 +2,16 @@ import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.Level; import net.neoforged.neoforge.items.ItemStackHandler; -import net.nuclearteam.createnuclear.CNItems; +import net.nuclearteam.createnuclear.api.multiblock.rods.RodType.TypeRodPredicate; import org.jetbrains.annotations.NotNull; public class ReactorRodInputInventory extends ItemStackHandler { private final ReactorRodInputEntity be; public ReactorRodInputInventory(ReactorRodInputEntity be) { - super(2); + super(1); this.be = be; } @@ -22,10 +23,10 @@ protected void onContentsChanged(int slot) { @Override public boolean isItemValid(int slot, @NotNull ItemStack stack) { + Level level = be.getLevel(); return switch (slot) { - case 0 -> CNItems.URANIUM_ROD.isIn(stack); - case 1 -> CNItems.GRAPHITE_ROD.isIn(stack); + case 0 -> level != null && (TypeRodPredicate.isFuel(stack, level) || TypeRodPredicate.isCooled(stack, level)); default -> !super.isItemValid(slot, stack); }; } -} \ No newline at end of file +} diff --git a/src/main/java/net/nuclearteam/createnuclear/content/multiblock/input/item/ReactorRodInputMenu.java b/src/main/java/net/nuclearteam/createnuclear/content/multiblock/input/item/ReactorRodInputMenu.java index bcca196..b40d581 100644 --- a/src/main/java/net/nuclearteam/createnuclear/content/multiblock/input/item/ReactorRodInputMenu.java +++ b/src/main/java/net/nuclearteam/createnuclear/content/multiblock/input/item/ReactorRodInputMenu.java @@ -30,13 +30,42 @@ public static ReactorRodInputMenu create(int id, Inventory inv, ReactorRodInputE } @Override - public ItemStack quickMoveStack(Player player, int index) { - Slot clickedSlot = getSlot(index); - if (!clickedSlot.hasItem()) return ItemStack.EMPTY; - ItemStack stack = clickedSlot.getItem(); - if (index < 2) moveItemStackTo(stack, 2, slots.size(), false); - else moveItemStackTo(stack, 0, 2, false); - return ItemStack.EMPTY; + public ItemStack quickMoveStack(Player playerIn, int index) { + ItemStack result = ItemStack.EMPTY; + Slot slot = this.slots.get(index); + if (slot != null && slot.hasItem()) { + ItemStack stackInSlot = slot.getItem(); + result = stackInSlot.copy(); + + int playerInventorySize = player.getInventory().items.size(); // normally 36 + int containerStart = playerInventorySize; + int containerEnd = containerStart + 1; // 1 machine slot + + // Clicked a machine slot (after the player slots) + if (index >= containerStart && index < containerEnd) { + if (!this.moveItemStackTo(stackInSlot, 0, playerInventorySize, true)) { + return ItemStack.EMPTY; + } + } else { + // Clicked a player slot — try moving the stack into the machine container + if (!this.moveItemStackTo(stackInSlot, containerStart, containerEnd, false)) { + return ItemStack.EMPTY; + } + } + + if (stackInSlot.isEmpty()) { + slot.set(ItemStack.EMPTY); + } else { + slot.setChanged(); + } + + if (stackInSlot.getCount() == result.getCount()) { + return ItemStack.EMPTY; + } + + slot.onTake(playerIn, stackInSlot); + } + return result; } @@ -71,13 +100,9 @@ protected void addSlots() { } } - Slot slot1 = new SlotItemHandler(contentHolder.inventory, 0, 24, 29); - Slot slot2 = new SlotItemHandler(contentHolder.inventory, 1, 57, 29); + Slot slot1 = new SlotItemHandler(contentHolder.inventory, 0, 42, 29); addSlot(slot1); - addSlot(slot2); - - } @Override diff --git a/src/main/java/net/nuclearteam/createnuclear/content/multiblock/output/ReactorOutput.java b/src/main/java/net/nuclearteam/createnuclear/content/multiblock/output/ReactorOutput.java index 7ed93a4..f56e114 100644 --- a/src/main/java/net/nuclearteam/createnuclear/content/multiblock/output/ReactorOutput.java +++ b/src/main/java/net/nuclearteam/createnuclear/content/multiblock/output/ReactorOutput.java @@ -1,25 +1,29 @@ package net.nuclearteam.createnuclear.content.multiblock.output; +import com.simibubi.create.AllBlocks; import com.simibubi.create.content.equipment.wrench.IWrenchable; import com.simibubi.create.content.kinetics.base.DirectionalKineticBlock; import com.simibubi.create.foundation.block.IBE; +import net.createmod.catnip.placement.IPlacementHelper; +import net.createmod.catnip.placement.PlacementHelpers; +import net.createmod.catnip.placement.PlacementOffset; import net.minecraft.MethodsReturnNonnullByDefault; import net.minecraft.core.BlockPos; import net.minecraft.core.Direction; import net.minecraft.core.Direction.Axis; -import net.minecraft.core.Vec3i; +import net.minecraft.world.level.block.state.properties.BlockStateProperties; import net.minecraft.world.InteractionHand; -import net.minecraft.world.InteractionResult; import net.minecraft.world.ItemInteractionResult; +import net.minecraft.world.entity.LivingEntity; import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.BlockItem; import net.minecraft.world.item.ItemStack; import net.minecraft.world.item.context.BlockPlaceContext; import net.minecraft.world.level.BlockGetter; import net.minecraft.world.level.Level; import net.minecraft.world.level.LevelReader; import net.minecraft.world.level.block.Block; -import net.minecraft.world.level.block.entity.BlockEntity; import net.minecraft.world.level.block.entity.BlockEntityType; import net.minecraft.world.level.block.state.BlockState; import net.minecraft.world.level.block.state.StateDefinition; @@ -29,76 +33,50 @@ import net.minecraft.world.phys.shapes.CollisionContext; import net.minecraft.world.phys.shapes.VoxelShape; import net.nuclearteam.createnuclear.*; -import net.nuclearteam.createnuclear.content.multiblock.controller.ReactorControllerBlock; +import net.nuclearteam.createnuclear.content.multiblock.MultiblockHelpers; import net.nuclearteam.createnuclear.content.multiblock.controller.ReactorControllerBlockEntity; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; import javax.annotation.ParametersAreNonnullByDefault; -import java.util.List; -import java.util.Objects; +import java.util.function.Predicate; @ParametersAreNonnullByDefault @MethodsReturnNonnullByDefault public class ReactorOutput extends DirectionalKineticBlock implements IWrenchable, IBE { - - //public static final IntegerProperty SPEED = IntegerProperty.create("speed", 0, 256); public static final IntegerProperty DIR = IntegerProperty.create("dir", 0, 2); + private static final int placementHelperId = PlacementHelpers.register(new PlacementHelper()); + public ReactorOutput(Properties properties) { super(properties); } @Override protected void createBlockStateDefinition(StateDefinition.Builder builder) { - //builder.add(SPEED); builder.add(DIR); super.createBlockStateDefinition(builder); } - @Override - public ItemInteractionResult useItemOn(ItemStack stack, BlockState state, Level level, BlockPos pos, Player player, InteractionHand hand, BlockHitResult hitResult) { - if (level.isClientSide) - return ItemInteractionResult.SUCCESS; - else { - ReactorControllerBlock controller = FindController(pos, level, level.players(), false); - if (controller != null){ - ReactorControllerBlockEntity entity = controller.getBlockEntity(level, pos.above(3)); - assert entity != null; - if (entity.getAssembled()){ - ReactorOutputEntity control = Objects.requireNonNull(getBlockEntity(level, pos)); - if (control.getDir() == 0) - control.setDir(1, level, pos); - else control.setDir(0, level, pos); - } - } - return ItemInteractionResult.CONSUME; - } - } @Override public void onPlace(BlockState state, Level level, BlockPos pos, BlockState oldState, boolean isMoving) { super.onPlace(state, level, pos, oldState, isMoving); - List players = level.players(); - FindController(pos, level, players, true); + MultiblockHelpers.handleOnPlace(pos, level, ReactorControllerBlockEntity::addOutput); } @Override - public void playerDestroy(Level level, Player player, BlockPos pos, BlockState state, @Nullable BlockEntity blockEntity, ItemStack tool) { - super.playerDestroy(level, player, pos, state, blockEntity, tool); - List players = level.players(); - FindController(pos, level, players, false); + public void setPlacedBy(Level level, BlockPos pos, BlockState state, @javax.annotation.Nullable LivingEntity pPlacer, ItemStack stack) { + super.setPlacedBy(level, pos, state, pPlacer, stack); + MultiblockHelpers.handleAdvancedPlacedBy(pos, level, pPlacer); } @Override public void onRemove(BlockState pState, Level pLevel, BlockPos pPos, BlockState pNewState, boolean pIsMoving) { super.onRemove(pState, pLevel, pPos, pNewState, pIsMoving); - List players = pLevel.players(); - FindController(pPos, pLevel, players, false); + MultiblockHelpers.handleRemoval(pPos, pLevel, ReactorControllerBlockEntity::removeOutput); } @Override - public @NotNull VoxelShape getShape(BlockState state, BlockGetter worldIn, BlockPos pos, CollisionContext context) { + public VoxelShape getShape(BlockState state, BlockGetter worldIn, BlockPos pos, CollisionContext context) { return CNShapes.REACTOR_OUTPUT.get(state.getValue(FACING)); } @@ -108,7 +86,21 @@ public BlockState getStateForPlacement(BlockPlaceContext context) { if ((context.getPlayer() != null && context.getPlayer() .isShiftKeyDown()) || preferred == null) return super.getStateForPlacement(context); - return defaultBlockState().setValue(FACING, preferred)/*.setValue(SPEED, 0)*/.setValue(DIR, 0); + return defaultBlockState().setValue(FACING, preferred).setValue(DIR, 0); + } + + @Override + public ItemInteractionResult useItemOn(ItemStack stack, BlockState state, Level level, BlockPos pos, Player player, InteractionHand hand, BlockHitResult hitResult) { + ItemStack heldItem = player.getItemInHand(hand); + IPlacementHelper placementHelper = PlacementHelpers.get(placementHelperId); + if (!player.isShiftKeyDown() && player.mayBuild()) { + if (placementHelper.matchesItem(heldItem) && placementHelper.getOffset(player, level, state, pos, hitResult) + .placeInWorld(level, (BlockItem) heldItem.getItem(), player, hand, hitResult) + .consumesAction()) + return ItemInteractionResult.SUCCESS; + } + + return super.useItemOn(stack, state, level, pos, player, hand, hitResult); } // IRotate: @@ -129,6 +121,11 @@ public boolean hideStressImpact() { return true; } + @Override + public boolean isPathfindable(BlockState state, PathComputationType type) { + return false; + } + @Override public Class getBlockEntityClass() { return ReactorOutputEntity.class; @@ -139,18 +136,27 @@ public BlockEntityType getBlockEntityType() { return CNBlockEntityTypes.REACTOR_OUTPUT.get(); } - public ReactorControllerBlock FindController(BlockPos blockPos, Level level, List players, boolean first) { - BlockPos newBlock; // to find the controller and verify the pattern - Vec3i pos = new Vec3i(blockPos.getX(), blockPos.getY(), blockPos.getZ()); - newBlock = new BlockPos(pos.getX(), pos.getY() + 3, pos.getZ()); - if (level.getBlockState(newBlock).is(CNBlocks.REACTOR_CONTROLLER.get())) { // verifying the pattern - ReactorControllerBlock controller = (ReactorControllerBlock) level.getBlockState(newBlock).getBlock(); - controller.Verify(level.getBlockState(newBlock), newBlock, level, players, first); - ReactorControllerBlockEntity entity = controller.getBlockEntity(level, newBlock); - if (entity.created) { - return controller; - } + private static class PlacementHelper implements IPlacementHelper { + + @Override + public Predicate getItemPredicate() { + return AllBlocks.SHAFT::isIn; + } + + @Override + public Predicate getStatePredicate() { + return s -> s.getBlock() instanceof ReactorOutput; + } + + @Override + public PlacementOffset getOffset(Player player, Level world, BlockState state, BlockPos pos, BlockHitResult ray) { + Direction facing = state.getValue(FACING); + BlockPos target = pos.relative(facing); + + if (!world.getBlockState(target).canBeReplaced()) + return PlacementOffset.fail(); + + return PlacementOffset.success(target, s -> s.setValue(BlockStateProperties.AXIS, facing.getAxis())); } - return null; } -} \ No newline at end of file +} diff --git a/src/main/java/net/nuclearteam/createnuclear/content/multiblock/output/ReactorOutputEntity.java b/src/main/java/net/nuclearteam/createnuclear/content/multiblock/output/ReactorOutputEntity.java index 69fc051..565e70f 100644 --- a/src/main/java/net/nuclearteam/createnuclear/content/multiblock/output/ReactorOutputEntity.java +++ b/src/main/java/net/nuclearteam/createnuclear/content/multiblock/output/ReactorOutputEntity.java @@ -2,10 +2,8 @@ import com.mojang.blaze3d.vertex.PoseStack; import com.simibubi.create.content.kinetics.base.GeneratingKineticBlockEntity; -import com.simibubi.create.content.kinetics.motor.KineticScrollValueBehaviour; import com.simibubi.create.foundation.blockEntity.behaviour.BlockEntityBehaviour; import com.simibubi.create.foundation.blockEntity.behaviour.ValueBoxTransform; -import com.simibubi.create.foundation.blockEntity.behaviour.scrollValue.ScrollValueBehaviour; import com.simibubi.create.foundation.utility.CreateLang; import dev.engine_room.flywheel.lib.transform.TransformStack; import net.createmod.catnip.math.AngleHelper; @@ -13,137 +11,130 @@ import net.minecraft.ChatFormatting; import net.minecraft.core.BlockPos; import net.minecraft.core.Direction; +import net.minecraft.core.HolderLookup; +import net.minecraft.nbt.CompoundTag; import net.minecraft.network.chat.Component; -import net.minecraft.world.level.BlockGetter; -import net.minecraft.world.level.Level; import net.minecraft.world.level.LevelAccessor; -import net.minecraft.world.level.block.Block; import net.minecraft.world.level.block.entity.BlockEntityType; import net.minecraft.world.level.block.state.BlockState; import net.minecraft.world.phys.Vec3; -import net.nuclearteam.createnuclear.CNBlocks; +import net.minecraft.util.Mth; -import net.nuclearteam.createnuclear.content.multiblock.controller.ReactorControllerBlock; -import net.nuclearteam.createnuclear.content.multiblock.controller.ReactorControllerBlockEntity; -import net.nuclearteam.createnuclear.foundation.utility.CreateNuclearLang; +import net.nuclearteam.createnuclear.content.multiblock.pattern.ReactorPattern; import java.util.List; -import java.util.Objects; - -import static net.nuclearteam.createnuclear.content.multiblock.output.ReactorOutput.DIR; - public class ReactorOutputEntity extends GeneratingKineticBlockEntity { - public int speed = 1; + public int speed = 0; public float heat = 0; - ReactorControllerBlock controller = null; - ReactorControllerBlockEntity controllerEntity = null; + protected ReactorPattern pattern = new ReactorPattern(); - protected ScrollValueBehaviour generatedSpeed; + protected float generatedSpeed; public ReactorOutputEntity(BlockEntityType type, BlockPos pos, BlockState state) { super(type, pos, state); } - //KineticBlockEntity + @Override public void addBehaviours(List behaviours) { super.addBehaviours(behaviours); - generatedSpeed = new KineticScrollValueBehaviour(CreateNuclearLang.translateDirect("kinetics.reactor_output.rotation_speed"), this, new ReactorOutputValue()); - generatedSpeed.between(-1500000, 1500000); - generatedSpeed.setValue(speed); - generatedSpeed.withCallback(i -> this.updateGeneratedRotation()); - behaviours.add(generatedSpeed); - - } - @Override - public void tick() { - super.tick(); - - BlockGetter level = getLevel(); - - if (level.getBlockState(getBlockPos().above(3)).getBlock() == CNBlocks.REACTOR_CONTROLLER.get()) { - controller = (ReactorControllerBlock) level.getBlockState(getBlockPos().above(3)).getBlock(); - controllerEntity = (ReactorControllerBlockEntity) level.getBlockEntity(getBlockPos().above(3)); - if (controllerEntity != null) { - if (!controllerEntity.getAssembled() && getSpeed() != 0) { - setSpeed(0); - } - } - } else setSpeed(0); } @Override - public boolean addToGoggleTooltip(List tooltip, boolean isPlayerSneaking) { + public void lazyTick() { + super.lazyTick(); - float stressBase = calculateAddedStressCapacity(); + determineSpeed(); + } - CreateLang.translate("gui.goggles.generator_stats") - .forGoggles(tooltip); - CreateLang.translate("tooltip.capacityProvided") - .style(ChatFormatting.GRAY) - .forGoggles(tooltip); + public void determineSpeed() { + int deterSpeed = this.speed; + setSpeedAndUpdate(deterSpeed); + } - float speed = getTheoreticalSpeed(); - speed = Math.abs(speed); + public void setSpeedAndUpdate(int speed) { + if (generatedSpeed == speed) return; - float stressTotal = stressBase * speed; + generatedSpeed = (float) speed; - CreateLang.number(stressTotal) - .translate("generic.unit.stress") - .style(ChatFormatting.AQUA) - .space() - .add(CreateLang.translate("gui.goggles.at_current_speed") - .style(ChatFormatting.DARK_GRAY)) - .forGoggles(tooltip, 1); - return true; + updateGeneratedRotation(); + setChanged(); } + // Tracks the output's linked block position for persistence across reloads. + private BlockPos outputPos; + @Override - public void initialize() { - super.initialize(); + protected void read(CompoundTag compound, HolderLookup.Provider registries, boolean clientPacket) { + super.read(compound, registries, clientPacket); - if (!hasSource() || getGeneratedSpeed() > getTheoreticalSpeed()) - { - FindController(getBlockPos(), Objects.requireNonNull(getLevel())); + // Restore the generated rotation speed + generatedSpeed = compound.getFloat("generatedSpeed"); + + // Restore the output position, if present in the tag + if (compound.contains("outputPos")) { + this.outputPos = BlockPos.of(compound.getLong("outputPos")); } } - public void FindController(BlockPos pos, Level level){ - if (level.getBlockState(pos.above(3)).getBlock() == CNBlocks.REACTOR_CONTROLLER.get()){ - ReactorControllerBlock controller = (ReactorControllerBlock)level.getBlockState(pos.above(3)).getBlock(); - controller.Verify(controller.defaultBlockState(), pos.above(3), level, level.players(), false); + @Override + public void write(CompoundTag compound, HolderLookup.Provider registries, boolean clientPacket) { + super.write(compound, registries, clientPacket); + + // Persist the generated rotation speed + compound.putFloat("generatedSpeed", generatedSpeed); + + // Persist the output position, if set + if (this.outputPos != null) { + compound.putLong("outputPos", this.outputPos.asLong()); } } - public void setSpeed(int speed) { - this.speed = speed; - } + @Override + public boolean addToGoggleTooltip(List tooltip, boolean isPlayerSneaking) { - public int getDir() { - BlockState state = getBlockState(); - return state.getValue(DIR); - } + float stressBase = calculateAddedStressCapacity(); - public void setDir(int dir, Level level, BlockPos pos) { - BlockState state = getBlockState(); - level.setBlockAndUpdate(pos, state.setValue(DIR, dir)); - } + CreateLang.translate("gui.goggles.generator_stats") + .forGoggles(tooltip); + CreateLang.translate("tooltip.capacityProvided") + .style(ChatFormatting.GRAY) + .forGoggles(tooltip); + + float speed = getTheoreticalSpeed(); + speed = Math.abs(speed); + + float stressTotal = stressBase * speed; + + CreateLang.number(stressTotal) + .translate("generic.unit.stress") + .style(ChatFormatting.AQUA) + .space() + .add(CreateLang.translate("gui.goggles.at_current_speed") + .style(ChatFormatting.DARK_GRAY)) + .forGoggles(tooltip, 1); + return true; + } @Override - public float getGeneratedSpeed() { - if (!CNBlocks.REACTOR_OUTPUT.has(getBlockState())) - return 0; - return speed; //convertToDirection(speed, getBlockState().getValue(ReactorOutput.FACING)); + public void initialize() { + super.initialize(); + + if (!hasSource() || getGeneratedSpeed() > getTheoreticalSpeed()) + { + assert level != null; + pattern.findController(getBlockPos(), level, true); + } } @Override - protected Block getStressConfigKey() { - return super.getStressConfigKey(); + public float getGeneratedSpeed() { + return Mth.clamp(generatedSpeed, 0, 1500000); } - class ReactorOutputValue extends ValueBoxTransform.Sided { + static class ReactorOutputValue extends ValueBoxTransform.Sided { @Override protected Vec3 getSouthLocation() { @@ -178,4 +169,4 @@ protected boolean isSideActive(BlockState state, Direction direction) { } } -} \ No newline at end of file +} diff --git a/src/main/java/net/nuclearteam/createnuclear/content/multiblock/pattern/ReactorPattern.java b/src/main/java/net/nuclearteam/createnuclear/content/multiblock/pattern/ReactorPattern.java index 2dd9510..c776855 100644 --- a/src/main/java/net/nuclearteam/createnuclear/content/multiblock/pattern/ReactorPattern.java +++ b/src/main/java/net/nuclearteam/createnuclear/content/multiblock/pattern/ReactorPattern.java @@ -1,27 +1,27 @@ package net.nuclearteam.createnuclear.content.multiblock.pattern; -import lib.multiblock.SimpleMultiBlockAislePatternBuilder; import net.minecraft.core.BlockPos; import net.minecraft.core.Vec3i; import net.minecraft.world.level.Level; import net.minecraft.world.level.block.Block; import net.minecraft.world.level.block.state.BlockState; import net.minecraft.world.level.block.state.pattern.BlockInWorld; +import net.minecraft.world.level.levelgen.structure.BoundingBox; import net.nuclearteam.createnuclear.CNBlocks; import net.nuclearteam.createnuclear.content.multiblock.ReactorAssembler; import net.nuclearteam.createnuclear.content.multiblock.controller.ReactorControllerBlockEntity; +import org.jetbrains.annotations.Nullable; import java.util.function.Predicate; public class ReactorPattern { - //public BlockPos VerifyPattern(char character) {} private static final Predicate blockInWorldAPredicate = state -> stateIs(CNBlocks.REACTOR_CASING.get()).test(state) || stateIs(CNBlocks.REACTOR_OUTPUT.get()).test(state) || stateIs(CNBlocks.REACTOR_ROD_INPUT.get()).test(state) || stateIs(CNBlocks.REACTOR_FLUID_INPUT.get()).test(state) - //|| stateIs(CNBlocks.REACTOR_ALARM.get()).test(state) + || stateIs(CNBlocks.REACTOR_ALARM.get()).test(state) ; private static Predicate stateIs(Block block) { @@ -31,78 +31,22 @@ private static Predicate stateIs(Block block) { }; } - public BlockPos VerifyPattern5x5(char character) { - return SimpleMultiBlockAislePatternBuilder.start() - .aisle("OOOOO", "OAAAO", "OAAAO", "OAAAO", "OOOOO") - .aisle("OABAO", "ODDDO", "BDCDB", "ODDDO", "OABAO") - .aisle("OABAO", "ODDDO", "BDCDB", "ODDDO", "OABAO") - .aisle("OABAO", "ODDDO", "BDCDB", "ODDDO", "OA*AO") - .aisle("OABAO", "ODDDO", "BDCDB", "ODDDO", "OABAO") - .aisle("OABAO", "ODDDO", "BDCDB", "ODDDO", "OABAO") - .aisle("OOOOO", "OAAAO", "OAAAO", "OAAAO", "OOOOO") - .where('A', blockInWorldAPredicate) - .where('B', a -> a.getState().is(CNBlocks.REACTOR_FRAME.get())) - .where('C', a -> a.getState().is(CNBlocks.REACTOR_CORE.get())) - .where('D', a -> a.getState().is(CNBlocks.REACTOR_COOLER.get())) - .where('*', a -> a.getState().is(CNBlocks.REACTOR_CONTROLLER.get())) - .where('O', a -> a.getState().is(CNBlocks.REACTOR_CASING.get())) - .getDistanceController(character); + @FunctionalInterface + private interface ControllerVisitor { + boolean visit(BlockPos controllerPos, ReactorControllerBlockEntity entity); } - public BlockPos VerifyPattern7x7(char character) { - return SimpleMultiBlockAislePatternBuilder.start() - .aisle("OOOOOOO", "OAAAAAO", "OAAAAAO", "OAAAAAO", "OAAAAAO", "OAAAAAO", "OOOOOOO") - .aisle("OABABAO", "ADDDDDA", "BDCDCDB", "ADDDDDA", "BDCDCDB", "ADDDDDA", "OABABAO") - .aisle("OABABAO", "ADDDDDA", "BDCDCDB", "ADDDDDA", "BDCDCDB", "ADDDDDA", "OABABAO") - .aisle("OABABAO", "ADDDDDA", "BDCDCDB", "ADDDDDA", "BDCDCDB", "ADDDDDA", "OABABAO") - .aisle("OABABAO", "ADDDDDA", "BDCDCDB", "ADDDDDA", "BDCDCDB", "ADDDDDA", "OAB*BAO") - .aisle("OABABAO", "ADDDDDA", "BDCDCDB", "ADDDDDA", "BDCDCDB", "ADDDDDA", "OABABAO") - .aisle("OABABAO", "ADDDDDA", "BDCDCDB", "ADDDDDA", "BDCDCDB", "ADDDDDA", "OABABAO") - .aisle("OABABAO", "ADDDDDA", "BDCDCDB", "ADDDDDA", "BDCDCDB", "ADDDDDA", "OABABAO") - .aisle("OOOOOOO", "OAAAAAO", "OAAAAAO", "OAAAAAO", "OAAAAAO", "OAAAAAO", "OOOOOOO") - .where('A', blockInWorldAPredicate) - .where('B', a -> a.getState().is(CNBlocks.REACTOR_FRAME.get())) - .where('C', a -> a.getState().is(CNBlocks.REACTOR_CORE.get())) - .where('D', a -> a.getState().is(CNBlocks.REACTOR_COOLER.get())) - .where('*', a -> a.getState().is(CNBlocks.REACTOR_CONTROLLER.get())) - .where('O', a -> a.getState().is(CNBlocks.REACTOR_CASING.get())) - .getDistanceController(character); - } - - public BlockPos VerifyPattern9x9(char character) { - return SimpleMultiBlockAislePatternBuilder.start() - .aisle("OOOOOOOOO", "OAAAAAAAO", "OAAAAAAAO", "OAAAAAAAO", "OAAAAAAAO", "OAAAAAAAO", "OAAAAAAAO", "OAAAAAAAO", "OOOOOOOOO") - .aisle("OBAABAABO", "BDDDDDDDB", "ADCDCDCDA", "ADDDDDDDA", "BDCDCDCDB", "ADDDDDDDA", "ADCDCDCDA", "BDDDDDDDB", "OBAABAABO") - .aisle("OBAABAABO", "BDDDDDDDB", "ADCDCDCDA", "ADDDDDDDA", "BDCDCDCDB", "ADDDDDDDA", "ADCDCDCDA", "BDDDDDDDB", "OBAABAABO") - .aisle("OBAABAABO", "BDDDDDDDB", "ADCDCDCDA", "ADDDDDDDA", "BDCDCDCDB", "ADDDDDDDA", "ADCDCDCDA", "BDDDDDDDB", "OBAABAABO") - .aisle("OBAABAABO", "BDDDDDDDB", "ADCDCDCDA", "ADDDDDDDA", "BDCDCDCDB", "ADDDDDDDA", "ADCDCDCDA", "BDDDDDDDB", "OBAABAABO") - .aisle("OBAABAABO", "BDDDDDDDB", "ADCDCDCDA", "ADDDDDDDA", "BDCDCDCDB", "ADDDDDDDA", "ADCDCDCDA", "BDDDDDDDB", "OBAA*AABO") - .aisle("OBAABAABO", "BDDDDDDDB", "ADCDCDCDA", "ADDDDDDDA", "BDCDCDCDB", "ADDDDDDDA", "ADCDCDCDA", "BDDDDDDDB", "OBAABAABO") - .aisle("OBAABAABO", "BDDDDDDDB", "ADCDCDCDA", "ADDDDDDDA", "BDCDCDCDB", "ADDDDDDDA", "ADCDCDCDA", "BDDDDDDDB", "OBAABAABO") - .aisle("OBAABAABO", "BDDDDDDDB", "ADCDCDCDA", "ADDDDDDDA", "BDCDCDCDB", "ADDDDDDDA", "ADCDCDCDA", "BDDDDDDDB", "OBAABAABO") - .aisle("OBAABAABO", "BDDDDDDDB", "ADCDCDCDA", "ADDDDDDDA", "BDCDCDCDB", "ADDDDDDDA", "ADCDCDCDA", "BDDDDDDDB", "OBAABAABO") - .aisle("OOOOOOOOO", "OAAAAAAAO", "OAAAAAAAO", "OAAAAAAAO", "OAAAAAAAO", "OAAAAAAAO", "OAAAAAAAO", "OAAAAAAAO", "OOOOOOOOO") - .where('A', blockInWorldAPredicate) - .where('B', a -> a.getState().is(CNBlocks.REACTOR_FRAME.get())) - .where('C', a -> a.getState().is(CNBlocks.REACTOR_CORE.get())) - .where('D', a -> a.getState().is(CNBlocks.REACTOR_COOLER.get())) - .where('*', a -> a.getState().is(CNBlocks.REACTOR_CONTROLLER.get())) - .where('O', a -> a.getState().is(CNBlocks.REACTOR_CASING.get())) - .getDistanceController(character); - } - - public void findController(BlockPos blockPos, Level level, boolean first){ + private void scanControllerCandidates(BlockPos blockPos, Level level, ControllerVisitor visitor) { BlockPos newBlock; Vec3i pos = new Vec3i(blockPos.getX(), blockPos.getY(), blockPos.getZ()); for (int y = pos.getY()-5; y != pos.getY()+6; y+=1) { for (int x = pos.getX()-9; x != pos.getX()+10; x+=1) { for (int z = pos.getZ()-9; z != pos.getZ()+10; z+=1) { newBlock = new BlockPos(x, y, z); - if (level.getBlockState(newBlock).is(CNBlocks.REACTOR_CONTROLLER.get())) { - if (first) { - ReactorAssembler.assemble(newBlock, level); - } else { - ReactorAssembler.disassemble(newBlock, level); + if (level.getBlockState(newBlock).is(CNBlocks.REACTOR_CONTROLLER.get()) + && level.getBlockEntity(newBlock) instanceof ReactorControllerBlockEntity entity) { + if (visitor.visit(newBlock, entity)) { + return; } } } @@ -110,31 +54,60 @@ public void findController(BlockPos blockPos, Level level, boolean first){ } } - public BlockPos findControllerPos(BlockPos blockPos, Level level){ - BlockPos newBlock; - Vec3i pos = new Vec3i(blockPos.getX(), blockPos.getY(), blockPos.getZ()); - for (int y = pos.getY()-5; y != pos.getY()+6; y+=1) { - for (int x = pos.getX()-9; x != pos.getX()+10; x+=1) { - for (int z = pos.getZ()-9; z != pos.getZ()+10; z+=1) { - newBlock = new BlockPos(x, y, z); - if (level.getBlockState(newBlock).is(CNBlocks.REACTOR_CONTROLLER.get())) { - if (level.getBlockEntity(newBlock) instanceof ReactorControllerBlockEntity entity) { - if (isInReactorRange(entity.getMultiblockPos(), blockPos)) { - return newBlock; - } - } - } + public void findController(BlockPos blockPos, Level level, boolean first){ + scanControllerCandidates(blockPos, level, ((controllerPos, entity) -> { + boolean inRange = isInReactorRange(entity.getMultiblockPos(), blockPos); + if (first) { + if (inRange || !entity.isAssembled()) { + ReactorAssembler.assemble(controllerPos, level); } + } else if (inRange) { + ReactorAssembler.disassemble(controllerPos, level); } - } - return null; + + return false; + })); + } + + public BlockPos findControllerPos(BlockPos blockPos, Level level, boolean first){ + BlockPos[] found = {null}; + scanControllerCandidates(blockPos, level, ((controllerPos, entity) -> { + boolean inRange = isInReactorRange(entity.getMultiblockPos(), blockPos); + if (first) { + if (inRange || !entity.isAssembled()) { + ReactorAssembler.assemble(controllerPos, level); + } + } else if (inRange) { + ReactorAssembler.disassemble(controllerPos, level); + } + + if (isInReactorRange(entity.getMultiblockPos(), blockPos)) { + found[0] = controllerPos; + return true; + } + + return false; + })); + + return found[0]; + } + + public BlockPos findControllerPos(BlockPos blockPos, Level level){ + BlockPos[] found = {null}; + scanControllerCandidates(blockPos, level, ((controllerPos, entity) -> { + ReactorAssembler.assemble(controllerPos, level); + if (isInReactorRange(entity.getMultiblockPos(), blockPos)) { + found[0] = controllerPos; + return true; + } + + return false; + })); + + return found[0]; } - public boolean isInReactorRange(int []reactorPos, BlockPos blockPos) { - //[xMin, xMax, yMin, yMax, zMin, zMax] - if (reactorPos == null || reactorPos.length < 6) return false; - return blockPos.getX() >= reactorPos[0] && blockPos.getX() <= reactorPos[1] - && blockPos.getY() >= reactorPos[2] && blockPos.getY() <= reactorPos[3] - && blockPos.getZ() >= reactorPos[4] && blockPos.getZ() <= reactorPos[5]; + public boolean isInReactorRange(@Nullable BoundingBox reactorPos, BlockPos blockPos) { + return reactorPos != null && reactorPos.isInside(blockPos); } } diff --git a/src/main/java/net/nuclearteam/createnuclear/content/multiblock/reactorLogic/DefaultHeatCalculator.java b/src/main/java/net/nuclearteam/createnuclear/content/multiblock/reactorLogic/DefaultHeatCalculator.java new file mode 100644 index 0000000..c51c075 --- /dev/null +++ b/src/main/java/net/nuclearteam/createnuclear/content/multiblock/reactorLogic/DefaultHeatCalculator.java @@ -0,0 +1,111 @@ +package net.nuclearteam.createnuclear.content.multiblock.reactorLogic; + +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Item; +import net.minecraft.world.level.Level; +import net.neoforged.neoforge.items.ItemStackHandler; +import net.nuclearteam.createnuclear.api.multiblock.fluid.ReactorFluidType; +import net.nuclearteam.createnuclear.api.multiblock.rods.RodType; +import net.nuclearteam.createnuclear.api.multiblock.rods.RodType.TypeRod; +import net.nuclearteam.createnuclear.api.multiblock.rods.RodType.TypeRodPredicate; +import net.nuclearteam.createnuclear.content.logistics.BigFluidStack; +import net.nuclearteam.createnuclear.content.multiblock.bluePrintItem.ReactorBluePrintItem; +import net.nuclearteam.createnuclear.content.multiblock.controller.ReactorControllerInventory; +import net.nuclearteam.createnuclear.content.multiblock.controller.display.ReactorDisplayState; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class DefaultHeatCalculator implements IHeatCalculator { + private static final int[][] FORMATTED_PATTERN = new int[][]{ + {99,99,99,0,1,2,99,99,99}, + {99,99,3,4,5,6,7,99,99}, + {99,8,9,10,11,12,13,14,99}, + {15,16,17,18,19,20,21,22,23}, + {24,25,26,27,28,29,30,31,32}, + {33,34,35,36,37,38,39,40,41}, + {99,42,43,44,45,46,47,48,99}, + {99,99,49,50,51,52,53,99,99}, + {99,99,99,54,55,56,99,99,99} + }; + private static final int[][] OFFSETS = { {1, 0}, {-1, 0}, {0, 1}, {0, -1} }; + + private static final Map> NEIGHBORS_BY_SLOT = buildNeighborsBySlot(); + + private static Map> buildNeighborsBySlot() { + Map> result = new HashMap<>(); + for (int row = 0; row < FORMATTED_PATTERN.length; row++) { + for (int col = 0; col < FORMATTED_PATTERN[row].length; col++) { + int slot = FORMATTED_PATTERN[row][col]; + if (slot == 99) continue; + + List neighbors = new ArrayList<>(); + for (int[] offset : OFFSETS) { + int nj = row + offset[0]; + int nk = col + offset[1]; + + if (nj < 0 || nj >= FORMATTED_PATTERN.length || nk < 0 || nk >= FORMATTED_PATTERN[nj].length) continue; + + int neighborSlot = FORMATTED_PATTERN[nj][nk]; + if (neighborSlot != 99) neighbors.add(neighborSlot); + } + + result.put(slot, neighbors); + } + } + + return result; + } + + @Override + public double computeHeat(BigFluidStack bigFluidStack, ReactorFluidType type, ReactorControllerInventory inventory, double overHeat, ReactorDisplayState displayState, Level level) { + double heat = 0; + + ItemStackHandler pattern = ReactorBluePrintItem.getItemStorage(inventory.getStackInSlot(0)); + + Map availableItems = displayState != null && displayState.items() != null + ? new HashMap<>(displayState.items()) + : new HashMap<>(); + + Map actualRods = new HashMap<>(); + + for (int slot = 0; slot < pattern.getSlots(); slot++) { + ItemStack currentStack = pattern.getStackInSlot(slot); + if (currentStack.isEmpty()) continue; + + Item rodItem = currentStack.getItem(); + if (availableItems.getOrDefault(rodItem, 0) > 0) { + availableItems.put(rodItem, availableItems.get(rodItem) - 1); + actualRods.put(slot, currentStack); + } + } + + for (Map.Entry entry : actualRods.entrySet()) { + int slot = entry.getKey(); + ItemStack currentStack = entry.getValue(); + RodType rod = RodType.resolveRodType(currentStack.getItem(), level); + + if (!rod.isNotEmptyItem() || rod.type() == TypeRod.NONE) continue; + + heat += rod.baseRodHeat().get(); + + // find position in formattedPattern and check neighbors + for (int neighborSlot : NEIGHBORS_BY_SLOT.getOrDefault(slot, List.of())) { + ItemStack neighborStack = actualRods.get(neighborSlot); + if (neighborStack == null) continue; + + RodType neighborRod = RodType.resolveRodType(neighborStack.getItem(), level); + if (!neighborRod.isNotEmptyItem()) continue; + + if (TypeRodPredicate.isFuel(rod) && TypeRodPredicate.isFuel(neighborRod)) { + heat += rod.proximityRodHeat().get(); + } else if (TypeRodPredicate.isCooled(rod) && TypeRodPredicate.isFuel(neighborRod)) { + heat += neighborRod.baseRodHeat().get() * rod.proximityRodHeat().get(); + } + } + } + return Math.max(0, heat + overHeat); + } +} diff --git a/src/main/java/net/nuclearteam/createnuclear/content/multiblock/reactorLogic/DefaultOverheatController.java b/src/main/java/net/nuclearteam/createnuclear/content/multiblock/reactorLogic/DefaultOverheatController.java new file mode 100644 index 0000000..54ab6e6 --- /dev/null +++ b/src/main/java/net/nuclearteam/createnuclear/content/multiblock/reactorLogic/DefaultOverheatController.java @@ -0,0 +1,45 @@ +package net.nuclearteam.createnuclear.content.multiblock.reactorLogic; + +import net.nuclearteam.createnuclear.api.multiblock.fluid.ReactorFluidType; +import net.nuclearteam.createnuclear.content.logistics.BigFluidStack; + +class DefaultOverheatController implements IOverheatController { + private int overFlowHeatTimer = 0; + private int overFlowLimiter = 30; + private double overHeat = 0; + + private final int liquidTimer = 3600; + + @Override + public void updateState(HeatBalance heatBalance, int currentHeat, BigFluidStack bigFluidStack, ReactorFluidType type) { + int fluidAmount = bigFluidStack == null ? 0 : bigFluidStack.amount; + int fluidEfficiency = type == null ? -1 : type.efficiency(); + boolean exceedsFluidMaxHeat = type != null && Math.abs(currentHeat) > type.maxHeat(); + + boolean rodMalus = heatBalance.resolve() == EquilibriumState.OVERHEATING; + boolean fluidMalus = fluidEfficiency == -1 || fluidAmount < fluidEfficiency || exceedsFluidMaxHeat; + + int malusPoints = (rodMalus ? 1 : 0) + (fluidMalus ? 1 : 0); + + if (malusPoints > 0) { + overFlowHeatTimer++; + if (overFlowHeatTimer >= overFlowLimiter) { + overHeat += malusPoints; + overFlowHeatTimer = 0; + overFlowLimiter = Math.max(2, overFlowLimiter - malusPoints); + } + } else { + overFlowHeatTimer = 0; + overFlowLimiter = 30; + if (overHeat > 0) overHeat -= 2; + else overHeat = 0; + } + } + + @Override + public double getOverHeat() { return overHeat; } + + + @Override + public int getLiquidTimer() { return liquidTimer; } +} diff --git a/src/main/java/net/nuclearteam/createnuclear/content/multiblock/reactorLogic/EquilibriumState.java b/src/main/java/net/nuclearteam/createnuclear/content/multiblock/reactorLogic/EquilibriumState.java new file mode 100644 index 0000000..3dbe6ea --- /dev/null +++ b/src/main/java/net/nuclearteam/createnuclear/content/multiblock/reactorLogic/EquilibriumState.java @@ -0,0 +1,21 @@ +package net.nuclearteam.createnuclear.content.multiblock.reactorLogic; + +/** + * Reactor thermal equilibrium state, derived from the fuel:cooler ratio + * (wiki reference: {@code heatPoints / coolingPoints} ratio compared to 6). + *

+ * Only {@link #OVERHEATING} is currently consumed by + * {@link DefaultOverheatController} (triggers the escalation toward + * shutdown/explosion). {@link #BALANCED} and {@link #OVERCOOLING} are kept + * for future versions (wiki-style status display, output bonus/malus) but + * have no effect on overheating today: a reactor in overcooling is simply + * suboptimal (less heat produced), not penalized. + */ +public enum EquilibriumState { + /** ratio > 6:1 — too much fuel for the cooling, leads to explosion. */ + OVERHEATING, + /** ratio == 6:1 — exact wiki equilibrium. */ + BALANCED, + /** ratio < 6:1 — safe but suboptimal output, no malus. */ + OVERCOOLING +} diff --git a/src/main/java/net/nuclearteam/createnuclear/content/multiblock/reactorLogic/HeatBalance.java b/src/main/java/net/nuclearteam/createnuclear/content/multiblock/reactorLogic/HeatBalance.java new file mode 100644 index 0000000..ea6b3f9 --- /dev/null +++ b/src/main/java/net/nuclearteam/createnuclear/content/multiblock/reactorLogic/HeatBalance.java @@ -0,0 +1,34 @@ +package net.nuclearteam.createnuclear.content.multiblock.reactorLogic; + +/** + * Thermal balance of a pattern: heat points (fuel) and cooling points + * (cooler), each weighted by {@link net.nuclearteam.createnuclear.api.multiblock.rods.RodType#ratio()}. + *

+ * Since {@code graphiteHeatRatio} switched to a positive value (1), + * {@code heatPoints} and {@code coolingPoints} are both raw positive sums + * (e.g. uranium=2/rod, thorium=1/rod, graphite=1/rod) — the 6:1 equilibrium + * ratio (wiki reference) is therefore no longer implicit in the config and + * must be compared explicitly via {@link #TARGET_RATIO}. + */ +public record HeatBalance(int heatPoints, int coolingPoints) { + /** Reference fuel:cooler ratio (wiki) at equilibrium. */ + public static final int TARGET_RATIO = 6; + + /** + * Resolves the equilibrium state from this balance. + * + * @return {@link EquilibriumState#OVERHEATING} if no cooler is present + * while there is fuel, otherwise the state corresponding to the + * comparison of {@code heatPoints / coolingPoints} against + * {@link #TARGET_RATIO} + */ + public EquilibriumState resolve() { + if (coolingPoints == 0) return heatPoints() > 0 ? EquilibriumState.OVERHEATING : EquilibriumState.BALANCED; + + double ration = (double) heatPoints() / coolingPoints(); + if (ration > TARGET_RATIO) return EquilibriumState.OVERHEATING; + if (ration < TARGET_RATIO) return EquilibriumState.OVERCOOLING; + + return EquilibriumState.BALANCED; + } +} diff --git a/src/main/java/net/nuclearteam/createnuclear/content/multiblock/reactorLogic/HeatManager.java b/src/main/java/net/nuclearteam/createnuclear/content/multiblock/reactorLogic/HeatManager.java new file mode 100644 index 0000000..e546466 --- /dev/null +++ b/src/main/java/net/nuclearteam/createnuclear/content/multiblock/reactorLogic/HeatManager.java @@ -0,0 +1,33 @@ +package net.nuclearteam.createnuclear.content.multiblock.reactorLogic; + +import net.minecraft.world.level.Level; +import net.nuclearteam.createnuclear.api.multiblock.fluid.ReactorFluidType; +import net.nuclearteam.createnuclear.content.logistics.BigFluidStack; +import net.nuclearteam.createnuclear.content.multiblock.controller.ReactorControllerInventory; +import net.nuclearteam.createnuclear.content.multiblock.controller.display.ReactorDisplayState; + +/** + * HeatManager facade delegating to extracted components (in separate files). + */ +public class HeatManager { + private final IHeatCalculator calculator; + private final IOverheatController overheatController; + + public HeatManager(IHeatCalculator calculator, IOverheatController overheatController) { + this.calculator = calculator; + this.overheatController = overheatController; + } + + public HeatManager() { + this(new DefaultHeatCalculator(), new DefaultOverheatController()); + } + + public double calculateHeat(BigFluidStack bigFluidStack, HeatBalance heatBalance, int previousHeat, ReactorControllerInventory inventory, Level level, ReactorDisplayState displayState) { + ReactorFluidType type = bigFluidStack == null ? null : bigFluidStack.getFluidtype(level); + overheatController.updateState(heatBalance, previousHeat, bigFluidStack, type); + + return calculator.computeHeat(bigFluidStack, type, inventory, overheatController.getOverHeat(), displayState, level); + } + + public int getLiquidTimer() { return overheatController.getLiquidTimer();} +} diff --git a/src/main/java/net/nuclearteam/createnuclear/content/multiblock/reactorLogic/IHeatCalculator.java b/src/main/java/net/nuclearteam/createnuclear/content/multiblock/reactorLogic/IHeatCalculator.java new file mode 100644 index 0000000..09dbba9 --- /dev/null +++ b/src/main/java/net/nuclearteam/createnuclear/content/multiblock/reactorLogic/IHeatCalculator.java @@ -0,0 +1,11 @@ +package net.nuclearteam.createnuclear.content.multiblock.reactorLogic; + +import net.minecraft.world.level.Level; +import net.nuclearteam.createnuclear.api.multiblock.fluid.ReactorFluidType; +import net.nuclearteam.createnuclear.content.logistics.BigFluidStack; +import net.nuclearteam.createnuclear.content.multiblock.controller.ReactorControllerInventory; +import net.nuclearteam.createnuclear.content.multiblock.controller.display.ReactorDisplayState; + +public interface IHeatCalculator { + double computeHeat(BigFluidStack bigFluidStack, ReactorFluidType type, ReactorControllerInventory inventory, double overHeat, ReactorDisplayState displayState, Level level); +} diff --git a/src/main/java/net/nuclearteam/createnuclear/content/multiblock/reactorLogic/IOverheatController.java b/src/main/java/net/nuclearteam/createnuclear/content/multiblock/reactorLogic/IOverheatController.java new file mode 100644 index 0000000..62090d5 --- /dev/null +++ b/src/main/java/net/nuclearteam/createnuclear/content/multiblock/reactorLogic/IOverheatController.java @@ -0,0 +1,10 @@ +package net.nuclearteam.createnuclear.content.multiblock.reactorLogic; + +import net.nuclearteam.createnuclear.api.multiblock.fluid.ReactorFluidType; +import net.nuclearteam.createnuclear.content.logistics.BigFluidStack; + +public interface IOverheatController { + void updateState(HeatBalance heatBalance, int currentHeat, BigFluidStack bigFluidStack, ReactorFluidType type); + double getOverHeat(); + int getLiquidTimer(); +} diff --git a/src/main/java/net/nuclearteam/createnuclear/content/rod/CNRodTypes.java b/src/main/java/net/nuclearteam/createnuclear/content/multiblock/rod/CNRodTypes.java similarity index 86% rename from src/main/java/net/nuclearteam/createnuclear/content/rod/CNRodTypes.java rename to src/main/java/net/nuclearteam/createnuclear/content/multiblock/rod/CNRodTypes.java index 2b90239..5a7a478 100644 --- a/src/main/java/net/nuclearteam/createnuclear/content/rod/CNRodTypes.java +++ b/src/main/java/net/nuclearteam/createnuclear/content/multiblock/rod/CNRodTypes.java @@ -1,4 +1,4 @@ -package net.nuclearteam.createnuclear.content.rod; +package net.nuclearteam.createnuclear.content.multiblock.rod; import net.minecraft.data.worldgen.BootstrapContext; import net.minecraft.resources.ResourceKey; @@ -9,8 +9,6 @@ import static net.nuclearteam.createnuclear.api.ItemRodTypesValue.DEFAULT_ROD_TYPE; public class CNRodTypes { - public static final ResourceKey FALLBACK = ResourceKey.create(CreateNuclearRegistries.ROD_TYPE, CreateNuclear.asResource("fallback")); - /** * Static registry of rod types ({@code RodType}) for the CreateNuclear mod. * @@ -25,7 +23,7 @@ public class CNRodTypes { * .baseRodHeat(int) * .proximityRodHeat(int) * .rodTimer(int) - * .addItems(Item) + * .item(Item) * .fuelRodType() * .build()); * } diff --git a/src/main/java/net/nuclearteam/createnuclear/content/radiation/RadiationEffect.java b/src/main/java/net/nuclearteam/createnuclear/content/radiation/RadiationEffect.java index fec1b43..0a86039 100644 --- a/src/main/java/net/nuclearteam/createnuclear/content/radiation/RadiationEffect.java +++ b/src/main/java/net/nuclearteam/createnuclear/content/radiation/RadiationEffect.java @@ -7,30 +7,53 @@ import net.nuclearteam.createnuclear.CNEffects; import net.nuclearteam.createnuclear.CNTags; import net.nuclearteam.createnuclear.CreateNuclear; +import net.nuclearteam.createnuclear.content.effects.VicinityEffect; import net.nuclearteam.createnuclear.content.radiation.capability.RadiationCapability; import net.minecraft.world.entity.ai.attributes.Attributes; import net.minecraft.world.entity.ai.attributes.AttributeModifier; +import net.nuclearteam.createnuclear.foundation.damageTypes.CNDamageSources; -public class RadiationEffect extends MobEffect { +public class RadiationEffect extends VicinityEffect { + private static final int CONTAGION_DURATION_TICKS = 300; /** * Constructs the RadiationEffect with harmful category and color. * Also applies attribute modifiers to reduce speed, attack damage, and attack speed. */ public RadiationEffect() { - super(MobEffectCategory.HARMFUL, 15453236); // Custom color (hex value) + super(MobEffectCategory.HARMFUL, 15453236, + amplifier -> 10, + RadiationCapability::canBeIrradiated, + timer -> {}); // Custom color (hex value) // Reduces movement speed by 20% - this.addAttributeModifier(Attributes.MOVEMENT_SPEED, - ResourceLocation.fromNamespaceAndPath(CreateNuclear.MOD_ID, "radiation_movement_speed"), -0.2D, AttributeModifier.Operation.ADD_MULTIPLIED_TOTAL); + this.addAttributeModifier( + Attributes.MOVEMENT_SPEED, + ResourceLocation.fromNamespaceAndPath(CreateNuclear.MOD_ID, "radiation_movement_speed"), + -0.2D, + AttributeModifier.Operation.ADD_MULTIPLIED_TOTAL + ); // Reduces attack damage by 20% - this.addAttributeModifier(Attributes.ATTACK_DAMAGE, - ResourceLocation.fromNamespaceAndPath(CreateNuclear.MOD_ID, "radiation_attack_damage"), -0.2D, AttributeModifier.Operation.ADD_MULTIPLIED_TOTAL); + this.addAttributeModifier( + Attributes.ATTACK_DAMAGE, + ResourceLocation.fromNamespaceAndPath(CreateNuclear.MOD_ID, "radiation_attack_damage"), + -0.2D, + AttributeModifier.Operation.ADD_MULTIPLIED_TOTAL + ); // Reduces attack speed by 20% - this.addAttributeModifier(Attributes.ATTACK_SPEED, - ResourceLocation.fromNamespaceAndPath(CreateNuclear.MOD_ID, "radiation_attack_speed"), -0.2D, AttributeModifier.Operation.ADD_MULTIPLIED_TOTAL); + this.addAttributeModifier( + Attributes.ATTACK_SPEED, + ResourceLocation.fromNamespaceAndPath(CreateNuclear.MOD_ID, "radiation_attack_speed"), + -0.2D, + AttributeModifier.Operation.ADD_MULTIPLIED_TOTAL + ); + } + + @Override + protected void onContaminate(LivingEntity nearby) { + RadiationCapability.applyContagion(nearby, 15D, CONTAGION_DURATION_TICKS); } /** @@ -44,27 +67,16 @@ public RadiationEffect() { */ @Override public boolean applyEffectTick(LivingEntity livingEntity, int amplifier) { - // If the entity is immune to radiation, remove the effect - if (livingEntity.getType().is(CNTags.CNEntityTags.IRRADIATED_IMMUNE.tag)) { - livingEntity.removeEffect(CNEffects.RADIATION); - return true; - } + super.applyEffectTick(livingEntity, amplifier); + + double resistance = RadiationCapability.getRadiationResistance(livingEntity); - // Full irradiated resistance (anti-radiation armor, iodine, ...) cancels the effect entirely, - // whatever its source (radiation dose, potion, explosion). - if (RadiationCapability.getRadiationResistance(livingEntity) >= 1.0) { - livingEntity.removeEffect(CNEffects.RADIATION); - return true; + float damage = (float) ((1 << amplifier) * (1.0 - resistance)); + + if (damage > 0f) { + livingEntity.hurt(CNDamageSources.radiation(livingEntity.level()), damage); } - // Apply radiation damage (magic type), scaled by amplifier - int damage = 1 << amplifier; - livingEntity.hurt(livingEntity.damageSources().magic(), damage); return true; } - - @Override - public boolean shouldApplyEffectTickThisTick(int duration, int amplifier) { - return duration >= 1; - } } \ No newline at end of file diff --git a/src/main/java/net/nuclearteam/createnuclear/content/radiation/capability/RadiationCapability.java b/src/main/java/net/nuclearteam/createnuclear/content/radiation/capability/RadiationCapability.java index 17a22aa..87a4ddb 100644 --- a/src/main/java/net/nuclearteam/createnuclear/content/radiation/capability/RadiationCapability.java +++ b/src/main/java/net/nuclearteam/createnuclear/content/radiation/capability/RadiationCapability.java @@ -222,4 +222,4 @@ private static void applyEffects(LivingEntity entity, double radiation) { entity.addEffect(new MobEffectInstance(CNEffects.RADIATION, 100, amp, true, true)); } } -} \ No newline at end of file +} diff --git a/src/main/java/net/nuclearteam/createnuclear/content/redstone/displayLink/source/AbstractReactorStatDisplaySource.java b/src/main/java/net/nuclearteam/createnuclear/content/redstone/displayLink/source/AbstractReactorStatDisplaySource.java new file mode 100644 index 0000000..1803ef7 --- /dev/null +++ b/src/main/java/net/nuclearteam/createnuclear/content/redstone/displayLink/source/AbstractReactorStatDisplaySource.java @@ -0,0 +1,54 @@ +package net.nuclearteam.createnuclear.content.redstone.displayLink.source; + +import com.simibubi.create.content.redstone.displayLink.DisplayLinkContext; +import com.simibubi.create.content.redstone.displayLink.source.NumericSingleLineDisplaySource; +import com.simibubi.create.content.redstone.displayLink.target.DisplayTargetStats; +import com.simibubi.create.foundation.gui.ModularGuiLineBuilder; +import net.minecraft.ChatFormatting; +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.MutableComponent; +import net.neoforged.api.distmarker.Dist; +import net.neoforged.api.distmarker.OnlyIn; +import net.nuclearteam.createnuclear.content.multiblock.MultiblockHelpers; +import net.nuclearteam.createnuclear.content.multiblock.controller.ReactorControllerBlockEntity; +import net.nuclearteam.createnuclear.foundation.utility.CreateNuclearLang; + +public abstract class AbstractReactorStatDisplaySource extends NumericSingleLineDisplaySource { + protected abstract String getLabelKey(); + protected abstract int getMax(); + protected abstract int computeValue(ReactorControllerBlockEntity controller, DisplayLinkContext context); + protected abstract ChatFormatting getColor(int value, ReactorControllerBlockEntity controller); + protected MutableComponent getUnitSuffix() { return null; } + + @Override + protected MutableComponent provideLine(DisplayLinkContext context, DisplayTargetStats stats) { + ReactorControllerBlockEntity controller = MultiblockHelpers.getControllerForPart(context.level(), context.getSourcePos()); + if (controller == null || controller.isRemoved()) return ZERO.copy(); + + MutableComponent label = CreateNuclearLang.translateDirect(getLabelKey()).append(" "); + int mode = context.sourceConfig().getInt("display_mode"); + int value = computeValue(controller, context); + int max = getMax(); + ChatFormatting color = getColor(value, controller); + + return label.append(switch (mode) { + case 1 -> Component.literal((value * 100 / max) + "%").withStyle(color); + case 2 -> ReactorGaugeRenderer.drawGauge(value, max, color, 6); + default -> { + MutableComponent base = Component.literal(String.valueOf(value)); + MutableComponent unit = getUnitSuffix(); + yield (unit != null ? base.append(unit) : base).withStyle(color); + } + }); + } + + @Override + @OnlyIn(Dist.CLIENT) + public void initConfigurationWidgets(DisplayLinkContext context, ModularGuiLineBuilder builder, boolean isFirstLine) { + if (isFirstLine) return; + builder.addSelectionScrollInput(0, 100, (input, l) -> input + .forOptions(CreateNuclearLang.translatedOptions("display_source.reactor.mode", "value", "percent", "gauge")), "display_mode"); + } + + @Override protected boolean allowsLabeling(DisplayLinkContext context) { return true; } +} diff --git a/src/main/java/net/nuclearteam/createnuclear/content/redstone/displayLink/source/CoolerDisplaySource.java b/src/main/java/net/nuclearteam/createnuclear/content/redstone/displayLink/source/CoolerDisplaySource.java new file mode 100644 index 0000000..ce7934b --- /dev/null +++ b/src/main/java/net/nuclearteam/createnuclear/content/redstone/displayLink/source/CoolerDisplaySource.java @@ -0,0 +1,46 @@ +package net.nuclearteam.createnuclear.content.redstone.displayLink.source; + +import com.simibubi.create.content.redstone.displayLink.DisplayLinkContext; +import net.minecraft.ChatFormatting; +import net.minecraft.world.item.Item; +import net.nuclearteam.createnuclear.api.multiblock.rods.RodType; +import net.nuclearteam.createnuclear.content.multiblock.controller.ReactorControllerBlockEntity; + +import java.util.Map; + +public class CoolerDisplaySource extends AbstractReactorStatDisplaySource { + + + @Override + protected String getLabelKey() { + return "display_source.reactor.cooler"; + } + + @Override + protected int getMax() { + return ReactorDisplayConstants.MAX_COOLER; + } + + @Override + protected ChatFormatting getColor(int value, ReactorControllerBlockEntity controller) { + return ChatFormatting.AQUA; + } + + @Override + protected int computeValue(ReactorControllerBlockEntity controller, DisplayLinkContext context) { + int cooler = 0; + if (controller.getDisplayState() != null && controller.getDisplayState().items() != null) { + for (Map.Entry entry : controller.getDisplayState().items().entrySet()) { + if (RodType.TypeRodPredicate.isCooled(entry.getKey().getDefaultInstance(), context.level())) { + cooler += entry.getValue(); + } + } + } + return cooler; + } + + @Override + protected String getTranslationKey() { + return "cooler"; + } +} \ No newline at end of file diff --git a/src/main/java/net/nuclearteam/createnuclear/content/redstone/displayLink/source/FuelDisplaySource.java b/src/main/java/net/nuclearteam/createnuclear/content/redstone/displayLink/source/FuelDisplaySource.java new file mode 100644 index 0000000..56a8dc2 --- /dev/null +++ b/src/main/java/net/nuclearteam/createnuclear/content/redstone/displayLink/source/FuelDisplaySource.java @@ -0,0 +1,45 @@ +package net.nuclearteam.createnuclear.content.redstone.displayLink.source; + +import com.simibubi.create.content.redstone.displayLink.DisplayLinkContext; +import net.minecraft.ChatFormatting; +import net.minecraft.world.item.Item; +import net.nuclearteam.createnuclear.api.multiblock.rods.RodType.TypeRodPredicate; +import net.nuclearteam.createnuclear.content.multiblock.controller.ReactorControllerBlockEntity; + +import java.util.Map; + +public class FuelDisplaySource extends AbstractReactorStatDisplaySource { + + @Override + protected String getLabelKey() { + return "display_source.reactor.fuel"; + } + + @Override + protected int getMax() { + return ReactorDisplayConstants.MAX_FUEL; + } + + @Override + protected ChatFormatting getColor(int value, ReactorControllerBlockEntity controller) { + return ChatFormatting.GREEN; + } + + @Override + protected int computeValue(ReactorControllerBlockEntity controller, DisplayLinkContext context) { + int fuel = 0; + if (controller.getDisplayState() != null && controller.getDisplayState().items() != null) { + for (Map.Entry entry : controller.getDisplayState().items().entrySet()) { + if (TypeRodPredicate.isFuel(entry.getKey().getDefaultInstance(), context.level())) { + fuel += entry.getValue(); + } + } + } + return fuel; + } + + @Override + protected String getTranslationKey() { + return "fuel"; + } +} \ No newline at end of file diff --git a/src/main/java/net/nuclearteam/createnuclear/content/redstone/displayLink/source/HeatDisplaySource.java b/src/main/java/net/nuclearteam/createnuclear/content/redstone/displayLink/source/HeatDisplaySource.java new file mode 100644 index 0000000..9c58ffb --- /dev/null +++ b/src/main/java/net/nuclearteam/createnuclear/content/redstone/displayLink/source/HeatDisplaySource.java @@ -0,0 +1,41 @@ +package net.nuclearteam.createnuclear.content.redstone.displayLink.source; + +import com.simibubi.create.content.redstone.displayLink.DisplayLinkContext; +import net.minecraft.ChatFormatting; +import net.nuclearteam.createnuclear.content.multiblock.IHeat; +import net.nuclearteam.createnuclear.content.multiblock.controller.ReactorControllerBlockEntity; + +public class HeatDisplaySource extends AbstractReactorStatDisplaySource { + + @Override + protected String getLabelKey() { + return "display_source.reactor.heat"; + } + + @Override + protected int getMax() { + return ReactorDisplayConstants.MAX_HEAT; + } + + @Override + protected ChatFormatting getColor(int value, ReactorControllerBlockEntity controller) { + return IHeat.HeatLevel.of(value, controller.getMultiblockSize()).getTextColor(); + } + + /** + * Divergence assumee vs Forge : Forge lit + * {@code controller.getConfiguredPattern().getOrCreateTag().getDouble("heat")}. + * En 1.21 la chaleur vit dans le data component {@code CNDataComponents.HEAT} ; + * relire le tag NBT de la stack renvoie une copie defensive, donc la valeur y est + * toujours absente. On passe par l'accesseur dedie du controleur. + */ + @Override + protected int computeValue(ReactorControllerBlockEntity controller, DisplayLinkContext context) { + return controller.getConfiguredPatternHeat(); + } + + @Override + protected String getTranslationKey() { + return "heat"; + } +} \ No newline at end of file diff --git a/src/main/java/net/nuclearteam/createnuclear/content/redstone/displayLink/source/LiquidLevelDisplaySource.java b/src/main/java/net/nuclearteam/createnuclear/content/redstone/displayLink/source/LiquidLevelDisplaySource.java new file mode 100644 index 0000000..bbcb119 --- /dev/null +++ b/src/main/java/net/nuclearteam/createnuclear/content/redstone/displayLink/source/LiquidLevelDisplaySource.java @@ -0,0 +1,43 @@ +package net.nuclearteam.createnuclear.content.redstone.displayLink.source; + +import com.simibubi.create.content.redstone.displayLink.DisplayLinkContext; +import net.minecraft.ChatFormatting; +import net.minecraft.network.chat.MutableComponent; +import net.nuclearteam.createnuclear.content.logistics.BigFluidStack; +import net.nuclearteam.createnuclear.content.multiblock.controller.ReactorControllerBlockEntity; +import net.nuclearteam.createnuclear.foundation.utility.CreateNuclearLang; + +import java.util.List; + +public class LiquidLevelDisplaySource extends AbstractReactorStatDisplaySource { + @Override + protected String getLabelKey() { + return "display_source.reactor.fluid"; + } + + @Override + protected int getMax() { + return ReactorDisplayConstants.MAX_FLUID; + } + + @Override + protected ChatFormatting getColor(int value, ReactorControllerBlockEntity controller) { + return ChatFormatting.BLUE; + } + + @Override + protected int computeValue(ReactorControllerBlockEntity controller, DisplayLinkContext context) { + List fluidList = controller.getBigFluidStack(); + return (fluidList != null && !fluidList.isEmpty()) ? fluidList.get(0).amount : 0; + } + + @Override + protected MutableComponent getUnitSuffix() { + return CreateNuclearLang.translateDirect("generic.unit.fluid.value"); + } + + @Override + protected String getTranslationKey() { + return "liquid_level"; + } +} \ No newline at end of file diff --git a/src/main/java/net/nuclearteam/createnuclear/content/redstone/displayLink/source/ReactorDisplayConstants.java b/src/main/java/net/nuclearteam/createnuclear/content/redstone/displayLink/source/ReactorDisplayConstants.java new file mode 100644 index 0000000..28360ff --- /dev/null +++ b/src/main/java/net/nuclearteam/createnuclear/content/redstone/displayLink/source/ReactorDisplayConstants.java @@ -0,0 +1,9 @@ +package net.nuclearteam.createnuclear.content.redstone.displayLink.source; + +final class ReactorDisplayConstants { + static final int MAX_FUEL = 64; + static final int MAX_COOLER = 64; + static final int MAX_FLUID = 16000; + static final int MAX_HEAT = 1000; + private ReactorDisplayConstants() {} +} diff --git a/src/main/java/net/nuclearteam/createnuclear/content/redstone/displayLink/source/ReactorGaugeRenderer.java b/src/main/java/net/nuclearteam/createnuclear/content/redstone/displayLink/source/ReactorGaugeRenderer.java new file mode 100644 index 0000000..4b590b1 --- /dev/null +++ b/src/main/java/net/nuclearteam/createnuclear/content/redstone/displayLink/source/ReactorGaugeRenderer.java @@ -0,0 +1,15 @@ +package net.nuclearteam.createnuclear.content.redstone.displayLink.source; + +import net.minecraft.ChatFormatting; +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.MutableComponent; +import net.minecraft.util.Mth; + +public final class ReactorGaugeRenderer { + private ReactorGaugeRenderer() {} + + static MutableComponent drawGauge(int current, int max, ChatFormatting color, int width) { + int filled = (int) (Mth.clamp((float) current / max, 0, 1) * width); + return Component.literal("█".repeat(filled) + "▒".repeat(Math.max(0, width - filled))).withStyle(color); + } +} diff --git a/src/main/java/net/nuclearteam/createnuclear/content/redstone/displayLink/source/ReactorSizeDisplaySource.java b/src/main/java/net/nuclearteam/createnuclear/content/redstone/displayLink/source/ReactorSizeDisplaySource.java new file mode 100644 index 0000000..04faf4d --- /dev/null +++ b/src/main/java/net/nuclearteam/createnuclear/content/redstone/displayLink/source/ReactorSizeDisplaySource.java @@ -0,0 +1,54 @@ +package net.nuclearteam.createnuclear.content.redstone.displayLink.source; + +import com.simibubi.create.content.redstone.displayLink.DisplayLinkContext; +import com.simibubi.create.content.redstone.displayLink.source.NumericSingleLineDisplaySource; +import com.simibubi.create.content.redstone.displayLink.target.DisplayTargetStats; +import com.simibubi.create.foundation.gui.ModularGuiLineBuilder; +import net.minecraft.ChatFormatting; +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.MutableComponent; +import net.neoforged.api.distmarker.Dist; +import net.neoforged.api.distmarker.OnlyIn; +import net.nuclearteam.createnuclear.content.multiblock.MultiblockHelpers; +import net.nuclearteam.createnuclear.content.multiblock.controller.ReactorControllerBlockEntity; +import net.nuclearteam.createnuclear.foundation.utility.CreateNuclearLang; + +public class ReactorSizeDisplaySource extends NumericSingleLineDisplaySource { + + @Override + protected MutableComponent provideLine(DisplayLinkContext context, DisplayTargetStats stats) { + ReactorControllerBlockEntity controller = MultiblockHelpers.getControllerForPart(context.level(), context.getSourcePos()); + if (controller == null || controller.isRemoved()) return ZERO.copy(); + + // Label + trailing space + MutableComponent label = CreateNuclearLang.translateDirect("display_source.reactor.size").append(" "); + + int mode = context.sourceConfig().getInt("display_mode"); + int size = controller.getMultiblockSize(); + int tier = size <= 5 ? 1 : size <= 7 ? 2 : 3; + + return label.append(switch (mode) { + case 1 -> Component.literal((tier * 100 / 3) + "%").withStyle(ChatFormatting.BLUE); + case 2 -> { + // Short 3-segment gauge representing the tier + yield Component.literal("█".repeat(tier) + "▒".repeat(3 - tier)).withStyle(ChatFormatting.BLUE); + } + default -> { + String key = tier == 1 ? "small" : tier == 2 ? "medium" : "large"; + yield CreateNuclearLang.translateDirect("display_source.reactor.size." + key).withStyle(ChatFormatting.BLUE); + } + }); + } + + @Override protected String getTranslationKey() { return "size"; } + + @Override + @OnlyIn(Dist.CLIENT) + public void initConfigurationWidgets(DisplayLinkContext context, ModularGuiLineBuilder builder, boolean isFirstLine) { + if (isFirstLine) return; + builder.addSelectionScrollInput(0, 100, (selectionScrollInput, l) -> selectionScrollInput + .forOptions(CreateNuclearLang.translatedOptions("display_source.reactor.mode", "value", "percent", "gauge")), "display_mode"); + } + + @Override protected boolean allowsLabeling(DisplayLinkContext context) { return true; } +} \ No newline at end of file diff --git a/src/main/java/net/nuclearteam/createnuclear/content/redstone/displayLink/source/ReactorSummaryDisplaySource.java b/src/main/java/net/nuclearteam/createnuclear/content/redstone/displayLink/source/ReactorSummaryDisplaySource.java index 22f4885..e61340c 100644 --- a/src/main/java/net/nuclearteam/createnuclear/content/redstone/displayLink/source/ReactorSummaryDisplaySource.java +++ b/src/main/java/net/nuclearteam/createnuclear/content/redstone/displayLink/source/ReactorSummaryDisplaySource.java @@ -154,14 +154,14 @@ private Optional getReactorSummary(DisplayLinkContext context, i int mode = context.sourceConfig().getInt("display_mode"); - int heat = (int) controller.getConfiguredPattern() - .getOrDefault(DataComponents.CUSTOM_DATA, CustomData.EMPTY) - .copyTag() - .getDouble("heat"); + // Divergence assumee vs Forge, qui lit getConfiguredPattern().getOrCreateTag().getDouble("heat") : + // en 1.21 la chaleur vit dans le data component CNDataComponents.HEAT, et relire le tag NBT de la + // stack renvoie une copie defensive ou la valeur est toujours absente (elle valait donc 0 ici). + int heat = controller.getConfiguredPatternHeat(); int fuel = 0; int cooler = 0; - /*if (controller.getDisplayState() != null && controller.getDisplayState().items() != null) { + if (controller.getDisplayState() != null && controller.getDisplayState().items() != null) { for (Map.Entry entry : controller.getDisplayState().items().entrySet()) { if (TypeRodPredicate.isFuel(entry.getKey().getDefaultInstance(), context.level())) { fuel += entry.getValue(); @@ -169,7 +169,7 @@ private Optional getReactorSummary(DisplayLinkContext context, i cooler += entry.getValue(); } } - }*/ + } int size = controller.getMultiblockSize(); List fluidList = controller.getBigFluidStack(); @@ -188,10 +188,10 @@ private Optional getReactorSummary(DisplayLinkContext context, i CreateNuclearLang.translateDirect("display_source.reactor.active").withStyle(ChatFormatting.GOLD) : CreateNuclearLang.translateDirect("display_source.reactor.idle").withStyle(ChatFormatting.GRAY)) .size(lSize, formatSize(size)) - //.fuel(lFuel, formatValue(fuel, ReactorDisplayConstants.MAX_FUEL, mode, false, ChatFormatting.GREEN, gaugeWidth)) - //.cooler(lCooler, formatValue(cooler, ReactorDisplayConstants.MAX_COOLER, mode, false, ChatFormatting.AQUA, gaugeWidth)) - //.fluid(lFluid, formatFluid(fluid, ReactorDisplayConstants.MAX_FLUID, mode, ChatFormatting.BLUE, gaugeWidth)) - //.heat(lHeat, formatValue(heat, ReactorDisplayConstants.MAX_HEAT, mode, true, IHeat.HeatLevel.of(heat, controller.getMultiblockSize()).getTextColor(), gaugeWidth)) + .fuel(lFuel, formatValue(fuel, ReactorDisplayConstants.MAX_FUEL, mode, false, ChatFormatting.GREEN, gaugeWidth)) + .cooler(lCooler, formatValue(cooler, ReactorDisplayConstants.MAX_COOLER, mode, false, ChatFormatting.AQUA, gaugeWidth)) + .fluid(lFluid, formatFluid(fluid, ReactorDisplayConstants.MAX_FLUID, mode, ChatFormatting.BLUE, gaugeWidth)) + .heat(lHeat, formatValue(heat, ReactorDisplayConstants.MAX_HEAT, mode, true, IHeat.HeatLevel.of(heat, controller.getMultiblockSize()).getTextColor(), gaugeWidth)) .build()); } @@ -205,13 +205,13 @@ private MutableComponent formatSize(int size) { } private MutableComponent formatFluid(int current, int max, int mode, ChatFormatting color, int width) { - //if (mode == 0 || mode == 3) return ReactorGaugeRenderer.drawGauge(current, max, color, width); + if (mode == 0 || mode == 3) return ReactorGaugeRenderer.drawGauge(current, max, color, width); if (mode == 2) return Component.literal((current * 100 / max) + "%").withStyle(color); return Component.literal(String.valueOf(current)).append(" ").append(CreateNuclearLang.translateDirect("generic.unit.fluid.value")).withStyle(color); } private MutableComponent formatValue(int current, int max, int mode, boolean gaugeOnNormal, ChatFormatting color, int width) { - //if (mode == 3 || (mode == 0 && gaugeOnNormal)) return ReactorGaugeRenderer.drawGauge(current, max, color, width); + if (mode == 3 || (mode == 0 && gaugeOnNormal)) return ReactorGaugeRenderer.drawGauge(current, max, color, width); if (mode == 2) return Component.literal((current * 100 / max) + "%").withStyle(color); return Component.literal(String.valueOf(current)).withStyle(color); } @@ -240,4 +240,4 @@ public void initConfigurationWidgets(DisplayLinkContext context, ModularGuiLineB protected String getTranslationKey() { return "reactor_summary"; } -} \ No newline at end of file +} diff --git a/src/main/java/net/nuclearteam/createnuclear/content/uraniumOre/UraniumOreItem.java b/src/main/java/net/nuclearteam/createnuclear/content/uraniumOre/UraniumOreItem.java new file mode 100644 index 0000000..5169bcb --- /dev/null +++ b/src/main/java/net/nuclearteam/createnuclear/content/uraniumOre/UraniumOreItem.java @@ -0,0 +1,21 @@ +package net.nuclearteam.createnuclear.content.uraniumOre; + +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.item.BlockItem; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.block.Block; +import net.nuclearteam.createnuclear.api.radiation.IRadiationSource; + +public class UraniumOreItem extends BlockItem implements IRadiationSource { + private final double radiation; + + public UraniumOreItem(Block block, Properties properties, double radiation) { + super(block, properties); + this.radiation = radiation; + } + + @Override + public double getRadiation(ItemStack stack, LivingEntity entity) { + return this.radiation * stack.getCount(); + } +} \ No newline at end of file diff --git a/src/main/java/net/nuclearteam/createnuclear/foundation/damagesTypes/CNDamageSources.java b/src/main/java/net/nuclearteam/createnuclear/foundation/damageTypes/CNDamageSources.java similarity index 95% rename from src/main/java/net/nuclearteam/createnuclear/foundation/damagesTypes/CNDamageSources.java rename to src/main/java/net/nuclearteam/createnuclear/foundation/damageTypes/CNDamageSources.java index 1c2dee6..eb82700 100644 --- a/src/main/java/net/nuclearteam/createnuclear/foundation/damagesTypes/CNDamageSources.java +++ b/src/main/java/net/nuclearteam/createnuclear/foundation/damageTypes/CNDamageSources.java @@ -16,9 +16,9 @@ public class CNDamageSources { public static DamageSource radiation(Level level) { return source(CNDamageTypes.RADIATION, level); } - /*public static DamageSource fanRadiation(Level level) { + public static DamageSource fanRadiation(Level level) { return source(CNDamageTypes.FAN_RADIATION, level); - }*/ + } private static DamageSource source(ResourceKey key, LevelReader level) { Registry registry = level.registryAccess().registryOrThrow(Registries.DAMAGE_TYPE); diff --git a/src/main/java/net/nuclearteam/createnuclear/foundation/data/recipe/CNDeployingRecipeGen.java b/src/main/java/net/nuclearteam/createnuclear/foundation/data/recipe/CNDeployingRecipeGen.java new file mode 100644 index 0000000..83227ef --- /dev/null +++ b/src/main/java/net/nuclearteam/createnuclear/foundation/data/recipe/CNDeployingRecipeGen.java @@ -0,0 +1,55 @@ +package net.nuclearteam.createnuclear.foundation.data.recipe; + +import com.simibubi.create.AllBlocks; +import com.simibubi.create.api.data.recipe.DeployingRecipeGen; +import net.minecraft.core.HolderLookup; +import net.minecraft.data.PackOutput; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.Items; +import net.minecraft.world.item.crafting.Ingredient; +import net.minecraft.world.level.ItemLike; +import net.nuclearteam.createnuclear.CNBlocks; +import net.nuclearteam.createnuclear.CreateNuclear; + +import java.util.concurrent.CompletableFuture; + +public class CNDeployingRecipeGen extends DeployingRecipeGen { + + GeneratedRecipe REACTOR_ROD_INPUT = deploying("reactor_rod_input_from_hopper_and_reactor_casing", + Items.HOPPER, + CNBlocks.REACTOR_CASING.get(), + CNBlocks.REACTOR_ROD_INPUT.get() + ); + + GeneratedRecipe REACTOR_FLUID_INPUT = deploying("reactor_fluid_input_from_fluid_pipe_and_reactor_casing", + AllBlocks.FLUID_PIPE.asItem(), + CNBlocks.REACTOR_CASING.get(), + CNBlocks.REACTOR_FLUID_INPUT.get() + ); + + GeneratedRecipe REACTOR_OUTPUT = deploying("reactor_output_from_shaft_and_reactor_casing", + AllBlocks.SHAFT.asItem(), + CNBlocks.REACTOR_CASING.get(), + CNBlocks.REACTOR_OUTPUT.get() + ); + + protected GeneratedRecipe deploying(String name, Ingredient ingredient, ItemLike input, ItemLike output) { + return create(CreateNuclear.asResource(name), b -> + b.require(input) + .require(ingredient) + .output(output) + ); + } + + protected GeneratedRecipe deploying(String name, Item ingredient, ItemLike input, ItemLike output) { + return create(CreateNuclear.asResource(name), b -> + b.require(input) + .require(ingredient) + .output(output) + ); + } + + public CNDeployingRecipeGen(PackOutput output, CompletableFuture registries) { + super(output, registries, CreateNuclear.MOD_ID); + } +} diff --git a/src/main/java/net/nuclearteam/createnuclear/foundation/data/recipe/CNEnrichedRecipeGen.java b/src/main/java/net/nuclearteam/createnuclear/foundation/data/recipe/CNEnrichedRecipeGen.java index cfa8df7..b271401 100644 --- a/src/main/java/net/nuclearteam/createnuclear/foundation/data/recipe/CNEnrichedRecipeGen.java +++ b/src/main/java/net/nuclearteam/createnuclear/foundation/data/recipe/CNEnrichedRecipeGen.java @@ -7,6 +7,7 @@ import net.nuclearteam.createnuclear.CNBlocks; import net.nuclearteam.createnuclear.CNItems; import net.nuclearteam.createnuclear.CreateNuclear; +import net.nuclearteam.createnuclear.api.data.recipe.EnrichedRecipeGen; import java.util.concurrent.CompletableFuture; diff --git a/src/main/java/net/nuclearteam/createnuclear/foundation/data/recipe/CNItemApplicationRecipeGen.java b/src/main/java/net/nuclearteam/createnuclear/foundation/data/recipe/CNItemApplicationRecipeGen.java index 67bcc6e..99a1c33 100644 --- a/src/main/java/net/nuclearteam/createnuclear/foundation/data/recipe/CNItemApplicationRecipeGen.java +++ b/src/main/java/net/nuclearteam/createnuclear/foundation/data/recipe/CNItemApplicationRecipeGen.java @@ -22,18 +22,6 @@ public class CNItemApplicationRecipeGen extends ItemApplicationRecipeGen { CNBlocks.REACTOR_CASING.get() ); - GeneratedRecipe REACTOR_OUTPUT = itemApplication("reactor_output_from_shaft_and_reactor_casing", - AllBlocks.SHAFT.asItem(), - CNBlocks.REACTOR_CASING.get(), - CNBlocks.REACTOR_OUTPUT.get() - ); - - GeneratedRecipe REACTOR_INPUT = itemApplication("reactor_input_from_hopper_and_reactor_casing", - Items.HOPPER, - CNBlocks.REACTOR_CASING.get(), - CNBlocks.REACTOR_ROD_INPUT.get() - ); - protected GeneratedRecipe itemApplication(String name, Ingredient ingredient, ItemLike input, ItemLike output) { return create(CreateNuclear.asResource(name), b -> b.require(input) diff --git a/src/main/java/net/nuclearteam/createnuclear/foundation/data/recipe/CNMixingRecipeGen.java b/src/main/java/net/nuclearteam/createnuclear/foundation/data/recipe/CNMixingRecipeGen.java index a95cba5..39b9fd1 100644 --- a/src/main/java/net/nuclearteam/createnuclear/foundation/data/recipe/CNMixingRecipeGen.java +++ b/src/main/java/net/nuclearteam/createnuclear/foundation/data/recipe/CNMixingRecipeGen.java @@ -3,6 +3,7 @@ import com.simibubi.create.api.data.recipe.MixingRecipeGen; import net.minecraft.core.HolderLookup; import net.minecraft.data.PackOutput; +import net.minecraft.world.item.Items; import net.neoforged.neoforge.common.Tags; import net.nuclearteam.createnuclear.CNFluids; import net.nuclearteam.createnuclear.CNItems; @@ -23,6 +24,13 @@ public class CNMixingRecipeGen extends MixingRecipeGen { URANIUM_FLUID = create("uranium_fluid", b -> b .require(CNTags.forgeItemTag("dusts/uranium")) .output(CNFluids.URANIUM.get(), 25) + ), + + NITROGEN = create("liquid_nitrogen", b -> b + .require(CNItems.COOLED_NITROGEN_CONCENTRATE) + .duration(20) + .require(Items.ICE) + .output(CNFluids.LIQUID_NITROGEN.get(), 100) ) ; diff --git a/src/main/java/net/nuclearteam/createnuclear/foundation/data/recipe/CNRecipeProvider.java b/src/main/java/net/nuclearteam/createnuclear/foundation/data/recipe/CNRecipeProvider.java index abee1bd..81369bd 100644 --- a/src/main/java/net/nuclearteam/createnuclear/foundation/data/recipe/CNRecipeProvider.java +++ b/src/main/java/net/nuclearteam/createnuclear/foundation/data/recipe/CNRecipeProvider.java @@ -1,12 +1,6 @@ package net.nuclearteam.createnuclear.foundation.data.recipe; -import com.simibubi.create.Create; import com.simibubi.create.api.data.recipe.ProcessingRecipeGen; -import com.simibubi.create.content.processing.recipe.ProcessingRecipe; -import com.simibubi.create.content.processing.recipe.ProcessingRecipeBuilder; -import com.simibubi.create.foundation.data.recipe.CreateRecipeProvider; -import com.simibubi.create.foundation.recipe.IRecipeTypeInfo; -import net.createmod.catnip.platform.CatnipServices; import net.minecraft.core.HolderLookup; import net.minecraft.data.CachedOutput; import net.minecraft.data.DataGenerator; @@ -14,16 +8,10 @@ import net.minecraft.data.PackOutput; import net.minecraft.data.recipes.RecipeOutput; import net.minecraft.data.recipes.RecipeProvider; -import net.minecraft.resources.ResourceLocation; -import net.minecraft.world.item.crafting.Ingredient; -import net.minecraft.world.level.ItemLike; -import net.nuclearteam.createnuclear.CreateNuclear; import java.util.ArrayList; import java.util.List; import java.util.concurrent.CompletableFuture; -import java.util.function.Supplier; -import java.util.function.UnaryOperator; public abstract class CNRecipeProvider extends RecipeProvider { @@ -44,14 +32,15 @@ public static void registerAllProcessing(DataGenerator gen, PackOutput output, C GENERATORS.add(new CNMixingRecipeGen(output, registries)); GENERATORS.add(new CNPressingRecipeGen(output, registries)); GENERATORS.add(new CNEnrichedRecipeGen(output, registries)); + GENERATORS.add(new CNSnowPowderRecipeGen(output, registries)); GENERATORS.add(new CNWashingRecipeGen(output, registries)); - + GENERATORS.add(new CNDeployingRecipeGen(output, registries)); gen.addProvider(true, new DataProvider() { @Override public String getName() { - return "Create's Processing Recipes"; + return "CreateNuclear's Processing Recipes"; } @Override diff --git a/src/main/java/net/nuclearteam/createnuclear/foundation/data/recipe/CNSnowPowderRecipeGen.java b/src/main/java/net/nuclearteam/createnuclear/foundation/data/recipe/CNSnowPowderRecipeGen.java new file mode 100644 index 0000000..627600d --- /dev/null +++ b/src/main/java/net/nuclearteam/createnuclear/foundation/data/recipe/CNSnowPowderRecipeGen.java @@ -0,0 +1,21 @@ +package net.nuclearteam.createnuclear.foundation.data.recipe; + +import net.minecraft.core.HolderLookup; +import net.minecraft.data.PackOutput; +import net.nuclearteam.createnuclear.CNItems; +import net.nuclearteam.createnuclear.CreateNuclear; +import net.nuclearteam.createnuclear.api.data.recipe.SnowPowderRecipeGen; + +import java.util.concurrent.CompletableFuture; + +public class CNSnowPowderRecipeGen extends SnowPowderRecipeGen { + + GeneratedRecipe + COOLED_NITROGEN_CONCENTRATE = convert(CNItems.NITROGEN_CONCENTRATE, CNItems.COOLED_NITROGEN_CONCENTRATE) + ; + + public CNSnowPowderRecipeGen(PackOutput output, CompletableFuture registries) { + super(output, registries, CreateNuclear.MOD_ID); + } + +} \ No newline at end of file diff --git a/src/main/java/net/nuclearteam/createnuclear/foundation/data/recipe/CNStandardRecipeGen.java b/src/main/java/net/nuclearteam/createnuclear/foundation/data/recipe/CNStandardRecipeGen.java index a71f614..90ecc62 100644 --- a/src/main/java/net/nuclearteam/createnuclear/foundation/data/recipe/CNStandardRecipeGen.java +++ b/src/main/java/net/nuclearteam/createnuclear/foundation/data/recipe/CNStandardRecipeGen.java @@ -96,7 +96,8 @@ public class CNStandardRecipeGen extends BaseRecipeProvider { URANIUM_ORE_TO_URANIUM_POWDER = blastFurnaceRecipeTags(() -> CNItems.RAW_URANIUM::get, () -> CNItemTags.URANIUM_ORES.tag, "_for_uranium_ore", 4), RAW_LEAD_ORES = blastFurnaceRecipeTags(() -> CNItems.LEAD_INGOT::get, () -> CNItemTags.LEAD_ORES.tag, "_for_lead_ore", 1), RAW_LEAD = blastFurnaceRecipeTags(CNItems.LEAD_INGOT::get, () -> CNTags.forgeItemTag("raw_materials/lead"), "_for_raw_lead", 1), - CRUSHED_RAW_LEAD_TO_LEAD_BLAST_FURNACE = blastFurnaceRecipe(CNItems.LEAD_INGOT::get, AllItems.CRUSHED_LEAD::get, "_for_lead", 1) + CRUSHED_RAW_LEAD_TO_LEAD_BLAST_FURNACE = blastFurnaceRecipe(CNItems.LEAD_INGOT::get, AllItems.CRUSHED_LEAD::get, "_for_lead", 1), + NITROGEN_CONCENTRATE = blastFurnaceRecipe(CNItems.NITROGEN_CONCENTRATE::get, CNItems.NITRATE::get, "_for_nitrogen_concentrate", 1) ; static class Marker { diff --git a/src/main/java/net/nuclearteam/createnuclear/foundation/events/RodsTooltipHandler.java b/src/main/java/net/nuclearteam/createnuclear/foundation/events/RodsTooltipHandler.java new file mode 100644 index 0000000..426baf0 --- /dev/null +++ b/src/main/java/net/nuclearteam/createnuclear/foundation/events/RodsTooltipHandler.java @@ -0,0 +1,35 @@ +package net.nuclearteam.createnuclear.foundation.events; + +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.neoforged.api.distmarker.Dist; +import net.neoforged.bus.api.SubscribeEvent; +import net.neoforged.fml.common.EventBusSubscriber; +import net.neoforged.neoforge.event.entity.player.ItemTooltipEvent; +import net.nuclearteam.createnuclear.CreateNuclear; +import net.nuclearteam.createnuclear.foundation.item.RodsStats; + +@EventBusSubscriber(modid = CreateNuclear.MOD_ID, value = Dist.CLIENT) +public class RodsTooltipHandler { + @SubscribeEvent + public static void onItemTooltip(ItemTooltipEvent event) { + ItemStack stack = event.getItemStack(); + Item item = stack.getItem(); + Player player = event.getEntity(); + + if (player == null) return; + + ResourceLocation id = BuiltInRegistries.ITEM.getKey(item); + // Mod items already get their rod tooltip via Registrate's setTooltipModifierFactory + // (CreateNuclear.REGISTRATE). This handler only serves EXTERNAL items (other mods or + // datapack-defined RodTypes resolved at runtime via RodType.resolveRodType), which + // setTooltipModifierFactory cannot cover. Do NOT remove/invert: doing so double-tooltips mod rods. + if (id != null && CreateNuclear.MOD_ID.equals(id.getNamespace())) return; + + RodsStats rodsStats = RodsStats.create(item); + rodsStats.modify(event); + } +} \ No newline at end of file diff --git a/src/main/java/net/nuclearteam/createnuclear/foundation/item/RodsStats.java b/src/main/java/net/nuclearteam/createnuclear/foundation/item/RodsStats.java new file mode 100644 index 0000000..d17bf13 --- /dev/null +++ b/src/main/java/net/nuclearteam/createnuclear/foundation/item/RodsStats.java @@ -0,0 +1,65 @@ +package net.nuclearteam.createnuclear.foundation.item; + +import com.simibubi.create.foundation.item.TooltipModifier; +import net.minecraft.ChatFormatting; +import net.minecraft.network.chat.CommonComponents; +import net.minecraft.network.chat.Component; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.Item; +import net.minecraft.world.level.Level; +import net.neoforged.neoforge.event.entity.player.ItemTooltipEvent; +import net.nuclearteam.createnuclear.api.multiblock.rods.RodType; +import net.nuclearteam.createnuclear.foundation.utility.CreateNuclearLang; + +import java.util.ArrayList; +import java.util.List; + +public record RodsStats(Item item) implements TooltipModifier { + + public static RodsStats create(Item item) { + return new RodsStats(item); + } + + @Override + public void modify(ItemTooltipEvent context) { + List rodTypesStat = getRodTypeStats(item, context.getEntity()); + if (!rodTypesStat.isEmpty()) { + List tooltip = context.getToolTip(); + tooltip.add(CommonComponents.EMPTY); + tooltip.addAll(rodTypesStat); + } + } + + private static List getRodTypeStats(Item item, Player player) { + Level world = player.level(); + List components = new ArrayList<>(); + + RodType rodType = RodType.resolveRodType(item, world); + + if (rodType.isNotEmptyItem()) { + CreateNuclearLang.translate("tooltip.statRod") + .style(ChatFormatting.GRAY) + .addTo(components); + + CreateNuclearLang.builder() + .add(CreateNuclearLang.translate("tooltip.baseRodHeat", rodType.baseRodHeat().get())) + .addTo(components); + CreateNuclearLang.builder() + .add(CreateNuclearLang.translate("tooltip.proximityRodHeat", rodType.proximityRodHeat().get())) + .addTo(components); + CreateNuclearLang.builder() + .add(CreateNuclearLang.translate("tooltip.rodTimer", rodType.rodTimer().get())) + .addTo(components); + CreateNuclearLang.builder() + .add(CreateNuclearLang.translate("tooltip.ratio", rodType.ratio().get())) + .addTo(components); + CreateNuclearLang.builder() + .add(CreateNuclearLang.translate("tooltip.typeRod", rodType.type().name())) + .style(rodType.type().equals(RodType.TypeRod.FUEL) ? ChatFormatting.GREEN : ChatFormatting.AQUA) + .addTo(components); + } + + return components; + + } +} \ No newline at end of file diff --git a/src/main/java/net/nuclearteam/createnuclear/foundation/mixin/client/CameraAccessor.java b/src/main/java/net/nuclearteam/createnuclear/foundation/mixin/client/CameraAccessor.java new file mode 100644 index 0000000..fed57d9 --- /dev/null +++ b/src/main/java/net/nuclearteam/createnuclear/foundation/mixin/client/CameraAccessor.java @@ -0,0 +1,12 @@ +package net.nuclearteam.createnuclear.foundation.mixin.client; + +import net.minecraft.client.Camera; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Invoker; + +@Mixin(Camera.class) +public interface CameraAccessor { + // Exposes the private Camera#move so it can be called from outside the class + @Invoker("move") + void callMove(float distanceOffset, float verticalOffset, float horizontalOffset); +} diff --git a/src/main/java/net/nuclearteam/createnuclear/foundation/mixin/client/GameRendererMixin.java b/src/main/java/net/nuclearteam/createnuclear/foundation/mixin/client/GameRendererMixin.java new file mode 100644 index 0000000..4e4b1b6 --- /dev/null +++ b/src/main/java/net/nuclearteam/createnuclear/foundation/mixin/client/GameRendererMixin.java @@ -0,0 +1,43 @@ +package net.nuclearteam.createnuclear.foundation.mixin.client; + +import net.minecraft.client.DeltaTracker; +import net.minecraft.client.renderer.GameRenderer; +import net.nuclearteam.createnuclear.CNClientProxy; +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(GameRenderer.class) +public abstract class GameRendererMixin { + + @Shadow + private float darkenWorldAmount; + + // Handles darkening the sky/world during a nuclear explosion + @Inject( + method = {"Lnet/minecraft/client/renderer/GameRenderer;tick()V"}, + remap = true, + at = @At(value = "TAIL") + ) + public void CN$tick(CallbackInfo ci) { + if (CNClientProxy.renderNukeSkyDarkFor > 0 && darkenWorldAmount < 1.0F) { + darkenWorldAmount = Math.min(darkenWorldAmount + 0.3F, 1.0F); + } + } + + // Triggers the white flash render (preScreenRender) + @Inject( + method = {"Lnet/minecraft/client/renderer/GameRenderer;render(Lnet/minecraft/client/DeltaTracker;Z)V"}, + remap = true, + at = @At( + value = "INVOKE", + target = "Lcom/mojang/blaze3d/platform/Lighting;setupFor3DItems()V", + shift = At.Shift.AFTER + ) + ) + public void CN$render(DeltaTracker deltaTracker, boolean renderLevel, CallbackInfo ci) { + CNClientProxy.preScreenRender(deltaTracker.getGameTimeDeltaPartialTick(false)); + } +} diff --git a/src/main/java/net/nuclearteam/createnuclear/gametest/DefaultHeatCalculatorGameTest.java b/src/main/java/net/nuclearteam/createnuclear/gametest/DefaultHeatCalculatorGameTest.java new file mode 100644 index 0000000..fdd8a74 --- /dev/null +++ b/src/main/java/net/nuclearteam/createnuclear/gametest/DefaultHeatCalculatorGameTest.java @@ -0,0 +1,368 @@ +package net.nuclearteam.createnuclear.gametest; + +import io.netty.buffer.Unpooled; +import net.minecraft.core.BlockPos; +import net.minecraft.gametest.framework.GameTest; +import net.minecraft.gametest.framework.GameTestAssertException; +import net.minecraft.gametest.framework.GameTestHelper; +import net.minecraft.nbt.Tag; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.neoforged.neoforge.gametest.GameTestHolder; +import net.neoforged.neoforge.gametest.PrefixGameTestTemplate; +import net.nuclearteam.createnuclear.CNBlocks; +import net.nuclearteam.createnuclear.CNDataComponents; +import net.nuclearteam.createnuclear.CNItems; +import net.nuclearteam.createnuclear.CreateNuclear; +import net.nuclearteam.createnuclear.content.multiblock.bluePrintItem.PatternData; +import net.nuclearteam.createnuclear.content.multiblock.bluePrintItem.ReactorBluePrintData; +import net.nuclearteam.createnuclear.content.multiblock.controller.ReactorControllerBlockEntity; +import net.nuclearteam.createnuclear.content.multiblock.controller.ReactorControllerInventory; +import net.nuclearteam.createnuclear.content.multiblock.controller.display.ReactorDisplayState; +import net.nuclearteam.createnuclear.content.multiblock.reactorLogic.DefaultHeatCalculator; +import net.nuclearteam.createnuclear.infrastructure.config.CNConfigs; + +import java.util.List; +import java.util.Map; + +/** + * GameTest coverage for {@link DefaultHeatCalculator#computeHeat}, run against a real + * {@code ServerLevel}, a real {@link ReactorControllerBlockEntity} and real registered + * rod items (no mocks) — plain JUnit cannot exercise this method at all: {@code Item}, + * {@code ItemStack} and {@code RodType} resolution all depend on Minecraft/NeoForge registries + * that only exist once the game has bootstrapped. + *

+ * All tests share the "empty_platform" structure and place a single + * {@code CNBlocks.REACTOR_CONTROLLER} to obtain a real {@link ReactorControllerInventory}. + * The reactor pattern (57 slots, indices per {@code DefaultHeatCalculator.FORMATTED_PATTERN}) + * is written into the blueprint's {@link ReactorBluePrintData} component and set into inventory + * slot 0, exactly as {@code ReactorBluePrintItem.getItemStorage} reads it back. + *

+ * This is where the port diverges from Forge: Forge writes the pattern into the blueprint's + * raw {@code pattern} NBT tag, which has no NeoForge equivalent since the blueprint carries a + * typed data component instead. + *

+ * Expected heat values are computed from the live {@code CNConfigs.server().rods} values + * rather than hardcoded, so these tests stay correct if the mod's default balance changes. + */ +@GameTestHolder(CreateNuclear.MOD_ID) +@PrefixGameTestTemplate(false) +public class DefaultHeatCalculatorGameTest { + + private static final String STRUCTURE = "empty_platform"; + private static final double DELTA = 1e-9; + private static final int PATTERN_SLOTS = 57; + + // ---------- test fixtures ---------- + + private static ReactorControllerInventory placeController(GameTestHelper helper, BlockPos rel) { + helper.setBlock(rel, CNBlocks.REACTOR_CONTROLLER.get().defaultBlockState()); + ReactorControllerBlockEntity be = (ReactorControllerBlockEntity) helper.getBlockEntity(rel); + return be.getInventoryObject(); + } + + /** + * Writes {@code rodsBySlot} into the blueprint's {@link ReactorBluePrintData} component and + * loads it into slot 0. Every one of the 57 slots must be present, since + * {@code ReactorBluePrintItem.getItemStorage} bails out on a shorter array. + */ + private static void loadPattern(ReactorControllerInventory inventory, Map rodsBySlot) { + inventory.setStackInSlot(0, buildBlueprint(rodsBySlot)); + } + + /** Builds the blueprint {@link #loadPattern} loads, without touching an inventory. */ + private static ItemStack buildBlueprint(Map rodsBySlot) { + PatternData[] pattern = new PatternData[PATTERN_SLOTS]; + for (int slot = 0; slot < PATTERN_SLOTS; slot++) { + Item rod = rodsBySlot.get(slot); + pattern[slot] = new PatternData(slot, rod == null ? ItemStack.EMPTY : new ItemStack(rod)); + } + + ItemStack blueprint = new ItemStack(CNItems.REACTOR_BLUEPRINT.get()); + blueprint.set(CNDataComponents.REACTOR_BLUE_PRINT_DATA, + new ReactorBluePrintData(0, 0, pattern)); + return blueprint; + } + + private static final DefaultHeatCalculator CALCULATOR = new DefaultHeatCalculator(); + + // ================================================================ + // 0. the heat component must survive being saved at zero + // ================================================================ + + /** + * Regression test for a world-save crash: {@code CNDataComponents.HEAT} was registered with + * {@code ExtraCodecs.POSITIVE_FLOAT}, which rejects {@code 0.0}. The controller writes the + * heat on every tick, so as soon as a blueprint sat in a stopped reactor the chunk save blew + * up with "Value must be positive: 0.0" and the block entity silently stopped persisting. + *

+ * A persistent codec that can refuse a value fails at save time, not at write time, which is + * why this is asserted here rather than left to the non-negative floor in + * {@link DefaultHeatCalculator#computeHeat}. + */ + @GameTest(template = STRUCTURE) + public static void heatComponent_atZero_survivesSerialization(GameTestHelper helper) { + ItemStack blueprint = new ItemStack(CNItems.REACTOR_BLUEPRINT.get()); + blueprint.set(CNDataComponents.HEAT, 0f); + + var registries = helper.getLevel().registryAccess(); + Tag saved; + try { + saved = blueprint.save(registries); + } catch (RuntimeException e) { + throw new GameTestAssertException( + "a blueprint carrying heat=0 must be serializable, got: " + e.getMessage()); + } + + ItemStack restored = ItemStack.parse(registries, saved).orElse(ItemStack.EMPTY); + helper.assertTrue(restored.getOrDefault(CNDataComponents.HEAT, -1f) == 0f, + "heat=0 should round-trip through save/parse, got " + + restored.getOrDefault(CNDataComponents.HEAT, -1f)); + helper.succeed(); + } + + // ================================================================ + // 0b. a blueprint with empty pattern slots must survive saving + // ================================================================ + + /** + * Regression test for a second world-save crash, same family as + * {@link #heatComponent_atZero_survivesSerialization} but on the pattern rather than the heat. + *

+ * {@code PatternData.CODEC} used {@link ItemStack#CODEC}, which rejects the empty stack + * ("Item must not be minecraft:air" / "Value must be within range [1;99]: 0"). A reactor + * pattern is 57 slots and is almost never full, so any blueprint sitting in a controller made + * {@code ReactorControllerBlockEntity.write} throw at chunk-save time. The block entity then + * silently stopped persisting — the controller lost its blueprint on every world reload. + *

+ * The fix is {@link ItemStack#OPTIONAL_CODEC}, which encodes the empty stack as {@code {}} + * and leaves the encoding of non-empty stacks untouched, so blueprints already on disk still + * load. + */ + @GameTest(template = STRUCTURE) + public static void blueprintWithEmptyPatternSlots_survivesSerialization(GameTestHelper helper) { + ItemStack blueprint = buildBlueprint(Map.of(18, CNItems.THORIUM_ROD.get())); + + var registries = helper.getLevel().registryAccess(); + Tag saved; + try { + saved = blueprint.save(registries); + } catch (RuntimeException e) { + throw new GameTestAssertException( + "a blueprint whose pattern has empty slots must be serializable, got: " + e.getMessage()); + } + + ItemStack restored = ItemStack.parse(registries, saved).orElse(ItemStack.EMPTY); + ReactorBluePrintData data = restored.get(CNDataComponents.REACTOR_BLUE_PRINT_DATA); + helper.assertTrue(data != null, "the pattern component should round-trip through save/parse"); + + helper.assertTrue(data.pattern().length == PATTERN_SLOTS, + "all " + PATTERN_SLOTS + " pattern slots should round-trip, got " + data.pattern().length); + helper.assertTrue(data.pattern()[18].stack().is(CNItems.THORIUM_ROD.get()), + "the populated slot should keep its rod, got " + data.pattern()[18].stack()); + helper.assertTrue(data.pattern()[0].stack().isEmpty(), + "an empty slot should round-trip as empty, got " + data.pattern()[0].stack()); + helper.succeed(); + } + + // ================================================================ + // 0c. the same blueprint must survive being synced to a client + // ================================================================ + + /** + * Same defect as {@link #blueprintWithEmptyPatternSlots_survivesSerialization}, on the network + * path: {@code PatternData.STREAM_CODEC} used {@link ItemStack#STREAM_CODEC}, which throws + * {@code EncoderException("Empty ItemStack not allowed")} on the empty stack. Since + * {@code REACTOR_BLUE_PRINT_DATA} is registered as {@code networkSynchronized}, merely holding + * a blueprint would fail to encode when the stack was sent to a client. + */ + @GameTest(template = STRUCTURE) + public static void blueprintWithEmptyPatternSlots_survivesNetworkSync(GameTestHelper helper) { + ItemStack blueprint = buildBlueprint(Map.of(18, CNItems.THORIUM_ROD.get())); + + RegistryFriendlyByteBuf buf = + new RegistryFriendlyByteBuf(Unpooled.buffer(), helper.getLevel().registryAccess()); + try { + ItemStack.STREAM_CODEC.encode(buf, blueprint); + } catch (RuntimeException e) { + throw new GameTestAssertException( + "a blueprint whose pattern has empty slots must be network-encodable, got: " + e.getMessage()); + } + + ItemStack restored = ItemStack.STREAM_CODEC.decode(buf); + ReactorBluePrintData data = restored.get(CNDataComponents.REACTOR_BLUE_PRINT_DATA); + helper.assertTrue(data != null, "the pattern component should round-trip over the network"); + helper.assertTrue(data.pattern()[18].stack().is(CNItems.THORIUM_ROD.get()), + "the populated slot should keep its rod, got " + data.pattern()[18].stack()); + helper.assertTrue(data.pattern()[0].stack().isEmpty(), + "an empty slot should round-trip as empty, got " + data.pattern()[0].stack()); + helper.succeed(); + } + + // ================================================================ + // 1. single fuel rod, no neighbors + // ================================================================ + + @GameTest(template = STRUCTURE) + public static void singleFuelRod_noNeighbors_addsOnlyItsOwnBaseRodHeat(GameTestHelper helper) { + Item thorium = CNItems.THORIUM_ROD.get(); + int baseRodHeat = CNConfigs.server().rods.baseValueThorium.get(); + + ReactorControllerInventory inventory = placeController(helper, new BlockPos(1, 1, 1)); + // slot 18: middle row, no adjacent slot populated -> no neighbor bonus possible + loadPattern(inventory, Map.of(18, thorium)); + + ReactorDisplayState displayState = new ReactorDisplayState(Map.of(thorium, 1), List.of(), 0); + double overHeat = 10.0; + + double heat = CALCULATOR.computeHeat(null, null, inventory, overHeat, displayState, helper.getLevel()); + + helper.assertTrue(Math.abs(heat - (baseRodHeat + overHeat)) < DELTA, + "an isolated fuel rod should only contribute its own baseRodHeat, plus overHeat: expected " + + (baseRodHeat + overHeat) + ", got " + heat); + helper.succeed(); + } + + // ================================================================ + // 2. single cooler rod, no neighbors + // ================================================================ + + @GameTest(template = STRUCTURE) + public static void singleCoolerRod_noNeighbors_addsOnlyItsOwnBaseRodHeat(GameTestHelper helper) { + Item graphite = CNItems.GRAPHITE_ROD.get(); + int baseRodHeat = CNConfigs.server().rods.graphiteBaseValue.get(); + + ReactorControllerInventory inventory = placeController(helper, new BlockPos(1, 1, 1)); + loadPattern(inventory, Map.of(18, graphite)); + + ReactorDisplayState displayState = new ReactorDisplayState(Map.of(graphite, 1), List.of(), 0); + + // computeHeat clamps its result to a minimum of 0 (Math.max(0, heat+overHeat)); an isolated + // cooler's own baseRodHeat is negative by default, so a large overHeat is added here to keep + // the total positive and actually observe baseRodHeat's contribution instead of it being + // swallowed by the floor. + double overHeat = 50.0; + + double heat = CALCULATOR.computeHeat(null, null, inventory, overHeat, displayState, helper.getLevel()); + + helper.assertTrue(Math.abs(heat - (baseRodHeat + overHeat)) < DELTA, + "a cooler contributes its own baseRodHeat exactly like a fuel rod would, when isolated: expected " + + (baseRodHeat + overHeat) + ", got " + heat); + helper.succeed(); + } + + // ================================================================ + // 3. fuel/cooler mix adjacency: cooler scans its own fuel neighbors symmetrically + // to a fuel rod, matching the wiki calculator / external spec + // ("Graphite extra: -1/4Q of the heating rod") + // ================================================================ + + /** + * Layout (row 3 of {@code FORMATTED_PATTERN}, three consecutive slots): + * uranium A (18) - thorium B (19) - graphite C (20). + *

+ * Expected total = sum of each rod's own baseRodHeat, PLUS proximity contributions from + * both fuel-fuel and cooler-fuel adjacency (cooler-cooler and fuel-cooler-from-the-fuel-side + * contribute nothing): + *

    + *
  • A's scan (fuel, neighbor B is fuel) -> + A.proximityRodHeat()
  • + *
  • B's scan (fuel, neighbor A is fuel) -> + B.proximityRodHeat()
  • + *
  • B's scan (fuel, neighbor C is cooler) -> contributes nothing (fuel only scores against fuel neighbors)
  • + *
  • C's scan (cooler, neighbor B is fuel) -> + B.baseRodHeat() * C.proximityRodHeat()
  • + *
+ */ + @GameTest(template = STRUCTURE) + public static void fuelCoolerMix_coolerScansItsOwnFuelNeighborsSymmetrically(GameTestHelper helper) { + Item uranium = CNItems.URANIUM_ROD.get(); + Item thorium = CNItems.THORIUM_ROD.get(); + Item graphite = CNItems.GRAPHITE_ROD.get(); + + int uraniumBase = CNConfigs.server().rods.uraniumBaseValue.get(); + float uraniumProxy = CNConfigs.server().rods.uraniumProximityBonus.get(); + int thoriumBase = CNConfigs.server().rods.baseValueThorium.get(); + float thoriumProxy = CNConfigs.server().rods.thoriumProxyBonus.get(); + int graphiteBase = CNConfigs.server().rods.graphiteBaseValue.get(); + float graphiteProxy = CNConfigs.server().rods.graphiteProximityMalus.getF(); + + ReactorControllerInventory inventory = placeController(helper, new BlockPos(1, 1, 1)); + loadPattern(inventory, Map.of(18, uranium, 19, thorium, 20, graphite)); + + ReactorDisplayState displayState = new ReactorDisplayState( + Map.of(uranium, 1, thorium, 1, graphite, 1), List.of(), 0); + + double heat = CALCULATOR.computeHeat(null, null, inventory, /*overHeat*/ 0.0, displayState, helper.getLevel()); + + double expected = (uraniumBase + thoriumBase + graphiteBase) // each rod's own baseRodHeat + + uraniumProxy // A's scan: fuel neighbor B (thorium) -> addition + + thoriumProxy // B's scan: fuel neighbor A (uranium) -> addition + + (thoriumBase * graphiteProxy); + // C's scan: fuel neighbor B (thorium) -> multiplication (neighbor's base * cooler's own proximity) + // B's scan towards C contributes 0: fuel only scores against fuel neighbors + + helper.assertTrue(Math.abs(heat - expected) < DELTA, + "cooler must score its own malus against its fuel neighbor: expected " + + expected + ", got " + heat); + helper.succeed(); + } + + // ================================================================ + // 4. 3x3 diamond of rods: cross-checked against the community wiki calculator + // ================================================================ + + /** + * Layout (rows 3-5, columns 3-5 of {@code FORMATTED_PATTERN} — a fully interior 3x3 block, + * every position outside it stays empty so neighbor counts match an isolated 3x3 grid): + *
+     * graphite(18) thorium(19) graphite(20)
+     * thorium(27)  uranium(28) thorium(29)
+     * graphite(36) thorium(37) graphite(38)
+     * 
+ * With the mod's default balance values this totals {@code 128}, matching the value produced + * by the community wiki calculator for the same pattern — but the assertion below is derived + * from the live config, not hardcoded, so it stays correct if the default balance changes: + *
    + *
  • 4 corner graphites, each with 2 thorium neighbors -> + * {@code graphiteBase + 2 * (thoriumBase * graphiteProxy)} each
  • + *
  • 4 edge thoriums, each with 2 graphite neighbors (ignored, cooler) + 1 uranium neighbor (fuel) -> + * {@code thoriumBase + thoriumProxy} each
  • + *
  • 1 center uranium, with 4 thorium neighbors (all fuel) -> + * {@code uraniumBase + 4 * uraniumProxy}
  • + *
+ */ + @GameTest(template = STRUCTURE) + public static void threeByThreeDiamond_matchesWikiCalculatorReferenceValue(GameTestHelper helper) { + Item uranium = CNItems.URANIUM_ROD.get(); + Item thorium = CNItems.THORIUM_ROD.get(); + Item graphite = CNItems.GRAPHITE_ROD.get(); + + int uraniumBase = CNConfigs.server().rods.uraniumBaseValue.get(); + float uraniumProxy = CNConfigs.server().rods.uraniumProximityBonus.get(); + int thoriumBase = CNConfigs.server().rods.baseValueThorium.get(); + float thoriumProxy = CNConfigs.server().rods.thoriumProxyBonus.get(); + int graphiteBase = CNConfigs.server().rods.graphiteBaseValue.get(); + float graphiteProxy = CNConfigs.server().rods.graphiteProximityMalus.getF(); + + ReactorControllerInventory inventory = placeController(helper, new BlockPos(1, 1, 1)); + loadPattern(inventory, Map.ofEntries( + Map.entry(18, graphite), Map.entry(19, thorium), Map.entry(20, graphite), + Map.entry(27, thorium), Map.entry(28, uranium), Map.entry(29, thorium), + Map.entry(36, graphite), Map.entry(37, thorium), Map.entry(38, graphite) + )); + + ReactorDisplayState displayState = new ReactorDisplayState( + Map.of(uranium, 1, thorium, 4, graphite, 4), List.of(), 0); + + double heat = CALCULATOR.computeHeat(null, null, inventory, /*overHeat*/ 0.0, displayState, helper.getLevel()); + + double cornerGraphite = graphiteBase + 2 * (thoriumBase * graphiteProxy); + double edgeThorium = thoriumBase + thoriumProxy; + double centerUranium = uraniumBase + 4 * uraniumProxy; + double expected = 4 * cornerGraphite + 4 * edgeThorium + centerUranium; + + helper.assertTrue(Math.abs(heat - expected) < DELTA, + "3x3 diamond should match the wiki calculator's reference value for this pattern: expected " + + expected + " (128 with default balance), got " + heat); + helper.succeed(); + } +} diff --git a/src/main/java/net/nuclearteam/createnuclear/gametest/ReactorInputFluidManagerGameTest.java b/src/main/java/net/nuclearteam/createnuclear/gametest/ReactorInputFluidManagerGameTest.java new file mode 100644 index 0000000..2ae58d0 --- /dev/null +++ b/src/main/java/net/nuclearteam/createnuclear/gametest/ReactorInputFluidManagerGameTest.java @@ -0,0 +1,455 @@ +package net.nuclearteam.createnuclear.gametest; + +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.gametest.framework.GameTest; +import net.minecraft.gametest.framework.GameTestHelper; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.ListTag; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.material.Fluids; +import net.neoforged.neoforge.capabilities.Capabilities; +import net.neoforged.neoforge.fluids.FluidStack; +import net.neoforged.neoforge.fluids.capability.IFluidHandler; +import net.neoforged.neoforge.fluids.capability.IFluidHandler.FluidAction; +import net.neoforged.neoforge.gametest.GameTestHolder; +import net.neoforged.neoforge.gametest.PrefixGameTestTemplate; +import net.nuclearteam.createnuclear.CNBlocks; +import net.nuclearteam.createnuclear.CreateNuclear; +import net.nuclearteam.createnuclear.content.multiblock.controller.manager.ReactorInputFluidManager; +import net.nuclearteam.createnuclear.content.multiblock.input.fluid.ReactorFluidInputEntity; +import net.nuclearteam.createnuclear.content.multiblock.input.fluid.VirtualReactorInputFluid; +import net.nuclearteam.createnuclear.foundation.block.MultiDirectionalReactorBlock; + +import java.util.List; + +/** + * GameTest coverage for {@link ReactorInputFluidManager}, run against a real + * {@code ServerLevel} and real {@link ReactorFluidInputEntity} blocks (no mocks). + *

+ * All tests share the "empty_platform" structure + * (data/createnuclear/structure/empty_platform.nbt): a 5x3x5 area with a + * stone floor at y=0, leaving y=1..2 free to place fluid input blocks. The structure + * is a real compressed structure-block export (a hand-authored .snbt cannot be loaded + * directly here). Note the folder is {@code structure}, singular: 1.21 renamed the + * data folders that Forge 1.20.1 spelled {@code structures}. + *

+ * Capability access goes through {@code level.getCapability(Capabilities.FluidHandler.BLOCK, ...)} + * instead of Forge's {@code be.getCapability(ForgeCapabilities.FLUID_HANDLER)}, and fluid ids are + * resolved via {@code BuiltInRegistries.FLUID} instead of {@code ForgeRegistries.FLUIDS}. + *

+ * Tests named "*_expectedContract" cover the two extractFluids defects fixed on both branches: + * fluidNeeded was never decremented between handlers (so a request of 10 units drained 10 from + * EVERY input), and a request for exactly one unit was silently dropped by an + * {@code if (toExtract > 1)} guard. The whole suite passes; the two characterization tests that + * pinned the buggy behaviour were removed with the fix. + */ +@GameTestHolder(CreateNuclear.MOD_ID) +@PrefixGameTestTemplate(false) +public class ReactorInputFluidManagerGameTest { + + private static final String STRUCTURE = "empty_platform"; + + // ---------- test fixtures ---------- + + private static void place(GameTestHelper helper, BlockPos rel, Direction facing) { + helper.setBlock(rel, CNBlocks.REACTOR_FLUID_INPUT.get().defaultBlockState() + .setValue(MultiDirectionalReactorBlock.FACING, facing)); + } + + private static IFluidHandler handler(GameTestHelper helper, BlockPos rel) { + return helper.getLevel().getCapability(Capabilities.FluidHandler.BLOCK, helper.absolutePos(rel), null); + } + + private static void fill(GameTestHelper helper, BlockPos rel, FluidStack stack) { + IFluidHandler h = handler(helper, rel); + if (h != null) h.fill(stack, FluidAction.EXECUTE); + } + + private static int tankAmount(GameTestHelper helper, BlockPos rel) { + IFluidHandler h = handler(helper, rel); + return h == null ? -1 : h.getFluidInTank(0).getAmount(); + } + + private static ReactorInputFluidManager manager(GameTestHelper helper, BlockPos... relPositions) { + ReactorInputFluidManager manager = new ReactorInputFluidManager(); + for (BlockPos rel : relPositions) manager.addBlock(helper.absolutePos(rel)); + return manager; + } + + // ================================================================ + // read / write (pure NBT, no world interaction required) + // ================================================================ + + @GameTest(template = STRUCTURE) + public static void read_absentKey_leavesEmpty(GameTestHelper helper) { + ReactorInputFluidManager manager = new ReactorInputFluidManager(); + manager.read(new CompoundTag()); + + helper.assertTrue(manager.size() == 0, "expected no tracked positions when NBT key is absent"); + helper.succeed(); + } + + @GameTest(template = STRUCTURE) + public static void read_clearsPreviousPositions(GameTestHelper helper) { + ReactorInputFluidManager manager = new ReactorInputFluidManager(); + manager.addBlock(new BlockPos(1, 2, 3)); + + manager.read(new CompoundTag()); + + helper.assertTrue(manager.size() == 0, "read() must clear previously tracked positions before loading"); + helper.succeed(); + } + + @GameTest(template = STRUCTURE) + public static void read_singleEntry_roundTrips(GameTestHelper helper) { + CompoundTag entry = new CompoundTag(); + entry.putInt("x", 5); + entry.putInt("y", 64); + entry.putInt("z", -3); + ListTag list = new ListTag(); + list.add(entry); + CompoundTag root = new CompoundTag(); + root.put("ReactorInputFluid", list); + + ReactorInputFluidManager manager = new ReactorInputFluidManager(); + manager.read(root); + + helper.assertTrue(manager.size() == 1, "expected exactly one entry after read"); + helper.assertTrue(manager.contains(new BlockPos(5, 64, -3)), "expected BlockPos(5,64,-3) to be present"); + helper.succeed(); + } + + @GameTest(template = STRUCTURE) + public static void read_multipleEntries_preservesOrder(GameTestHelper helper) { + BlockPos a = new BlockPos(0, 0, 0); + BlockPos b = new BlockPos(1, 1, 1); + BlockPos c = new BlockPos(2, 2, 2); + + ListTag list = new ListTag(); + for (BlockPos p : List.of(a, b, c)) { + CompoundTag entry = new CompoundTag(); + entry.putInt("x", p.getX()); + entry.putInt("y", p.getY()); + entry.putInt("z", p.getZ()); + list.add(entry); + } + CompoundTag root = new CompoundTag(); + root.put("ReactorInputFluid", list); + + ReactorInputFluidManager manager = new ReactorInputFluidManager(); + manager.read(root); + + helper.assertTrue(manager.getBlocksPosition().equals(List.of(a, b, c)), + "expected insertion order to be preserved, found " + manager.getBlocksPosition()); + helper.succeed(); + } + + @GameTest(template = STRUCTURE) + public static void write_emptyPositions_stillWritesKey(GameTestHelper helper) { + CompoundTag tag = new CompoundTag(); + new ReactorInputFluidManager().write(tag); + + helper.assertTrue(tag.contains("ReactorInputFluid"), "write() should always write the NBT key, even when empty"); + helper.succeed(); + } + + @GameTest(template = STRUCTURE) + public static void write_then_read_roundTrip(GameTestHelper helper) { + BlockPos a = new BlockPos(1, 2, 3); + BlockPos b = new BlockPos(-5, 64, 100); + + ReactorInputFluidManager original = new ReactorInputFluidManager(); + original.addBlock(a); + original.addBlock(b); + + CompoundTag tag = new CompoundTag(); + original.write(tag); + + ReactorInputFluidManager restored = new ReactorInputFluidManager(); + restored.read(tag); + + helper.assertTrue(restored.getBlocksPosition().equals(List.of(a, b)), + "round-trip through NBT should preserve exact positions and order"); + helper.succeed(); + } + + // ================================================================ + // clearInvalid + // ================================================================ + + @GameTest(template = STRUCTURE) + public static void clearInvalid_missingBlockEntity_removesPosition(GameTestHelper helper) { + BlockPos rel = new BlockPos(1, 1, 1); // never placed: air, no block entity + ReactorInputFluidManager manager = manager(helper, rel); + + manager.clearInvalid(helper.getLevel()); + + helper.assertTrue(manager.size() == 0, "a position without a block entity should be removed"); + helper.succeed(); + } + + @GameTest(template = STRUCTURE) + public static void clearInvalid_noCapability_removesPosition(GameTestHelper helper) { + BlockPos rel = new BlockPos(1, 1, 1); + helper.setBlock(rel, Blocks.CHEST.defaultBlockState()); // real BE, no fluid capability + ReactorInputFluidManager manager = manager(helper, rel); + + manager.clearInvalid(helper.getLevel()); + + helper.assertTrue(manager.size() == 0, "a block entity without a fluid capability should be removed"); + helper.succeed(); + } + + @GameTest(template = STRUCTURE) + public static void clearInvalid_validCapability_keepsPosition(GameTestHelper helper) { + BlockPos rel = new BlockPos(1, 1, 1); + place(helper, rel, Direction.NORTH); + ReactorInputFluidManager manager = manager(helper, rel); + + manager.clearInvalid(helper.getLevel()); + + helper.assertTrue(manager.size() == 1, "a valid ReactorFluidInputEntity should be kept"); + helper.succeed(); + } + + @GameTest(template = STRUCTURE) + public static void clearInvalid_mixedSet_onlyInvalidRemoved(GameTestHelper helper) { + BlockPos valid = new BlockPos(1, 1, 1); + BlockPos chest = new BlockPos(3, 1, 1); + BlockPos missing = new BlockPos(1, 1, 3); + + place(helper, valid, Direction.NORTH); + helper.setBlock(chest, Blocks.CHEST.defaultBlockState()); + + ReactorInputFluidManager manager = manager(helper, valid, chest, missing); + manager.clearInvalid(helper.getLevel()); + + helper.assertTrue(manager.getBlocksPosition().equals(List.of(helper.absolutePos(valid))), + "only the valid fluid input position should remain, found " + manager.getBlocksPosition()); + helper.succeed(); + } + + // ================================================================ + // getBlocksPosition(Level) + // ================================================================ + + @GameTest(template = STRUCTURE) + public static void getBlocksPosition_filtersNonMatchingEntityType(GameTestHelper helper) { + BlockPos validRel = new BlockPos(1, 1, 1); + BlockPos otherRel = new BlockPos(3, 1, 1); + place(helper, validRel, Direction.NORTH); + helper.setBlock(otherRel, Blocks.CHEST.defaultBlockState()); + + ReactorInputFluidManager manager = manager(helper, validRel, otherRel); + + List result = manager.getBlocksPosition(helper.getLevel()); + + helper.assertTrue(result.equals(List.of(helper.absolutePos(validRel))), + "only the ReactorFluidInputEntity position should be returned, found " + result); + helper.succeed(); + } + + // ================================================================ + // getFuildHandlers + // ================================================================ + + @GameTest(template = STRUCTURE) + public static void getFuildHandlers_capabilityPresent_included(GameTestHelper helper) { + BlockPos rel = new BlockPos(1, 1, 1); + place(helper, rel, Direction.NORTH); + fill(helper, rel, new FluidStack(Fluids.WATER, 500)); + + List handlers = manager(helper, rel).getFuildHandlers(helper.getLevel()); + + helper.assertTrue(handlers.size() == 1, "expected exactly one handler"); + helper.assertTrue(handlers.get(0).getFluidInTank(0).getAmount() == 500, + "handler should expose the filled tank content"); + helper.succeed(); + } + + @GameTest(template = STRUCTURE) + public static void getFuildHandlers_preservesTrackingOrder(GameTestHelper helper) { + BlockPos rel1 = new BlockPos(1, 1, 1); + BlockPos rel2 = new BlockPos(3, 1, 1); + place(helper, rel1, Direction.NORTH); + place(helper, rel2, Direction.NORTH); + fill(helper, rel1, new FluidStack(Fluids.WATER, 100)); + fill(helper, rel2, new FluidStack(Fluids.LAVA, 200)); + + List handlers = manager(helper, rel1, rel2).getFuildHandlers(helper.getLevel()); + + helper.assertTrue(handlers.size() == 2, "expected two handlers"); + helper.assertTrue(handlers.get(0).getFluidInTank(0).getFluid() == Fluids.WATER, + "first handler should be the water tank (matches insertion order)"); + helper.assertTrue(handlers.get(1).getFluidInTank(0).getFluid() == Fluids.LAVA, + "second handler should be the lava tank (matches insertion order)"); + helper.succeed(); + } + + // ================================================================ + // getInventory + // ================================================================ + + @GameTest(template = STRUCTURE) + public static void getInventory_noHandlers_returnsEmpty(GameTestHelper helper) { + VirtualReactorInputFluid inventory = new ReactorInputFluidManager().getInventory(helper.getLevel()); + + helper.assertTrue(inventory.fluids().isEmpty(), "expected an empty inventory with no tracked handlers"); + helper.succeed(); + } + + @GameTest(template = STRUCTURE) + public static void getInventory_aggregatesAcrossHandlers(GameTestHelper helper) { + BlockPos rel1 = new BlockPos(1, 1, 1); + BlockPos rel2 = new BlockPos(3, 1, 1); + place(helper, rel1, Direction.NORTH); + place(helper, rel2, Direction.NORTH); + fill(helper, rel1, new FluidStack(Fluids.WATER, 300)); + fill(helper, rel2, new FluidStack(Fluids.WATER, 250)); + + VirtualReactorInputFluid inventory = manager(helper, rel1, rel2).getInventory(helper.getLevel()); + + ResourceLocation waterId = BuiltInRegistries.FLUID.getKey(Fluids.WATER); + long total = inventory.getAmount(waterId); + helper.assertTrue(total == 550, "expected aggregated total of 550, found " + total); + helper.succeed(); + } + + // ================================================================ + // extractFluids — core of the audited bug + // ================================================================ + + @GameTest(template = STRUCTURE) + public static void extractFluids_nullLevel_returnsFalse(GameTestHelper helper) { + boolean result = new ReactorInputFluidManager().extractFluids(null, 10); + helper.assertTrue(!result, "extractFluids should return false for a null level"); + helper.succeed(); + } + + @GameTest(template = STRUCTURE) + public static void extractFluids_noHandlers_returnsFalse(GameTestHelper helper) { + boolean result = new ReactorInputFluidManager().extractFluids(helper.getLevel(), 10); + helper.assertTrue(!result, "extractFluids should return false when there are no handlers"); + helper.succeed(); + } + + @GameTest(template = STRUCTURE) + public static void extractFluids_singleHandlerEnoughStock_drainsExactAmount(GameTestHelper helper) { + BlockPos rel = new BlockPos(1, 1, 1); + place(helper, rel, Direction.NORTH); + fill(helper, rel, new FluidStack(Fluids.WATER, 1000)); + + boolean result = manager(helper, rel).extractFluids(helper.getLevel(), 400); + + helper.assertTrue(result, "extraction should succeed when enough fluid is available"); + helper.assertTrue(tankAmount(helper, rel) == 600, "expected 600 remaining, found " + tankAmount(helper, rel)); + helper.succeed(); + } + + @GameTest(template = STRUCTURE) + public static void extractFluids_singleHandlerInsufficientStock_drainsAvailable(GameTestHelper helper) { + BlockPos rel = new BlockPos(1, 1, 1); + place(helper, rel, Direction.NORTH); + fill(helper, rel, new FluidStack(Fluids.WATER, 30)); + + boolean result = manager(helper, rel).extractFluids(helper.getLevel(), 100); + + helper.assertTrue(result, "extraction should be reported successful even for a partial drain"); + helper.assertTrue(tankAmount(helper, rel) == 0, "all 30 available units should have been drained"); + helper.succeed(); + } + + /** + * A request for exactly 1 unit with stock available must succeed. This used to fail against + * an {@code if (toExtract > 1)} guard that silently dropped single-unit requests. + */ + @GameTest(template = STRUCTURE) + public static void extractFluids_needOfOne_shouldSucceed_expectedContract(GameTestHelper helper) { + BlockPos rel = new BlockPos(1, 1, 1); + place(helper, rel, Direction.NORTH); + fill(helper, rel, new FluidStack(Fluids.WATER, 5)); + + boolean result = manager(helper, rel).extractFluids(helper.getLevel(), 1); + + helper.assertTrue(result, "a request for exactly 1 unit with stock available should succeed"); + helper.assertTrue(tankAmount(helper, rel) == 4, "expected exactly 1 unit to be drained"); + helper.succeed(); + } + + /** + * Extracting 10 units from a 20-unit pool spread across two handlers must leave 10 total + * remaining. The requested amount is a total across every input, not a per-input quota; + * this used to drain both handlers in full because {@code fluidNeeded} was never + * decremented between them. + */ + @GameTest(template = STRUCTURE) + public static void extractFluids_twoHandlers_shouldNotOverExtract_expectedContract(GameTestHelper helper) { + BlockPos rel1 = new BlockPos(1, 1, 1); + BlockPos rel2 = new BlockPos(3, 1, 1); + place(helper, rel1, Direction.NORTH); + place(helper, rel2, Direction.NORTH); + fill(helper, rel1, new FluidStack(Fluids.WATER, 10)); + fill(helper, rel2, new FluidStack(Fluids.WATER, 10)); + + manager(helper, rel1, rel2).extractFluids(helper.getLevel(), 10); + + int totalRemaining = tankAmount(helper, rel1) + tankAmount(helper, rel2); + helper.assertTrue(totalRemaining == 10, + "extracting 10 out of a 20-unit pool should leave 10 total remaining, found " + totalRemaining); + helper.succeed(); + } + + @GameTest(template = STRUCTURE) + public static void extractFluids_needZero_leavesTanksUntouched(GameTestHelper helper) { + BlockPos rel1 = new BlockPos(1, 1, 1); + BlockPos rel2 = new BlockPos(3, 1, 1); + place(helper, rel1, Direction.NORTH); + place(helper, rel2, Direction.NORTH); + fill(helper, rel1, new FluidStack(Fluids.WATER, 10)); + fill(helper, rel2, new FluidStack(Fluids.WATER, 10)); + + boolean result = manager(helper, rel1, rel2).extractFluids(helper.getLevel(), 0); + + helper.assertTrue(!result, "a non-positive fluidNeeded should never extract anything"); + helper.assertTrue(tankAmount(helper, rel1) == 10 && tankAmount(helper, rel2) == 10, + "no tank should be touched when fluidNeeded <= 0"); + helper.succeed(); + } + + @GameTest(template = STRUCTURE) + public static void extractFluids_emptyStackHandler_skippedGracefully(GameTestHelper helper) { + BlockPos rel1 = new BlockPos(1, 1, 1); + BlockPos rel2 = new BlockPos(3, 1, 1); + place(helper, rel1, Direction.NORTH); // left empty + place(helper, rel2, Direction.NORTH); + fill(helper, rel2, new FluidStack(Fluids.WATER, 20)); + + boolean result = manager(helper, rel1, rel2).extractFluids(helper.getLevel(), 15); + + helper.assertTrue(result, "the second, non-empty handler should still be usable"); + helper.assertTrue(tankAmount(helper, rel1) == 0, "empty handler should stay untouched"); + helper.assertTrue(tankAmount(helper, rel2) == 5, "expected 15 drained out of 20, 5 remaining"); + helper.succeed(); + } + + /** + * A partial extraction still reports success: the reactor consumes whatever coolant it can + * reach rather than refusing to run. The Javadoc used to promise "true if the full amount + * was extracted", which never matched the implementation; it now documents this contract. + */ + @GameTest(template = STRUCTURE) + public static void extractFluids_partialExtraction_stillReportsSuccess(GameTestHelper helper) { + BlockPos rel = new BlockPos(1, 1, 1); + place(helper, rel, Direction.NORTH); + fill(helper, rel, new FluidStack(Fluids.WATER, 30)); + + boolean result = manager(helper, rel).extractFluids(helper.getLevel(), 100); + + helper.assertTrue(result, "a partial extraction should still report success"); + helper.assertTrue(tankAmount(helper, rel) == 0, "all available fluid should have been drained"); + helper.succeed(); + } +} diff --git a/src/main/java/net/nuclearteam/createnuclear/infrastructure/config/CExplose.java b/src/main/java/net/nuclearteam/createnuclear/infrastructure/config/CExplode.java similarity index 94% rename from src/main/java/net/nuclearteam/createnuclear/infrastructure/config/CExplose.java rename to src/main/java/net/nuclearteam/createnuclear/infrastructure/config/CExplode.java index 1f27167..279a7c1 100644 --- a/src/main/java/net/nuclearteam/createnuclear/infrastructure/config/CExplose.java +++ b/src/main/java/net/nuclearteam/createnuclear/infrastructure/config/CExplode.java @@ -2,7 +2,7 @@ import net.createmod.catnip.config.ConfigBase; -public class CExplose extends ConfigBase { +public class CExplode extends ConfigBase { public final ConfigInt size = i(10, "Size of the reactor explosion"); public final ConfigInt type = i(1, 0, 2, "Type of explosion", Comments.type); public final ConfigInt time = i(600, 100, 1200, "Duration before exploration", Comments.explosionTime, Comments.hintExplosion); diff --git a/src/main/java/net/nuclearteam/createnuclear/infrastructure/config/CNCCommon.java b/src/main/java/net/nuclearteam/createnuclear/infrastructure/config/CNCCommon.java index cf642f7..9d85f11 100644 --- a/src/main/java/net/nuclearteam/createnuclear/infrastructure/config/CNCCommon.java +++ b/src/main/java/net/nuclearteam/createnuclear/infrastructure/config/CNCCommon.java @@ -4,8 +4,6 @@ public class CNCCommon extends ConfigBase { public final CWorldGen worldGen = nested(0, CWorldGen::new, Comments.worldGen); - public final CRods rods = nested(0, CRods::new, Comments.rods); - public final CExplose explode = nested(0, CExplose::new, Comments.explode); @Override public String getName() { @@ -13,8 +11,6 @@ public String getName() { } private static class Comments { - static String worldGen = "Modify CreateNuclear's impact on your terrain"; - static String rods = "Modify rods time and config"; - static String explode = "Explose: pas d'idée"; + static String worldGen = "Modify how CreateNuclear affects world generation."; } } diff --git a/src/main/java/net/nuclearteam/createnuclear/infrastructure/config/CNCServer.java b/src/main/java/net/nuclearteam/createnuclear/infrastructure/config/CNCServer.java index 5ae7b0b..ac41f5d 100644 --- a/src/main/java/net/nuclearteam/createnuclear/infrastructure/config/CNCServer.java +++ b/src/main/java/net/nuclearteam/createnuclear/infrastructure/config/CNCServer.java @@ -6,18 +6,19 @@ public class CNCServer extends ConfigBase { public final CRods rods = nested(0, CRods::new, Comments.rods); public final CNotify notify = nested(0, CNotify::new, Comments.notify); public final CRadiation radiation = nested(0, CRadiation::new, Comments.radiation); + public final CReactorHeat reactorHeat = nested(0, CReactorHeat::new, Comments.reactorHeat); public final CBiomeRestore biomeRestore = nested(0, CBiomeRestore::new, Comments.biomeRestore); @Override public String getName() { - return "server"; + return "Server"; } private static class Comments { static String rods = "Modify rod durations and related parameters."; - static String explode = "[WIP] Modify explosion parameters."; - static String radiation = "Enable or disable radiation effects emitted by mod items."; + static String radiation = "Enable or disable radiation effects emitted by mod items. "; static String notify = "Notification settings for reactor warnings."; + static String reactorHeat = "Heat thresholds for different reactor sizes"; static String biomeRestore = "Control how irradiated biomes are restored back to their original biome."; } -} +} \ No newline at end of file diff --git a/src/main/java/net/nuclearteam/createnuclear/infrastructure/config/CNConfigs.java b/src/main/java/net/nuclearteam/createnuclear/infrastructure/config/CNConfigs.java index bca78d1..fd1ddc5 100644 --- a/src/main/java/net/nuclearteam/createnuclear/infrastructure/config/CNConfigs.java +++ b/src/main/java/net/nuclearteam/createnuclear/infrastructure/config/CNConfigs.java @@ -15,7 +15,7 @@ import java.util.Map.Entry; import java.util.function.Supplier; -@EventBusSubscriber() +@EventBusSubscriber public class CNConfigs { private static final Map CONFIGS = new EnumMap<>(ModConfig.Type.class); diff --git a/src/main/java/net/nuclearteam/createnuclear/infrastructure/config/CNotify.java b/src/main/java/net/nuclearteam/createnuclear/infrastructure/config/CNotify.java index e53a67c..7ef0c44 100644 --- a/src/main/java/net/nuclearteam/createnuclear/infrastructure/config/CNotify.java +++ b/src/main/java/net/nuclearteam/createnuclear/infrastructure/config/CNotify.java @@ -3,7 +3,6 @@ import net.createmod.catnip.config.ConfigBase; public class CNotify extends ConfigBase { - public final ConfigBool warnAllPlayers = b(false, "warn_all_players", Comments.warning); public final ConfigInt warningDistance = i(100, 30, 5000, "warning_distance_blocks", Comments.rangeOfWarning); diff --git a/src/main/java/net/nuclearteam/createnuclear/infrastructure/config/CRadiation.java b/src/main/java/net/nuclearteam/createnuclear/infrastructure/config/CRadiation.java index 892d91d..14fc636 100644 --- a/src/main/java/net/nuclearteam/createnuclear/infrastructure/config/CRadiation.java +++ b/src/main/java/net/nuclearteam/createnuclear/infrastructure/config/CRadiation.java @@ -46,16 +46,16 @@ public static class ConfiguredLists extends ConfigBase { public void registerAll(ModConfigSpec.Builder builder) { ENTITY_BLACKLIST = builder - .comment(Comments.blackListEntity) - .defineListAllowEmpty( - List.of("entity_blacklist"), - () -> List.of( - "minecraft:armor_stand", - "minecraft:item_frame", - "minecraft:glow_item_frame" - ), - obj -> obj instanceof String - ); + .comment(Comments.blackListEntity) + .defineListAllowEmpty( + List.of("entity_blacklist"), + () -> List.of( + "minecraft:armor_stand", + "minecraft:item_frame", + "minecraft:glow_item_frame" + ), + obj -> obj instanceof String + ); } @Override diff --git a/src/main/java/net/nuclearteam/createnuclear/infrastructure/config/CReactorHeat.java b/src/main/java/net/nuclearteam/createnuclear/infrastructure/config/CReactorHeat.java new file mode 100644 index 0000000..6d10f36 --- /dev/null +++ b/src/main/java/net/nuclearteam/createnuclear/infrastructure/config/CReactorHeat.java @@ -0,0 +1,14 @@ +package net.nuclearteam.createnuclear.infrastructure.config; + +import net.createmod.catnip.config.ConfigBase; + +public class CReactorHeat extends ConfigBase { + public final ConfigInt size5Danger = i(256, 0, 8192, "size5Danger", "Heat threshold for DANGER level (5x5 reactor)"); + public final ConfigInt size7Danger = i(1024, 0, 8192, "size7Danger", "Heat threshold for DANGER level (7x7 reactor)"); + public final ConfigInt size9Danger = i(4096, 0, 8192, "size9Danger", "Heat threshold for DANGER level (9x9 reactor)"); + + @Override + public String getName() { + return "ReactorHeat"; + } +} \ No newline at end of file diff --git a/src/main/java/net/nuclearteam/createnuclear/infrastructure/config/CRods.java b/src/main/java/net/nuclearteam/createnuclear/infrastructure/config/CRods.java index 947b9f4..ec7edcd 100644 --- a/src/main/java/net/nuclearteam/createnuclear/infrastructure/config/CRods.java +++ b/src/main/java/net/nuclearteam/createnuclear/infrastructure/config/CRods.java @@ -1,22 +1,51 @@ package net.nuclearteam.createnuclear.infrastructure.config; import net.createmod.catnip.config.ConfigBase; +import net.minecraft.MethodsReturnNonnullByDefault; +@MethodsReturnNonnullByDefault public class CRods extends ConfigBase { - public final ConfigInt uraniumRodLifetime = i(3600, 100, 5000, "Uranium rod lifespan", Comments.UraniumRodLifetime, Comments.hintTick); - public final ConfigInt graphiteRodLifetime = i(3600, 100, 5000, "Graphite rod lifespan", Comments.GraphiteRodLifetime, Comments.hintTick); - public final ConfigInt maxHeat = i(1000, 200, 1000, "Maximum reactor heat", Comments.maxHeat, Comments.hintHeat); + public final ConfigInt uraniumRodLifetime = i(3600, 100, 5000, "uranium_rod_lifetime_ticks", Comments.uraniumRodLifetime, Comments.hintTick); + public final ConfigInt uraniumProximityBonus = i(32, -70, 70, "uranium_proximity_bonus", Comments.warning, Comments.uraniumProximityBonus); + public final ConfigInt uraniumBaseValue = i(64, -70, 70, "uranium_base_value", Comments.warning, Comments.uraniumBaseValue); + public final ConfigInt uraniumHeatRatio = i(2, -10, 10, "Heat ratio for uranium rods", Comments.warning, Comments.uraniumHeatRatio); + + public final ConfigInt graphiteRodLifetime = i(3600, 100, 5000, "graphite_rod_lifetime_ticks", Comments.graphiteRodLifetime, Comments.hintTick); + // the proximity value of coolant items are divisions so if the heat of the fuel is 64, the calcul will be "heat + 64/-4" + public final ConfigFloat graphiteProximityMalus = f(-0.25f, -70, 70, "graphite_proximity_penalty", Comments.warning, Comments.graphiteProximityMalus); + public final ConfigInt graphiteBaseValue = i(-32, -50, 50, "graphite_base_value", Comments.warning, Comments.graphiteBaseValue); + public final ConfigInt graphiteHeatRatio = i(1, -10, 10, "Heat ratio for graphite rods", Comments.warning, Comments.graphiteHeatRatio); + + public final ConfigInt thoriumRodLifetime = i(3600, 100, 5000, "Thorium rod lifetime (in ticks)", Comments.thoriumRodLifetime, Comments.hintTick); + public final ConfigInt thoriumProxyBonus = i(8, -70, 70, "Heat bonus from nearby thorium rods", Comments.warning, Comments.thoriumProximityBonus); + public final ConfigInt baseValueThorium = i(16, -70, 70, "Base heat value for thorium rods", Comments.warning, Comments.thoriumBaseValue); + public final ConfigInt thoriumHeatRatio = i(1, -10, 10, "Heat ratio for thorium rods", Comments.warning, Comments.thoriumHeatRatio); + @Override public String getName() { - return "Rods"; + return "rods"; } private static class Comments { static String hintTick = "20 ticks = 1 second"; - static String UraniumRodLifetime = "Uranium rod lifespan in reactor"; - static String GraphiteRodLifetime = "Graphite rod lifespan in reactor"; - static String maxHeat = "Maximum heat a reactor block can handle"; - static String hintHeat = "Avoids reactor failure due to excessive heat"; + static String warning = "Changing these values may unbalance reactor behavior."; + static String maxFuelPerCooled = "Maximum fuel rods supported by a single cooled rod"; + + static String uraniumRodLifetime = "Lifetime of uranium rods in ticks."; + static String uraniumProximityBonus = "Heat bonus applied when uranium rods are adjacent."; + static String uraniumBaseValue = "Base heat contribution of a uranium rod before modifiers."; + static String uraniumHeatRatio = "Heat ratio modifier for a uranium rod"; + + static String graphiteRodLifetime = "Lifetime of graphite (cooling) rods in ticks."; + static String graphiteProximityMalus = "Heat penalty applied when graphite rods are adjacent."; + static String graphiteBaseValue = "Base heat contribution of a graphite rod before modifiers."; + static String graphiteHeatRatio = "Heat ratio modifier for a graphite rod"; + + static String thoriumRodLifetime = "Lifetime of thorium rods in the reactor"; + static String thoriumProximityBonus = "Bonus heat applied when thorium rods are adjacent"; + static String thoriumBaseValue = "Base heat contribution of a thorium rod before modifiers"; + static String thoriumHeatRatio = "Heat ratio modifier for a thorium rod"; + } -} +} \ No newline at end of file diff --git a/src/main/java/net/nuclearteam/createnuclear/infrastructure/config/CWorldGen.java b/src/main/java/net/nuclearteam/createnuclear/infrastructure/config/CWorldGen.java index ea7de34..e716179 100644 --- a/src/main/java/net/nuclearteam/createnuclear/infrastructure/config/CWorldGen.java +++ b/src/main/java/net/nuclearteam/createnuclear/infrastructure/config/CWorldGen.java @@ -3,7 +3,7 @@ import net.createmod.catnip.config.ConfigBase; public class CWorldGen extends ConfigBase { - public final ConfigBool disable = b(false, "disableWorldGen", Comments.disable); + public final ConfigBool enable = b(true, "enable_world_gen", Comments.enable); @Override public String getName() { @@ -11,6 +11,6 @@ public String getName() { } private static class Comments { - static String disable = "Prevents all worldgen added by Create from taking effect"; + static String enable = "When true, CreateNuclear world generation features are active. Set to false to disable all added ores/structures."; } -} +} \ No newline at end of file diff --git a/src/main/java/net/nuclearteam/createnuclear/infrastructure/data/CNDamageTypeTagsProvider.java b/src/main/java/net/nuclearteam/createnuclear/infrastructure/data/CNDamageTypeTagsProvider.java new file mode 100644 index 0000000..a56e86f --- /dev/null +++ b/src/main/java/net/nuclearteam/createnuclear/infrastructure/data/CNDamageTypeTagsProvider.java @@ -0,0 +1,36 @@ +package net.nuclearteam.createnuclear.infrastructure.data; + +import net.minecraft.core.HolderLookup; +import net.minecraft.core.registries.Registries; +import net.minecraft.data.PackOutput; +import net.minecraft.data.tags.TagsProvider; +import net.minecraft.tags.DamageTypeTags; +import net.minecraft.world.damagesource.DamageType; +import net.neoforged.neoforge.common.data.ExistingFileHelper; +import net.nuclearteam.createnuclear.CNDamageTypes; +import net.nuclearteam.createnuclear.CreateNuclear; + +import java.util.concurrent.CompletableFuture; + +// DamageType is a datapack registry: our entries (CNDamageTypes) only exist in the enriched +// HolderLookup.Provider produced by GeneratedEntriesProvider, not in Registrate's own lookup +// provider. That's why this is a plain TagsProvider wired manually in CreateNuclearDatagen, +// instead of a Registrate dynamic tag generator (which would fail validation, missing references). +public class CNDamageTypeTagsProvider extends TagsProvider { + public CNDamageTypeTagsProvider(PackOutput output, CompletableFuture lookupProvider, ExistingFileHelper existingFileHelper) { + super(output, Registries.DAMAGE_TYPE, lookupProvider, CreateNuclear.MOD_ID, existingFileHelper); + } + + @Override + protected void addTags(HolderLookup.Provider provider) { + tag(DamageTypeTags.NO_KNOCKBACK) + .add(CNDamageTypes.RADIATION) + .add(CNDamageTypes.FAN_RADIATION) + ; + } + + @Override + public String getName() { + return "CreateNuclear Damage Type Tags"; + } +} diff --git a/src/main/java/net/nuclearteam/createnuclear/infrastructure/data/CreateNuclearDatagen.java b/src/main/java/net/nuclearteam/createnuclear/infrastructure/data/CreateNuclearDatagen.java index 77fab59..ea2a4d6 100644 --- a/src/main/java/net/nuclearteam/createnuclear/infrastructure/data/CreateNuclearDatagen.java +++ b/src/main/java/net/nuclearteam/createnuclear/infrastructure/data/CreateNuclearDatagen.java @@ -53,6 +53,7 @@ public static void gatherData(GatherDataEvent event) { generator.addProvider(event.includeServer(), new CNAdvancement(output, lookupProvider)); generator.addProvider(event.includeClient(), CNSoundEvents.provider(generator)); + generator.addProvider(event.includeServer(), new CNDamageTypeTagsProvider(output, lookupProvider, existingFileHelper)); if (event.includeServer()) { CNRecipeProvider.registerAllProcessing(generator, output, lookupProvider); diff --git a/src/main/java/net/nuclearteam/createnuclear/infrastructure/data/CreateNuclearRegistrateTags.java b/src/main/java/net/nuclearteam/createnuclear/infrastructure/data/CreateNuclearRegistrateTags.java index 9d8cf74..3819605 100644 --- a/src/main/java/net/nuclearteam/createnuclear/infrastructure/data/CreateNuclearRegistrateTags.java +++ b/src/main/java/net/nuclearteam/createnuclear/infrastructure/data/CreateNuclearRegistrateTags.java @@ -9,6 +9,7 @@ import net.minecraft.world.entity.EntityType; import net.minecraft.world.item.Item; import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.Blocks; import net.minecraft.world.level.material.Fluid; import net.nuclearteam.createnuclear.CNBlocks; import net.nuclearteam.createnuclear.CNEntityType; @@ -38,6 +39,10 @@ private static void genBlockTags(RegistrateTagsProvider provIn) { .add(CNBlocks.ENRICHING_FIRE.get()) ; + prov.tag(CNBlockTags.FAN_PROCESSING_CATALYSTS_SNOW_POWDER.tag) + .add(Blocks.POWDER_SNOW) + ; + for (CNBlockTags tag : CNBlockTags.values()) { if (tag.alwaysDatagen) { prov.getOrCreateRawBuilder(tag.tag); diff --git a/src/main/java/net/nuclearteam/createnuclear/infrastructure/data/GeneratedEntriesProvider.java b/src/main/java/net/nuclearteam/createnuclear/infrastructure/data/GeneratedEntriesProvider.java index 7bf61c1..21b4d48 100644 --- a/src/main/java/net/nuclearteam/createnuclear/infrastructure/data/GeneratedEntriesProvider.java +++ b/src/main/java/net/nuclearteam/createnuclear/infrastructure/data/GeneratedEntriesProvider.java @@ -11,7 +11,7 @@ import net.nuclearteam.createnuclear.CreateNuclear; import net.nuclearteam.createnuclear.api.CreateNuclearRegistries; import net.nuclearteam.createnuclear.content.multiblock.fluid.CNReactorFluidTypes; -import net.nuclearteam.createnuclear.content.rod.CNRodTypes; +import net.nuclearteam.createnuclear.content.multiblock.rod.CNRodTypes; import net.nuclearteam.createnuclear.infrastructure.worldgen.CNBiomeModifiers; import net.nuclearteam.createnuclear.infrastructure.worldgen.CNConfiguredFeatures; import net.nuclearteam.createnuclear.infrastructure.worldgen.CNPlacedFeatures; diff --git a/src/main/java/net/nuclearteam/createnuclear/infrastructure/worldgen/CNPlacedFeatures.java b/src/main/java/net/nuclearteam/createnuclear/infrastructure/worldgen/CNPlacedFeatures.java index c1e5675..a8efc39 100644 --- a/src/main/java/net/nuclearteam/createnuclear/infrastructure/worldgen/CNPlacedFeatures.java +++ b/src/main/java/net/nuclearteam/createnuclear/infrastructure/worldgen/CNPlacedFeatures.java @@ -1,6 +1,5 @@ package net.nuclearteam.createnuclear.infrastructure.worldgen; -import com.simibubi.create.infrastructure.worldgen.ConfigPlacementFilter; import net.minecraft.core.Holder; import net.minecraft.core.HolderGetter; import net.minecraft.core.registries.Registries; diff --git a/src/main/java/net/nuclearteam/createnuclear/infrastructure/worldgen/CNPlacementModifiers.java b/src/main/java/net/nuclearteam/createnuclear/infrastructure/worldgen/CNPlacementModifiers.java new file mode 100644 index 0000000..c940802 --- /dev/null +++ b/src/main/java/net/nuclearteam/createnuclear/infrastructure/worldgen/CNPlacementModifiers.java @@ -0,0 +1,18 @@ +package net.nuclearteam.createnuclear.infrastructure.worldgen; + +import net.minecraft.core.registries.Registries; +import net.minecraft.world.level.levelgen.placement.PlacementModifierType; +import net.neoforged.bus.api.IEventBus; +import net.neoforged.neoforge.registries.DeferredHolder; +import net.neoforged.neoforge.registries.DeferredRegister; +import net.nuclearteam.createnuclear.CreateNuclear; + +public class CNPlacementModifiers { + private static final DeferredRegister> REGISTER = DeferredRegister.create(Registries.PLACEMENT_MODIFIER_TYPE, CreateNuclear.MOD_ID); + + public static final DeferredHolder, PlacementModifierType> CONFIG_FILTER = REGISTER.register("config_filter", () -> () -> ConfigPlacementFilter.CODEC); + + public static void register(IEventBus modEventBus) { + REGISTER.register(modEventBus); + } +} \ No newline at end of file diff --git a/src/main/java/net/nuclearteam/createnuclear/infrastructure/worldgen/ConfigPlacementFilter.java b/src/main/java/net/nuclearteam/createnuclear/infrastructure/worldgen/ConfigPlacementFilter.java new file mode 100644 index 0000000..b727387 --- /dev/null +++ b/src/main/java/net/nuclearteam/createnuclear/infrastructure/worldgen/ConfigPlacementFilter.java @@ -0,0 +1,24 @@ +package net.nuclearteam.createnuclear.infrastructure.worldgen; + +import com.mojang.serialization.MapCodec; +import net.minecraft.core.BlockPos; +import net.minecraft.util.RandomSource; +import net.minecraft.world.level.levelgen.placement.PlacementContext; +import net.minecraft.world.level.levelgen.placement.PlacementFilter; +import net.minecraft.world.level.levelgen.placement.PlacementModifierType; +import net.nuclearteam.createnuclear.infrastructure.config.CNConfigs; + +public class ConfigPlacementFilter extends PlacementFilter { + public static final ConfigPlacementFilter INSTANCE = new ConfigPlacementFilter(); + public static final MapCodec CODEC = MapCodec.unit(() -> INSTANCE); + + @Override + protected boolean shouldPlace(PlacementContext pContext, RandomSource pRandom, BlockPos pPos) { + return CNConfigs.common().worldGen.enable.get(); + } + + @Override + public PlacementModifierType type() { + return CNPlacementModifiers.CONFIG_FILTER.get(); + } +} \ No newline at end of file diff --git a/src/main/resources/META-INF/neoforge.mods.toml b/src/main/resources/META-INF/neoforge.mods.toml index dc88615..eacbed9 100644 --- a/src/main/resources/META-INF/neoforge.mods.toml +++ b/src/main/resources/META-INF/neoforge.mods.toml @@ -1,6 +1,6 @@ modLoader="javafml" #mandatory loaderVersion="${loader_version_range}" #mandatory This is typically bumped every Minecraft version by Forge. See our download page for lists of versions. -issueTrackerURL="https://github.com/Giovanniricotta2002/CreateNuclearForge/issues" +issueTrackerURL="https://github.com/Create-Nuclear-Team/CreateNuclearNeoForge/issues" license="${mod_license}" diff --git a/src/main/resources/assets/createnuclear/lang/default/reactor.json b/src/main/resources/assets/createnuclear/lang/default/reactor.json index 67908b9..1ca9977 100644 --- a/src/main/resources/assets/createnuclear/lang/default/reactor.json +++ b/src/main/resources/assets/createnuclear/lang/default/reactor.json @@ -4,5 +4,14 @@ "reactor.info.assembled.creator": "Reactor has been assembled", "reactor.info.is": "controller is", "reactor.item.gui.name": "Config Pattern", - "reactor.update.casing.input": "Update the casing in input" + "reactor.update.casing.input": "Update the casing in input", + + "createnuclear.reactor.debug.title": "--- Reactor diagnostics ---", + "createnuclear.reactor.debug.summary": "%s: %s registered, %s valid", + "createnuclear.reactor.debug.registered_position": " registered @ %s", + "createnuclear.reactor.debug.valid_position": " valid @ %s", + "createnuclear.reactor.debug.label.input": "Input", + "createnuclear.reactor.debug.label.fluid_input": "Fluid Input", + "createnuclear.reactor.debug.label.output": "Output", + "createnuclear.reactor.debug.label.alarm": "Alarm" } \ No newline at end of file diff --git a/src/main/resources/assets/createnuclear/lang/default/tooltips.json b/src/main/resources/assets/createnuclear/lang/default/tooltips.json index c081563..85fa69a 100644 --- a/src/main/resources/assets/createnuclear/lang/default/tooltips.json +++ b/src/main/resources/assets/createnuclear/lang/default/tooltips.json @@ -54,6 +54,7 @@ "createnuclear.tooltip.baseRodHeat": "Base Rod Heat: %d Q", "createnuclear.tooltip.proximityRodHeat": "Proximity Rod Heat: %.2f Q", "createnuclear.tooltip.rodTimer": "Rod Duration: %d tick", + "createnuclear.tooltip.ratio": "Rod Value for ratio: %d", "createnuclear.tooltip.typeRod": "Rod Type: %s", "createnuclear.tooltip.cloth.color": "Color of armor piece: %s", diff --git a/src/main/resources/assets/createnuclear/textures/block/reactor/casing/casing_inner.png b/src/main/resources/assets/createnuclear/textures/block/reactor/casing/casing_inner.png deleted file mode 100644 index 5783d71..0000000 Binary files a/src/main/resources/assets/createnuclear/textures/block/reactor/casing/casing_inner.png and /dev/null differ diff --git a/src/main/resources/assets/createnuclear/textures/block/reactor/casing/casing_top.png b/src/main/resources/assets/createnuclear/textures/block/reactor/casing/casing_top.png deleted file mode 100644 index c4d4460..0000000 Binary files a/src/main/resources/assets/createnuclear/textures/block/reactor/casing/casing_top.png and /dev/null differ diff --git a/src/main/resources/assets/createnuclear/textures/block/reactor/casing/reactor_casing_connected.png b/src/main/resources/assets/createnuclear/textures/block/reactor/casing/reactor_casing_connected.png index 5f6f8ba..b2d67db 100644 Binary files a/src/main/resources/assets/createnuclear/textures/block/reactor/casing/reactor_casing_connected.png and b/src/main/resources/assets/createnuclear/textures/block/reactor/casing/reactor_casing_connected.png differ diff --git a/src/main/resources/assets/createnuclear/textures/block/reactor/controller/controller_panel.png b/src/main/resources/assets/createnuclear/textures/block/reactor/controller/controller_panel.png deleted file mode 100644 index 52c7d1b..0000000 Binary files a/src/main/resources/assets/createnuclear/textures/block/reactor/controller/controller_panel.png and /dev/null differ diff --git a/src/main/resources/createnuclear.neoforge.mixins.json b/src/main/resources/createnuclear.neoforge.mixins.json index 9add340..95c1b01 100644 --- a/src/main/resources/createnuclear.neoforge.mixins.json +++ b/src/main/resources/createnuclear.neoforge.mixins.json @@ -10,7 +10,9 @@ ], "client": [ "client.RadiationHeartMixin", - "client.AntiRadiationArmorTextureMixin" + "client.AntiRadiationArmorTextureMixin", + "client.CameraAccessor", + "client.GameRendererMixin" ], "injectors": { "defaultRequire": 1 diff --git a/src/main/resources/data/createnuclear/structure/empty_platform.nbt b/src/main/resources/data/createnuclear/structure/empty_platform.nbt new file mode 100644 index 0000000..1181be1 Binary files /dev/null and b/src/main/resources/data/createnuclear/structure/empty_platform.nbt differ