From 69b8c79e5782311d867e33ed49e15f1d7e690da0 Mon Sep 17 00:00:00 2001 From: Andrew nuark G Date: Sat, 9 May 2026 09:52:05 +0700 Subject: [PATCH] feat: working custom arch. cutter machine --- .gitignore | 1 + build.gradle | 56 ---- mclschcannoncompat.ipr | 104 ++++++ mclschcannoncompat.iws | 207 ++++++++++++ settings.gradle | 2 +- .../37d63bb25ad2a1f1b3cea75df8f510fa196f89ed | 3 +- .../6e801767522783b716853a5ad8062111be83b958 | 4 +- .../9ba92730b9239f67da1ce4d3431e6be06116236d | 4 +- .../9fb1092f32d4fcbf9e061ffd718d4ec689c6c95e | 4 +- .../assets/mclschcannoncompat/lang/en_us.json | 11 +- .../assets/mclschcannoncompat/lang/ru_ru.json | 11 +- .../models/item/ornament_fabricator.json | 3 + .../decorations/ornament_fabricator.json | 32 ++ .../recipe/ornament_fabricator.json | 24 ++ .../utils/ODBlockRecipiesCache.java | 54 +++ .../mclschcannoncompat/Mclschcannoncompat.kt | 41 ++- .../block/ModBlockEntityTypes.kt | 24 ++ .../mclschcannoncompat/block/ModBlocks.kt | 11 + .../block/OrnamentFabricatorBlock.kt | 61 ++++ .../entity/OrnamentFabricatorBlockEntity.kt | 310 ++++++++++++++++++ .../datagen/ModItemModelProvider.kt | 4 +- .../datagen/ModLanguageProviders.kt | 22 +- .../datagen/ModRecipeProvider.kt | 13 +- .../item/BuilderAssistantStaff.kt | 2 +- .../mcmod/mclschcannoncompat/item/ModItems.kt | 17 +- .../mclschcannoncompat/menu/ModMenuTypes.kt | 19 ++ .../menu/OrnamentFabricatorMenu.kt | 191 +++++++++++ .../screen/OrnamentFabricatorScreen.kt | 181 ++++++++++ .../blockstates/ornament_fabricator.json | 5 + .../models/block/ornament_fabricator.json | 6 + .../textures/block/ornament_fabricator.png | Bin 0 -> 6610 bytes .../textures/gui/ornament_fabricator.png | Bin 0 -> 1719 bytes 32 files changed, 1334 insertions(+), 93 deletions(-) create mode 100644 mclschcannoncompat.ipr create mode 100644 mclschcannoncompat.iws create mode 100644 src/generated/resources/assets/mclschcannoncompat/models/item/ornament_fabricator.json create mode 100644 src/generated/resources/data/mclschcannoncompat/advancement/recipes/decorations/ornament_fabricator.json create mode 100644 src/generated/resources/data/mclschcannoncompat/recipe/ornament_fabricator.json create mode 100644 src/main/java/xyz/nuark/mcmod/mclschcannoncompat/utils/ODBlockRecipiesCache.java create mode 100644 src/main/kotlin/xyz/nuark/mcmod/mclschcannoncompat/block/ModBlockEntityTypes.kt create mode 100644 src/main/kotlin/xyz/nuark/mcmod/mclschcannoncompat/block/OrnamentFabricatorBlock.kt create mode 100644 src/main/kotlin/xyz/nuark/mcmod/mclschcannoncompat/block/entity/OrnamentFabricatorBlockEntity.kt create mode 100644 src/main/kotlin/xyz/nuark/mcmod/mclschcannoncompat/menu/ModMenuTypes.kt create mode 100644 src/main/kotlin/xyz/nuark/mcmod/mclschcannoncompat/menu/OrnamentFabricatorMenu.kt create mode 100644 src/main/kotlin/xyz/nuark/mcmod/mclschcannoncompat/screen/OrnamentFabricatorScreen.kt create mode 100644 src/main/resources/assets/mclschcannoncompat/blockstates/ornament_fabricator.json create mode 100644 src/main/resources/assets/mclschcannoncompat/models/block/ornament_fabricator.json create mode 100644 src/main/resources/assets/mclschcannoncompat/textures/block/ornament_fabricator.png create mode 100644 src/main/resources/assets/mclschcannoncompat/textures/gui/ornament_fabricator.png diff --git a/.gitignore b/.gitignore index d18fc84..ef48ebd 100644 --- a/.gitignore +++ b/.gitignore @@ -20,3 +20,4 @@ hs_err_pid* # Common working directory run /repo/ +/.kotlin/ diff --git a/build.gradle b/build.gradle index 5de1e9f..8a4a2a1 100644 --- a/build.gradle +++ b/build.gradle @@ -42,7 +42,6 @@ java.toolchain.languageVersion = JavaLanguageVersion.of(21) kotlin.jvmToolchain(21) neoForge { - // Specify the version of NeoForge to use. version = project.neo_version parchment { @@ -50,16 +49,10 @@ neoForge { minecraftVersion = project.parchment_minecraft_version } - // This line is optional. Access Transformers are automatically detected - // accessTransformers.add('src/main/resources/META-INF/accesstransformer.cfg') - - // Default run configurations. - // These can be tweaked, removed, or duplicated as needed. runs { client { client() - // Comma-separated list of namespaces to load gametests from. Empty = all namespaces. systemProperty 'neoforge.enabledGameTestNamespaces', project.mod_id } @@ -69,9 +62,6 @@ neoForge { systemProperty 'neoforge.enabledGameTestNamespaces', project.mod_id } - // This run config launches GameTestServer and runs all registered gametests, then exits. - // By default, the server will crash when no gametests are provided. - // The gametest system is also enabled by default for other run configs under the /test command. gameTestServer { type = "gameTestServer" systemProperty 'neoforge.enabledGameTestNamespaces', project.mod_id @@ -80,41 +70,23 @@ neoForge { data { data() - // example of overriding the workingDirectory set in configureEach above, uncomment if you want to use it - // gameDirectory = project.file('run-data') - - // Specify the modid for data generation, where to output the resulting resource, and where to look for existing resources. programArguments.addAll '--mod', project.mod_id, '--all', '--output', file('src/generated/resources/').getAbsolutePath(), '--existing', file('src/main/resources/').getAbsolutePath() } - // applies to all the run configs above configureEach { - // Recommended logging data for a userdev environment - // The markers can be added/remove as needed separated by commas. - // "SCAN": For mods scan. - // "REGISTRIES": For firing of registry events. - // "REGISTRYDUMP": For getting the contents of all registries. systemProperty 'forge.logging.markers', 'REGISTRIES' - // Recommended logging level for the console - // You can set various levels here. - // Please read: https://stackoverflow.com/questions/2031163/when-to-use-the-different-log-levels logLevel = org.slf4j.event.Level.DEBUG } } mods { - // define mod <-> source bindings - // these are used to tell the game which sources are for which mod - // mostly optional in a single mod project - // but multi mod projects should define one per mod "${mod_id}" { sourceSet(sourceSets.main) } } } -// Include resources generated by data generators. sourceSets.main.resources { srcDir 'src/generated/resources' } @@ -135,31 +107,8 @@ dependencies { runtimeOnly "curse.maven:sophisticated-core-618298:8046952" runtimeOnly "curse.maven:sophisticated-storage-619320:8034906" runtimeOnly "curse.maven:pocket-storage-367734:6834323" - - // Example mod dependency with JEI - // The JEI API is declared for compile time use, while the full JEI artifact is used at runtime - // compileOnly "mezz.jei:jei-${mc_version}-common-api:${jei_version}" - // compileOnly "mezz.jei:jei-${mc_version}-forge-api:${jei_version}" - // runtimeOnly "mezz.jei:jei-${mc_version}-forge:${jei_version}" - - // Example mod dependency using a mod jar from ./libs with a flat dir repository - // This maps to ./libs/coolmod-${mc_version}-${coolmod_version}.jar - // The group id is ignored when searching -- in this case, it is "blank" - // implementation "blank:coolmod-${mc_version}:${coolmod_version}" - - // Example mod dependency using a file as dependency - // implementation files("libs/coolmod-${mc_version}-${coolmod_version}.jar") - - // Example project dependency using a sister or child project: - // implementation project(":myproject") - - // For more info: - // http://www.gradle.org/docs/current/userguide/artifact_dependencies_tutorial.html - // http://www.gradle.org/docs/current/userguide/dependency_management.html } -// This block of code expands all declared replace properties in the specified resource targets. -// A missing property will result in an error. Properties are expanded using ${} Groovy notation. var generateModMetadata = tasks.register("generateModMetadata", ProcessResources) { var replaceProperties = [minecraft_version : minecraft_version, minecraft_version_range: minecraft_version_range, @@ -178,13 +127,9 @@ var generateModMetadata = tasks.register("generateModMetadata", ProcessResources into "build/generated/sources/modMetadata" } -// Include the output of "generateModMetadata" as an input directory for the build -// this works with both building through Gradle and the IDE. sourceSets.main.resources.srcDir generateModMetadata -// To avoid having to run "generateModMetadata" manually, make it run on every project reload neoForge.ideSyncTask generateModMetadata -// Example configuration to allow publishing using the maven-publish plugin publishing { publications { register('mavenJava', MavenPublication) { @@ -198,7 +143,6 @@ publishing { } } -// IDEA no longer automatically downloads sources/javadoc jars for dependencies, so we need to explicitly enable the behavior. idea { module { downloadSources = true diff --git a/mclschcannoncompat.ipr b/mclschcannoncompat.ipr new file mode 100644 index 0000000..758a596 --- /dev/null +++ b/mclschcannoncompat.ipr @@ -0,0 +1,104 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 1.6 + + + + + + + + + + + + + diff --git a/mclschcannoncompat.iws b/mclschcannoncompat.iws new file mode 100644 index 0000000..d5bc759 --- /dev/null +++ b/mclschcannoncompat.iws @@ -0,0 +1,207 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + localhost + 5050 + + + + + + + + + + + + + + + diff --git a/settings.gradle b/settings.gradle index ada876e..90ae98f 100644 --- a/settings.gradle +++ b/settings.gradle @@ -7,5 +7,5 @@ pluginManagement { } plugins { - id 'org.gradle.toolchains.foojay-resolver-convention' version '0.8.0' + id 'org.gradle.toolchains.foojay-resolver-convention' version '0.9.0' } diff --git a/src/generated/resources/.cache/37d63bb25ad2a1f1b3cea75df8f510fa196f89ed b/src/generated/resources/.cache/37d63bb25ad2a1f1b3cea75df8f510fa196f89ed index 2cf11f4..4a2193c 100644 --- a/src/generated/resources/.cache/37d63bb25ad2a1f1b3cea75df8f510fa196f89ed +++ b/src/generated/resources/.cache/37d63bb25ad2a1f1b3cea75df8f510fa196f89ed @@ -1,4 +1,5 @@ -// 1.21.1 2026-05-05T23:41:11.6384884 Item Models: mclschcannoncompat +// 1.21.1 2026-05-07T18:31:21.2712371 Item Models: mclschcannoncompat 52b70fbe9824ebbf99b2c3464e62faeb93a3ce63 assets/mclschcannoncompat/models/item/builder_assistant_staff.json acd5e3967cb0dcf277da711c0b8f0efbc6271d6c assets/mclschcannoncompat/models/item/builder_assistant_staff_t2.json d996164a02ec5598f0d421cc1ad0755b29621414 assets/mclschcannoncompat/models/item/builder_assistant_staff_t3.json +a395cfa9425fbce9e6dad13f0044deceb4df8dcc assets/mclschcannoncompat/models/item/ornament_fabricator.json diff --git a/src/generated/resources/.cache/6e801767522783b716853a5ad8062111be83b958 b/src/generated/resources/.cache/6e801767522783b716853a5ad8062111be83b958 index 0330876..4210393 100644 --- a/src/generated/resources/.cache/6e801767522783b716853a5ad8062111be83b958 +++ b/src/generated/resources/.cache/6e801767522783b716853a5ad8062111be83b958 @@ -1,2 +1,2 @@ -// 1.21.1 2026-05-07T13:46:09.9996673 Languages: ru_ru for mod: mclschcannoncompat -3deacd406d126128077ef1e97aa26c68ef1759e7 assets/mclschcannoncompat/lang/ru_ru.json +// 1.21.1 2026-05-09T02:10:42.4551668 Languages: ru_ru for mod: mclschcannoncompat +a1fbe8c5922e6b9c566a16b14d42d6c67cc7e9d2 assets/mclschcannoncompat/lang/ru_ru.json diff --git a/src/generated/resources/.cache/9ba92730b9239f67da1ce4d3431e6be06116236d b/src/generated/resources/.cache/9ba92730b9239f67da1ce4d3431e6be06116236d index 524a983..5e1719c 100644 --- a/src/generated/resources/.cache/9ba92730b9239f67da1ce4d3431e6be06116236d +++ b/src/generated/resources/.cache/9ba92730b9239f67da1ce4d3431e6be06116236d @@ -1,2 +1,2 @@ -// 1.21.1 2026-05-07T13:46:10.0056649 Languages: en_us for mod: mclschcannoncompat -a01abd18da057bba316103cb554505de33093034 assets/mclschcannoncompat/lang/en_us.json +// 1.21.1 2026-05-09T02:10:42.4571469 Languages: en_us for mod: mclschcannoncompat +f808ba452243bef6c6dc1efa6ce3a9d2ae74cb9e assets/mclschcannoncompat/lang/en_us.json diff --git a/src/generated/resources/.cache/9fb1092f32d4fcbf9e061ffd718d4ec689c6c95e b/src/generated/resources/.cache/9fb1092f32d4fcbf9e061ffd718d4ec689c6c95e index ad13b51..a376e69 100644 --- a/src/generated/resources/.cache/9fb1092f32d4fcbf9e061ffd718d4ec689c6c95e +++ b/src/generated/resources/.cache/9fb1092f32d4fcbf9e061ffd718d4ec689c6c95e @@ -1,7 +1,9 @@ -// 1.21.1 2026-05-05T23:41:11.6364906 Recipes +// 1.21.1 2026-05-07T18:31:21.2702367 Recipes +895b7a18109dacaaa77bafba58a50c941a9c2f56 data/mclschcannoncompat/advancement/recipes/decorations/ornament_fabricator.json a4d166c9c7d5b081e15495f87588fa70f62bee84 data/mclschcannoncompat/advancement/recipes/tools/rec_fct1.json c1b2ebefe2e42c4a01db164c8fbf90dd0aadea78 data/mclschcannoncompat/advancement/recipes/tools/rec_fct2.json 909da6934e35b03c890689105c7f29bae51ef330 data/mclschcannoncompat/advancement/recipes/tools/rec_fct3.json +f42203a3bee2a747b9a5e1637a27fb043b1b97bd data/mclschcannoncompat/recipe/ornament_fabricator.json 594aec36a3be123023816c430342c9f081fd5bd4 data/mclschcannoncompat/recipe/rec_fct1.json 4d05680c3a8c9923bf0610a8429a821cf57bbfff data/mclschcannoncompat/recipe/rec_fct2.json e4bfc8d84f08ce1637c0d24090653218ba04f5be data/mclschcannoncompat/recipe/rec_fct3.json diff --git a/src/generated/resources/assets/mclschcannoncompat/lang/en_us.json b/src/generated/resources/assets/mclschcannoncompat/lang/en_us.json index 05c3d04..1b1875b 100644 --- a/src/generated/resources/assets/mclschcannoncompat/lang/en_us.json +++ b/src/generated/resources/assets/mclschcannoncompat/lang/en_us.json @@ -1,6 +1,13 @@ { - "item.asdf.assistantstaff.maxdepth": "Maximum depth: %s", + "block.mclschcannoncompat.ornament_fabricator": "Ornament Fabricator", + "item.assistantstaff.hover.maxdepth": "Maximum depth: %s", "item.mclschcannoncompat.builder_assistant_staff": "Builder staff T1", "item.mclschcannoncompat.builder_assistant_staff_t2": "Builder staff T2", - "item.mclschcannoncompat.builder_assistant_staff_t3": "Builder staff T3" + "item.mclschcannoncompat.builder_assistant_staff_t3": "Builder staff T3", + "tooltip.btn.cyclestyle.down": "Cycle style backward", + "tooltip.btn.cyclestyle.up": "Cycle style forward", + "tooltip.btn.cyclevariant.down": "Cycle variant backward", + "tooltip.btn.cyclevariant.up": "Cycle variant forward", + "tooltip.btn.startcrafting": "Start crafting", + "tooltip.ornament_fabricator.no_preview": "Recipe is not supported for provided ingredients" } \ No newline at end of file diff --git a/src/generated/resources/assets/mclschcannoncompat/lang/ru_ru.json b/src/generated/resources/assets/mclschcannoncompat/lang/ru_ru.json index 9263fba..e01edd1 100644 --- a/src/generated/resources/assets/mclschcannoncompat/lang/ru_ru.json +++ b/src/generated/resources/assets/mclschcannoncompat/lang/ru_ru.json @@ -1,6 +1,13 @@ { - "item.asdf.assistantstaff.maxdepth": "Максимальная блоков: %s", + "block.mclschcannoncompat.ornament_fabricator": "Орнаментный фабрикатор", + "item.assistantstaff.hover.maxdepth": "Максимальная глубина: %s", "item.mclschcannoncompat.builder_assistant_staff": "Посох строителя T1", "item.mclschcannoncompat.builder_assistant_staff_t2": "Посох строителя T2", - "item.mclschcannoncompat.builder_assistant_staff_t3": "Посох строителя T3" + "item.mclschcannoncompat.builder_assistant_staff_t3": "Посох строителя T3", + "tooltip.btn.cyclestyle.down": "Переключить стиль назад", + "tooltip.btn.cyclestyle.up": "Переключить стиль вперёд", + "tooltip.btn.cyclevariant.down": "Переключить вариант назад", + "tooltip.btn.cyclevariant.up": "Переключить вариант вперёд", + "tooltip.btn.startcrafting": "Начать крафт", + "tooltip.ornament_fabricator.no_preview": "Рецепт не подходит этим ингредиентам" } \ No newline at end of file diff --git a/src/generated/resources/assets/mclschcannoncompat/models/item/ornament_fabricator.json b/src/generated/resources/assets/mclschcannoncompat/models/item/ornament_fabricator.json new file mode 100644 index 0000000..f34e5d0 --- /dev/null +++ b/src/generated/resources/assets/mclschcannoncompat/models/item/ornament_fabricator.json @@ -0,0 +1,3 @@ +{ + "parent": "mclschcannoncompat:block/ornament_fabricator" +} \ No newline at end of file diff --git a/src/generated/resources/data/mclschcannoncompat/advancement/recipes/decorations/ornament_fabricator.json b/src/generated/resources/data/mclschcannoncompat/advancement/recipes/decorations/ornament_fabricator.json new file mode 100644 index 0000000..1f724c0 --- /dev/null +++ b/src/generated/resources/data/mclschcannoncompat/advancement/recipes/decorations/ornament_fabricator.json @@ -0,0 +1,32 @@ +{ + "parent": "minecraft:recipes/root", + "criteria": { + "has_amethyst": { + "conditions": { + "items": [ + { + "items": "minecraft:amethyst_shard" + } + ] + }, + "trigger": "minecraft:inventory_changed" + }, + "has_the_recipe": { + "conditions": { + "recipe": "mclschcannoncompat:ornament_fabricator" + }, + "trigger": "minecraft:recipe_unlocked" + } + }, + "requirements": [ + [ + "has_the_recipe", + "has_amethyst" + ] + ], + "rewards": { + "recipes": [ + "mclschcannoncompat:ornament_fabricator" + ] + } +} \ No newline at end of file diff --git a/src/generated/resources/data/mclschcannoncompat/recipe/ornament_fabricator.json b/src/generated/resources/data/mclschcannoncompat/recipe/ornament_fabricator.json new file mode 100644 index 0000000..6674b99 --- /dev/null +++ b/src/generated/resources/data/mclschcannoncompat/recipe/ornament_fabricator.json @@ -0,0 +1,24 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "misc", + "key": { + "A": { + "item": "minecraft:amethyst_shard" + }, + "C": { + "item": "minecraft:crafting_table" + }, + "I": { + "item": "minecraft:iron_ingot" + } + }, + "pattern": [ + "III", + "ACA", + "III" + ], + "result": { + "count": 1, + "id": "mclschcannoncompat:ornament_fabricator" + } +} \ No newline at end of file diff --git a/src/main/java/xyz/nuark/mcmod/mclschcannoncompat/utils/ODBlockRecipiesCache.java b/src/main/java/xyz/nuark/mcmod/mclschcannoncompat/utils/ODBlockRecipiesCache.java new file mode 100644 index 0000000..0d1be18 --- /dev/null +++ b/src/main/java/xyz/nuark/mcmod/mclschcannoncompat/utils/ODBlockRecipiesCache.java @@ -0,0 +1,54 @@ +package xyz.nuark.mcmod.mclschcannoncompat.utils; + +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.ItemStack; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import xyz.nuark.mcmod.mclschcannoncompat.Mclschcannoncompat; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; + +public class ODBlockRecipiesCache { + private static final Logger LOGGER = LogManager.getLogger(Mclschcannoncompat.ID); + + private static final List blockTypesCache = new ArrayList<>(); + private static final TreeMap> blockVariantsCache = new TreeMap<>(); + private static final TreeMap> recipesCache = new TreeMap<>(); + + public static List getBlockTypesCache() { + return blockTypesCache; + } + public static Map> getBlockVariantsCache() { + return blockVariantsCache; + } + public static Map> getRecipesCache() { + return recipesCache; + } + + public static void initRecipesCache() { + if (!recipesCache.isEmpty()) return; + + LOGGER.info("Started caching DO recipes..."); + var startTime = System.currentTimeMillis(); + var compIG = com.ldtteam.domumornamentum.block.ModBlocks.getInstance().getOrComputeItemGroups(); + for (var ig : compIG.entrySet()) { + var loc = ig.getKey(); + + var variantsList = new ArrayList(); + for (var variant : ig.getValue()) { + variantsList.add(variant.copy()); + } + + if (variantsList.isEmpty()) continue; + + blockTypesCache.add(loc); + blockVariantsCache.put(blockTypesCache.size() - 1, variantsList); + recipesCache.put(loc, variantsList); + } + var elapsedTime = System.currentTimeMillis() - startTime; + LOGGER.info("Done caching DO recipes in $elapsedTime millis"); + } +} diff --git a/src/main/kotlin/xyz/nuark/mcmod/mclschcannoncompat/Mclschcannoncompat.kt b/src/main/kotlin/xyz/nuark/mcmod/mclschcannoncompat/Mclschcannoncompat.kt index 8bcbe6f..57543a7 100644 --- a/src/main/kotlin/xyz/nuark/mcmod/mclschcannoncompat/Mclschcannoncompat.kt +++ b/src/main/kotlin/xyz/nuark/mcmod/mclschcannoncompat/Mclschcannoncompat.kt @@ -1,6 +1,5 @@ package xyz.nuark.mcmod.mclschcannoncompat -import net.minecraft.client.Minecraft import net.minecraft.resources.ResourceLocation import net.minecraft.world.item.CreativeModeTabs import net.neoforged.bus.api.SubscribeEvent @@ -10,6 +9,7 @@ import net.neoforged.fml.common.Mod import net.neoforged.fml.event.lifecycle.FMLClientSetupEvent import net.neoforged.fml.event.lifecycle.FMLCommonSetupEvent import net.neoforged.fml.event.lifecycle.FMLDedicatedServerSetupEvent +import net.neoforged.neoforge.client.event.RegisterMenuScreensEvent import net.neoforged.neoforge.data.event.GatherDataEvent import net.neoforged.neoforge.event.BuildCreativeModeTabContentsEvent import net.neoforged.neoforge.event.RegisterCommandsEvent @@ -19,12 +19,16 @@ import org.apache.logging.log4j.LogManager import org.apache.logging.log4j.Logger import thedarkcolour.kotlinforforge.neoforge.forge.MOD_BUS import thedarkcolour.kotlinforforge.neoforge.forge.runForDist +import xyz.nuark.mcmod.mclschcannoncompat.block.ModBlockEntityTypes import xyz.nuark.mcmod.mclschcannoncompat.block.ModBlocks import xyz.nuark.mcmod.mclschcannoncompat.comands.ModCommands import xyz.nuark.mcmod.mclschcannoncompat.datagen.ModItemModelProvider import xyz.nuark.mcmod.mclschcannoncompat.datagen.ModLanguageProviders import xyz.nuark.mcmod.mclschcannoncompat.datagen.ModRecipeProvider import xyz.nuark.mcmod.mclschcannoncompat.item.ModItems +import xyz.nuark.mcmod.mclschcannoncompat.menu.ModMenuTypes +import xyz.nuark.mcmod.mclschcannoncompat.menu.OrnamentFabricatorMenu +import xyz.nuark.mcmod.mclschcannoncompat.screen.OrnamentFabricatorScreen @Mod(Mclschcannoncompat.ID) @EventBusSubscriber @@ -37,23 +41,28 @@ object Mclschcannoncompat { LOGGER.log(Level.INFO, "Hello world!") ModBlocks.REGISTRY.register(MOD_BUS) - - val obj = runForDist(clientTarget = { - MOD_BUS.addListener(::onClientSetup) - Minecraft.getInstance() - }, serverTarget = { - MOD_BUS.addListener(::onServerSetup) - "test" - }) - + ModBlockEntityTypes.REGISTRY.register(MOD_BUS) + ModMenuTypes.REGISTRY.register(MOD_BUS) ModItems.REGISTRY.register(MOD_BUS) - println(obj) + runForDist( + clientTarget = { + MOD_BUS.addListener(::onClientSetup) + MOD_BUS.addListener(::onRegisterMenuScreens) + }, + serverTarget = { + MOD_BUS.addListener(::onServerSetup) + } + ) } private fun onClientSetup(@Suppress("unused") event: FMLClientSetupEvent) { } + private fun onRegisterMenuScreens(event: RegisterMenuScreensEvent) { + event.register(ModMenuTypes.ORNAMENT_FABRICATOR.get(), ::OrnamentFabricatorScreen) + } + private fun onServerSetup(@Suppress("unused") event: FMLDedicatedServerSetupEvent) { } @@ -68,6 +77,9 @@ object Mclschcannoncompat { event.accept(ModItems.BUILDER_ASSISTANT_STAFF_T2.get()) event.accept(ModItems.BUILDER_ASSISTANT_STAFF_T3.get()) } + if (event.tabKey === CreativeModeTabs.FUNCTIONAL_BLOCKS) { + event.accept(ModItems.ORNAMENT_FABRICATOR_ITEM.get()) + } } @SubscribeEvent @@ -76,10 +88,9 @@ object Mclschcannoncompat { } @SubscribeEvent - fun onNetworkRegistry(event: RegisterPayloadHandlersEvent) - { - val modVersion = ModList.get().getModContainerById(ID).get().getModInfo().getVersion().toString() - @Suppress("unused") val registry = event.registrar(ID).versioned(modVersion) + fun onNetworkRegistry(event: RegisterPayloadHandlersEvent) { + val modVersion = ModList.get().getModContainerById(ID).get().modInfo.version.toString() + @Suppress("unused") val registrar = event.registrar(ID).versioned(modVersion) } @SubscribeEvent diff --git a/src/main/kotlin/xyz/nuark/mcmod/mclschcannoncompat/block/ModBlockEntityTypes.kt b/src/main/kotlin/xyz/nuark/mcmod/mclschcannoncompat/block/ModBlockEntityTypes.kt new file mode 100644 index 0000000..b77835f --- /dev/null +++ b/src/main/kotlin/xyz/nuark/mcmod/mclschcannoncompat/block/ModBlockEntityTypes.kt @@ -0,0 +1,24 @@ +package xyz.nuark.mcmod.mclschcannoncompat.block + +import net.minecraft.world.level.block.entity.BlockEntityType +import net.neoforged.neoforge.registries.DeferredRegister +import xyz.nuark.mcmod.mclschcannoncompat.Mclschcannoncompat +import xyz.nuark.mcmod.mclschcannoncompat.block.entity.OrnamentFabricatorBlockEntity +import java.util.function.Supplier + +object ModBlockEntityTypes { + val REGISTRY = DeferredRegister.create( + net.minecraft.core.registries.BuiltInRegistries.BLOCK_ENTITY_TYPE, + Mclschcannoncompat.ID + ) + + val ORNAMENT_FABRICATOR: Supplier> = REGISTRY.register( + "ornament_fabricator", + Supplier { + BlockEntityType.Builder.of( + ::OrnamentFabricatorBlockEntity, + ModBlocks.ORNAMENT_FABRICATOR.get() + ).build(null) as BlockEntityType + } + ) +} diff --git a/src/main/kotlin/xyz/nuark/mcmod/mclschcannoncompat/block/ModBlocks.kt b/src/main/kotlin/xyz/nuark/mcmod/mclschcannoncompat/block/ModBlocks.kt index 6d5e6e8..f79a15c 100644 --- a/src/main/kotlin/xyz/nuark/mcmod/mclschcannoncompat/block/ModBlocks.kt +++ b/src/main/kotlin/xyz/nuark/mcmod/mclschcannoncompat/block/ModBlocks.kt @@ -1,8 +1,19 @@ package xyz.nuark.mcmod.mclschcannoncompat.block +import net.minecraft.world.level.block.Blocks +import net.minecraft.world.level.block.state.BlockBehaviour +import net.minecraft.world.level.material.MapColor import net.neoforged.neoforge.registries.DeferredRegister import xyz.nuark.mcmod.mclschcannoncompat.Mclschcannoncompat object ModBlocks { val REGISTRY = DeferredRegister.createBlocks(Mclschcannoncompat.ID) + + val ORNAMENT_FABRICATOR = REGISTRY.registerBlock( + "ornament_fabricator", + ::OrnamentFabricatorBlock, + BlockBehaviour.Properties.ofFullCopy(Blocks.STONE) + .mapColor(MapColor.STONE) + .strength(3.0f, 6.0f) + ) } diff --git a/src/main/kotlin/xyz/nuark/mcmod/mclschcannoncompat/block/OrnamentFabricatorBlock.kt b/src/main/kotlin/xyz/nuark/mcmod/mclschcannoncompat/block/OrnamentFabricatorBlock.kt new file mode 100644 index 0000000..ff34b9b --- /dev/null +++ b/src/main/kotlin/xyz/nuark/mcmod/mclschcannoncompat/block/OrnamentFabricatorBlock.kt @@ -0,0 +1,61 @@ +package xyz.nuark.mcmod.mclschcannoncompat.block + +import com.mojang.serialization.MapCodec +import net.minecraft.core.BlockPos +import net.minecraft.server.level.ServerPlayer +import net.minecraft.world.InteractionResult +import net.minecraft.world.entity.player.Player +import net.minecraft.world.item.ItemStack +import net.minecraft.world.level.Level +import net.minecraft.world.level.LevelAccessor +import net.minecraft.world.level.block.BaseEntityBlock +import net.minecraft.world.level.block.RenderShape +import net.minecraft.world.level.block.entity.BlockEntity +import net.minecraft.world.level.block.entity.BlockEntityTicker +import net.minecraft.world.level.block.entity.BlockEntityType +import net.minecraft.world.level.block.state.BlockState +import net.minecraft.world.level.storage.loot.LootParams +import net.minecraft.world.phys.BlockHitResult +import net.neoforged.neoforge.common.extensions.IPlayerExtension +import xyz.nuark.mcmod.mclschcannoncompat.block.entity.OrnamentFabricatorBlockEntity + +class OrnamentFabricatorBlock(properties: Properties) : BaseEntityBlock(properties) { + override fun codec(): MapCodec = simpleCodec(::OrnamentFabricatorBlock) + + override fun newBlockEntity(pos: BlockPos, state: BlockState): BlockEntity = + OrnamentFabricatorBlockEntity(pos, state) + + override fun getRenderShape(state: BlockState): RenderShape = RenderShape.MODEL + + override fun useWithoutItem( + state: BlockState, + level: Level, + pos: BlockPos, + player: Player, + hitResult: BlockHitResult + ): InteractionResult { + if (level.isClientSide) return InteractionResult.SUCCESS + + if (player is ServerPlayer) { + player.openMenu(state.getMenuProvider(level, pos), pos) + } + + return InteractionResult.sidedSuccess(level.isClientSide) + } + + override fun getTicker( + level: Level, + state: BlockState, + type: BlockEntityType + ): BlockEntityTicker? { + if (level.isClientSide) return null + return createTickerHelper(type, ModBlockEntityTypes.ORNAMENT_FABRICATOR.get()) { _, _, _, be -> + be.tick() + } + } +} + +data class OrnamentFabricatorEntry( + val block: net.minecraft.world.level.block.Block, + val components: List +) diff --git a/src/main/kotlin/xyz/nuark/mcmod/mclschcannoncompat/block/entity/OrnamentFabricatorBlockEntity.kt b/src/main/kotlin/xyz/nuark/mcmod/mclschcannoncompat/block/entity/OrnamentFabricatorBlockEntity.kt new file mode 100644 index 0000000..a335741 --- /dev/null +++ b/src/main/kotlin/xyz/nuark/mcmod/mclschcannoncompat/block/entity/OrnamentFabricatorBlockEntity.kt @@ -0,0 +1,310 @@ +package xyz.nuark.mcmod.mclschcannoncompat.block.entity + +import com.ldtteam.domumornamentum.block.IMateriallyTexturedBlock +import com.ldtteam.domumornamentum.client.model.data.MaterialTextureData +import com.ldtteam.domumornamentum.recipe.ModRecipeTypes +import com.ldtteam.domumornamentum.recipe.architectscutter.ArchitectsCutterRecipeInput +import com.minecolonies.api.util.ItemStackUtils +import net.minecraft.core.BlockPos +import net.minecraft.core.HolderLookup +import net.minecraft.core.component.DataComponents +import net.minecraft.nbt.CompoundTag +import net.minecraft.network.chat.Component +import net.minecraft.world.Containers +import net.minecraft.world.MenuProvider +import net.minecraft.world.SimpleContainer +import net.minecraft.world.entity.player.Inventory +import net.minecraft.world.entity.player.Player +import net.minecraft.world.inventory.AbstractContainerMenu +import net.minecraft.world.inventory.ContainerData +import net.minecraft.world.item.BlockItem +import net.minecraft.world.item.ItemStack +import net.minecraft.world.item.component.BlockItemStateProperties +import net.minecraft.world.level.block.entity.BlockEntity +import net.minecraft.world.level.block.state.BlockState +import net.neoforged.neoforge.items.ItemStackHandler +import xyz.nuark.mcmod.mclschcannoncompat.block.ModBlockEntityTypes +import xyz.nuark.mcmod.mclschcannoncompat.menu.OrnamentFabricatorMenu +import xyz.nuark.mcmod.mclschcannoncompat.utils.ODBlockRecipiesCache + +class OrnamentFabricatorBlockEntity( + pos: BlockPos, + state: BlockState +) : BlockEntity( + ModBlockEntityTypes.ORNAMENT_FABRICATOR.get(), pos, state +), MenuProvider { + init { + ODBlockRecipiesCache.initRecipesCache() + } + + val itemHandler = object : ItemStackHandler(TOTAL_SLOTS) { + override fun onContentsChanged(slot: Int) { + if (slot != SLOT_PREVIEW) { + updateRecipe() + } + setChanged() + } + + override fun isItemValid(slot: Int, stack: ItemStack): Boolean = + slot != SLOT_PREVIEW + + override fun extractItem(slot: Int, amount: Int, simulate: Boolean): ItemStack { + if (slot == SLOT_PREVIEW) return ItemStack.EMPTY + return super.extractItem(slot, amount, simulate) + } + } + + var blockTypeIndex: Int = 0 + set(value) { + field = value; setChanged() + } + + var blockSubtypeIndex: Int = 0 + set(value) { + field = value; setChanged() + } + + var currentProcess: Int = 0 + set(value) { + field = value; setChanged() + } + + var maxProcess: Int = DEFAULT_PROCESS_TIME + set(value) { + field = value; setChanged() + } + + var craftingEnabled: Boolean = false + set(value) { + field = value; setChanged() + } + + private var cachedRecipe: FabricatorRecipe? = null + + val containerData = object : ContainerData { + override fun get(index: Int): Int = when (index) { + DATA_BLOCK_TYPE -> blockTypeIndex + DATA_BLOCK_SUBTYPE -> blockSubtypeIndex + DATA_CURRENT -> currentProcess + DATA_MAX -> maxProcess + DATA_CRAFTING_ENABLED -> if (craftingEnabled) 1 else 0 + else -> 0 + } + + override fun set(index: Int, value: Int) { + when (index) { + DATA_BLOCK_TYPE -> blockTypeIndex = value + DATA_BLOCK_SUBTYPE -> blockSubtypeIndex = value + DATA_CURRENT -> currentProcess = value + DATA_MAX -> maxProcess = value + DATA_CRAFTING_ENABLED -> craftingEnabled = value == 1 + } + + updateRecipe() + } + + override fun getCount(): Int = DATA_SIZE + } + + override fun getDisplayName(): Component = + Component.translatable("block.mclschcannoncompat.ornament_fabricator") + + override fun createMenu(containerId: Int, playerInventory: Inventory, player: Player): AbstractContainerMenu = + OrnamentFabricatorMenu(containerId, playerInventory, this, containerData) + + fun tick() { + val level = level ?: return + if (level.isClientSide) return + + val inputA = itemHandler.getStackInSlot(SLOT_INPUT_A) + val inputB = itemHandler.getStackInSlot(SLOT_INPUT_B) + val output = itemHandler.getStackInSlot(SLOT_OUTPUT) + + updateRecipe() + val recipe = cachedRecipe?.takeIf { ItemStackUtils.compareItemStacksIgnoreStackSize(it.result, itemHandler.getStackInSlot(SLOT_PREVIEW)) } ?: updateRecipe() + + if (recipe != null && craftingEnabled) { + val canOutput = output.isEmpty || + (ItemStack.isSameItemSameComponents(output, recipe.result) && + output.count + recipe.result.count <= output.maxStackSize) + + if (canOutput) { + currentProcess++ + if (currentProcess >= maxProcess) { + currentProcess = 0 + craftingEnabled = false + + itemHandler.extractItem(SLOT_INPUT_A, 1, false) + itemHandler.extractItem(SLOT_INPUT_B, 1, false) + + if (output.isEmpty) { + itemHandler.setStackInSlot(SLOT_OUTPUT, recipe.result.copy()) + } else { + output.grow(recipe.result.count) + } + setChanged() + } + } else { + currentProcess = 0 + } + } else if (recipe == null) { + currentProcess = 0 + maxProcess = DEFAULT_PROCESS_TIME + } + } + + private data class FabricatorRecipe( + val result: ItemStack, + val resultPreview: ItemStack, + val processTicks: Int + ) + + private fun findRecipe(inputA: ItemStack, inputB: ItemStack): FabricatorRecipe? { + if (inputA.isEmpty) return null + val level = level ?: return null + + val input = ArchitectsCutterRecipeInput(SimpleContainer(inputA, inputB)) + + val type = ODBlockRecipiesCache.getBlockTypesCache().getOrNull(blockTypeIndex) ?: return null + val variant = ODBlockRecipiesCache.getBlockVariantsCache().getOrDefault(blockTypeIndex, emptyList()) + .getOrNull(blockSubtypeIndex) ?: return null + + val matchedRecipe = level.recipeManager.getAllRecipesFor(ModRecipeTypes.ARCHITECTS_CUTTER.get()) + .firstNotNullOfOrNull { recipeHolder -> + val recipe = recipeHolder.value + val resultItem = recipe.getResultItem(level.registryAccess()) + if (resultItem.item == variant.item) { + val resultBlockState = + resultItem.getOrDefault(DataComponents.BLOCK_STATE, BlockItemStateProperties.EMPTY) + val currentBlockState = + variant.getOrDefault(DataComponents.BLOCK_STATE, BlockItemStateProperties.EMPTY) + + if (resultBlockState.isEmpty && currentBlockState.isEmpty || resultBlockState == currentBlockState) { + val output = recipe.assemble(input, level.registryAccess()) + + val textureData = MaterialTextureData.readFromItemStack(output)?.takeIf { !it.isEmpty } + ?: return@firstNotNullOfOrNull null + + val generatedBlock = + recipe.block as? IMateriallyTexturedBlock ?: return@firstNotNullOfOrNull null + val recipeComponents = generatedBlock.components.toList() + + return@firstNotNullOfOrNull when (textureData.getTexturedComponents.size) { + 2 if (!inputA.isEmpty && !inputB.isEmpty) -> { + val itemA = inputA.item.takeIf { it is BlockItem } as? BlockItem + ?: return@firstNotNullOfOrNull null + val itemB = inputB.item.takeIf { it is BlockItem } as? BlockItem + ?: return@firstNotNullOfOrNull null + + val aOk = itemA.block.defaultBlockState().`is`(recipeComponents[0].validSkins) + val bOk = itemB.block.defaultBlockState().`is`(recipeComponents[1].validSkins) + + aOk && bOk + recipeHolder to output + } + + 1 if (inputB.isEmpty) -> { + val itemA = inputA.item.takeIf { it is BlockItem } as? BlockItem + ?: return@firstNotNullOfOrNull null + + itemA.block.defaultBlockState().`is`(recipeComponents[0].validSkins) + recipeHolder to output + } + + else -> null + } + } + } + + null + } ?: return null + + val (recipeUsed, output) = matchedRecipe + + return FabricatorRecipe( + output, + output, + 100 + ) + } + + private fun updateRecipe(): FabricatorRecipe? { + val inputA = itemHandler.getStackInSlot(SLOT_INPUT_A) + val inputB = itemHandler.getStackInSlot(SLOT_INPUT_B) + + val prevRecipe = cachedRecipe + val recipe = findRecipe(inputA, inputB) + cachedRecipe = recipe + + recipe?.let { + val previewStack = recipe.resultPreview.copy().also { it.count = recipe.result.count } + itemHandler.setStackInSlot(SLOT_PREVIEW, previewStack) + + maxProcess = recipe.processTicks + } ?: run { + if (!itemHandler.getStackInSlot(SLOT_PREVIEW).isEmpty) { + itemHandler.setStackInSlot(SLOT_PREVIEW, ItemStack.EMPTY) + } + } + + level.takeIf { it != null && !it.isClientSide && !ItemStackUtils.compareItemStacksIgnoreStackSize(prevRecipe?.result, cachedRecipe?.result) }?.let { + craftingEnabled = false + currentProcess = 0 + } + + return recipe + } + + override fun saveAdditional(tag: CompoundTag, registries: HolderLookup.Provider) { + super.saveAdditional(tag, registries) + tag.put("Inventory", itemHandler.serializeNBT(registries)) + tag.putInt("BlockTypeIndex", blockTypeIndex) + tag.putInt("BlockSubtypeIndex", blockSubtypeIndex) + tag.putInt("CurrentProcess", currentProcess) + tag.putInt("MaxProcess", maxProcess) + tag.putBoolean("CraftingEnabled", craftingEnabled) + } + + override fun loadAdditional(tag: CompoundTag, registries: HolderLookup.Provider) { + super.loadAdditional(tag, registries) + itemHandler.deserializeNBT(registries, tag.getCompound("Inventory")) + blockTypeIndex = tag.getInt("BlockTypeIndex") + blockSubtypeIndex = tag.getInt("BlockSubtypeIndex") + currentProcess = tag.getInt("CurrentProcess") + maxProcess = tag.getInt("MaxProcess") + craftingEnabled = tag.getBoolean("CraftingEnabled") + } + + override fun onLoad() { + super.onLoad() + + updateRecipe() + } + + fun drops() { + val inventory = SimpleContainer(TOTAL_SLOTS) + for (i in 0 until TOTAL_SLOTS) { + if (i == SLOT_PREVIEW) continue + + inventory.setItem(i, itemHandler.getStackInSlot(i)) + } + level?.let { Containers.dropContents(it, blockPos, inventory) } + } + + companion object { + const val SLOT_INPUT_A = 0 + const val SLOT_INPUT_B = 1 + const val SLOT_OUTPUT = 2 + const val SLOT_PREVIEW = 3 + const val TOTAL_SLOTS = 4 + + const val DATA_BLOCK_TYPE = 0 + const val DATA_BLOCK_SUBTYPE = 1 + const val DATA_CURRENT = 2 + const val DATA_MAX = 3 + const val DATA_CRAFTING_ENABLED = 4 + const val DATA_SIZE = 5 + + const val DEFAULT_PROCESS_TIME = 200 // in ticks + } +} diff --git a/src/main/kotlin/xyz/nuark/mcmod/mclschcannoncompat/datagen/ModItemModelProvider.kt b/src/main/kotlin/xyz/nuark/mcmod/mclschcannoncompat/datagen/ModItemModelProvider.kt index 6d05685..624a0e6 100644 --- a/src/main/kotlin/xyz/nuark/mcmod/mclschcannoncompat/datagen/ModItemModelProvider.kt +++ b/src/main/kotlin/xyz/nuark/mcmod/mclschcannoncompat/datagen/ModItemModelProvider.kt @@ -4,6 +4,7 @@ import net.minecraft.data.PackOutput import net.neoforged.neoforge.client.model.generators.ItemModelProvider import net.neoforged.neoforge.common.data.ExistingFileHelper import xyz.nuark.mcmod.mclschcannoncompat.Mclschcannoncompat +import xyz.nuark.mcmod.mclschcannoncompat.block.ModBlocks import xyz.nuark.mcmod.mclschcannoncompat.item.ModItems class ModItemModelProvider(output: PackOutput, existingFileHelper: ExistingFileHelper) : ItemModelProvider(output, Mclschcannoncompat.ID, existingFileHelper) { @@ -11,5 +12,6 @@ class ModItemModelProvider(output: PackOutput, existingFileHelper: ExistingFileH basicItem(ModItems.BUILDER_ASSISTANT_STAFF_T1.get()) basicItem(ModItems.BUILDER_ASSISTANT_STAFF_T2.get()) basicItem(ModItems.BUILDER_ASSISTANT_STAFF_T3.get()) + simpleBlockItem(ModBlocks.ORNAMENT_FABRICATOR.get()) } -} \ No newline at end of file +} diff --git a/src/main/kotlin/xyz/nuark/mcmod/mclschcannoncompat/datagen/ModLanguageProviders.kt b/src/main/kotlin/xyz/nuark/mcmod/mclschcannoncompat/datagen/ModLanguageProviders.kt index 127b1b6..771fa01 100644 --- a/src/main/kotlin/xyz/nuark/mcmod/mclschcannoncompat/datagen/ModLanguageProviders.kt +++ b/src/main/kotlin/xyz/nuark/mcmod/mclschcannoncompat/datagen/ModLanguageProviders.kt @@ -12,7 +12,15 @@ object ModLanguageProviders { add(ModItems.BUILDER_ASSISTANT_STAFF_T1.get(), "Посох строителя T1") add(ModItems.BUILDER_ASSISTANT_STAFF_T2.get(), "Посох строителя T2") add(ModItems.BUILDER_ASSISTANT_STAFF_T3.get(), "Посох строителя T3") - add("item.asdf.assistantstaff.maxdepth", "Максимальная блоков: %s") + add(ModItems.ORNAMENT_FABRICATOR_ITEM.get(), "Орнаментный фабрикатор") + add("item.assistantstaff.hover.maxdepth", "Максимальная глубина: %s") + + add("tooltip.ornament_fabricator.no_preview", "Рецепт не подходит этим ингредиентам") + add("tooltip.btn.cyclestyle.up", "Переключить стиль вперёд") + add("tooltip.btn.cyclestyle.down", "Переключить стиль назад") + add("tooltip.btn.cyclevariant.up", "Переключить вариант вперёд") + add("tooltip.btn.cyclevariant.down", "Переключить вариант назад") + add("tooltip.btn.startcrafting", "Начать крафт") } } @@ -21,7 +29,15 @@ object ModLanguageProviders { add(ModItems.BUILDER_ASSISTANT_STAFF_T1.get(), "Builder staff T1") add(ModItems.BUILDER_ASSISTANT_STAFF_T2.get(), "Builder staff T2") add(ModItems.BUILDER_ASSISTANT_STAFF_T3.get(), "Builder staff T3") - add("item.asdf.assistantstaff.maxdepth", "Maximum depth: %s") + add(ModItems.ORNAMENT_FABRICATOR_ITEM.get(), "Ornament Fabricator") + add("item.assistantstaff.hover.maxdepth", "Maximum depth: %s") + + add("tooltip.ornament_fabricator.no_preview", "Recipe is not supported for provided ingredients") + add("tooltip.btn.cyclestyle.up", "Cycle style forward") + add("tooltip.btn.cyclestyle.down", "Cycle style backward") + add("tooltip.btn.cyclevariant.up", "Cycle variant forward") + add("tooltip.btn.cyclevariant.down", "Cycle variant backward") + add("tooltip.btn.startcrafting", "Start crafting") } } @@ -29,4 +45,4 @@ object ModLanguageProviders { generator.addProvider(shouldRun, ModRuRuLanguageProvider(generator.packOutput)) generator.addProvider(shouldRun, ModEnUsLanguageProvider(generator.packOutput)) } -} \ No newline at end of file +} diff --git a/src/main/kotlin/xyz/nuark/mcmod/mclschcannoncompat/datagen/ModRecipeProvider.kt b/src/main/kotlin/xyz/nuark/mcmod/mclschcannoncompat/datagen/ModRecipeProvider.kt index 2f2dee6..562b55c 100644 --- a/src/main/kotlin/xyz/nuark/mcmod/mclschcannoncompat/datagen/ModRecipeProvider.kt +++ b/src/main/kotlin/xyz/nuark/mcmod/mclschcannoncompat/datagen/ModRecipeProvider.kt @@ -11,7 +11,6 @@ import xyz.nuark.mcmod.mclschcannoncompat.Mclschcannoncompat import xyz.nuark.mcmod.mclschcannoncompat.item.ModItems import java.util.concurrent.CompletableFuture - class ModRecipeProvider(output: PackOutput, registries: CompletableFuture) : RecipeProvider(output, registries) { override fun buildRecipes(recipeOutput: RecipeOutput) { ShapedRecipeBuilder.shaped(RecipeCategory.TOOLS, ModItems.BUILDER_ASSISTANT_STAFF_T1.get()) @@ -43,5 +42,15 @@ class ModRecipeProvider(output: PackOutput, registries: CompletableFuture BuilderAssistantStaff("builder_assistant_staff", props, 1) } - val BUILDER_ASSISTANT_STAFF_T2 = REGISTRY.registerItem("builder_assistant_staff_t2") { props -> BuilderAssistantStaff("builder_assistant_staff_t2", props, 3) } - val BUILDER_ASSISTANT_STAFF_T3 = REGISTRY.registerItem("builder_assistant_staff_t3") { props -> BuilderAssistantStaff("builder_assistant_staff_t3", props, 5) } -} \ No newline at end of file + val BUILDER_ASSISTANT_STAFF_T1 = REGISTRY.registerItem("builder_assistant_staff") { props -> + BuilderAssistantStaff("builder_assistant_staff", props, 1) + } + val BUILDER_ASSISTANT_STAFF_T2 = REGISTRY.registerItem("builder_assistant_staff_t2") { props -> + BuilderAssistantStaff("builder_assistant_staff_t2", props, 3) + } + val BUILDER_ASSISTANT_STAFF_T3 = REGISTRY.registerItem("builder_assistant_staff_t3") { props -> + BuilderAssistantStaff("builder_assistant_staff_t3", props, 5) + } + + val ORNAMENT_FABRICATOR_ITEM = REGISTRY.registerSimpleBlockItem(ModBlocks.ORNAMENT_FABRICATOR) +} diff --git a/src/main/kotlin/xyz/nuark/mcmod/mclschcannoncompat/menu/ModMenuTypes.kt b/src/main/kotlin/xyz/nuark/mcmod/mclschcannoncompat/menu/ModMenuTypes.kt new file mode 100644 index 0000000..f009a06 --- /dev/null +++ b/src/main/kotlin/xyz/nuark/mcmod/mclschcannoncompat/menu/ModMenuTypes.kt @@ -0,0 +1,19 @@ +package xyz.nuark.mcmod.mclschcannoncompat.menu + +import net.minecraft.world.inventory.MenuType +import net.neoforged.neoforge.common.extensions.IMenuTypeExtension +import net.neoforged.neoforge.registries.DeferredRegister +import xyz.nuark.mcmod.mclschcannoncompat.Mclschcannoncompat +import java.util.function.Supplier + +object ModMenuTypes { + val REGISTRY = DeferredRegister.create( + net.minecraft.core.registries.BuiltInRegistries.MENU, + Mclschcannoncompat.ID + ) + + val ORNAMENT_FABRICATOR: Supplier> = + REGISTRY.register("ornament_fabricator", Supplier { + IMenuTypeExtension.create(OrnamentFabricatorMenu::fromNetwork) + }) +} diff --git a/src/main/kotlin/xyz/nuark/mcmod/mclschcannoncompat/menu/OrnamentFabricatorMenu.kt b/src/main/kotlin/xyz/nuark/mcmod/mclschcannoncompat/menu/OrnamentFabricatorMenu.kt new file mode 100644 index 0000000..70ab254 --- /dev/null +++ b/src/main/kotlin/xyz/nuark/mcmod/mclschcannoncompat/menu/OrnamentFabricatorMenu.kt @@ -0,0 +1,191 @@ +package xyz.nuark.mcmod.mclschcannoncompat.menu + +import net.minecraft.client.player.LocalPlayer +import net.minecraft.network.FriendlyByteBuf +import net.minecraft.network.protocol.game.ServerboundContainerButtonClickPacket +import net.minecraft.world.entity.player.Inventory +import net.minecraft.world.entity.player.Player +import net.minecraft.world.inventory.AbstractContainerMenu +import net.minecraft.world.inventory.ContainerData +import net.minecraft.world.inventory.Slot +import net.minecraft.world.item.ItemStack +import net.neoforged.neoforge.items.SlotItemHandler +import xyz.nuark.mcmod.mclschcannoncompat.block.entity.OrnamentFabricatorBlockEntity +import xyz.nuark.mcmod.mclschcannoncompat.utils.ODBlockRecipiesCache + +class OrnamentFabricatorMenu( + containerId: Int, + private val playerInventory: Inventory, + private val blockEntity: OrnamentFabricatorBlockEntity, + private val data: ContainerData +) : AbstractContainerMenu( + ModMenuTypes.ORNAMENT_FABRICATOR.get(), containerId +) { + init { + require(blockEntity.itemHandler.slots >= OrnamentFabricatorBlockEntity.TOTAL_SLOTS) { "Container size ${blockEntity.itemHandler.slots} is smaller than expected ${OrnamentFabricatorBlockEntity.TOTAL_SLOTS}" } + checkContainerDataCount(data, OrnamentFabricatorBlockEntity.DATA_SIZE) + + addSlot(SlotItemHandler(blockEntity.itemHandler, OrnamentFabricatorBlockEntity.SLOT_INPUT_A, 12, 50)) + addSlot(SlotItemHandler(blockEntity.itemHandler, OrnamentFabricatorBlockEntity.SLOT_INPUT_B, 30, 50)) + addSlot(SlotItemHandler(blockEntity.itemHandler, OrnamentFabricatorBlockEntity.SLOT_OUTPUT, 84, 50)) + + addSlot(object : SlotItemHandler( + blockEntity.itemHandler, OrnamentFabricatorBlockEntity.SLOT_PREVIEW, 156, 50 + ) { + override fun mayPlace(stack: ItemStack) = false + override fun mayPickup(player: Player) = false + override fun isActive() = true + }) + + for (row in 0..2) { + for (col in 0..8) { + addSlot(Slot(playerInventory, col + row * 9 + 9, INV_X + col * 18, INV_Y + row * 18)) + } + } + + for (col in 0..8) { + addSlot(Slot(playerInventory, col, INV_X + col * 18, HOTBAR_Y)) + } + + addDataSlots(data) + } + + val blockTypeIndex: Int get() = data.get(OrnamentFabricatorBlockEntity.DATA_BLOCK_TYPE) + val blockSubtypeIndex: Int get() = data.get(OrnamentFabricatorBlockEntity.DATA_BLOCK_SUBTYPE) + val currentProcess: Int get() = data.get(OrnamentFabricatorBlockEntity.DATA_CURRENT) + val maxProcess: Int get() = data.get(OrnamentFabricatorBlockEntity.DATA_MAX) + val craftingEnabled: Boolean get() = data.get(OrnamentFabricatorBlockEntity.DATA_CRAFTING_ENABLED) == 1 + + val progressFraction: Float + get() = if (maxProcess > 0) currentProcess.toFloat() / maxProcess.toFloat() else 0f + + override fun quickMoveStack(player: Player, slotIndex: Int): ItemStack { + var remainder = ItemStack.EMPTY + val slot = slots.getOrNull(slotIndex) ?: return remainder + if (!slot.hasItem()) return remainder + + val stack = slot.item + remainder = stack.copy() + + val beSlots = 0 until OrnamentFabricatorBlockEntity.TOTAL_SLOTS + val invSlots = OrnamentFabricatorBlockEntity.TOTAL_SLOTS until + OrnamentFabricatorBlockEntity.TOTAL_SLOTS + 27 + val barSlots = OrnamentFabricatorBlockEntity.TOTAL_SLOTS + 27 until + OrnamentFabricatorBlockEntity.TOTAL_SLOTS + 36 + + if (slotIndex in beSlots) { + if (!moveItemStackTo(stack, invSlots.first, barSlots.last + 1, true)) + return ItemStack.EMPTY + } else { + if (!moveItemStackTo( + stack, OrnamentFabricatorBlockEntity.SLOT_INPUT_A, + OrnamentFabricatorBlockEntity.SLOT_INPUT_B + 1, false + ) + ) { + if (slotIndex in invSlots) { + if (!moveItemStackTo(stack, barSlots.first, barSlots.last + 1, false)) + return ItemStack.EMPTY + } else { + if (!moveItemStackTo(stack, invSlots.first, invSlots.last + 1, false)) + return ItemStack.EMPTY + } + } + } + + if (stack.isEmpty) slot.set(ItemStack.EMPTY) else slot.setChanged() + if (stack.count == remainder.count) return ItemStack.EMPTY + slot.onTake(player, stack) + return remainder + } + + override fun stillValid(player: Player): Boolean = + !blockEntity.isRemoved && player.canInteractWithBlock(blockEntity.blockPos, 8.0) + + fun cycleBlockTypeUp() = sendTypeChangePacket(ButtonIds.BUTTON_TYPE_UP) + fun cycleBlockTypeDown() = sendTypeChangePacket(ButtonIds.BUTTON_TYPE_DOWN) + fun cycleSubtypeUp() = sendTypeChangePacket(ButtonIds.BUTTON_SUBTYPE_UP) + fun cycleSubtypeDown() = sendTypeChangePacket(ButtonIds.BUTTON_SUBTYPE_DOWN) + fun startCrafting() = sendTypeChangePacket(ButtonIds.BUTTON_BUTTON_START_CRAFTING) + + /** + * Client-side handler for button clicks made by the user. + */ + private fun sendTypeChangePacket(buttonId: Int) { + (playerInventory.player as? LocalPlayer)?.connection?.send( + ServerboundContainerButtonClickPacket(containerId, buttonId) + ) + } + + /** + * Server-side handler for button clicks sent by the client. + */ + override fun clickMenuButton(player: Player, id: Int): Boolean { + when (id) { + ButtonIds.BUTTON_TYPE_UP -> { + blockEntity.blockTypeIndex = (blockEntity.blockTypeIndex + 1).coerceIn( + ODBlockRecipiesCache.getBlockTypesCache().indices + ) + blockEntity.blockSubtypeIndex = 0 + + blockEntity.craftingEnabled = false + blockEntity.currentProcess = 0 + } + + ButtonIds.BUTTON_TYPE_DOWN -> { + blockEntity.blockTypeIndex = (blockEntity.blockTypeIndex - 1).coerceIn( + ODBlockRecipiesCache.getBlockTypesCache().indices + ) + blockEntity.blockSubtypeIndex = 0 + + blockEntity.craftingEnabled = false + blockEntity.currentProcess = 0 + } + + ButtonIds.BUTTON_SUBTYPE_UP -> { + val range = ODBlockRecipiesCache.getBlockVariantsCache()[blockEntity.blockTypeIndex]?.indices ?: 0..0 + blockEntity.blockSubtypeIndex = (blockEntity.blockSubtypeIndex + 1).coerceIn(range) + + blockEntity.craftingEnabled = false + blockEntity.currentProcess = 0 + } + + ButtonIds.BUTTON_SUBTYPE_DOWN -> { + val range = ODBlockRecipiesCache.getBlockVariantsCache()[blockEntity.blockTypeIndex]?.indices ?: 0..0 + blockEntity.blockSubtypeIndex = (blockEntity.blockSubtypeIndex - 1).coerceIn(range) + + blockEntity.craftingEnabled = false + blockEntity.currentProcess = 0 + } + + ButtonIds.BUTTON_BUTTON_START_CRAFTING -> { + blockEntity.craftingEnabled = true + } + + else -> { + return false + } + } + blockEntity.setChanged() + return true + } + + companion object { + fun fromNetwork(containerId: Int, playerInventory: Inventory, buf: FriendlyByteBuf): OrnamentFabricatorMenu { + val pos = buf.readBlockPos() + val be = playerInventory.player.level().getBlockEntity(pos) as OrnamentFabricatorBlockEntity + return OrnamentFabricatorMenu(containerId, playerInventory, be, be.containerData) + } + + const val INV_X = 12 + const val INV_Y = 81 + const val HOTBAR_Y = 139 + + object ButtonIds { + const val BUTTON_TYPE_UP = 0 + const val BUTTON_TYPE_DOWN = 1 + const val BUTTON_SUBTYPE_UP = 2 + const val BUTTON_SUBTYPE_DOWN = 3 + const val BUTTON_BUTTON_START_CRAFTING = 4 + } + } +} diff --git a/src/main/kotlin/xyz/nuark/mcmod/mclschcannoncompat/screen/OrnamentFabricatorScreen.kt b/src/main/kotlin/xyz/nuark/mcmod/mclschcannoncompat/screen/OrnamentFabricatorScreen.kt new file mode 100644 index 0000000..336f3f7 --- /dev/null +++ b/src/main/kotlin/xyz/nuark/mcmod/mclschcannoncompat/screen/OrnamentFabricatorScreen.kt @@ -0,0 +1,181 @@ +package xyz.nuark.mcmod.mclschcannoncompat.screen + +import net.minecraft.client.gui.GuiGraphics +import net.minecraft.client.gui.components.Button +import net.minecraft.client.gui.components.Tooltip +import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen +import net.minecraft.network.chat.Component +import net.minecraft.resources.ResourceLocation +import net.minecraft.world.entity.player.Inventory +import xyz.nuark.mcmod.mclschcannoncompat.Mclschcannoncompat +import xyz.nuark.mcmod.mclschcannoncompat.block.entity.OrnamentFabricatorBlockEntity +import xyz.nuark.mcmod.mclschcannoncompat.menu.OrnamentFabricatorMenu +import xyz.nuark.mcmod.mclschcannoncompat.utils.ODBlockRecipiesCache + +class OrnamentFabricatorScreen( + menu: OrnamentFabricatorMenu, + playerInventory: Inventory, + title: Component +) : AbstractContainerScreen(menu, playerInventory, title) { + private lateinit var btnTypeUp: Button + private lateinit var btnTypeDown: Button + private lateinit var btnSubtypeUp: Button + private lateinit var btnSubtypeDown: Button + private lateinit var btnStartCrafting: Button + + init { + imageWidth = BG_WIDTH + imageHeight = BG_HEIGHT + + titleLabelX = 11 + inventoryLabelY = 6 + + inventoryLabelX = 11 + inventoryLabelY = 70 + } + + override fun init() { + super.init() + + val bx = leftPos + val by = topPos + + btnTypeUp = Button.builder(Component.literal(">")) { menu.cycleBlockTypeUp() } + .pos(bx + 161, by + 17) + .size(BTN_W, BTN_H) + .tooltip(Tooltip.create(Component.translatable("tooltip.btn.cyclestyle.up"))) + .build() + .also { addRenderableWidget(it) } + + btnTypeDown = Button.builder(Component.literal("<")) { menu.cycleBlockTypeDown() } + .pos(bx + 11, by + 17) + .size(BTN_W, BTN_H) + .tooltip(Tooltip.create(Component.translatable("tooltip.btn.cyclestyle.down"))) + .build() + .also { addRenderableWidget(it) } + + btnSubtypeUp = Button.builder(Component.literal(">")) { menu.cycleSubtypeUp() } + .pos(bx + 161, by + 33) + .size(BTN_W, BTN_H) + .tooltip(Tooltip.create(Component.translatable("tooltip.btn.cyclevariant.up"))) + .build() + .also { addRenderableWidget(it) } + + btnSubtypeDown = Button.builder(Component.literal("<")) { menu.cycleSubtypeDown() } + .pos(bx + 11, by + 33) + .size(BTN_W, BTN_H) + .tooltip(Tooltip.create(Component.translatable("tooltip.btn.cyclevariant.down"))) + .build() + .also { addRenderableWidget(it) } + + btnStartCrafting = Button.builder(Component.literal("v")) { menu.startCrafting() } + .pos(bx + 105, by + 52) + .size(BTN_W, BTN_H) + .tooltip(Tooltip.create(Component.translatable("tooltip.btn.startcrafting"))) + .build() + .also { addRenderableWidget(it) } + } + + override fun renderBg(graphics: GuiGraphics, partialTick: Float, mouseX: Int, mouseY: Int) { + graphics.blit( + TEXTURE, + leftPos, topPos, // screen position + 0f, 0f, // UV origin of background + imageWidth, imageHeight, // size to blit + TEX_WIDTH, TEX_HEIGHT + ) + + val progressWidth = (PROGRESS_WIDTH * menu.progressFraction).toInt().coerceAtLeast(0) + if (progressWidth > 0) { + graphics.blit( + TEXTURE, + leftPos + PROGRESS_X, + topPos + PROGRESS_Y, + PROGRESS_U.toFloat(), PROGRESS_V.toFloat(), + progressWidth, PROGRESS_HEIGHT, + TEX_WIDTH, TEX_HEIGHT + ) + } + } + + override fun render(graphics: GuiGraphics, mouseX: Int, mouseY: Int, partialTick: Float) { + super.render(graphics, mouseX, mouseY, partialTick) + renderTooltip(graphics, mouseX, mouseY) + + val type = ODBlockRecipiesCache.getBlockTypesCache().getOrNull(menu.blockTypeIndex) + val variant = ODBlockRecipiesCache.getBlockVariantsCache().getOrDefault(menu.blockTypeIndex, emptyList()).getOrNull(menu.blockSubtypeIndex) + + graphics.drawString( + font, + if (type != null) Component.literal("${menu.blockTypeIndex}. ").append(Component.translatable("cuttergroup.${type.namespace}.${type.path}")) else Component.translatable("tooltip.ornament_fabricator.no_preview"), + leftPos + 29, topPos + 19, + 0xBEBEBE, false + ) + graphics.drawString( + font, + if (variant != null) Component.literal("${menu.blockSubtypeIndex}. ${variant.displayName.string}") else Component.translatable("tooltip.ornament_fabricator.no_preview"), + leftPos + 29, topPos + 35, + 0xBEBEBE, false + ) + } + + override fun renderTooltip(graphics: GuiGraphics, mouseX: Int, mouseY: Int) { + super.renderTooltip(graphics, mouseX, mouseY) + + val previewAbsX = leftPos + 156 + val previewAbsY = topPos + 50 + + if (mouseX in previewAbsX..(previewAbsX + 16) && + mouseY in previewAbsY..(previewAbsY + 16) + ) { + val previewStack = menu.slots[3].item + if (!previewStack.isEmpty) { + graphics.renderTooltip( + this.font, + this.getTooltipFromContainerItem(previewStack), + previewStack.getTooltipImage(), + previewStack, + mouseX, + mouseY + ) + } else { + graphics.renderTooltip( + font, + Component.translatable("tooltip.ornament_fabricator.no_preview"), + mouseX, mouseY + ) + } + } + } + + override fun renderLabels(graphics: GuiGraphics, mouseX: Int, mouseY: Int) { + graphics.drawString(font, title, titleLabelX, titleLabelY, 0xBEBEBE, false) + graphics.drawString(font, playerInventoryTitle, inventoryLabelX, inventoryLabelY, 0xBEBEBE, false) + } + + companion object { + private val TEXTURE = ResourceLocation.fromNamespaceAndPath( + Mclschcannoncompat.ID, "textures/gui/ornament_fabricator.png" + ) + + // Background size (must match your PNG atlas dimensions you blit) + private const val BG_WIDTH = 184 + private const val BG_HEIGHT = 163 + + // Progress bar (blitted as a partial sprite) + private const val PROGRESS_X = 51 + private const val PROGRESS_Y = 53 + private const val PROGRESS_U = 184 // UV X in texture + private const val PROGRESS_V = 0 // UV Y in texture + private const val PROGRESS_WIDTH = 28 + private const val PROGRESS_HEIGHT = 10 + + // Texture sheet total size (usually 256×256) + private const val TEX_WIDTH = 256 + private const val TEX_HEIGHT = 256 + + // Button positions (relative to background top-left, i.e. imageX / imageY) + private const val BTN_W = 12 + private const val BTN_H = 12 + } +} diff --git a/src/main/resources/assets/mclschcannoncompat/blockstates/ornament_fabricator.json b/src/main/resources/assets/mclschcannoncompat/blockstates/ornament_fabricator.json new file mode 100644 index 0000000..6f7e2d8 --- /dev/null +++ b/src/main/resources/assets/mclschcannoncompat/blockstates/ornament_fabricator.json @@ -0,0 +1,5 @@ +{ + "variants": { + "": { "model": "mclschcannoncompat:block/ornament_fabricator" } + } +} diff --git a/src/main/resources/assets/mclschcannoncompat/models/block/ornament_fabricator.json b/src/main/resources/assets/mclschcannoncompat/models/block/ornament_fabricator.json new file mode 100644 index 0000000..f05c5eb --- /dev/null +++ b/src/main/resources/assets/mclschcannoncompat/models/block/ornament_fabricator.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:block/cube_all", + "textures": { + "all": "mclschcannoncompat:block/ornament_fabricator" + } +} diff --git a/src/main/resources/assets/mclschcannoncompat/textures/block/ornament_fabricator.png b/src/main/resources/assets/mclschcannoncompat/textures/block/ornament_fabricator.png new file mode 100644 index 0000000000000000000000000000000000000000..ff62ecf366b31e982da3e27fe0bf018278f49be8 GIT binary patch literal 6610 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 literal 0 HcmV?d00001