diff --git a/build.gradle b/build.gradle index 4d41eb6..de218b1 100644 --- a/build.gradle +++ b/build.gradle @@ -17,7 +17,12 @@ version = mod_version group = mod_group_id repositories { + maven { + setUrl("https://maven.shedaniel.me/") + } + maven { url = 'https://maven.minecraftforge.net/' } mavenLocal() + mavenCentral() } base { @@ -37,7 +42,7 @@ neoForge { } // This line is optional. Access Transformers are automatically detected - // accessTransformers = project.files('src/main/resources/META-INF/accesstransformer.cfg') + accessTransformers = project.files('src/main/resources/META-INF/accesstransformer.cfg') // Default run configurations. // These can be tweaked, removed, or duplicated as needed. @@ -133,6 +138,16 @@ dependencies { // For more info: // http://www.gradle.org/docs/current/userguide/artifact_dependencies_tutorial.html // http://www.gradle.org/docs/current/userguide/dependency_management.html + compileOnly(annotationProcessor("org.projectlombok:lombok:1.18.30")) + implementation(additionalRuntimeClasspath("blue.endless:jankson:$jankson_version")) + implementation("com.moandjiezana.toml:toml4j:$toml4j_version") + implementation("org.exjson:xjs-data:$xjs_data_version") + implementation("org.exjson:xjs-compat:$xjs_compat_version") + implementation("com.personthecat:fresult:$fresult_version") + implementation("me.shedaniel.cloth:cloth-config-neoforge:${cloth_config_version}") + + //implementation("com.github.glitchfiend:TerraBlender-neoforge:${terrablender_version}") + } // This block of code expands all declared replace properties in the specified resource targets. diff --git a/gradle.properties b/gradle.properties index 5891370..996fd52 100644 --- a/gradle.properties +++ b/gradle.properties @@ -21,3 +21,12 @@ mod_version=1.0.0 mod_group_id=net.frozenblock.frozenlib mod_authors=LiukRast, Lunade, Treetrain1 mod_description=NeoForge port of FrozenLib + +toml4j_version=0.7.2 +jankson_version=1.2.3 +xjs_data_version=0.6 +xjs_compat_version=0.8 +fresult_version=3.1 + +cloth_config_version=15.0.130 +terrablender_version=1.21-4.0.0.2 diff --git a/src/main/java/net/frozenblock/lib/Config.java b/src/main/java/net/frozenblock/lib/Config.java deleted file mode 100644 index dd28e63..0000000 --- a/src/main/java/net/frozenblock/lib/Config.java +++ /dev/null @@ -1,63 +0,0 @@ -package net.frozenblock.lib; - -import java.util.List; -import java.util.Set; -import java.util.stream.Collectors; - -import net.minecraft.core.registries.BuiltInRegistries; -import net.minecraft.resources.ResourceLocation; -import net.minecraft.world.item.Item; -import net.neoforged.bus.api.SubscribeEvent; -import net.neoforged.fml.common.EventBusSubscriber; -import net.neoforged.fml.event.config.ModConfigEvent; -import net.neoforged.neoforge.common.ModConfigSpec; - -// An example config class. This is not required, but it's a good idea to have one to keep your config organized. -// Demonstrates how to use Neo's config APIs -@EventBusSubscriber(modid = FrozenSharedConstants.MOD_ID, bus = EventBusSubscriber.Bus.MOD) -public class Config -{ - private static final ModConfigSpec.Builder BUILDER = new ModConfigSpec.Builder(); - - private static final ModConfigSpec.BooleanValue LOG_DIRT_BLOCK = BUILDER - .comment("Whether to log the dirt block on common setup") - .define("logDirtBlock", true); - - private static final ModConfigSpec.IntValue MAGIC_NUMBER = BUILDER - .comment("A magic number") - .defineInRange("magicNumber", 42, 0, Integer.MAX_VALUE); - - public static final ModConfigSpec.ConfigValue MAGIC_NUMBER_INTRODUCTION = BUILDER - .comment("What you want the introduction message to be for the magic number") - .define("magicNumberIntroduction", "The magic number is... "); - - // a list of strings that are treated as resource locations for items - private static final ModConfigSpec.ConfigValue> ITEM_STRINGS = BUILDER - .comment("A list of items to log on common setup.") - .defineListAllowEmpty("items", List.of("minecraft:iron_ingot"), Config::validateItemName); - - static final ModConfigSpec SPEC = BUILDER.build(); - - public static boolean logDirtBlock; - public static int magicNumber; - public static String magicNumberIntroduction; - public static Set items; - - private static boolean validateItemName(final Object obj) - { - return obj instanceof String itemName && BuiltInRegistries.ITEM.containsKey(ResourceLocation.parse(itemName)); - } - - @SubscribeEvent - static void onLoad(final ModConfigEvent event) - { - logDirtBlock = LOG_DIRT_BLOCK.get(); - magicNumber = MAGIC_NUMBER.get(); - magicNumberIntroduction = MAGIC_NUMBER_INTRODUCTION.get(); - - // convert the list of strings into a set of items - items = ITEM_STRINGS.get().stream() - .map(itemName -> BuiltInRegistries.ITEM.get(ResourceLocation.parse(itemName))) - .collect(Collectors.toSet()); - } -} diff --git a/src/main/java/net/frozenblock/lib/FrozenBools.java b/src/main/java/net/frozenblock/lib/FrozenBools.java index bd050d8..faadb1b 100644 --- a/src/main/java/net/frozenblock/lib/FrozenBools.java +++ b/src/main/java/net/frozenblock/lib/FrozenBools.java @@ -1,6 +1,8 @@ package net.frozenblock.lib; import net.neoforged.fml.ModList; +import net.neoforged.fml.loading.LoadingModList; +import net.neoforged.neoforgespi.language.IModInfo; public class FrozenBools { @@ -22,6 +24,15 @@ public class FrozenBools { public static final boolean HAS_TERRALITH = hasMod("terralith"); public static boolean hasMod(String id) { - return ModList.get().getModFileById(id) != null; + if(ModList.get() == null) { + for(IModInfo info : LoadingModList.get().getMods()) { + if (info.getModId().equals(id)) return true; + } + } else { + for (IModInfo info : ModList.get().getMods()) { + if (info.getModId().equals(id)) return true; + } + } + return false; } } diff --git a/src/main/java/net/frozenblock/lib/FrozenClient.java b/src/main/java/net/frozenblock/lib/FrozenClient.java index 8409ca7..1a1aa9d 100644 --- a/src/main/java/net/frozenblock/lib/FrozenClient.java +++ b/src/main/java/net/frozenblock/lib/FrozenClient.java @@ -16,42 +16,81 @@ */ package net.frozenblock.lib; +import net.frozenblock.lib.entrypoint.api.FrozenClientEntrypoint; +import net.frozenblock.lib.event.api.ClientEvent; +import net.frozenblock.lib.integration.api.ModIntegrations; +import net.frozenblock.lib.menu.api.Panoramas; +import net.frozenblock.lib.networking.FrozenClientNetworking; +import net.frozenblock.lib.screenshake.api.client.ScreenShaker; +import net.frozenblock.lib.sound.api.FlyBySoundHub; +import net.frozenblock.lib.sound.impl.block_sound_group.BlockSoundGroupManager; +import net.frozenblock.lib.wind.api.ClientWindManager; +import net.minecraft.client.Minecraft; import net.minecraft.resources.ResourceLocation; -import net.minecraft.server.packs.PackType; +import net.minecraft.server.packs.resources.ReloadableResourceManager; import net.neoforged.api.distmarker.Dist; import net.neoforged.api.distmarker.OnlyIn; import net.neoforged.bus.api.SubscribeEvent; import net.neoforged.fml.common.EventBusSubscriber; import net.neoforged.fml.event.lifecycle.FMLClientSetupEvent; +import net.neoforged.neoforge.client.event.RegisterClientReloadListenersEvent; +import net.neoforged.neoforge.common.NeoForge; +import org.quiltmc.qsl.frozenblock.core.registry.impl.sync.client.ClientRegistrySync; +import org.quiltmc.qsl.frozenblock.misc.datafixerupper.impl.QuiltDataFixesInternals; @OnlyIn(Dist.CLIENT) +@EventBusSubscriber(modid = FrozenSharedConstants.MOD_ID, bus = EventBusSubscriber.Bus.MOD, value = Dist.CLIENT) public class FrozenClient { + @SubscribeEvent public static void onInitializeClient(FMLClientSetupEvent event) { - /*ModIntegrations.initializePreFreeze(); // Mod integrations must run after normal mod initialization + ModIntegrations.initializePreFreeze(); // Mod integrations must run after normal mod initialization // QUILT INIT - ClientFreezer.onInitializeClient(); ClientRegistrySync.registerHandlers(); // CONTINUE FROZENLIB INIT - registerClientEvents(); - FrozenClientNetworking.registerClientReceivers(); - // PARTICLES - ParticleFactoryRegistry particleRegistry = ParticleFactoryRegistry.getInstance(); + // PARTICLES TODO: ADD PARTICLE FACTORY! + //ParticleFactoryRegistry particleRegistry = ParticleFactoryRegistry.getInstance(); - particleRegistry.register(FrozenParticleTypes.DEBUG_POS, DebugPosParticle.Provider::new); + //particleRegistry.register(FrozenParticleTypes.DEBUG_POS, DebugPosParticle.Provider::new); Panoramas.addPanorama(ResourceLocation.withDefaultNamespace("textures/gui/title/background/panorama")); - var resourceLoader = ResourceManagerHelper.get(PackType.CLIENT_RESOURCES); - resourceLoader.registerReloadListener(BlockSoundGroupManager.INSTANCE); + NeoForge.EVENT_BUS.post(new FrozenClientEntrypoint()); - FrozenClientEntrypoint.EVENT.invoker().init(); // also includes dev init*/ } - private static void registerClientEvents() { + @SubscribeEvent + public static void registerClientResourceManager(RegisterClientReloadListenersEvent event) { + event.registerReloadListener(BlockSoundGroupManager.INSTANCE); + } + + @SubscribeEvent + public static void clientFreezer(ClientEvent.Started event) { + FrozenLogUtils.log("[Quilt DFU API] Clientside DataFixer Registry is about to freeze", true); + QuiltDataFixesInternals.get().freeze(); + FrozenLogUtils.log("[Quilt DFU API] Clientside DataFixer Registry was frozen", true); + } + + @SubscribeEvent + public static void startWorldTick(ClientEvent.LevelStart event) { + ClientWindManager.tick(event.source); + ScreenShaker.tick(event.source); + FlyBySoundHub.update(Minecraft.getInstance(), Minecraft.getInstance().cameraEntity, true); + } + @SubscribeEvent + public static void clientTickStart(ClientEvent.Started event) { + ClientWindManager.clearAndSwitchWindDisturbances(); } + + public static void disconnect() { + ScreenShaker.clear(); + ClientWindManager.clearAllWindDisturbances(); + } + + + } diff --git a/src/main/java/net/frozenblock/lib/FrozenEventListener.java b/src/main/java/net/frozenblock/lib/FrozenEventListener.java new file mode 100644 index 0000000..5f14c0a --- /dev/null +++ b/src/main/java/net/frozenblock/lib/FrozenEventListener.java @@ -0,0 +1,98 @@ +package net.frozenblock.lib; + +import net.frozenblock.lib.config.api.instance.Config; +import net.frozenblock.lib.config.api.registry.ConfigRegistry; +import net.frozenblock.lib.config.frozenlib_config.FrozenLibConfig; +import net.frozenblock.lib.config.impl.ConfigCommand; +import net.frozenblock.lib.entity.api.EntityUtils; +import net.frozenblock.lib.entity.api.command.ScaleEntityCommand; +import net.frozenblock.lib.event.api.PlayerJoinEvent; +import net.frozenblock.lib.event.api.RegistryFreezeEvent; +import net.frozenblock.lib.integration.api.ModIntegrations; +import net.frozenblock.lib.screenshake.api.ScreenShakeManager; +import net.frozenblock.lib.screenshake.api.command.ScreenShakeCommand; +import net.frozenblock.lib.screenshake.impl.ScreenShakeStorage; +import net.frozenblock.lib.tag.api.TagListCommand; +import net.frozenblock.lib.wind.api.WindManager; +import net.frozenblock.lib.wind.api.command.WindOverrideCommand; +import net.frozenblock.lib.wind.impl.WindStorage; +import net.minecraft.server.commands.WardenSpawnTrackerCommand; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.level.storage.DimensionDataStorage; +import net.neoforged.bus.api.SubscribeEvent; +import net.neoforged.neoforge.event.RegisterCommandsEvent; +import net.neoforged.neoforge.event.level.LevelEvent; +import net.neoforged.neoforge.event.tick.LevelTickEvent; + +/** + * A basic eventListener class + * This class listens to NEO FORGE EVENT BUS only + * */ +public class FrozenEventListener { + @SubscribeEvent + public static void levelLoad(LevelEvent.Load loadEvent) { + if(!loadEvent.getLevel().isClientSide()) { + final ServerLevel level = (ServerLevel) loadEvent.getLevel(); + DimensionDataStorage dimensionDataStorage = level.getDataStorage(); + WindManager windManager = WindManager.getWindManager(level); + dimensionDataStorage.computeIfAbsent(windManager.createData(), WindStorage.WIND_FILE_ID); + ScreenShakeManager screenShakeManager = ScreenShakeManager.getScreenShakeManager(level); + dimensionDataStorage.computeIfAbsent(screenShakeManager.createData(), ScreenShakeStorage.SCREEN_SHAKE_FILE_ID); + } + } + + @SubscribeEvent + public static void levelUnload(LevelEvent.Unload unloadEvent) { + if(!unloadEvent.getLevel().isClientSide()) { + final ServerLevel serverLevel = (ServerLevel) unloadEvent.getLevel(); + EntityUtils.clearEntitiesPerLevel(serverLevel); + WindManager.getWindManager(serverLevel).clearAllWindDisturbances(); + } + } + + @SubscribeEvent + public static void serverStartTick(LevelTickEvent.Pre pre) { + if(!pre.getLevel().isClientSide) { + final ServerLevel serverLevel = (ServerLevel) pre.getLevel(); + WindManager.getWindManager(serverLevel).clearAndSwitchWindDisturbances(); + WindManager.getWindManager(serverLevel).tick(serverLevel); + ScreenShakeManager.getScreenShakeManager(serverLevel).tick(serverLevel); + EntityUtils.populateEntitiesPerLevel(serverLevel); + } + } + + @SubscribeEvent + public static void onPlayerAddedTopLevel(PlayerJoinEvent.Level event) { + WindManager windManager = WindManager.getWindManager(event.level); + windManager.sendSyncToPlayer(windManager.createSyncPacket(), event.player); + } + + @SubscribeEvent + public static void startRegistryFreeze(RegistryFreezeEvent.Start event) { + if (!event.allRegistries) return; + + ModIntegrations.initialize(); + } + + @SubscribeEvent + public static void endRegistryFreeze(RegistryFreezeEvent.End event) { + if (!event.allRegistries) return; + + for (Config config : ConfigRegistry.getAllConfigs()) { + config.save(); + } + } + + @SubscribeEvent + public static void registerCommands(RegisterCommandsEvent event) { + final var dispatcher = event.getDispatcher(); + WindOverrideCommand.register(dispatcher); + ScreenShakeCommand.register(dispatcher); + ConfigCommand.register(dispatcher); + TagListCommand.register(dispatcher); + ScaleEntityCommand.register(dispatcher); + + if(FrozenLibConfig.get().wardenSpawnTrackerCommand) + WardenSpawnTrackerCommand.register(dispatcher); + } +} diff --git a/src/main/java/net/frozenblock/lib/FrozenMain.java b/src/main/java/net/frozenblock/lib/FrozenMain.java index d7cb532..185f1fc 100644 --- a/src/main/java/net/frozenblock/lib/FrozenMain.java +++ b/src/main/java/net/frozenblock/lib/FrozenMain.java @@ -1,48 +1,85 @@ package net.frozenblock.lib; -import org.slf4j.Logger; - -import com.mojang.logging.LogUtils; - -import net.neoforged.api.distmarker.Dist; +import net.frozenblock.lib.core.impl.DataPackReloadMarker; +import net.frozenblock.lib.entrypoint.api.FrozenMainEntrypoint; +import net.frozenblock.lib.entrypoint.api.FrozenModInitializer; +import net.frozenblock.lib.gravity.api.GravityAPI; +import net.frozenblock.lib.ingamedevtools.RegisterInGameDevTools; +import net.frozenblock.lib.networking.FrozenNetworking; +import net.frozenblock.lib.particle.api.FrozenParticleTypes; +import net.frozenblock.lib.registry.api.FrozenRegistry; +import net.frozenblock.lib.sound.api.predicate.SoundPredicate; +import net.frozenblock.lib.spotting_icons.api.SpottingIconPredicate; +import net.frozenblock.lib.tag.api.TagKeyArgument; +import net.frozenblock.lib.wind.api.WindDisturbanceLogic; +import net.frozenblock.lib.worldgen.feature.api.FrozenFeatures; +import net.frozenblock.lib.worldgen.feature.api.placementmodifier.FrozenPlacementModifiers; +import net.frozenblock.lib.worldgen.structure.impl.FrozenRuleBlockEntityModifiers; +import net.frozenblock.lib.worldgen.structure.impl.FrozenStructureProcessorTypes; +import net.frozenblock.lib.worldgen.surface.impl.BiomeTagConditionSource; +import net.frozenblock.lib.worldgen.surface.impl.OptimizedBiomeTagConditionSource; +import net.minecraft.commands.synchronization.ArgumentTypeInfos; +import net.minecraft.core.registries.BuiltInRegistries; import net.neoforged.bus.api.IEventBus; import net.neoforged.bus.api.SubscribeEvent; import net.neoforged.fml.ModContainer; -import net.neoforged.fml.common.EventBusSubscriber; import net.neoforged.fml.common.Mod; -import net.neoforged.fml.config.ModConfig; -import net.neoforged.fml.event.lifecycle.FMLClientSetupEvent; -import net.neoforged.fml.event.lifecycle.FMLCommonSetupEvent; import net.neoforged.neoforge.common.NeoForge; -import net.neoforged.neoforge.event.server.ServerStartingEvent; +import net.neoforged.neoforge.registries.RegisterEvent; +import org.quiltmc.qsl.frozenblock.core.registry.api.sync.ModProtocol; +import org.quiltmc.qsl.frozenblock.core.registry.impl.sync.server.ServerRegistrySync; +import org.quiltmc.qsl.frozenblock.misc.datafixerupper.impl.ServerFreezer; @Mod(FrozenSharedConstants.MOD_ID) -public class FrozenMain { +public class FrozenMain extends FrozenModInitializer { public FrozenMain(IEventBus modEventBus, ModContainer modContainer) { + super(FrozenSharedConstants.MOD_ID, modEventBus, modContainer, FrozenEventListener.class); + } - modEventBus.addListener(this::commonSetup); + @Override + public void onInitialize(String modId, IEventBus eventBus, ModContainer container) { + FrozenRegistry.initRegistry(); - NeoForge.EVENT_BUS.register(this); + // QUILT INIT - modContainer.registerConfig(ModConfig.Type.COMMON, Config.SPEC); - } + ServerFreezer.onInitialize(); + ModProtocol.loadVersions(); + ServerRegistrySync.registerHandlers(); - private void commonSetup(final FMLCommonSetupEvent event) { + // We call the entrypoint, which on forge is an event + NeoForge.EVENT_BUS.post(new FrozenMainEntrypoint()); + // Extra Listeners + NeoForge.EVENT_BUS.register(DataPackReloadMarker.class); + eventBus.register(FrozenNetworking.class); + NeoForge.EVENT_BUS.register(GravityAPI.class); + eventBus.register(FrozenRegistry.class); } @SubscribeEvent - public void onServerStarting(ServerStartingEvent event) { - FrozenServer.onInitializeServer(event); - } - - @EventBusSubscriber(modid = FrozenSharedConstants.MOD_ID, bus = EventBusSubscriber.Bus.MOD, value = Dist.CLIENT) - public static class ClientModEvents { - @SubscribeEvent - public static void onClientSetup(FMLClientSetupEvent event) { - FrozenClient.onInitializeClient(event); - } + public void registerEvent(RegisterEvent event) { + event.register(BuiltInRegistries.RULE_BLOCK_ENTITY_MODIFIER.key(), FrozenRuleBlockEntityModifiers::init); + event.register(BuiltInRegistries.ITEM.key(), RegisterInGameDevTools::register); + event.register(BuiltInRegistries.STRUCTURE_PROCESSOR.key(), FrozenStructureProcessorTypes::init); + event.register(FrozenRegistry.SOUND_PREDICATE.key(), SoundPredicate::init); + event.register(FrozenRegistry.SPOTTING_ICON_PREDICATE.key(), SpottingIconPredicate::init); + event.register(FrozenRegistry.WIND_DISTURBANCE_LOGIC.key(), WindDisturbanceLogic::init); + event.register(BuiltInRegistries.FEATURE.key(), FrozenFeatures::init); + event.register(BuiltInRegistries.PLACEMENT_MODIFIER_TYPE.key(), FrozenPlacementModifiers::init); + event.register(BuiltInRegistries.MATERIAL_CONDITION.key(), registry -> { + registry.register(FrozenSharedConstants.id("biome_tag_condition_source"), BiomeTagConditionSource.CODEC.codec()); + registry.register(FrozenSharedConstants.id("optimized_biome_tag_condition_source"), OptimizedBiomeTagConditionSource.CODEC.codec()); + }); + event.register(BuiltInRegistries.PARTICLE_TYPE.key(), FrozenParticleTypes::registerParticles); + event.register(BuiltInRegistries.COMMAND_ARGUMENT_TYPE.key(), registry -> { + ArgumentTypeInfos.register( + BuiltInRegistries.COMMAND_ARGUMENT_TYPE, + FrozenSharedConstants.string("tag_key"), + ArgumentTypeInfos.fixClassType(TagKeyArgument.class), + new TagKeyArgument.Info<>() + ); + }); } } diff --git a/src/main/java/net/frozenblock/lib/FrozenServer.java b/src/main/java/net/frozenblock/lib/FrozenServer.java index c32dbf0..b011c12 100644 --- a/src/main/java/net/frozenblock/lib/FrozenServer.java +++ b/src/main/java/net/frozenblock/lib/FrozenServer.java @@ -16,6 +16,7 @@ */ package net.frozenblock.lib; +import net.frozenblock.lib.integration.api.ModIntegrations; import net.neoforged.api.distmarker.Dist; import net.neoforged.api.distmarker.OnlyIn; import net.neoforged.bus.api.SubscribeEvent; @@ -23,9 +24,11 @@ import net.neoforged.neoforge.event.server.ServerStartingEvent; @OnlyIn(Dist.DEDICATED_SERVER) +@EventBusSubscriber(modid = FrozenSharedConstants.MOD_ID, bus = EventBusSubscriber.Bus.MOD, value = Dist.DEDICATED_SERVER) public class FrozenServer { + @SubscribeEvent public static void onInitializeServer(ServerStartingEvent event) { - //ModIntegrations.initializePreFreeze(); // Mod integrations must run after normal mod initialization + ModIntegrations.initializePreFreeze(); // Mod integrations must run after normal mod initialization } } diff --git a/src/main/java/net/frozenblock/lib/FrozenSharedConstants.java b/src/main/java/net/frozenblock/lib/FrozenSharedConstants.java index 3f970f4..0dcce0c 100644 --- a/src/main/java/net/frozenblock/lib/FrozenSharedConstants.java +++ b/src/main/java/net/frozenblock/lib/FrozenSharedConstants.java @@ -17,6 +17,7 @@ package net.frozenblock.lib; +import net.frozenblock.lib.entrypoint.api.DevelopmentCheck; import net.minecraft.resources.ResourceLocation; import net.neoforged.fml.ModList; import org.apache.logging.log4j.util.InternalApi; @@ -27,7 +28,7 @@ import org.slf4j.helpers.NOPLogger; @InternalApi -public class FrozenSharedConstants { +public class FrozenSharedConstants implements DevelopmentCheck { public static final String PROJECT_ID = "FrozenLib"; public static final String MOD_ID = "frozenlib"; public static final Logger LOGGER = LoggerFactory.getLogger(PROJECT_ID); @@ -37,11 +38,12 @@ public class FrozenSharedConstants { *

* It's smart to use this for at least registries. */ - public static boolean UNSTABLE_LOGGING = !ModList.get().getModFileById(MOD_ID).getFile().getFilePath().endsWith(".jar"); + public static boolean UNSTABLE_LOGGING = DevelopmentCheck.isDevelopment(); public static final int DATA_VERSION = 2; @Contract("_ -> new") @NotNull + public static ResourceLocation id(String path) { return ResourceLocation.fromNamespaceAndPath(FrozenSharedConstants.MOD_ID, path); } diff --git a/src/main/java/net/frozenblock/lib/advancement/api/AdvancementAPI.java b/src/main/java/net/frozenblock/lib/advancement/api/AdvancementAPI.java new file mode 100644 index 0000000..17e2d16 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/advancement/api/AdvancementAPI.java @@ -0,0 +1,113 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.advancement.api; + +import net.minecraft.advancements.Advancement; +import net.minecraft.advancements.AdvancementRequirements; +import net.minecraft.advancements.AdvancementRewards; +import net.minecraft.advancements.Criterion; +import net.minecraft.resources.ResourceKey; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.level.storage.loot.LootTable; + +import java.util.*; + +public final class AdvancementAPI { + private AdvancementAPI() {} + + /** + * Makes a copy of {@link AdvancementRewards#EMPTY} for use in the Advancement API + *

+ * Use only when needed, as this will increase memory usage + */ + public static Advancement setupRewards(Advancement advancement) { + AdvancementRewards rewards = advancement.rewards(); + if (advancement.rewards() == AdvancementRewards.EMPTY) { + rewards = new AdvancementRewards(0, List.of(), List.of(), Optional.empty()); + } + return new Advancement(advancement.parent(), advancement.display(), rewards, advancement.criteria(), advancement.requirements(), advancement.sendsTelemetryEvent(), advancement.name()); + } + + /** + * Makes a copy of {@link AdvancementRequirements#EMPTY} for use in the Advancement API + *

+ * Use only when needed, as this will increase memory usage + */ + public static Advancement setupRequirements(Advancement advancement) { + AdvancementRequirements requirements = advancement.requirements(); + if (advancement.requirements() == AdvancementRequirements.EMPTY) { + requirements = new AdvancementRequirements(List.of()); + } + return new Advancement(advancement.parent(), advancement.display(), advancement.rewards(), advancement.criteria(), requirements, advancement.sendsTelemetryEvent(), advancement.name()); + } + + public static Advancement setupCriteria(Advancement advancement) { + Map> criteria = advancement.criteria(); + if (!(advancement.criteria() instanceof HashMap>)) { + criteria = new HashMap<>(advancement.criteria()); + } + return new Advancement(advancement.parent(), advancement.display(), advancement.rewards(), criteria, advancement.requirements(), advancement.sendsTelemetryEvent(), advancement.name()); + } + + public static void addCriteria(Advancement advancement, String key, Criterion criterion) { + if (criterion == null) return; + setupCriteria(advancement); + advancement.criteria().putIfAbsent(key, criterion); + } + + public static Advancement addRequirementsAsNewList(Advancement advancement, AdvancementRequirements requirements) { + if (requirements == null || requirements.isEmpty()) return advancement; + setupRequirements(advancement); + List> list = new ArrayList<>(advancement.requirements().requirements()); + list.addAll(requirements.requirements()); + return new Advancement(advancement.parent(), advancement.display(), advancement.rewards(), advancement.criteria(), new AdvancementRequirements(Collections.unmodifiableList(list)), advancement.sendsTelemetryEvent(), advancement.name()); + } + + public static Advancement addRequirementsToList(Advancement advancement, List requirements) { + if (requirements == null || requirements.isEmpty()) return advancement; + setupRequirements(advancement); + List> list = new ArrayList<>(advancement.requirements().requirements()); + if (list.isEmpty()) { + list.add(requirements); + } else { + List existingList = list.getFirst(); + List finalList = new ArrayList<>(existingList); + finalList.addAll(requirements); + list.add(Collections.unmodifiableList(finalList)); + list.remove(existingList); + } + return new Advancement(advancement.parent(), advancement.display(), advancement.rewards(), advancement.criteria(), new AdvancementRequirements(Collections.unmodifiableList(list)), advancement.sendsTelemetryEvent(), advancement.name()); + } + + public static Advancement addLootTables(Advancement advancement, List> lootTables) { + if (lootTables.isEmpty()) return advancement; + setupRewards(advancement); + AdvancementRewards rewards = advancement.rewards(); + List> newLoot = new ArrayList<>(rewards.loot()); + newLoot.addAll(lootTables); + return new Advancement(advancement.parent(), advancement.display(), new AdvancementRewards(advancement.rewards().experience(), Collections.unmodifiableList(newLoot), advancement.rewards().recipes(), advancement.rewards().function()), advancement.criteria(), advancement.requirements(), advancement.sendsTelemetryEvent(), advancement.name()); + + } + + public static Advancement addRecipes(Advancement advancement, List recipes) { + AdvancementRewards rewards = advancement.rewards(); + List newLoot = new ArrayList<>(rewards.recipes()); + newLoot.addAll(recipes); + return new Advancement(advancement.parent(), advancement.display(), new AdvancementRewards(advancement.rewards().experience(), advancement.rewards().loot(), Collections.unmodifiableList(newLoot), advancement.rewards().function()), advancement.criteria(), advancement.requirements(), advancement.sendsTelemetryEvent(), advancement.name()); + } +} diff --git a/src/main/java/net/frozenblock/lib/advancement/api/AdvancementEvent.java b/src/main/java/net/frozenblock/lib/advancement/api/AdvancementEvent.java new file mode 100644 index 0000000..1721f04 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/advancement/api/AdvancementEvent.java @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.advancement.api; + +import net.minecraft.advancements.AdvancementHolder; +import net.minecraft.core.HolderLookup; +import net.neoforged.bus.api.Event; + +public class AdvancementEvent extends Event { + private AdvancementEvent() {} + + public static class Init extends AdvancementEvent { + public final AdvancementHolder holder; + public final HolderLookup.Provider registries; + + public Init(AdvancementHolder holder, HolderLookup.Provider registries) { + this.holder = holder; + this.registries = registries; + } + } +} diff --git a/src/main/java/net/frozenblock/lib/advancement/mixin/ServerAdvancementManagerMixin.java b/src/main/java/net/frozenblock/lib/advancement/mixin/ServerAdvancementManagerMixin.java new file mode 100644 index 0000000..a84900b --- /dev/null +++ b/src/main/java/net/frozenblock/lib/advancement/mixin/ServerAdvancementManagerMixin.java @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.advancement.mixin; + +import com.google.gson.JsonElement; +import net.frozenblock.lib.FrozenMain; +import net.frozenblock.lib.advancement.api.AdvancementEvent; +import net.minecraft.advancements.AdvancementHolder; +import net.minecraft.core.HolderLookup; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.ServerAdvancementManager; +import net.minecraft.server.packs.resources.ResourceManager; +import net.minecraft.util.profiling.ProfilerFiller; +import net.neoforged.neoforge.common.NeoForge; +import org.spongepowered.asm.mixin.Final; +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; + +import java.util.Map; + +@Mixin(value = ServerAdvancementManager.class, priority = 1500) +public class ServerAdvancementManagerMixin { + + @Shadow + private Map advancements; + + @Shadow + @Final + private HolderLookup.Provider registries; + + @Inject( + method = "apply(Ljava/util/Map;Lnet/minecraft/server/packs/resources/ResourceManager;Lnet/minecraft/util/profiling/ProfilerFiller;)V", + at = @At( + value = "INVOKE", + target = "Ljava/util/Map;values()Ljava/util/Collection;" + ) + ) + private void modifyAdvancement(Map object, ResourceManager resourceManager, ProfilerFiller profiler, CallbackInfo ci) { + for (AdvancementHolder holder : advancements.values()) { + NeoForge.EVENT_BUS.post(new AdvancementEvent.Init(holder, this.registries)); + } + } +} diff --git a/src/main/java/net/frozenblock/lib/block/api/FaceBlock.java b/src/main/java/net/frozenblock/lib/block/api/FaceBlock.java new file mode 100644 index 0000000..8b881de --- /dev/null +++ b/src/main/java/net/frozenblock/lib/block/api/FaceBlock.java @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.block.api; + +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.world.level.BlockGetter; +import net.minecraft.world.level.block.MultifaceBlock; +import net.minecraft.world.level.block.state.BlockState; + +/** + * A {@link MultifaceBlock}, but only one face is permitted. + */ +public abstract class FaceBlock extends MultifaceBlock { + public FaceBlock(Properties properties) { + super(properties); + } + + @Override + public boolean isValidStateForPlacement(BlockGetter level, BlockState state, BlockPos pos, Direction direction) { + if (this.isFaceSupported(direction) && !state.is(this)) { + BlockPos blockPos = pos.relative(direction); + return canAttachTo(level, direction, blockPos, level.getBlockState(blockPos)); + } else { + return false; + } + } +} diff --git a/src/main/java/net/frozenblock/lib/block/api/FaceClusterBlock.java b/src/main/java/net/frozenblock/lib/block/api/FaceClusterBlock.java new file mode 100644 index 0000000..cdfdac3 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/block/api/FaceClusterBlock.java @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.block.api; + +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.world.level.BlockGetter; +import net.minecraft.world.level.block.state.BlockState; + +/** + * A {@link MultifaceClusterBlock}, but only one face is permitted. + */ +public abstract class FaceClusterBlock extends MultifaceClusterBlock { + public FaceClusterBlock(int height, int xzOffset, Properties properties) { + super(height, xzOffset, properties); + } + + @Override + public boolean isValidStateForPlacement(BlockGetter level, BlockState state, BlockPos pos, Direction direction) { + if (this.isFaceSupported(direction) && !state.is(this)) { + BlockPos blockPos = pos.relative(direction); + return canAttachTo(level, direction, blockPos, level.getBlockState(blockPos)); + } else { + return false; + } + } +} diff --git a/src/main/java/net/frozenblock/lib/block/api/FrozenCeilingHangingSignBlock.java b/src/main/java/net/frozenblock/lib/block/api/FrozenCeilingHangingSignBlock.java new file mode 100644 index 0000000..2462020 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/block/api/FrozenCeilingHangingSignBlock.java @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.block.api; + +import net.minecraft.resources.ResourceKey; +import net.minecraft.world.level.block.CeilingHangingSignBlock; +import net.minecraft.world.level.block.state.properties.WoodType; +import net.minecraft.world.level.storage.loot.LootTable; + +import java.util.Objects; + +public class FrozenCeilingHangingSignBlock extends CeilingHangingSignBlock implements LootTableInjection { + + public final ResourceKey lootTable; + + public FrozenCeilingHangingSignBlock(Properties settings, WoodType signType, ResourceKey lootTable) { + super(signType, settings); + this.lootTable = lootTable; + } + + + public ResourceKey getLootTableForInject() { + if (!Objects.equals(this.drops, this.lootTable)) { + this.drops = this.lootTable; + } + + assert this.drops != null; + return this.drops; + } +} diff --git a/src/main/java/net/frozenblock/lib/block/api/FrozenSignBlock.java b/src/main/java/net/frozenblock/lib/block/api/FrozenSignBlock.java new file mode 100644 index 0000000..6de4fdd --- /dev/null +++ b/src/main/java/net/frozenblock/lib/block/api/FrozenSignBlock.java @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.block.api; + +import net.minecraft.resources.ResourceKey; +import net.minecraft.world.level.block.StandingSignBlock; +import net.minecraft.world.level.block.state.properties.WoodType; +import net.minecraft.world.level.storage.loot.LootTable; + +import java.util.Objects; + +public class FrozenSignBlock extends StandingSignBlock implements LootTableInjection { + public final ResourceKey lootTable; + + public FrozenSignBlock(Properties settings, WoodType signType, ResourceKey lootTable) { + super(signType, settings); + this.lootTable = lootTable; + } + + public ResourceKey getLootTableForInject() { + if (!Objects.equals(this.drops, this.lootTable)) { + this.drops = this.lootTable; + } + + assert this.drops != null; + return this.drops; + } +} diff --git a/src/main/java/net/frozenblock/lib/block/api/FrozenWallHangingSignBlock.java b/src/main/java/net/frozenblock/lib/block/api/FrozenWallHangingSignBlock.java new file mode 100644 index 0000000..a3adad4 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/block/api/FrozenWallHangingSignBlock.java @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.block.api; + +import net.minecraft.resources.ResourceKey; +import net.minecraft.world.level.block.WallHangingSignBlock; +import net.minecraft.world.level.block.state.properties.WoodType; +import net.minecraft.world.level.storage.loot.LootTable; + +import java.util.Objects; + +public class FrozenWallHangingSignBlock extends WallHangingSignBlock implements LootTableInjection { + + public final ResourceKey lootTable; + + public FrozenWallHangingSignBlock(Properties settings, WoodType signType, ResourceKey lootTable) { + super(signType, settings); + this.lootTable = lootTable; + } + + public ResourceKey getLootTableForInject() { + if (!Objects.equals(this.drops, this.lootTable)) { + this.drops = this.lootTable; + } + + assert this.drops != null; + return this.drops; + } +} diff --git a/src/main/java/net/frozenblock/lib/block/api/FrozenWallSignBlock.java b/src/main/java/net/frozenblock/lib/block/api/FrozenWallSignBlock.java new file mode 100644 index 0000000..04847bf --- /dev/null +++ b/src/main/java/net/frozenblock/lib/block/api/FrozenWallSignBlock.java @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.block.api; + +import net.minecraft.resources.ResourceKey; +import net.minecraft.world.level.block.WallSignBlock; +import net.minecraft.world.level.block.state.properties.WoodType; +import net.minecraft.world.level.storage.loot.LootTable; + +import java.util.Objects; + +public class FrozenWallSignBlock extends WallSignBlock implements LootTableInjection{ + public final ResourceKey lootTable; + + public FrozenWallSignBlock(Properties settings, WoodType signType, ResourceKey lootTable) { + super(signType, settings); + this.lootTable = lootTable; + } + + @Override + public ResourceKey getLootTableForInject() { + if (!Objects.equals(this.drops, this.lootTable)) { + this.drops = this.lootTable; + } + + assert this.drops != null; + return this.drops; + } +} diff --git a/src/main/java/net/frozenblock/lib/block/api/LootTableInjection.java b/src/main/java/net/frozenblock/lib/block/api/LootTableInjection.java new file mode 100644 index 0000000..fb675b7 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/block/api/LootTableInjection.java @@ -0,0 +1,8 @@ +package net.frozenblock.lib.block.api; + +import net.minecraft.resources.ResourceKey; +import net.minecraft.world.level.storage.loot.LootTable; + +public interface LootTableInjection { + ResourceKey getLootTableForInject(); +} diff --git a/src/main/java/net/frozenblock/lib/block/api/MultifaceClusterBlock.java b/src/main/java/net/frozenblock/lib/block/api/MultifaceClusterBlock.java new file mode 100644 index 0000000..149452a --- /dev/null +++ b/src/main/java/net/frozenblock/lib/block/api/MultifaceClusterBlock.java @@ -0,0 +1,127 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.block.api; + +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Maps; +import net.minecraft.Util; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.world.level.BlockGetter; +import net.minecraft.world.level.LevelAccessor; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.MultifaceBlock; +import net.minecraft.world.level.block.SimpleWaterloggedBlock; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.block.state.StateDefinition; +import net.minecraft.world.level.block.state.properties.BlockStateProperties; +import net.minecraft.world.level.block.state.properties.BooleanProperty; +import net.minecraft.world.level.material.FluidState; +import net.minecraft.world.level.material.Fluids; +import net.minecraft.world.level.material.PushReaction; +import net.minecraft.world.phys.shapes.CollisionContext; +import net.minecraft.world.phys.shapes.Shapes; +import net.minecraft.world.phys.shapes.VoxelShape; +import org.jetbrains.annotations.NotNull; + +import java.util.Map; +import java.util.Objects; + +/** + * A block that combines an amethyst cluster-type block with a multiface block. + */ +public abstract class MultifaceClusterBlock extends MultifaceBlock implements SimpleWaterloggedBlock { + public static final BooleanProperty WATERLOGGED = BlockStateProperties.WATERLOGGED; + public static final BooleanProperty UP = BlockStateProperties.UP; + + protected final VoxelShape northAabb; + protected final VoxelShape southAabb; + protected final VoxelShape eastAabb; + protected final VoxelShape westAabb; + protected final VoxelShape upAabb; + protected final VoxelShape downAabb; + + private final Map shapeByDirection; + private final ImmutableMap shapesCache; + + + public MultifaceClusterBlock(int height, int xzOffset, Properties properties) { + super(properties.pushReaction(PushReaction.DESTROY)); + this.registerDefaultState(this.defaultBlockState().setValue(WATERLOGGED, false)); + this.upAabb = Block.box(xzOffset, 0.0, xzOffset, 16 - xzOffset, height, (16 - xzOffset)); + this.downAabb = Block.box(xzOffset, (16 - height), xzOffset, (16 - xzOffset), 16.0, (16 - xzOffset)); + this.northAabb = Block.box(xzOffset, xzOffset, (16 - height), (16 - xzOffset), (16 - xzOffset), 16.0); + this.southAabb = Block.box(xzOffset, xzOffset, 0.0, (16 - xzOffset), (16 - xzOffset), height); + this.eastAabb = Block.box(0.0, xzOffset, xzOffset, height, (16 - xzOffset), (16 - xzOffset)); + this.westAabb = Block.box((16 - height), xzOffset, xzOffset, 16.0, (16 - xzOffset), (16 - xzOffset)); + this.shapeByDirection = Util.make(Maps.newEnumMap(Direction.class), shapes -> { + shapes.put(Direction.NORTH, this.southAabb); + shapes.put(Direction.EAST, this.westAabb); + shapes.put(Direction.SOUTH, this.northAabb); + shapes.put(Direction.WEST, this.eastAabb); + shapes.put(Direction.UP, this.downAabb); + shapes.put(Direction.DOWN, this.upAabb); + }); + this.shapesCache = this.getShapeForEachState(this::calculateMultifaceShape); + } + + @Override + @NotNull + public VoxelShape getShape(@NotNull BlockState state, @NotNull BlockGetter world, @NotNull BlockPos pos, @NotNull CollisionContext context) { + return Objects.requireNonNull(this.shapesCache.get(state)); + } + + public VoxelShape calculateMultifaceShape(BlockState state) { + VoxelShape voxelShape = Shapes.empty(); + + for(Direction direction : DIRECTIONS) { + if (hasFace(state, direction)) { + voxelShape = Shapes.or(voxelShape, this.shapeByDirection.get(direction)); + } + } + + return voxelShape.isEmpty() ? Shapes.block() : voxelShape; + } + + @Override + @NotNull + public BlockState updateShape(BlockState state, @NotNull Direction direction, @NotNull BlockState neighborState, @NotNull LevelAccessor world, @NotNull BlockPos pos, @NotNull BlockPos neighborPos) { + if (state.getValue(WATERLOGGED)) { + world.scheduleTick(pos, Fluids.WATER, Fluids.WATER.getTickDelay(world)); + } + + return super.updateShape(state, direction, neighborState, world, pos, neighborPos); + } + + @Override + @NotNull + public FluidState getFluidState(BlockState state) { + return state.getValue(WATERLOGGED) ? Fluids.WATER.getSource(false) : super.getFluidState(state); + } + + @Override + protected void createBlockStateDefinition(StateDefinition.@NotNull Builder builder) { + super.createBlockStateDefinition(builder); + builder.add(WATERLOGGED); + } + + @Override + public boolean propagatesSkylightDown(BlockState blockState, BlockGetter blockGetter, BlockPos blockPos) { + return blockState.getFluidState().isEmpty(); + } +} diff --git a/src/main/java/net/frozenblock/lib/block/api/WaterloggableSaplingBlock.java b/src/main/java/net/frozenblock/lib/block/api/WaterloggableSaplingBlock.java new file mode 100644 index 0000000..5b6b357 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/block/api/WaterloggableSaplingBlock.java @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.block.api; + +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.world.item.context.BlockPlaceContext; +import net.minecraft.world.level.BlockGetter; +import net.minecraft.world.level.LevelAccessor; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.SaplingBlock; +import net.minecraft.world.level.block.SimpleWaterloggedBlock; +import net.minecraft.world.level.block.grower.TreeGrower; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.block.state.StateDefinition; +import net.minecraft.world.level.block.state.properties.BlockStateProperties; +import net.minecraft.world.level.block.state.properties.BooleanProperty; +import net.minecraft.world.level.material.FluidState; +import net.minecraft.world.level.material.Fluids; +import org.jetbrains.annotations.Nullable; + +import java.util.Objects; + +public class WaterloggableSaplingBlock extends SaplingBlock implements SimpleWaterloggedBlock { + private static final BooleanProperty WATERLOGGED = BlockStateProperties.WATERLOGGED; + + public WaterloggableSaplingBlock(TreeGrower generator, Properties settings) { + super(generator, settings); + this.registerDefaultState(this.stateDefinition.any().setValue(STAGE, 0).setValue(WATERLOGGED, false)); + } + + @Override + protected void createBlockStateDefinition(StateDefinition.Builder builder) { + builder.add(STAGE).add(WATERLOGGED); + } + + @Override + protected boolean mayPlaceOn(BlockState floor, BlockGetter world, BlockPos pos) { + return super.mayPlaceOn(floor, world, pos) || floor.is(Blocks.CLAY) || floor.is(Blocks.MUD) || floor.is(Blocks.SAND); + } + + @Override + @Nullable + public BlockState getStateForPlacement(BlockPlaceContext ctx) { + FluidState fluidState = ctx.getLevel().getFluidState(ctx.getClickedPos()); + boolean bl = fluidState.getType() == Fluids.WATER; + return Objects.requireNonNull(super.getStateForPlacement(ctx)).setValue(WATERLOGGED, bl); + } + + @Override + public BlockState updateShape(BlockState state, Direction direction, BlockState neighborState, LevelAccessor world, BlockPos pos, BlockPos neighborPos) { + if (state.getValue(WATERLOGGED)) { + world.scheduleTick(pos, Fluids.WATER, Fluids.WATER.getTickDelay(world)); + } + + return direction == Direction.UP && !state.canSurvive(world, pos) ? Blocks.AIR.defaultBlockState() : super.updateShape(state, direction, neighborState, world, pos, neighborPos); + } + + @Override + public FluidState getFluidState(BlockState state) { + return state.getValue(WATERLOGGED) ? Fluids.WATER.getSource(false) : super.getFluidState(state); + } +} diff --git a/src/main/java/net/frozenblock/lib/block/api/WaterloggableTallFlowerBlock.java b/src/main/java/net/frozenblock/lib/block/api/WaterloggableTallFlowerBlock.java new file mode 100644 index 0000000..ebf5dfb --- /dev/null +++ b/src/main/java/net/frozenblock/lib/block/api/WaterloggableTallFlowerBlock.java @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.block.api; + +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.world.item.context.BlockPlaceContext; +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.SimpleWaterloggedBlock; +import net.minecraft.world.level.block.TallFlowerBlock; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.block.state.StateDefinition; +import net.minecraft.world.level.block.state.properties.BlockStateProperties; +import net.minecraft.world.level.block.state.properties.BooleanProperty; +import net.minecraft.world.level.material.FluidState; +import net.minecraft.world.level.material.Fluids; +import org.jetbrains.annotations.Nullable; + +public class WaterloggableTallFlowerBlock extends TallFlowerBlock implements SimpleWaterloggedBlock { + public static final BooleanProperty WATERLOGGED = BlockStateProperties.WATERLOGGED; + + public WaterloggableTallFlowerBlock(Properties settings) { + super(settings); + this.registerDefaultState(this.defaultBlockState().setValue(WATERLOGGED, false)); + } + + @Override + public BlockState updateShape(BlockState state, Direction direction, BlockState neighborState, LevelAccessor world, BlockPos pos, BlockPos neighborPos) { + if (state.getValue(WATERLOGGED)) { + world.scheduleTick(pos, Fluids.WATER, Fluids.WATER.getTickDelay(world)); + } + return super.updateShape(state, direction, neighborState, world, pos, neighborPos); + } + + @Nullable + public BlockState getStateForPlacement(BlockPlaceContext ctx) { + BlockPos blockPos = ctx.getClickedPos(); + Level world = ctx.getLevel(); + FluidState fluidState = world.getFluidState(blockPos); + return blockPos.getY() < world.getMaxBuildHeight() - 1 && world.getBlockState(blockPos.above()).canBeReplaced(ctx) ? + this.defaultBlockState().setValue(WATERLOGGED, fluidState.getType() == Fluids.WATER) : null; + } + + @Override + public FluidState getFluidState(BlockState state) { + return state.getValue(WATERLOGGED) ? Fluids.WATER.getSource(false) : super.getFluidState(state); + } + + @Override + protected void createBlockStateDefinition(StateDefinition.Builder builder) { + super.createBlockStateDefinition(builder); + builder.add(WATERLOGGED); + } +} \ No newline at end of file diff --git a/src/main/java/net/frozenblock/lib/block/api/dripstone/DripstoneDripLavaFrom.java b/src/main/java/net/frozenblock/lib/block/api/dripstone/DripstoneDripLavaFrom.java new file mode 100644 index 0000000..8931b49 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/block/api/dripstone/DripstoneDripLavaFrom.java @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.block.api.dripstone; + +import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; +import net.minecraft.core.BlockPos; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.PointedDripstoneBlock; + +import java.util.Map; + +public class DripstoneDripLavaFrom { + + public static final Map ON_DRIP_BLOCK = new Object2ObjectOpenHashMap<>(); + + @FunctionalInterface + public interface InjectedOnDrip { + void drip(ServerLevel world, PointedDripstoneBlock.FluidInfo fluidInfo, BlockPos pos); + } + +} diff --git a/src/main/java/net/frozenblock/lib/block/api/dripstone/DripstoneDripWaterFrom.java b/src/main/java/net/frozenblock/lib/block/api/dripstone/DripstoneDripWaterFrom.java new file mode 100644 index 0000000..c11e6d7 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/block/api/dripstone/DripstoneDripWaterFrom.java @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.block.api.dripstone; + +import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; +import net.minecraft.core.BlockPos; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.PointedDripstoneBlock; + +import java.util.Map; + +public class DripstoneDripWaterFrom { + + public static final Map ON_DRIP_BLOCK = new Object2ObjectOpenHashMap<>(); + + @FunctionalInterface + public interface InjectedOnDrip { + void drip(ServerLevel world, PointedDripstoneBlock.FluidInfo fluidInfo, BlockPos pos); + } + +} diff --git a/src/main/java/net/frozenblock/lib/block/api/dripstone/DripstoneUtils.java b/src/main/java/net/frozenblock/lib/block/api/dripstone/DripstoneUtils.java new file mode 100644 index 0000000..1ac4163 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/block/api/dripstone/DripstoneUtils.java @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.block.api.dripstone; + +import net.minecraft.core.BlockPos; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.level.block.PointedDripstoneBlock; +import net.minecraft.world.level.material.Fluid; +import net.minecraft.world.level.material.Fluids; + +public class DripstoneUtils { + + public static Fluid getDripstoneFluid(ServerLevel level, BlockPos pos) { + BlockPos blockPos = PointedDripstoneBlock.findStalactiteTipAboveCauldron(level, pos); + if (blockPos == null) { + return Fluids.EMPTY; + } + return PointedDripstoneBlock.getCauldronFillFluidType(level, blockPos); + } + +} diff --git a/src/main/java/net/frozenblock/lib/block/api/entity/BillboardBlockEntityRenderer.java b/src/main/java/net/frozenblock/lib/block/api/entity/BillboardBlockEntityRenderer.java new file mode 100644 index 0000000..5128041 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/block/api/entity/BillboardBlockEntityRenderer.java @@ -0,0 +1,73 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.block.api.entity; + +import com.mojang.blaze3d.vertex.PoseStack; +import com.mojang.math.Axis; +import net.minecraft.client.Minecraft; +import net.minecraft.client.model.geom.ModelPart; +import net.minecraft.client.model.geom.PartPose; +import net.minecraft.client.model.geom.builders.CubeListBuilder; +import net.minecraft.client.model.geom.builders.LayerDefinition; +import net.minecraft.client.model.geom.builders.MeshDefinition; +import net.minecraft.client.model.geom.builders.PartDefinition; +import net.minecraft.client.renderer.MultiBufferSource; +import net.minecraft.client.renderer.RenderType; +import net.minecraft.client.renderer.blockentity.BlockEntityRenderer; +import net.minecraft.client.renderer.blockentity.BlockEntityRendererProvider.Context; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.neoforged.api.distmarker.Dist; +import net.neoforged.api.distmarker.OnlyIn; +import org.jetbrains.annotations.NotNull; +import org.joml.Quaternionf; + +@OnlyIn(Dist.CLIENT) +public abstract class BillboardBlockEntityRenderer implements BlockEntityRenderer { + private final ModelPart base; + + public BillboardBlockEntityRenderer(Context ctx) { + ModelPart root = this.getRoot(ctx); + this.base = root.getChild("base"); + } + + @NotNull + public static LayerDefinition getTexturedModelData() { + MeshDefinition modelData = new MeshDefinition(); + PartDefinition modelPartData = modelData.getRoot(); + modelPartData.addOrReplaceChild("base", CubeListBuilder.create().texOffs(0, 0).addBox(-8.0F, -16.0F, 0.0F, 16.0F, 16.0F, 0.0F), PartPose.offsetAndRotation(0.0F, 0.0F, 0.0F, (float) Math.PI, 0.0F, 0.0F)); + return LayerDefinition.create(modelData, 16, 16); + } + + private final Quaternionf rotation = new Quaternionf(0F, 0F, 0F, 1F); + + @Override + public void render(@NotNull T entity, float tickDelta, @NotNull PoseStack poseStack, @NotNull MultiBufferSource vertexConsumers, int light, int overlay) { + this.rotation.set(0.0f, 0.0f, 0.0f, 1.0f); + this.rotation.mul(Axis.YP.rotationDegrees(-Minecraft.getInstance().gameRenderer.getMainCamera().yRot)); + poseStack.translate(0.5, 0, 0.5); + poseStack.pushPose(); + poseStack.mulPose(this.rotation); + this.base.render(poseStack, vertexConsumers.getBuffer(RenderType.entityCutout(this.getTexture(entity))), light, overlay); + poseStack.popPose(); + } + + public abstract ResourceLocation getTexture(T entity); + + public abstract ModelPart getRoot(Context ctx); +} diff --git a/src/main/java/net/frozenblock/lib/block/api/entity/BlockEntityWithoutLevelRendererRegistry.java b/src/main/java/net/frozenblock/lib/block/api/entity/BlockEntityWithoutLevelRendererRegistry.java new file mode 100644 index 0000000..a6accef --- /dev/null +++ b/src/main/java/net/frozenblock/lib/block/api/entity/BlockEntityWithoutLevelRendererRegistry.java @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.block.api.entity; + +import it.unimi.dsi.fastutil.objects.Object2ObjectLinkedOpenHashMap; +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.entity.BlockEntityType; +import org.jetbrains.annotations.NotNull; + +import java.util.Optional; + +public class BlockEntityWithoutLevelRendererRegistry { + private static final Object2ObjectLinkedOpenHashMap BLOCK_TO_BLOCK_ENTITY_MAP = new Object2ObjectLinkedOpenHashMap<>(); + + public static void register(Block block, @NotNull BlockEntityType blockEntityType) { + BLOCK_TO_BLOCK_ENTITY_MAP.put(block, blockEntityType.create(BlockPos.ZERO, block.defaultBlockState())); + } + + public static Optional getBlockEntity(Block block) { + return Optional.ofNullable(BLOCK_TO_BLOCK_ENTITY_MAP.get(block)); + } + + public static boolean hasBlock(Block block) { + return BLOCK_TO_BLOCK_ENTITY_MAP.containsKey(block); + } + +} diff --git a/src/main/java/net/frozenblock/lib/block/api/shape/FrozenShapes.java b/src/main/java/net/frozenblock/lib/block/api/shape/FrozenShapes.java new file mode 100644 index 0000000..b787204 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/block/api/shape/FrozenShapes.java @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.block.api.shape; + +import com.google.common.collect.Maps; +import net.minecraft.Util; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.util.Mth; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.phys.Vec3; +import net.minecraft.world.phys.shapes.VoxelShape; +import org.jetbrains.annotations.NotNull; + +import java.util.Map; +import java.util.Optional; + +public class FrozenShapes { + private static final VoxelShape UP_PLANE = Block.box(0.0, 15.0, 0.0, 16.0, 16.0, 16.0); + private static final VoxelShape DOWN_PLANE = Block.box(0.0, 0.0, 0.0, 16.0, 1.0, 16.0); + private static final VoxelShape WEST_PLANE = Block.box(0.0, 0.0, 0.0, 1.0, 16.0, 16.0); + private static final VoxelShape EAST_PLANE = Block.box(15.0, 0.0, 0.0, 16.0, 16.0, 16.0); + private static final VoxelShape NORTH_PLANE = Block.box(0.0, 0.0, 0.0, 16.0, 16.0, 1.0); + private static final VoxelShape SOUTH_PLANE = Block.box(0.0, 0.0, 15.0, 16.0, 16.0, 16.0); + + public static final Map PLANE_BY_DIRECTION = Util.make(Maps.newEnumMap(Direction.class), shapes -> { + shapes.put(Direction.NORTH, NORTH_PLANE); + shapes.put(Direction.EAST, EAST_PLANE); + shapes.put(Direction.SOUTH, SOUTH_PLANE); + shapes.put(Direction.WEST, WEST_PLANE); + shapes.put(Direction.UP, UP_PLANE); + shapes.put(Direction.DOWN, DOWN_PLANE); + }); + + @NotNull + public static VoxelShape makePlaneFromDirection(@NotNull Direction direction, float fromSide) { + double minX = direction.equals(Direction.EAST) ? 16F - fromSide : 0F; + double minY = direction.equals(Direction.UP) ? 16F - fromSide : 0F; + double minZ = direction.equals(Direction.SOUTH) ? 16F - fromSide : 0F; + double maxX = direction.equals(Direction.WEST) ? 0F + fromSide : 16F; + double maxY = direction.equals(Direction.DOWN) ? 0F + fromSide : 16F; + double maxZ = direction.equals(Direction.NORTH) ? 0F + fromSide : 16F; + return Block.box(minX, minY, minZ, maxX, maxY, maxZ); + } + + public static Optional closestPointTo(BlockPos originalPos, @NotNull VoxelShape blockShape, Vec3 point) { + if (blockShape.isEmpty()) { + return Optional.empty(); + } + double x = originalPos.getX(); + double y = originalPos.getY(); + double z = originalPos.getZ(); + Vec3[] vec3s = new Vec3[1]; + blockShape.forAllBoxes((minX, minY, minZ, maxX, maxY, maxZ) -> { + double d = Mth.clamp(point.x(), minX + x, maxX + x); + double e = Mth.clamp(point.y(), minY + y, maxY + y); + double f = Mth.clamp(point.z(), minZ + z, maxZ + z); + if (vec3s[0] == null || point.distanceToSqr(d, e, f) < point.distanceToSqr(vec3s[0])) { + vec3s[0] = new Vec3(d, e, f); + } + }); + return Optional.of(vec3s[0]); + } +} diff --git a/src/main/java/net/frozenblock/lib/block/api/tick/BlockScheduledTicks.java b/src/main/java/net/frozenblock/lib/block/api/tick/BlockScheduledTicks.java new file mode 100644 index 0000000..e3d2773 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/block/api/tick/BlockScheduledTicks.java @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.block.api.tick; + +import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; +import net.minecraft.core.BlockPos; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.util.RandomSource; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.state.BlockState; + +import java.util.Map; + +public class BlockScheduledTicks { + + public static final Map TICKS = new Object2ObjectOpenHashMap<>(); + + @FunctionalInterface + public interface InjectedScheduledTick { + void tick(BlockState state, ServerLevel world, BlockPos pos, RandomSource random); + } + +} diff --git a/src/main/java/net/frozenblock/lib/block/mixin/AbstractCauldronBlockAccessor.java b/src/main/java/net/frozenblock/lib/block/mixin/AbstractCauldronBlockAccessor.java new file mode 100644 index 0000000..e07d45b --- /dev/null +++ b/src/main/java/net/frozenblock/lib/block/mixin/AbstractCauldronBlockAccessor.java @@ -0,0 +1,14 @@ +package net.frozenblock.lib.block.mixin; + +import net.minecraft.world.level.block.AbstractCauldronBlock; +import net.minecraft.world.level.material.Fluid; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Invoker; + +@Mixin(AbstractCauldronBlock.class) +public interface AbstractCauldronBlockAccessor { + + @Invoker("canReceiveStalactiteDrip") + public boolean invokeCanReceiveStalactiteDrip(Fluid fluid); + +} diff --git a/src/main/java/net/frozenblock/lib/block/mixin/BlockBehaviourMixin.java b/src/main/java/net/frozenblock/lib/block/mixin/BlockBehaviourMixin.java new file mode 100644 index 0000000..16c7db8 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/block/mixin/BlockBehaviourMixin.java @@ -0,0 +1,21 @@ +package net.frozenblock.lib.block.mixin; + +import net.frozenblock.lib.block.api.LootTableInjection; +import net.minecraft.resources.ResourceKey; +import net.minecraft.world.level.block.state.BlockBehaviour; +import net.minecraft.world.level.storage.loot.LootTable; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +@Mixin(BlockBehaviour.class) +public class BlockBehaviourMixin { + + @Inject(at = @At("HEAD"), method = "getLootTable", cancellable = true) + public void inject(CallbackInfoReturnable> cir) { + if(((BlockBehaviour)(Object)this) instanceof LootTableInjection that) { + cir.setReturnValue(that.getLootTableForInject()); + } + } +} diff --git a/src/main/java/net/frozenblock/lib/block/mixin/dripstone/PointedDripstoneBlockMixin.java b/src/main/java/net/frozenblock/lib/block/mixin/dripstone/PointedDripstoneBlockMixin.java new file mode 100644 index 0000000..fb8604c --- /dev/null +++ b/src/main/java/net/frozenblock/lib/block/mixin/dripstone/PointedDripstoneBlockMixin.java @@ -0,0 +1,166 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.block.mixin.dripstone; + +import com.llamalad7.mixinextras.sugar.Local; +import net.frozenblock.lib.FrozenBools; +import net.frozenblock.lib.block.api.dripstone.DripstoneDripLavaFrom; +import net.frozenblock.lib.block.api.dripstone.DripstoneDripWaterFrom; +import net.frozenblock.lib.block.mixin.AbstractCauldronBlockAccessor; +import net.frozenblock.lib.tag.api.FrozenBlockTags; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.level.BlockGetter; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.LevelAccessor; +import net.minecraft.world.level.block.AbstractCauldronBlock; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.PointedDripstoneBlock; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.material.Fluid; +import net.minecraft.world.level.material.Fluids; +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; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +import java.util.Optional; +import java.util.function.BiPredicate; +import java.util.function.Predicate; + +@Mixin(PointedDripstoneBlock.class) +public class PointedDripstoneBlockMixin { + + @SuppressWarnings("UnresolvedMixinReference") + @Inject( + method = {"m_ulptarvl", "method_33279", "lambda$getFluidAboveStalactite$11"}, + at = @At( + value = "INVOKE_ASSIGN", + target = "Lnet/minecraft/world/level/block/state/BlockState;is(Lnet/minecraft/world/level/block/Block;)Z", + shift = At.Shift.BEFORE + ), + cancellable = true, + require = 1 + ) + private static void frozenLib$getFluidAboveStalactite( + Level level, BlockPos pos, CallbackInfoReturnable info, + @Local(ordinal = 1) BlockPos blockPos, @Local BlockState blockState + ) { + if (!FrozenBools.useNewDripstoneLiquid && blockPos != null) { + if (DripstoneDripWaterFrom.ON_DRIP_BLOCK.containsKey(blockState.getBlock()) && !level.dimensionType().ultraWarm()) { + info.setReturnValue(new PointedDripstoneBlock.FluidInfo(blockPos, Fluids.WATER, blockState)); + } else if (DripstoneDripLavaFrom.ON_DRIP_BLOCK.containsKey(blockState.getBlock())) { + info.setReturnValue(new PointedDripstoneBlock.FluidInfo(blockPos, Fluids.LAVA, blockState)); + } + } + } + + @Inject(at = @At("HEAD"), method = "getFluidAboveStalactite", cancellable = true) + private static void getFluidAboveStalactite(Level level, BlockPos pos, BlockState state, CallbackInfoReturnable> info) { + if (FrozenBools.useNewDripstoneLiquid) { + info.setReturnValue( + !isStalactite(state) ? Optional.empty() : findRootBlock(level, pos, state, 11).map((posx) -> { + + BlockState firstState = level.getBlockState(posx); + if (DripstoneDripWaterFrom.ON_DRIP_BLOCK.containsKey(firstState.getBlock()) && !level.dimensionType().ultraWarm()) { + return new PointedDripstoneBlock.FluidInfo(posx, Fluids.WATER, firstState); + } else if (DripstoneDripLavaFrom.ON_DRIP_BLOCK.containsKey(firstState.getBlock())) { + return new PointedDripstoneBlock.FluidInfo(posx, Fluids.LAVA, firstState); + } + BlockPos blockPos = posx.above(); + BlockState blockState = level.getBlockState(blockPos); + Fluid fluid; + if (DripstoneDripWaterFrom.ON_DRIP_BLOCK.containsKey(blockState.getBlock()) && !level.dimensionType().ultraWarm()) { + return new PointedDripstoneBlock.FluidInfo(blockPos, Fluids.WATER, blockState); + } else if (DripstoneDripLavaFrom.ON_DRIP_BLOCK.containsKey(blockState.getBlock())) { + return new PointedDripstoneBlock.FluidInfo(blockPos, Fluids.LAVA, blockState); + } else { + fluid = level.getFluidState(blockPos).getType(); + } + + return new PointedDripstoneBlock.FluidInfo(blockPos, fluid, blockState); + }) + ); + } + } + + @Inject( + method = "maybeTransferFluid", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/world/level/block/state/BlockState;is(Lnet/minecraft/world/level/block/Block;)Z", + shift = At.Shift.BEFORE + ), + cancellable = true + ) + private static void frozenLib$maybeTransferFluid( + BlockState state, ServerLevel level, BlockPos pos, float randChance, CallbackInfo info, + @Local Optional optional, @Local Fluid fluid, @Local(ordinal = 1) BlockPos blockPos + ) { + if (optional.isPresent()) { + PointedDripstoneBlock.FluidInfo fluidInfo = optional.get(); + Block block = optional.get().sourceState().getBlock(); + if (DripstoneDripWaterFrom.ON_DRIP_BLOCK.containsKey(block) && fluid == Fluids.WATER) { + DripstoneDripWaterFrom.ON_DRIP_BLOCK.get(block).drip(level, fluidInfo, blockPos); + info.cancel(); + } + if (DripstoneDripLavaFrom.ON_DRIP_BLOCK.containsKey(block) && fluid == Fluids.LAVA) { + DripstoneDripLavaFrom.ON_DRIP_BLOCK.get(block).drip(level, fluidInfo, blockPos); + info.cancel(); + } + } + } + + @Inject(at = @At("HEAD"), method = "findFillableCauldronBelowStalactiteTip", cancellable = true) + private static void frozenLib$findFillableCauldronBelowStalactiteTip(Level world, BlockPos pos2, Fluid fluid, CallbackInfoReturnable info) { + Predicate tagPredicate = state -> state.is(FrozenBlockTags.DRIPSTONE_CAN_DRIP_ON); + if (tagPredicate.test(world.getBlockState(pos2.mutable().move(Direction.get(Direction.DOWN.getAxisDirection(), Direction.Axis.Y))))) { + Predicate predicate = tagPredicate.or(state -> ( + state.getBlock() instanceof AbstractCauldronBlock && + ((AbstractCauldronBlockAccessor) state.getBlock()).invokeCanReceiveStalactiteDrip(fluid))); + + BiPredicate biPredicate = (pos, state) -> canDripThrough(world, pos, state); + info.setReturnValue(findBlockVertical(world, pos2, Direction.DOWN.getAxisDirection(), biPredicate, predicate, 11).orElse(null)); + } + + } + + @Shadow + private static boolean canDripThrough(BlockGetter world, BlockPos pos, BlockState state) { + throw new AssertionError("Mixin injection failed - FrozenLib PointedDripstoneBlockMixin."); + } + + @Shadow + private static Optional findBlockVertical(LevelAccessor world, BlockPos pos, Direction.AxisDirection direction, BiPredicate continuePredicate, Predicate stopPredicate, int range) { + throw new AssertionError("Mixin injection failed - FrozenLib PointedDripstoneBlockMixin."); + } + + @Shadow + private static boolean isStalactite(BlockState state) { + throw new AssertionError("Mixin injection failed - FrozenLib PointedDripstoneBlockMixin."); + } + + @Shadow + private static Optional findRootBlock(Level level, BlockPos pos, BlockState state, int maxIterations) { + throw new AssertionError("Mixin injection failed - FrozenLib PointedDripstoneBlockMixin."); + } + +} diff --git a/src/main/java/net/frozenblock/lib/block/mixin/entity/client/BlockEntityWithoutLevelRendererMixin.java b/src/main/java/net/frozenblock/lib/block/mixin/entity/client/BlockEntityWithoutLevelRendererMixin.java new file mode 100644 index 0000000..4ea703d --- /dev/null +++ b/src/main/java/net/frozenblock/lib/block/mixin/entity/client/BlockEntityWithoutLevelRendererMixin.java @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.block.mixin.entity.client; + +import com.llamalad7.mixinextras.injector.wrapoperation.Operation; +import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; +import com.llamalad7.mixinextras.sugar.Share; +import com.llamalad7.mixinextras.sugar.ref.LocalRef; +import com.mojang.blaze3d.vertex.PoseStack; +import net.frozenblock.lib.block.api.entity.BlockEntityWithoutLevelRendererRegistry; +import net.minecraft.client.renderer.BlockEntityWithoutLevelRenderer; +import net.minecraft.client.renderer.MultiBufferSource; +import net.minecraft.client.renderer.blockentity.BlockEntityRenderDispatcher; +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.neoforged.api.distmarker.Dist; +import net.neoforged.api.distmarker.OnlyIn; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Slice; + +@OnlyIn(Dist.CLIENT) +@Mixin(BlockEntityWithoutLevelRenderer.class) +public class BlockEntityWithoutLevelRendererMixin { + + @WrapOperation( + method = "renderByItem", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/world/level/block/state/BlockState;is(Lnet/minecraft/world/level/block/Block;)Z", + ordinal = 0 + ), + slice = @Slice( + from = @At( + value = "FIELD", + target = "Lnet/minecraft/world/level/block/Blocks;CONDUIT:Lnet/minecraft/world/level/block/Block;" + ) + ) + ) + public boolean frozenLib$selectBlockEntity( + BlockState instance, Block block, Operation original, + @Share("frozenLib$block") LocalRef customBlock + ) { + Block usedBlock = instance.getBlock(); + customBlock.set(usedBlock); + return original.call(instance, block) || BlockEntityWithoutLevelRendererRegistry.hasBlock(usedBlock); + } + + @WrapOperation( + method = "renderByItem", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/client/renderer/blockentity/BlockEntityRenderDispatcher;renderItem(Lnet/minecraft/world/level/block/entity/BlockEntity;Lcom/mojang/blaze3d/vertex/PoseStack;Lnet/minecraft/client/renderer/MultiBufferSource;II)Z", + ordinal = 0 + ) + ) + public boolean frozenLib$replaceWithNewBlockEntity( + BlockEntityRenderDispatcher instance, BlockEntity blockEntity, PoseStack poseStack, MultiBufferSource bufferSource, int packedLight, int packedOverlay, Operation original, + @Share("frozenLib$block") LocalRef customBlock + ) { + blockEntity = BlockEntityWithoutLevelRendererRegistry.getBlockEntity(customBlock.get()).orElse(blockEntity); + return original.call(instance, blockEntity, poseStack, bufferSource, packedLight, packedOverlay); + } + +} diff --git a/src/main/java/net/frozenblock/lib/block/mixin/tick/BlockBehaviourMixin.java b/src/main/java/net/frozenblock/lib/block/mixin/tick/BlockBehaviourMixin.java new file mode 100644 index 0000000..ba5d4af --- /dev/null +++ b/src/main/java/net/frozenblock/lib/block/mixin/tick/BlockBehaviourMixin.java @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.block.mixin.tick; + +import net.frozenblock.lib.block.api.tick.BlockScheduledTicks; +import net.minecraft.core.BlockPos; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.util.RandomSource; +import net.minecraft.world.level.block.state.BlockBehaviour; +import net.minecraft.world.level.block.state.BlockState; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(BlockBehaviour.class) +public class BlockBehaviourMixin { + + @Inject(method = "tick", at = @At("HEAD")) + public void tickScheduled(BlockState state, ServerLevel world, BlockPos pos, RandomSource random, CallbackInfo info) { + if (BlockScheduledTicks.TICKS.containsKey(state.getBlock())) { + BlockScheduledTicks.TICKS.get(state.getBlock()).tick(state, world, pos, random); + } + } + +} diff --git a/src/main/java/net/frozenblock/lib/config/api/entry/TypedEntry.java b/src/main/java/net/frozenblock/lib/config/api/entry/TypedEntry.java new file mode 100644 index 0000000..6da9f38 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/config/api/entry/TypedEntry.java @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.config.api.entry; + +import net.frozenblock.lib.config.impl.entry.TypedEntryImpl; + +public interface TypedEntry { + + TypedEntryType type(); + + T value(); + + void setValue(T value); + + static TypedEntry create(TypedEntryType type, T value) { + return new TypedEntryImpl<>(type, value); + } +} diff --git a/src/main/java/net/frozenblock/lib/config/api/entry/TypedEntryType.java b/src/main/java/net/frozenblock/lib/config/api/entry/TypedEntryType.java new file mode 100644 index 0000000..0eb7a8a --- /dev/null +++ b/src/main/java/net/frozenblock/lib/config/api/entry/TypedEntryType.java @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.config.api.entry; + +import com.mojang.serialization.Codec; +import net.frozenblock.lib.config.api.registry.ConfigRegistry; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; + +public record TypedEntryType(String modId, Codec codec) { + + @NotNull + public TypedEntryType register() { + return register(this); + } + + @Contract("_ -> param1") + public static @NotNull TypedEntryType register(TypedEntryType type) { + return ConfigRegistry.register(type); + } +} diff --git a/src/main/java/net/frozenblock/lib/config/api/instance/Config.java b/src/main/java/net/frozenblock/lib/config/api/instance/Config.java new file mode 100644 index 0000000..6e7f743 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/config/api/instance/Config.java @@ -0,0 +1,217 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.config.api.instance; + +import com.mojang.datafixers.DataFixer; +import com.mojang.datafixers.types.templates.Check; +import lombok.Getter; +import lombok.Setter; +import net.frozenblock.lib.FrozenBools; +import net.frozenblock.lib.FrozenLogUtils; +import net.frozenblock.lib.FrozenMain; +import net.frozenblock.lib.FrozenSharedConstants; +import net.frozenblock.lib.config.api.registry.ConfigEvent; +import net.frozenblock.lib.config.api.sync.annotation.UnsyncableConfig; +import net.frozenblock.lib.config.impl.network.ConfigSyncPacket; +import net.neoforged.fml.loading.FMLLoader; +import net.neoforged.neoforge.common.NeoForge; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.nio.file.Path; + +public abstract class Config { + + private final String modId; + private final Path path; + private final boolean supportsModification; + @Nullable + private final DataFixer dataFixer; + @Nullable + private final Integer version; + private final Class configClass; + private T configInstance; + private final T defaultInstance; + @Getter + @Setter + private boolean synced = false; + + protected Config(String modId, Class configClass, Path path, boolean supportsModification, @Nullable DataFixer dataFixer, @Nullable Integer version) { + this.modId = modId; + this.path = path; + this.supportsModification = supportsModification; + this.configClass = configClass; + try { + this.defaultInstance = this.configInstance = configClass.getConstructor().newInstance(); + } catch (Exception e) { + throw new IllegalStateException("No default constructor for default config instance.", e); + } + this.dataFixer = dataFixer; + this.version = version; + } + + @NotNull + @Contract(pure = true) + public static Path makePath(String modId, String extension) { + return Path.of("./config/" + modId + "." + extension); + } + + public String modId() { + return this.modId; + } + + public Path path() { + return this.path; + } + + public boolean supportsModification() { + return this.supportsModification; + } + + @Nullable + public DataFixer dataFixer() { + return this.dataFixer; + } + + @Nullable + public Integer version() { + return this.version; + } + + /** + * @return The current config instance with modifications if applicable + */ + public T config() { + if (this.supportsModification()) return ConfigModification.modifyConfig(this, this.instance(), false); + return this.instance(); + } + + /** + * @return The current config instance with config sync modifications + * @since 1.5 + */ + public T configWithSync() { + if (!this.supportsSync()) { + //TODO: Possibly remove before release? This causes log spam. Up to you, Tree. Might be best with JavaDoc instead. + String formatted = String.format("Config %s from %s", this.configClass().getSimpleName(), this.modId()); + FrozenLogUtils.logWarning(formatted + " does not support modification, returning unmodified instance."); + return this.instance(); + } + return ConfigModification.modifyConfig(this, this.instance(), true); + } + + /** + * @return If the current config supports modification and does not have the {@link UnsyncableConfig} annotation. + * @since 1.5 + */ + public boolean supportsSync() { + return this.supportsModification() && !this.configClass().isAnnotationPresent(UnsyncableConfig.class); + } + + /** + * @return The unmodified current config instance + */ + public T instance() { + return this.configInstance; + } + + public void setConfig(T configInstance) { + this.configInstance = configInstance; + } + + public T defaultInstance() { + return this.defaultInstance; + } + + public Class configClass() { + return this.configClass; + } + + public void onSync(T syncInstance) {} + + /** + * @since 1.5 + */ + protected String formattedName() { + return String.format("config %s from %s", this.configClass().getSimpleName(), this.modId()); + } + + protected abstract void onSave() throws Exception; + + protected abstract boolean onLoad() throws Exception; + + public final void save() { + String formatted = this.formattedName(); + FrozenSharedConstants.LOGGER.info("Saving {}", formatted); + try { + this.onSave(); + + if (FrozenBools.isInitialized) { + if (FMLLoader.getDist().isClient()) + ConfigSyncPacket.trySendC2S(this); + + invokeSaveEvents(); + } + } catch (Exception e) { + FrozenLogUtils.logError("Error while saving " + formatted, e); + } + } + + public final boolean load() { + String formatted = this.formattedName(); + FrozenSharedConstants.LOGGER.info("Loading {}", formatted); + try { + boolean loadVal = this.onLoad(); + + if (FrozenBools.isInitialized) { + invokeLoadEvents(); + } + return loadVal; + } catch (Exception e) { + FrozenLogUtils.logError("Error while loading " + formatted, e); + return false; + } + } + + private void invokeSaveEvents() { + String formatted = this.formattedName(); + try { + NeoForge.EVENT_BUS.post(new ConfigEvent.Save(this)); + + if (FMLLoader.getDist().isClient()) { + NeoForge.EVENT_BUS.post(new ConfigEvent.Save.Client(this)); + } + } catch (Exception e) { + FrozenLogUtils.logError("Error in config save events for " + formatted, e); + } + } + + private void invokeLoadEvents() { + String formatted = this.formattedName(); + try { + NeoForge.EVENT_BUS.post(new ConfigEvent.Load(this)); + + if (FMLLoader.getDist().isClient()) { + NeoForge.EVENT_BUS.post(new ConfigEvent.Load.Client(this)); + } + } catch (Exception e) { + FrozenLogUtils.logError("Error in config load events for " + formatted, e); + } + } +} diff --git a/src/main/java/net/frozenblock/lib/config/api/instance/ConfigModification.java b/src/main/java/net/frozenblock/lib/config/api/instance/ConfigModification.java new file mode 100644 index 0000000..5213ddc --- /dev/null +++ b/src/main/java/net/frozenblock/lib/config/api/instance/ConfigModification.java @@ -0,0 +1,107 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.config.api.instance; + +import net.frozenblock.lib.FrozenLogUtils; +import net.frozenblock.lib.config.api.registry.ConfigRegistry; +import net.frozenblock.lib.config.impl.network.ConfigSyncModification; +import net.minecraft.network.chat.Component; +import net.neoforged.api.distmarker.Dist; +import net.neoforged.api.distmarker.OnlyIn; +import org.jetbrains.annotations.NotNull; + +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.util.Map; +import java.util.Optional; +import java.util.function.Consumer; + +/** + * Wrapper class for modifying configs + * @param modification The consumer for applying modifications + * @param The type of the config class + */ +public record ConfigModification(Consumer modification) { + + public static T modifyConfig(Config config, T original, boolean excludeNonSync) { + try { + // clone + T instance = config.configClass().getConstructor().newInstance(); + copyInto(original, instance); + + // modify + var list = ConfigRegistry.getModificationsForConfig(config) + .entrySet() + .stream() + .sorted(Map.Entry.comparingByValue()) + .toList(); + + config.setSynced(false); + for (Map.Entry, Integer> modification : list) { + var consumer = modification.getKey().modification; + if (consumer instanceof ConfigSyncModification || !excludeNonSync) { + modification.getKey().modification.accept(instance); + } + } + + return instance; + } catch (Exception e) { + FrozenLogUtils.logError("Failed to modify config, returning original.", true, e); + return original; + } + } + + public static void copyInto(@NotNull T source, T destination, boolean isSyncModification) { + Class clazz = source.getClass(); + while (!clazz.equals(Object.class)) { + for (Field field : clazz.getDeclaredFields()) { + if (Modifier.isStatic(field.getModifiers())) continue; + field.setAccessible(true); + if (isSyncModification && !ConfigSyncModification.isSyncable(field)) continue; + try { + field.set(destination, field.get(source)); + } catch (IllegalAccessException e) { + FrozenLogUtils.logError("Failed to copy field " + field.getName(), true, e); + } + } + clazz = clazz.getSuperclass(); + } + } + + public static void copyInto(@NotNull T source, T destination) { + copyInto(source, destination, false); + } + + @OnlyIn(Dist.CLIENT) + public enum EntryPermissionType { + CAN_MODIFY(true, Optional.empty(), Optional.empty()), + LOCKED_FOR_UNKNOWN_REASON(false, Optional.of(Component.translatable("tooltip.frozenlib.locked_due_to_unknown_reason")), Optional.of(Component.translatable("tooltip.frozenlib.locked_due_to_unknown_reason"))), + LOCKED_DUE_TO_SERVER(false, Optional.of(Component.translatable("tooltip.frozenlib.locked_due_to_server")), Optional.of(Component.translatable("tooltip.frozenlib.locked_due_to_server_lan"))), + LOCKED_DUE_TO_SYNC(false, Optional.of(Component.translatable("tooltip.frozenlib.locked_due_to_sync")), Optional.of(Component.translatable("tooltip.frozenlib.locked_due_to_sync_lan"))); + + public final boolean canModify; + public final Optional tooltip; + public final Optional lanTooltip; + + EntryPermissionType(boolean canModify, Optional tooltip, Optional lanTooltip) { + this.canModify = canModify; + this.tooltip = tooltip; + this.lanTooltip = lanTooltip; + } + } +} diff --git a/src/main/java/net/frozenblock/lib/config/api/instance/ConfigSerialization.java b/src/main/java/net/frozenblock/lib/config/api/instance/ConfigSerialization.java new file mode 100644 index 0000000..efb18aa --- /dev/null +++ b/src/main/java/net/frozenblock/lib/config/api/instance/ConfigSerialization.java @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.config.api.instance; + +import blue.endless.jankson.Jankson; +import blue.endless.jankson.JsonElement; +import blue.endless.jankson.JsonGrammar; +import com.mojang.datafixers.DataFixer; +import net.frozenblock.lib.config.api.entry.TypedEntry; +import net.frozenblock.lib.config.api.instance.json.JanksonTypedEntrySerializer; +import net.frozenblock.lib.jankson.JanksonDataBuilder; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public class ConfigSerialization { + private ConfigSerialization() {} + + // JANKSON + + public static final JsonGrammar JSON5_UNQUOTED_KEYS = JsonGrammar.builder() + .withComments(true) + .printTrailingCommas(true) + .bareSpecialNumerics(true) + .printUnquotedKeys(true) + .build(); + + public static Jankson createJankson(@NotNull Jankson.Builder builder, String modId) { + JanksonTypedEntrySerializer typedEntrySerializer = new JanksonTypedEntrySerializer(modId); + return builder + .registerSerializer(TypedEntry.class, typedEntrySerializer) + .registerDeserializer(JsonElement.class, TypedEntry.class, typedEntrySerializer) + .build(); + } + + public static Jankson createJankson(String modId, @Nullable DataFixer dataFixer) { + return createJankson(JanksonDataBuilder.withFixer(new JanksonDataBuilder(), dataFixer), modId); + } + + public static Jankson createJankson(String modId) { + return createJankson(modId, null); + } +} diff --git a/src/main/java/net/frozenblock/lib/config/api/instance/json/JanksonOps.java b/src/main/java/net/frozenblock/lib/config/api/instance/json/JanksonOps.java new file mode 100644 index 0000000..bca9613 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/config/api/instance/json/JanksonOps.java @@ -0,0 +1,454 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.config.api.instance.json; + +import blue.endless.jankson.*; +import com.google.common.collect.Lists; +import com.mojang.datafixers.util.Pair; +import com.mojang.serialization.*; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.math.BigDecimal; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.function.BiConsumer; +import java.util.function.Consumer; +import java.util.function.UnaryOperator; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; + +public class JanksonOps implements DynamicOps { + public static final JanksonOps INSTANCE = new JanksonOps(false); + public static final JanksonOps COMPRESSED = new JanksonOps(true); + + private final boolean compressed; + + protected JanksonOps(final boolean compressed) { + this.compressed = compressed; + } + + @Override + public JsonElement empty() { + return JsonNull.INSTANCE; + } + + @Override + public U convertTo(final DynamicOps outOps, final JsonElement input) { + if (input instanceof JsonObject) { + return convertMap(outOps, input); + } + if (input instanceof JsonArray) { + return convertList(outOps, input); + } + if (input instanceof JsonNull) { + return outOps.empty(); + } + if (input instanceof JsonPrimitive primitive) { + if (primitive.getValue() instanceof String string) { + return outOps.createString(string); + } + if (primitive.getValue() instanceof Boolean bool) { + return outOps.createBoolean(bool); + } + if (primitive.getValue() instanceof BigDecimal value) { + try { + final long l = value.longValueExact(); + if ((byte) l == l) { + return outOps.createByte((byte) l); + } + if ((short) l == l) { + return outOps.createShort((short) l); + } + if ((int) l == l) { + return outOps.createInt((int) l); + } + return outOps.createLong(l); + } catch (final ArithmeticException e) { + final double d = value.doubleValue(); + if ((float) d == d) { + return outOps.createFloat((float) d); + } + return outOps.createDouble(d); + } + } + } + return null; + } + + @Override + public DataResult getNumberValue(final JsonElement input) { + if (input instanceof JsonPrimitive primitive) { + if (primitive.getValue() instanceof Number number) { + return DataResult.success(number); + } else if (primitive.getValue() instanceof Boolean bool) { + return DataResult.success(bool ? 1 : 0); + } + if (compressed && primitive.getValue() instanceof String string) { + try { + return DataResult.success(Integer.parseInt(string)); + } catch (final NumberFormatException e) { + return DataResult.error(() -> "Not a number: " + e + " " + input); + } + } + } + if (input instanceof JsonPrimitive primitive && primitive.getValue() instanceof Boolean bool) { + return DataResult.success(bool ? 1 : 0); + } + return DataResult.error(() -> "Not a number: " + input); + } + + @Override + public JsonElement createNumeric(final Number i) { + return new JsonPrimitive(i); + } + + @Override + public DataResult getBooleanValue(final JsonElement input) { + if (input instanceof JsonPrimitive primitive) { + if (primitive.getValue() instanceof Boolean bool) { + return DataResult.success(bool); + } else if (primitive.getValue() instanceof Number number) { + return DataResult.success(number.byteValue() != 0); + } + } + return DataResult.error(() -> "Not a boolean: " + input); + } + + @Override + public JsonElement createBoolean(final boolean value) { + return new JsonPrimitive(value); + } + + @Override + public DataResult getStringValue(final JsonElement input) { + if (input instanceof JsonPrimitive primitive) { + if (primitive.getValue() instanceof String || primitive.getValue() instanceof Number && compressed) { + return DataResult.success(primitive.getValue().toString()); + } + } + return DataResult.error(() -> "Not a string: " + input); + } + + @Override + public JsonElement createString(final String value) { + return new JsonPrimitive(value); + } + + @Override + public DataResult mergeToList(final JsonElement list, final JsonElement value) { + if (!(list instanceof JsonArray) && list != empty()) { + return DataResult.error(() -> "mergeToList called with not a list: " + list, list); + } + + final JsonArray result = new JsonArray(); + if (list != empty()) { + assert list instanceof JsonArray; + result.addAll((JsonArray) list); + } + result.add(value); + return DataResult.success(result); + } + + @Override + public DataResult mergeToList(final JsonElement list, final List values) { + if (!(list instanceof JsonArray) && list != empty()) { + return DataResult.error(() -> "mergeToList called with not a list: " + list, list); + } + + final JsonArray result = new JsonArray(); + if (list != empty()) { + assert list instanceof JsonArray; + result.addAll((JsonArray) list); + } + values.forEach(result::add); + return DataResult.success(result); + } + + @Override + public DataResult mergeToMap(final JsonElement map, final JsonElement key, final JsonElement value) { + if (!(map instanceof JsonObject) && map != empty()) { + return DataResult.error(() -> "mergeToMap called with not a map: " + map, map); + } + if (!(key instanceof JsonPrimitive primitive) || !(primitive.getValue() instanceof String) && !compressed) { + return DataResult.error(() -> "key is not a string: " + key, map); + } + + final JsonObject output = new JsonObject(); + if (map != empty()) { + assert map instanceof JsonObject; + var object = (JsonObject) map; + output.putAll(object); + } + output.put(((JsonPrimitive) key).asString(), value); + + return DataResult.success(output); + } + + @Override + public DataResult mergeToMap(final JsonElement map, final MapLike values) { + if (!(map instanceof JsonObject) && map != empty()) { + return DataResult.error(() -> "mergeToMap called with not a map: " + map, map); + } + + final JsonObject output = new JsonObject(); + if (map != empty()) { + assert map instanceof JsonObject; + var object = (JsonObject) map; + output.putAll(object); + } + + final List missed = Lists.newArrayList(); + + values.entries().forEach(entry -> { + final JsonElement key = entry.getFirst(); + if (!(key instanceof JsonPrimitive primitive) || !(primitive.getValue() instanceof String) && !compressed) { + missed.add(key); + return; + } + output.put(((JsonPrimitive) key).asString(), entry.getSecond()); + }); + + if (!missed.isEmpty()) { + return DataResult.error(() -> "some keys are not strings: " + missed, output); + } + + return DataResult.success(output); + } + + @Override + public DataResult>> getMapValues(final JsonElement input) { + if (!(input instanceof JsonObject object)) { + return DataResult.error(() -> "Not a JSON object: " + input); + } else { + return DataResult.success(object.entrySet().stream().map(entry -> Pair.of(new JsonPrimitive(entry.getKey()), entry.getValue() instanceof JsonNull ? null : entry.getValue()))); + } + } + + @Override + public DataResult>> getMapEntries(final JsonElement input) { + if (!(input instanceof JsonObject object)) { + return DataResult.error(() -> "Not a JSON object: " + input); + } else { + return DataResult.success(c -> { + for (final Map.Entry entry : object.entrySet()) { + c.accept(createString(entry.getKey()), entry.getValue() instanceof JsonNull ? null : entry.getValue()); + } + }); + } + } + + @Override + public DataResult> getMap(final JsonElement input) { + if (!(input instanceof JsonObject object)) { + return DataResult.error(() -> "Not a JSON object: " + input); + } else { + return DataResult.success(new MapLike<>() { + @Nullable + @Override + public JsonElement get(final JsonElement key) { + final JsonElement element = object.get(((JsonPrimitive) key).asString()); + if (element instanceof JsonNull) { + return null; + } + return element; + } + + @Nullable + @Override + public JsonElement get(final String key) { + final JsonElement element = object.get(key); + if (element instanceof JsonNull) { + return null; + } + return element; + } + + @Override + public Stream> entries() { + return object.entrySet().stream().map(e -> Pair.of(new JsonPrimitive(e.getKey()), e.getValue())); + } + + @Override + public String toString() { + return "MapLike[" + object + "]"; + } + }); + } + } + + @Override + public JsonElement createMap(final @NotNull Stream> map) { + final JsonObject result = new JsonObject(); + map.forEach(p -> result.put(((JsonPrimitive) p.getFirst()).asString(), p.getSecond())); + return result; + } + + @Override + public DataResult> getStream(final JsonElement input) { + if (input instanceof JsonArray array) { + return DataResult.success(StreamSupport.stream(array.spliterator(), false).map(e -> e instanceof JsonNull ? null : e)); + } + return DataResult.error(() -> "Not a json array: " + input); + } + + @Override + public DataResult>> getList(final JsonElement input) { + if (input instanceof JsonArray array) { + return DataResult.success(c -> { + for (final JsonElement element : array) { + c.accept(element instanceof JsonNull ? null : element); + } + }); + } + return DataResult.error(() -> "Not a json array: " + input); + } + + @Override + public JsonElement createList(final @NotNull Stream input) { + final JsonArray result = new JsonArray(); + input.forEach(result::add); + return result; + } + + @Override + public JsonElement remove(final JsonElement input, final String key) { + if (input instanceof JsonObject object) { + final JsonObject result = new JsonObject(); + object.entrySet().stream().filter(entry -> !Objects.equals(entry.getKey(), key)).forEach(entry -> result.put(entry.getKey(), entry.getValue())); + return result; + } + return input; + } + + @Override + public String toString() { + return "JSON"; + } + + @Override + public ListBuilder listBuilder() { + return new ArrayBuilder(); + } + + private static final class ArrayBuilder implements ListBuilder { + private DataResult builder = DataResult.success(new JsonArray(), Lifecycle.stable()); + + @Override + public DynamicOps ops() { + return INSTANCE; + } + + @Override + public ListBuilder add(final JsonElement value) { + builder = builder.map(b -> { + b.add(value); + return b; + }); + return this; + } + + @Override + public ListBuilder add(final DataResult value) { + builder = builder.apply2stable((b, element) -> { + b.add(element); + return b; + }, value); + return this; + } + + @Override + public ListBuilder withErrorsFrom(final DataResult result) { + builder = builder.flatMap(r -> result.map(v -> r)); + return this; + } + + @Override + public ListBuilder mapError(final UnaryOperator onError) { + builder = builder.mapError(onError); + return this; + } + + @Override + public DataResult build(final JsonElement prefix) { + final DataResult result = builder.flatMap(b -> { + if (!(prefix instanceof JsonArray) && prefix != ops().empty()) { + return DataResult.error(() -> "Cannot append a list to not a list: " + prefix, prefix); + } + + final JsonArray array = new JsonArray(); + if (prefix != ops().empty()) { + assert prefix instanceof JsonArray; + array.addAll((JsonArray) prefix); + } + array.addAll(b); + return DataResult.success(array, Lifecycle.stable()); + }); + + builder = DataResult.success(new JsonArray(), Lifecycle.stable()); + return result; + } + } + + @Override + public boolean compressMaps() { + return compressed; + } + + @Override + public RecordBuilder mapBuilder() { + return new JsonRecordBuilder(); + } + + private class JsonRecordBuilder extends RecordBuilder.AbstractStringBuilder { + protected JsonRecordBuilder() { + super(JanksonOps.this); + } + + @NotNull + @Contract(value = " -> new", pure = true) + @Override + protected JsonObject initBuilder() { + return new JsonObject(); + } + + @NotNull + @Contract("_, _, _ -> param3") + @Override + protected JsonObject append(final String key, final JsonElement value, final @NotNull JsonObject builder) { + builder.put(key, value); + return builder; + } + + @Override + protected DataResult build(final JsonObject builder, final JsonElement prefix) { + if (prefix == null || prefix instanceof JsonNull) { + return DataResult.success(builder); + } + if (prefix instanceof JsonObject object) { + final JsonObject result = new JsonObject(); + result.putAll(object); + result.putAll(builder); + return DataResult.success(result); + } + return DataResult.error(() -> "mergeToMap called with not a map: " + prefix, prefix); + } + } +} diff --git a/src/main/java/net/frozenblock/lib/config/api/instance/json/JanksonTypedEntrySerializer.java b/src/main/java/net/frozenblock/lib/config/api/instance/json/JanksonTypedEntrySerializer.java new file mode 100644 index 0000000..07e7933 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/config/api/instance/json/JanksonTypedEntrySerializer.java @@ -0,0 +1,117 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.config.api.instance.json; + +import blue.endless.jankson.JsonElement; +import blue.endless.jankson.api.DeserializationException; +import blue.endless.jankson.api.DeserializerFunction; +import blue.endless.jankson.api.Marshaller; +import com.google.gson.JsonParseException; +import com.mojang.datafixers.util.Pair; +import com.mojang.serialization.DataResult; +import net.frozenblock.lib.FrozenLogUtils; +import net.frozenblock.lib.FrozenSharedConstants; +import net.frozenblock.lib.config.api.entry.TypedEntry; +import net.frozenblock.lib.config.api.entry.TypedEntryType; +import net.frozenblock.lib.config.api.registry.ConfigRegistry; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Collection; +import java.util.Objects; +import java.util.function.BiFunction; + +public class JanksonTypedEntrySerializer implements BiFunction, DeserializerFunction { + + private final String modId; + + public JanksonTypedEntrySerializer(String modId) { + this.modId = modId; + } + + /** + * Serializes a {@link TypedEntry} to a {@link JsonElement}. + */ + @Override + @SuppressWarnings("unchecked") + public JsonElement apply(TypedEntry src, Marshaller marshaller) { + if (src != null) { + var type = src.type(); + if (type != null && Objects.equals(type.modId(), this.modId)) { + var codec = type.codec(); + if (codec != null) { + var encoded = codec.encodeStart(JanksonOps.INSTANCE, src.value()); + if (encoded != null && encoded.error().isEmpty()) { + var optional = encoded.result(); + if (optional.isPresent()) { + return (JsonElement) optional.get(); + } + } + } + } + } + throw new JsonParseException("Failed to serialize typed entry " + src); + } + + /** + * Deserializes a {@link JsonElement} to a {@link TypedEntry}. + */ + @Override + @SuppressWarnings("rawtypes") + public TypedEntry apply(JsonElement json, Marshaller m) throws DeserializationException { + var modEntry = getFromRegistry(json, ConfigRegistry.getTypedEntryTypesForMod(this.modId)); + if (modEntry != null) { + return modEntry; + } + throw new DeserializationException("Failed to deserialize typed entry " + json); + } + + @Nullable + @SuppressWarnings("unchecked") + private TypedEntry getFromRegistry(JsonElement json, @NotNull Collection> registry) throws ClassCastException { + for (TypedEntryType entryType : registry) { + TypedEntryType newType = (TypedEntryType) entryType; + TypedEntry entry = getFromType(json, newType); + if (entry != null) { + return entry; + } + } + return null; + } + + @Nullable + private TypedEntry getFromType(JsonElement json, @NotNull TypedEntryType entryType) throws ClassCastException { + if (entryType.modId().equals(modId)) { + var codec = entryType.codec(); + DataResult> result = codec.decode(JanksonOps.INSTANCE, json); + + if (result.error().isEmpty()) { + var optional = result.result(); + + if (optional.isPresent()) { + Pair pair = optional.get(); + T first = pair.getFirst(); + TypedEntry entry = TypedEntry.create(entryType, first); + FrozenLogUtils.log("Built typed entry " + entry, FrozenSharedConstants.UNSTABLE_LOGGING); + return entry; + } + } + } + return null; + } +} diff --git a/src/main/java/net/frozenblock/lib/config/api/instance/json/JsonConfig.java b/src/main/java/net/frozenblock/lib/config/api/instance/json/JsonConfig.java new file mode 100644 index 0000000..72bcf86 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/config/api/instance/json/JsonConfig.java @@ -0,0 +1,88 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.config.api.instance.json; + +import blue.endless.jankson.Jankson; +import com.mojang.datafixers.DataFixer; +import net.frozenblock.lib.config.api.instance.Config; +import net.frozenblock.lib.config.api.instance.ConfigSerialization; +import net.frozenblock.lib.jankson.JanksonDataBuilder; +import org.jetbrains.annotations.Nullable; + +import java.io.BufferedWriter; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; + +/** + * Serializes and deserializes config data with Jankson. + */ +public class JsonConfig extends Config { + + private final Jankson jankson; + + private final JsonType type; + + public JsonConfig(String modId, Class config, @Nullable DataFixer dataFixer, @Nullable Integer version) { + this(modId, config, true, dataFixer, version); + } + + public JsonConfig(String modId, Class config, JsonType type, @Nullable DataFixer dataFixer, @Nullable Integer version) { + this(modId, config, type, true, dataFixer, version); + } + + public JsonConfig(String modId, Class config, Path path, JsonType type, @Nullable DataFixer dataFixer, @Nullable Integer version) { + this(modId, config, path, type, true, dataFixer, version); + } + + public JsonConfig(String modId, Class config, boolean supportsModification, @Nullable DataFixer dataFixer, @Nullable Integer version) { + this(modId, config, JsonType.JSON, supportsModification, dataFixer, version); + } + + public JsonConfig(String modId, Class config, JsonType type, boolean supportsModification, @Nullable DataFixer dataFixer, @Nullable Integer version) { + this(modId, config, makePath(modId, type.getSerializedName()), type, supportsModification, dataFixer, version); + } + + public JsonConfig(String modId, Class config, Path path, JsonType type, boolean supportsModification, @Nullable DataFixer dataFixer, @Nullable Integer version) { + super(modId, config, path, supportsModification, dataFixer, version); + var janksonBuilder = JanksonDataBuilder.withBoth(new JanksonDataBuilder(), dataFixer, version); + + this.jankson = ConfigSerialization.createJankson(janksonBuilder, modId); + this.type = type; + + if (this.load()) { + this.save(); + } + } + + @Override + public void onSave() throws Exception { + Files.createDirectories(this.path().getParent()); + try (BufferedWriter writer = Files.newBufferedWriter(this.path(), StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING)) { + writer.write(this.jankson.toJson(this.instance()).toJson(this.type.getGrammar())); + } + } + + @Override + public boolean onLoad() throws Exception { + if (Files.exists(this.path())) { + this.setConfig(this.jankson.fromJson(this.jankson.load(this.path().toFile()), this.configClass())); + } + return true; + } +} diff --git a/src/main/java/net/frozenblock/lib/config/api/instance/json/JsonType.java b/src/main/java/net/frozenblock/lib/config/api/instance/json/JsonType.java new file mode 100644 index 0000000..7d75dbd --- /dev/null +++ b/src/main/java/net/frozenblock/lib/config/api/instance/json/JsonType.java @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.config.api.instance.json; + +import blue.endless.jankson.JsonGrammar; +import net.frozenblock.lib.config.api.instance.ConfigSerialization; +import net.minecraft.util.StringRepresentable; +import org.jetbrains.annotations.NotNull; + +public enum JsonType implements StringRepresentable { + JSON("json", JsonGrammar.STRICT), + JSON5("json5", JsonGrammar.JSON5), + /** + * Like JSON5 but it supports not having quotes on keys + */ + JSON5_UNQUOTED_KEYS("json5", ConfigSerialization.JSON5_UNQUOTED_KEYS); + + @NotNull + private final String name; + + @NotNull + private final JsonGrammar grammar; + + JsonType(@NotNull String name, @NotNull JsonGrammar grammar) { + this.name = name; + this.grammar = grammar; + } + + @Override + @NotNull + public String getSerializedName() { + return this.name; + } + + @NotNull + public JsonGrammar getGrammar() { + return this.grammar; + } +} diff --git a/src/main/java/net/frozenblock/lib/config/api/instance/toml/TomlConfig.java b/src/main/java/net/frozenblock/lib/config/api/instance/toml/TomlConfig.java new file mode 100644 index 0000000..04b9867 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/config/api/instance/toml/TomlConfig.java @@ -0,0 +1,84 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.config.api.instance.toml; + +import com.moandjiezana.toml.Toml; +import com.moandjiezana.toml.TomlWriter; +import net.frozenblock.lib.config.api.instance.Config; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; + +import java.io.BufferedWriter; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; + +/** + * Serializes and deserializes config data with TOML4J. + * @since 1.4 + */ +@ApiStatus.Experimental +public class TomlConfig extends Config { + + public static final String EXTENSION = "toml"; + + private final TomlWriter tomlWriter; + + + public TomlConfig(String modId, Class config) { + this(modId, config, new TomlWriter.Builder()); + } + + public TomlConfig(String modId, Class config, TomlWriter.Builder builder) { + this(modId, config, makePath(modId, EXTENSION), builder); + } + + public TomlConfig(String modId, Class config, Path path, TomlWriter.@NotNull Builder builder) { + super(modId, config, path, true, null, null); + this.tomlWriter = builder.build(); + + if (this.load()) { + this.save(); + } + } + + @Override + public void onSave() throws Exception { + Files.createDirectories(this.path().getParent()); + BufferedWriter writer = Files.newBufferedWriter(this.path(), StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.CREATE); + this.tomlWriter.write(this.instance(), writer); + writer.close(); + } + + @Override + public boolean onLoad() throws Exception { + if (Files.exists(this.path())) { + var tomlReader = getDefaultToml(); + try (var reader = Files.newBufferedReader(this.path())) { + this.setConfig(tomlReader.read(reader).to(this.configClass())); + } + } + return true; + } + + @NotNull + private Toml getDefaultToml() { + Toml toml = new Toml(); + return new Toml(toml.read(tomlWriter.write(defaultInstance()))); + } +} diff --git a/src/main/java/net/frozenblock/lib/config/api/instance/xjs/InvalidEnumConstantException.java b/src/main/java/net/frozenblock/lib/config/api/instance/xjs/InvalidEnumConstantException.java new file mode 100644 index 0000000..ca90db2 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/config/api/instance/xjs/InvalidEnumConstantException.java @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.config.api.instance.xjs; + +import org.jetbrains.annotations.NotNull; + +import java.util.Arrays; + +/* + Source: https://github.com/PersonTheCat/CatLib + License: GNU GPL-3.0 + */ +public class InvalidEnumConstantException extends RuntimeException { + public InvalidEnumConstantException(final String name, final Class> clazz) { + super(createMessage(name, clazz)); + } + + private static String createMessage(final String name, final Class> clazz) { + final String values = Arrays.toString(clazz.getEnumConstants()); + return f("{} \"{}\" does not exist. Valid options are: {}", clazz.getSimpleName(), name, values); + } + + /** + * Interpolates strings by replacing instances of {} in order. + * + * @param s The template string being formatted. + * @param args A list of arguments to interpolate into this string. + * @return A formatted, interpolated string. + */ + @NotNull + static String f(final String s, final Object... args) { + int begin = 0, si = 0, oi = 0; + final StringBuilder sb = new StringBuilder(); + while (true) { + si = s.indexOf("{}", si); + if (si >= 0) { + sb.append(s, begin, si); + sb.append(args[oi++]); + begin = si = si + 2; + } else { + break; + } + } + sb.append(s.substring(begin)); + return sb.toString(); + } +} diff --git a/src/main/java/net/frozenblock/lib/config/api/instance/xjs/JsonFormatException.java b/src/main/java/net/frozenblock/lib/config/api/instance/xjs/JsonFormatException.java new file mode 100644 index 0000000..1e1bbce --- /dev/null +++ b/src/main/java/net/frozenblock/lib/config/api/instance/xjs/JsonFormatException.java @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.config.api.instance.xjs; + +/* + Source: https://github.com/PersonTheCat/CatLib + License: GNU GPL-3.0 + */ +public class JsonFormatException extends RuntimeException{ + public JsonFormatException(final String msg) { + super(msg); + } +} diff --git a/src/main/java/net/frozenblock/lib/config/api/instance/xjs/JsonPath.java b/src/main/java/net/frozenblock/lib/config/api/instance/xjs/JsonPath.java new file mode 100644 index 0000000..2f94795 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/config/api/instance/xjs/JsonPath.java @@ -0,0 +1,491 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.config.api.instance.xjs; + +import com.mojang.brigadier.LiteralMessage; +import com.mojang.brigadier.Message; +import com.mojang.brigadier.StringReader; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import com.mojang.brigadier.exceptions.SimpleCommandExceptionType; +import com.mojang.datafixers.util.Either; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import personthecat.fresult.Result; +import xjs.data.JsonContainer; +import xjs.data.JsonObject; +import xjs.data.JsonValue; +import xjs.data.PathFilter; + +import java.util.*; +import java.util.stream.Collectors; + +/* + Source: https://github.com/PersonTheCat/CatLib + License: GNU GPL-3.0 + */ +/** + * An object representing every accessor in a JSON object leading to a value. + * + *

In other words, this object is a container holding keys and indices which + * point to a value at some arbitrary depth in a JSON array or object. + */ +public class JsonPath implements Iterable> { + + private final List> path; + private final String raw; + + public JsonPath(final List> path) { + this.path = path; + this.raw = serialize(path); + } + + public JsonPath(final List> path, final String raw) { + this.path = path; + this.raw = raw; + } + + /** + * Creates a new JSON path builder, used for programmatically generating new + * JSON path representations. + * + * @return A new {@link JsonPathBuilder} for constructing JSON paths. + */ + public static JsonPathBuilder builder() { + return new JsonPathBuilder(); + } + + /** + * A lightweight, immutable alternative to {@link JsonPathBuilder}, specifically + * intended for tracking paths over time in scenarios where an actual {@link JsonPath} + * may not be needed. + * + *

For example, an application performing analysis on a body of JSON data + * might "track" the current path using one of these objects. If for some reason + * a specific path needs to be saved, the dev might call {@link Stub#capture()} + * to generate a proper {@link JsonPath}, which can be reflected on at a later + * time. + * + *

This is equivalent to using a regular {@link JsonPathBuilder}, while being + * modestly less expensive in that context. However, because it is immutable, it + * may be repeatedly passed into various other methods without the threat of any + * accidental mutations further down the stack. + * + * @return {@link Stub#EMPTY}, for building raw JSON paths. + */ + public static Stub stub() { + return Stub.EMPTY; + } + + /** + * Deserializes the given raw path into a collection of keys and indices. + * + * @throws CommandSyntaxException If the path is formatted incorrectly. + * @param raw The raw JSON path being deserialized. + * @return An object representing every accessor leading to a JSON value. + */ + public static JsonPath parse(final String raw) throws CommandSyntaxException { + return parse(new StringReader(raw)); + } + + /** + * Deserializes the given raw path into a collection of keys and indices. + * + * @throws CommandSyntaxException If the path is formatted incorrectly. + * @param reader A reader exposing the raw JSON path being deserialized. + * @return An object representing every accessor leading to a JSON value. + */ + public static JsonPath parse(final StringReader reader) throws CommandSyntaxException { + final List> path = new ArrayList<>(); + final int begin = reader.getCursor(); + + while(reader.canRead() && reader.peek() != ' ') { + final char c = reader.read(); + if (c == '.') { + checkDot(reader, begin); + } else if (inKey(c)) { + path.add(Either.left(c + readKey(reader))); + } else if (c == '[') { + checkDot(reader, begin); + path.add(Either.right(reader.readInt())); + reader.expect(']'); + } else { + throw cmdSyntax(reader, "Invalid character"); + } + } + return new JsonPath(path, reader.getString().substring(begin, reader.getCursor())); + } + + private static String readKey(final StringReader reader) { + final int start = reader.getCursor(); + while (reader.canRead() && inKey(reader.peek())) { + reader.skip(); + } + return reader.getString().substring(start, reader.getCursor()); + } + + private static boolean inKey(final char c) { + return c != '.' && c != ' ' && c != '['; + } + + private static void checkDot(final StringReader reader, final int begin) throws CommandSyntaxException { + final int cursor = reader.getCursor(); + final char last = reader.getString().charAt(cursor - 2); + if (cursor - 1 == begin || last == '.') { + throw cmdSyntax(reader, "Unexpected accessor"); + } + } + + /** + * Variant of {@link #parse(String)} which returns instead of throwing + * an exception. + * + * @param raw The raw JSON path being deserialized. + * @return An object representing every accessor leading to a JSON value. + */ + public static Result tryParse(final String raw) { + return Result.of(() -> parse(raw)).ifErr(Result::IGNORE); + } + + /** + * Generates a new JsonPath from a string containing only keys. + * + *

This method is intended as optimization in cases where no + * arrays are needed. + * + * @param raw The raw JSON path containing keys only. + * @return A new object representing this path. + */ + public static JsonPath objectOnly(final String raw) { + final List> path = new ArrayList<>(); + for (final String key : raw.split("\\.")) { + path.add(Either.left(key)); + } + return new JsonPath(path, raw); + } + + /** + * Converts the given JSON path data into a raw string. + * + * @param path The parsed JSON path being serialized. + * @return A string representing the equivalent path. + */ + public static String serialize(final Collection> path) { + final StringBuilder sb = new StringBuilder(); + for (final Either either : path) { + either.ifLeft(s -> { + sb.append('.'); + sb.append(s); + }); + either.ifRight(i -> { + sb.append('['); + sb.append(i); + sb.append(']'); + }); + } + final String s = sb.toString(); + return s.startsWith(".") ? s.substring(1) : s; + } + + /** + * Generates a list of every possible JSON path in this object. + * + * @param json The json containing the expected paths. + * @return A list of objects representing these paths. + */ + public static List getAllPaths(final JsonObject json) { + return toPaths(json.getPaths()); + } + + /** + * Generates a list of every used JSON path in this object. + * + * @param json The json containing the expected paths. + * @return A list of objects representing these paths. + */ + public static List getUsedPaths(final JsonObject json) { + return toPaths(json.getPaths(PathFilter.USED)); + } + + /** + * Generates a list of every unused JSON path in this object. + * + * @param json The json containing the expected paths. + * @return A list of objects representing these paths. + */ + public static List getUnusedPaths(final JsonObject json) { + return toPaths(json.getPaths(PathFilter.UNUSED)); + } + + private static List toPaths(final List raw) { + return raw.stream() + .map(JsonPath::parseUnchecked) + .collect(Collectors.toList()); + } + + private static JsonPath parseUnchecked(final String path) { + try { + return parse(path); + } catch (final CommandSyntaxException e) { + throw new IllegalStateException("JSON lib returned unusable path", e); + } + } + + public JsonContainer getLastContainer(final JsonObject json) { + return XjsUtils.getLastContainer(json, this); + } + + public Optional getValue(final JsonObject json) { + return XjsUtils.getValueFromPath(json, this); + } + + public void setValue(final JsonObject json, final @Nullable JsonValue value) { + XjsUtils.setValueFromPath(json, this, value); + } + + public JsonPath getClosestMatch(final JsonObject json) { + return XjsUtils.getClosestMatch(json, this); + } + + public int getLastAvailable(final JsonObject json) { + return XjsUtils.getLastAvailable(json, this); + } + + public JsonPathBuilder toBuilder() { + return new JsonPathBuilder(new ArrayList<>(this.path), new StringBuilder(this.raw)); + } + + public Stub beginTracking() { + return new Stub(this.raw); + } + + public Collection> asCollection() { + return Collections.unmodifiableCollection(this.path); + } + + public String asRawPath() { + return this.raw; + } + + public boolean isEmpty() { + return this.path.isEmpty(); + } + + public int size() { + return this.path.size(); + } + + public Either get(final int index) { + return this.path.get(index); + } + + public int indexOf(final String key) { + return this.path.indexOf(Either.left(key)); + } + + public int lastIndexOf(final String key) { + return this.path.lastIndexOf(Either.left(key)); + } + + public List> subList(final int s, final int e) { + return this.path.subList(s, e); + } + + public JsonPath subPath(final String key) { + final int index = this.indexOf(key); + return index < 0 ? this : this.subPath(index, this.size()); + } + + public JsonPath subPath(final int s, final int e) { + return new JsonPath(this.subList(s, e)); + } + + public JsonPath append(final JsonPath path) { + return this.append(path, 0, path.size()); + } + + public JsonPath append(final JsonPath path, final int startInclusive) { + return this.append(path, startInclusive, path.size()); + } + + public JsonPath append(final JsonPath path, final int startInclusive, final int endExclusive) { + return this.toBuilder().append(path, startInclusive, endExclusive).build(); + } + + @NotNull + @Override + public Iterator> iterator() { + return this.path.iterator(); + } + + @Override + public boolean equals(final Object o) { + if (o instanceof JsonPath) { + return this.path.equals(((JsonPath) o).path); + } + return false; + } + + @Override + public int hashCode() { + return this.path.hashCode(); + } + + @Override + public String toString() { + return this.raw; + } + + /** + * A builder used for manually constructing JSON paths in-code. + */ + public static class JsonPathBuilder { + + private final List> path; + private final StringBuilder raw; + + private JsonPathBuilder() { + this(new ArrayList<>(), new StringBuilder()); + } + + private JsonPathBuilder(final List> path, final StringBuilder raw) { + this.path = path; + this.raw = raw; + } + + public JsonPathBuilder key(final String key) { + this.path.add(Either.left(key)); + if (this.raw.length() > 0) { + this.raw.append('.'); + } + this.raw.append(key); + return this; + } + + public JsonPathBuilder index(final int index) { + this.path.add(Either.right(index)); + this.raw.append('[').append(index).append(']'); + return this; + } + + public JsonPathBuilder up(final int count) { + JsonPathBuilder builder = this; + for (int i = 0; i < count; i++) { + builder = builder.up(); + } + return builder; + } + + public JsonPathBuilder up() { + if (this.path.isEmpty()) { + return this; + } else if (this.path.size() == 1) { + return new JsonPathBuilder(); + } + this.path.remove(this.path.size() - 1); + final int dot = this.raw.lastIndexOf("."); + final int bracket = this.raw.lastIndexOf("["); + this.raw.delete(Math.max(dot, bracket), this.raw.length()); + return this; + } + + public JsonPathBuilder append(final JsonPath path, final int startInclusive, final int endExclusive) { + for (int i = startInclusive; i < endExclusive; i++) { + path.get(i).ifLeft(this::key).ifRight(this::index); + } + return this; + } + + public JsonPath build() { + return new JsonPath(this.path, this.raw.toString()); + } + + @Override + public int hashCode() { + return this.path.hashCode(); + } + + @Override + public boolean equals(final Object o) { + if (o instanceof JsonPathBuilder) { + return this.path.equals(((JsonPathBuilder) o).path); + } + return false; + } + } + + /** + * A lightweight, immutable builder designed for appending paths over time, wherein + * the more expensive {@link JsonPath} is typically unneeded. + */ + public static class Stub { + + private static final Stub EMPTY = new Stub(""); + + private final String path; + + private Stub(final String path) { + this.path = path; + } + + public Stub key(final String key) { + if (this.path.isEmpty()) { + return new Stub(key); + } + return new Stub(this.path + "." + key); + } + + public Stub index(final int index) { + return new Stub(this.path + "[" + index + "]"); + } + + public JsonPath capture() { + try { + return parse(this.path); + } catch (final CommandSyntaxException e) { + throw new IllegalArgumentException("Invalid characters in stub: " + this.path); + } + } + + @Override + public int hashCode() { + return this.path.hashCode(); + } + + @Override + public boolean equals(final Object o) { + if (o instanceof Stub) { + return this.path.equals(((Stub) o).path); + } + return false; + } + } + + /** + * Shorthand for a simple {@link CommandSyntaxException}. + * + * @param reader The reader being used to parse an argument. + * @param msg The error message to display. + * @return A new {@link CommandSyntaxException}. + */ + private static CommandSyntaxException cmdSyntax(final StringReader reader, final String msg) { + final int cursor = reader.getCursor(); + final String input = reader.getString().substring(0, cursor); + final Message m = new LiteralMessage(msg); + return new CommandSyntaxException(new SimpleCommandExceptionType(m), m, input, cursor); + } +} diff --git a/src/main/java/net/frozenblock/lib/config/api/instance/xjs/NonSerializableObjectException.java b/src/main/java/net/frozenblock/lib/config/api/instance/xjs/NonSerializableObjectException.java new file mode 100644 index 0000000..4651905 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/config/api/instance/xjs/NonSerializableObjectException.java @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.config.api.instance.xjs; + +/* + Source: https://github.com/PersonTheCat/CatLib + License: GNU GPL-3.0 + */ +public class NonSerializableObjectException extends Exception { + public NonSerializableObjectException(final String msg) { + super(msg); + } + + public static NonSerializableObjectException unsupportedKey(final Object key) { + return new NonSerializableObjectException("Cannot serialize map of type " + key.getClass() + ". Keys must be strings."); + } + + public static NonSerializableObjectException defaultRequired() { + return new NonSerializableObjectException("Cannot serialize object. Generic types must have defaults."); + } +} diff --git a/src/main/java/net/frozenblock/lib/config/api/instance/xjs/UnreachableException.java b/src/main/java/net/frozenblock/lib/config/api/instance/xjs/UnreachableException.java new file mode 100644 index 0000000..9b696a8 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/config/api/instance/xjs/UnreachableException.java @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.config.api.instance.xjs; + +/* + Source: https://github.com/PersonTheCat/CatLib + License: GNU GPL-3.0 + */ +public class UnreachableException extends RuntimeException { + public UnreachableException() { + super("Unreachable"); + } +} diff --git a/src/main/java/net/frozenblock/lib/config/api/instance/xjs/UnsafeUtils.java b/src/main/java/net/frozenblock/lib/config/api/instance/xjs/UnsafeUtils.java new file mode 100644 index 0000000..92dd2b2 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/config/api/instance/xjs/UnsafeUtils.java @@ -0,0 +1,72 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.config.api.instance.xjs; + +import lombok.experimental.UtilityClass; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; + +// Source: Cloth Config +@UtilityClass +public class UnsafeUtils { + + public static V constructUnsafely(Class cls) { + try { + Constructor constructor = cls.getDeclaredConstructor(); + constructor.setAccessible(true); + return constructor.newInstance(); + } catch (ReflectiveOperationException var2) { + throw new RuntimeException(var2); + } + } + + @SuppressWarnings("unchecked") + public static V getUnsafely(Field field, Object obj) { + if (obj == null) { + return null; + } else { + try { + field.setAccessible(true); + return (V) field.get(obj); + } catch (ReflectiveOperationException var3) { + throw new RuntimeException(var3); + } + } + } + + public static V getUnsafely(Field field, Object obj, V defaultValue) { + V ret = getUnsafely(field, obj); + if (ret == null) { + ret = defaultValue; + } + + return ret; + } + + public static void setUnsafely(Field field, Object obj, Object newValue) { + if (obj != null) { + try { + field.setAccessible(true); + field.set(obj, newValue); + } catch (ReflectiveOperationException var4) { + throw new RuntimeException(var4); + } + } + } +} diff --git a/src/main/java/net/frozenblock/lib/config/api/instance/xjs/XjsConfig.java b/src/main/java/net/frozenblock/lib/config/api/instance/xjs/XjsConfig.java new file mode 100644 index 0000000..6654472 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/config/api/instance/xjs/XjsConfig.java @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.config.api.instance.xjs; + +import net.frozenblock.lib.config.api.instance.Config; +import xjs.data.JsonValue; +import xjs.data.serialization.writer.ValueWriter; + +import java.nio.file.Files; +import java.nio.file.Path; + +/** + * Serializes and deserializes config data with XJS. + * @since 1.7 + */ +public class XjsConfig extends Config { + + private final XjsFormat format; + + public XjsConfig(String modId, Class config) { + this(modId, config, true); + } + + public XjsConfig(String modId, Class config, XjsFormat type) { + this(modId, config, type, true); + } + + public XjsConfig(String modId, Class config, Path path, XjsFormat type) { + this(modId, config, path, type, true); + } + + public XjsConfig(String modId, Class config, boolean supportsModification) { + this(modId, config, XjsFormat.DJS_FORMATTED, supportsModification); + } + + public XjsConfig(String modId, Class config, XjsFormat type, boolean supportsModification) { + this(modId, config, makePath(modId, type.getSerializedName()), type, supportsModification); + } + + public XjsConfig(String modId, Class config, Path path, XjsFormat type, boolean supportsModification) { + super(modId, config, path, supportsModification, null, null); + + this.format = type; + + if (this.load()) { + this.save(); + } + } + + @Override + public void onSave() throws Exception { + Files.createDirectories(this.path().getParent()); + JsonValue value = XjsObjectMapper.toJsonObject(this.instance()); + try ( + ValueWriter writer = this.format.createWriter(this.path().toFile()) + ) { + writer.write(value); + } + } + + @Override + public boolean onLoad() throws Exception { + if (Files.exists(this.path())) { + this.setConfig(XjsObjectMapper.deserializeObject(this.modId(), this.path(), this.configClass())); + } + return true; + } +} diff --git a/src/main/java/net/frozenblock/lib/config/api/instance/xjs/XjsFormat.java b/src/main/java/net/frozenblock/lib/config/api/instance/xjs/XjsFormat.java new file mode 100644 index 0000000..4a085d6 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/config/api/instance/xjs/XjsFormat.java @@ -0,0 +1,144 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.config.api.instance.xjs; + +import net.minecraft.util.StringRepresentable; +import org.jetbrains.annotations.NotNull; +import xjs.compat.serialization.util.UBTyping; +import xjs.compat.serialization.writer.HjsonWriter; +import xjs.compat.serialization.writer.TxtWriter; +import xjs.compat.serialization.writer.UbjsonWriter; +import xjs.data.serialization.writer.DjsWriter; +import xjs.data.serialization.writer.JsonWriter; +import xjs.data.serialization.writer.ValueWriter; + +import java.io.File; +import java.io.IOException; +import java.util.function.Function; + +public enum XjsFormat implements StringRepresentable { + /** + * Prints unformatted, regular JSON with no whitespace. + */ + JSON("json", writer -> { + try { + return new JsonWriter(writer, false); + } catch (IOException e) { + throw new RuntimeException(e); + } + }), + + /** + * Pretty prints regular JSON with whitespace. + */ + JSON_FORMATTED(JSON.getSerializedName(), writer -> { + try { + return new JsonWriter(writer, true); + } catch (IOException e) { + throw new RuntimeException(e); + } + }), + + /** + * Prints unformatted HJSON with no whitespace. + */ + HJSON("hjson", writer -> { + try { + return new HjsonWriter(writer, false); + } catch (IOException e) { + throw new RuntimeException(e); + } + }), + + /** + * Pretty prints HJSON with whitespace. + */ + HJSON_FORMATTED(HJSON.getSerializedName(), writer -> { + try { + return new HjsonWriter(writer, true); + } catch (IOException e) { + throw new RuntimeException(e); + } + }), + + /** + * Prints unformatted DJS with no whitespace. + */ + DJS("djs", writer -> { + try { + return new DjsWriter(writer, false); + } catch (IOException e) { + throw new RuntimeException(e); + } + }), + + /** + * Pretty prints DJS with whitespace. + */ + DJS_FORMATTED(DJS.getSerializedName(), writer -> { + try { + return new DjsWriter(writer, true); + } catch (IOException e) { + throw new RuntimeException(e); + } + }), + + /** + * Prints into a txt file. + */ + TXT("txt", writer -> { + try { + return new TxtWriter(writer); + } catch (IOException e) { + throw new RuntimeException(e); + } + }), + + /** + * Prints Universal Binary JSON. + */ + UBJSON("ubjson", writer -> { + try { + return new UbjsonWriter(writer, UBTyping.BALANCED); + } catch (IOException e) { + throw new RuntimeException(e); + } + }); + + @NotNull + private final String name; + + @NotNull + private final Function writer; + + XjsFormat(@NotNull String name, @NotNull Function writer) { + this.name = name; + this.writer = writer; + } + + @Override + @NotNull + public String getSerializedName() { + return this.name; + } + + @NotNull + public ValueWriter createWriter(File writer) { + return this.writer.apply(writer); + } +} diff --git a/src/main/java/net/frozenblock/lib/config/api/instance/xjs/XjsObjectMapper.java b/src/main/java/net/frozenblock/lib/config/api/instance/xjs/XjsObjectMapper.java new file mode 100644 index 0000000..dba38f4 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/config/api/instance/xjs/XjsObjectMapper.java @@ -0,0 +1,313 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.config.api.instance.xjs; + +import blue.endless.jankson.Comment; +import net.frozenblock.lib.config.api.entry.TypedEntry; +import net.frozenblock.lib.jankson.SaveToggle; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import xjs.data.Json; +import xjs.data.JsonArray; +import xjs.data.JsonObject; +import xjs.data.JsonValue; + +import java.io.IOException; +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.nio.file.Path; +import java.util.*; +import java.util.function.Predicate; + +/* + Source: https://github.com/PersonTheCat/CatLib + License: GNU GPL-3.0 + */ +public class XjsObjectMapper { + + public static void serializeObject(final Path p, final Object o) throws IOException, NonSerializableObjectException { + XjsUtils.writeJson(toJsonObject(o), p.toFile()).throwIfErr(); + } + + public static T deserializeObject(final Path p, final Class clazz) throws NonSerializableObjectException { + return deserializeObject(null, p, clazz); + } + + public static T deserializeObject(final @Nullable String modId, final Path p, final Class clazz) throws NonSerializableObjectException { + final T t = UnsafeUtils.constructUnsafely(clazz); + + final Optional read = XjsUtils.readJson(p.toFile()); + if (read.isEmpty()) return t; + final JsonObject json = read.get(); + if (json.isEmpty()) return t; + + writeObjectInto(modId, t, json); + + return t; + } + + public static JsonValue toJsonValue(final Object o) throws NonSerializableObjectException { + if (o.getClass().isArray()) { + return toJsonArray((Object[]) o); + } else if (o.getClass().isEnum()) { + return Json.value(((Enum) o).name()); + } else if (o instanceof String) { + return Json.value((String) o); + } else if (o instanceof Integer) { + return Json.value((Integer) o); + } else if (o instanceof Long) { + return Json.value((Long) o); + } else if (o instanceof Float || o instanceof Double) { + return Json.value(((Number) o).doubleValue()); + } else if (o instanceof Boolean) { + return Json.value((Boolean) o); + } else if (o instanceof Collection) { + return toJsonArray((Collection) o); + } else if (o instanceof Map) { + return toJsonObject((Map) o); + } else if (o instanceof TypedEntry) { + return XjsTypedEntrySerializer.toJsonValue((TypedEntry) o); + } + return toJsonObject(o); + } + + public static JsonObject toJsonObject(final Object o) throws NonSerializableObjectException { + final JsonObject json = new JsonObject(); + + final Class c = o.getClass(); + for (final Field f : c.getDeclaredFields()) { + if (!Modifier.isStatic(f.getModifiers()) && !Modifier.isTransient(f.getModifiers())) { + final JsonValue value = toJsonValue(UnsafeUtils.getUnsafely(f, o)); + + final String comment = getComment(f); + if (comment != null) value.setComment(comment); + + if (getSaveToggle(f)) { + json.add(f.getName(), value); + } + } + } + return json; + } + + @Nullable + public static String getComment(final Field f) { + final Comment[] comments = f.getAnnotationsByType(Comment.class); + if (comments.length == 0) return null; + return comments[0].value(); + } + + public static boolean getSaveToggle(final Field f) { + final SaveToggle[] toggles = f.getAnnotationsByType(SaveToggle.class); + if (toggles.length == 0) return true; + return toggles[0].value(); + } + + public static JsonObject toJsonObject(final Map map) throws NonSerializableObjectException { + final JsonObject json = new JsonObject(); + for (final Map.Entry entry : map.entrySet()) { + if (!(entry.getKey() instanceof String)) { + throw NonSerializableObjectException.unsupportedKey(entry.getKey()); + } + json.add((String) entry.getKey(), toJsonValue(entry.getValue())); + } + return json; + } + + public static JsonArray toJsonArray(final Object[] a) throws NonSerializableObjectException { + final JsonArray json = new JsonArray(); + for (final Object o : a) { + json.add(toJsonValue(o)); + } + return json; + } + + public static JsonArray toJsonArray(final Collection a) throws NonSerializableObjectException { + final JsonArray json = new JsonArray(); + if (a.isEmpty()) return json; + for (final Object o : a) { + json.add(toJsonValue(o)); + } + return json; + } + + private static void writeObjectInto(final @Nullable String modId, final Object o, final JsonObject json) throws NonSerializableObjectException { + final Class clazz = o.getClass(); + for (final JsonObject.Member member : json) { + final Field f = getField(clazz, member.getKey()); + if (f == null || !getSaveToggle(f)) continue; + + final Object def = UnsafeUtils.getUnsafely(f, o); + UnsafeUtils.setUnsafely(f, o, getValueByType(modId, f.getType(), def, member.getValue())); + } + } + + private static Field getField(final Class clazz, final String name) { + for (final Field f : clazz.getDeclaredFields()) { + if (name.equals(f.getName())) { + return f; + } + } + return null; + } + + @SuppressWarnings({"unchecked", "rawtypes"}) + private static Object getValueByType(final @Nullable String modId, final Class type, final Object def, final JsonValue value) throws NonSerializableObjectException { + if (type.isAssignableFrom(TypedEntry.class)) { + return XjsTypedEntrySerializer.fromJsonValue(modId, value); + } else if (type.isArray()) { + return toArray(modId, type, def, value); + } else if (type.isEnum()) { + return assertEnumConstant(value.asString(), (Class) type); + } else if (type.isAssignableFrom(String.class)) { + return value.asString(); + } else if (type.isAssignableFrom(Integer.class) || type.isAssignableFrom(int.class)) { + return value.asInt(); + } else if (type.isAssignableFrom(Long.class) || type.isAssignableFrom(long.class)) { + return value.asLong(); + } else if (type.isAssignableFrom(Float.class) || type.isAssignableFrom(float.class)) { + return value.asFloat(); + } else if (type.isAssignableFrom(Double.class) || type.isAssignableFrom(double.class)) { + return value.asDouble(); + } else if (type.isAssignableFrom(Boolean.class) || type.isAssignableFrom(boolean.class)) { + return value.asBoolean(); + } else if (type.isAssignableFrom(List.class)) { + return toList(modId, value, def); + } else if (type.isAssignableFrom(Set.class)) { + return new HashSet<>(toList(modId, value, def)); + } else if (type.equals(Collection.class)) { + return toList(modId, value, def); + } else if (type.isAssignableFrom(Map.class)) { + return toMap(modId, value, def); + } + final Object o = UnsafeUtils.constructUnsafely(type); + writeObjectInto(modId, o, value.asObject()); + return o; + } + + @SuppressWarnings({"unchecked", "rawtypes"}) + private static Object toArray(final @Nullable String modId, final Class type, Object def, final JsonValue value) throws NonSerializableObjectException { + final JsonArray json = value.asArray(); + final Object[] array = new Object[json.size()]; + + final Object[] defaults = (Object[]) def; + def = defaults != null && defaults.length > 0 ? defaults[0] : null; + + for (int i = 0; i < json.size(); i++) { + array[i] = getValueByType(modId, type.getComponentType(), def, json.get(i)); + } + return Arrays.copyOf(array, array.length, (Class) type); + } + + private static List toList(final @Nullable String modId, final JsonValue value, Object def) throws NonSerializableObjectException { + final Collection defaults = (Collection) def; + def = defaults != null && !defaults.isEmpty() ? defaults.iterator().next() : null; + + if (def == null) throw NonSerializableObjectException.defaultRequired(); + + final List list = new ArrayList<>(); + for (final JsonValue v : value.asArray()) { + list.add(getValueByType(modId, def.getClass(), def, v)); + } + return list; + } + + private static Map toMap(final @Nullable String modId, final JsonValue value, Object def) throws NonSerializableObjectException { + final Map map = new HashMap<>(); + + final Map defaults = (Map) def; + def = defaults != null && !defaults.isEmpty() ? defaults.entrySet().iterator().next().getValue() : null; + + if (def == null) throw NonSerializableObjectException.defaultRequired(); + + for (final JsonObject.Member member : value.asObject()) { + map.put(member.getKey(), getValueByType(modId, def.getClass(), def, member.getValue())); + } + return map; + } + + /** + * Uses a linear search algorithm to locate a value in an array, matching + * the predicate `by`. Shorthand for Stream#findFirst. + * + *

Example:

+ *
{@code
+	 *    // Find x by x.name
+	 *    Object[] vars = getObjectsWithNames();
+	 *    Optional var = find(vars, (x) -> x.name.equals("Cat"));
+	 *    // You can then get the value -> NPE
+	 *    Object result = var.get()
+	 *    // Or use an alternative. Standard java.util.Optional. -> no NPE
+	 *    Object result = var.orElse(new Object("Cat"))
+	 * }
+	 *
+	 * @param  The type of array being passed in.
+	 * @param values The actual array containing the value.
+	 * @param by A predicate which determines which value to return.
+	 * @return The value, or else {@link Optional#empty}.
+	 */
+	@NotNull
+	private static  Optional find(final T[] values, final Predicate by) {
+		for (final T val : values) {
+			if (by.test(val)) {
+				return Optional.of(val);
+			}
+		}
+		return Optional.empty();
+	}
+
+	/**
+	 * Retrieves an enum constant by name.
+	 *
+	 * @throws InvalidEnumConstantException If the given key is invalid.
+	 * @param s The name of the constant being researched.
+	 * @param clazz The enum class which contains the expected constant.
+	 * @param  The type of constant being researched.
+	 * @return The expected constant.
+	 */
+	@NotNull
+	private static > T assertEnumConstant(final String s, final Class clazz) {
+		return getEnumConstant(s, clazz).orElseThrow(() -> new InvalidEnumConstantException(s, clazz));
+	}
+
+	/**
+	 * Retrieves an enum constant by name.
+	 *
+	 * @param s The name of the constant being researched.
+	 * @param clazz The enum class which contains the expected constant.
+	 * @param  The type of constant being researched.
+	 * @return The expected constant, or else {@link Optional#empty}.
+	 */
+	private static > Optional getEnumConstant(final String s, final Class clazz) {
+		return find(clazz.getEnumConstants(), e -> isFormatted(e, s));
+	}
+
+	/**
+	 * Determines whether a string matches the given enum constant's name, ignoring
+	 * case and underscores (_).
+	 *
+	 * @param e The enum constant being compared.
+	 * @param s The string identifier for this constant.
+	 * @param  The type of enum value.
+	 * @return Whether this string is a valid identifier for the constant.
+	 */
+	private static > boolean isFormatted(final E e, final String s) {
+		final String id = e.name().replace("_", "");
+		return id.equalsIgnoreCase(s.replace("_", ""));
+	}
+}
diff --git a/src/main/java/net/frozenblock/lib/config/api/instance/xjs/XjsOps.java b/src/main/java/net/frozenblock/lib/config/api/instance/xjs/XjsOps.java
new file mode 100644
index 0000000..d023a01
--- /dev/null
+++ b/src/main/java/net/frozenblock/lib/config/api/instance/xjs/XjsOps.java
@@ -0,0 +1,345 @@
+/*
+ * Copyright (C) 2024 FrozenBlock
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see .
+ */
+
+package net.frozenblock.lib.config.api.instance.xjs;
+
+import com.mojang.datafixers.util.Pair;
+import com.mojang.serialization.DataResult;
+import com.mojang.serialization.DynamicOps;
+import com.mojang.serialization.MapLike;
+import org.jetbrains.annotations.Nullable;
+import xjs.data.*;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.function.BiConsumer;
+import java.util.function.Consumer;
+import java.util.stream.Stream;
+
+/*
+ Source: https://github.com/PersonTheCat/CatLib
+ License: GNU GPL-3.0
+ */
+@SuppressWarnings("unused")
+public class XjsOps implements DynamicOps {
+
+	public static final XjsOps INSTANCE = new XjsOps(false);
+	public static final XjsOps COMPRESSED = new XjsOps(true);
+
+	private static final JsonValue EMPTY = JsonLiteral.jsonNull();
+
+	private final boolean compressed;
+
+	private XjsOps(final boolean compressed) {
+		this.compressed = compressed;
+	}
+
+	@Override
+	public JsonValue empty() {
+		return EMPTY;
+	}
+
+	@Override
+	public  U convertTo(final DynamicOps outOps, final JsonValue input) {
+		if (input == null || input.isNull()) {
+			return outOps.empty();
+		} else if (input.isObject()) {
+			return this.convertMap(outOps, input);
+		} else if (input.isArray()) {
+			return this.convertList(outOps, input);
+		} else if (input.isString()) {
+			return outOps.createString(input.asString());
+		} else if (input.isBoolean()) {
+			return outOps.createBoolean(input.asBoolean());
+		} else if (input.isNumber()) {
+			return this.toNumber(outOps, input.asDouble());
+		}
+		return null;
+	}
+
+	private  U toNumber(final DynamicOps outOps, final double number) {
+		if ((byte) number == number) {
+			return outOps.createByte((byte) number);
+		} else if ((short) number == number) {
+			return outOps.createShort((short) number);
+		} else if ((int) number == number) {
+			return outOps.createInt((int) number);
+		} else if ((float) number == number) {
+			return outOps.createFloat((float) number);
+		}
+		return outOps.createDouble(number);
+	}
+
+	@Override
+	public DataResult getNumberValue(final JsonValue input) {
+		if (input == null || input.isNull()) {
+			return DataResult.error(() -> "Not a number: null");
+		} else if (input.isNumber()) {
+			return DataResult.success(input.asDouble());
+		} else if (input.isBoolean()) {
+			return DataResult.success(input.asBoolean() ? 1 : 0);
+		}
+		if (this.compressed && input.isString()) {
+			try {
+				return DataResult.success(Integer.parseInt(input.asString()));
+			} catch (final NumberFormatException e) {
+				return DataResult.error(() -> "Not a number: " + e + " " + input);
+			}
+		}
+		return DataResult.error(() -> "Not a number: " + input);
+	}
+
+	@Override
+	public JsonValue createNumeric(final Number i) {
+		return Json.value(i.doubleValue());
+	}
+
+	@Override
+	public DataResult getBooleanValue(final JsonValue input) {
+		if (input == null || input.isNull()) {
+			return DataResult.error(() -> "Not a boolean: null");
+		} else if (input.isBoolean()) {
+			return DataResult.success(input.asBoolean());
+		} else if (input.isNumber()) {
+			return DataResult.success(input.asDouble() != 0);
+		}
+		return DataResult.error(() -> "Not a boolean: " + input);
+	}
+
+	@Override
+	public JsonValue createBoolean(final boolean value) {
+		return Json.value(value);
+	}
+
+	@Override
+	public DataResult getStringValue(final JsonValue input) {
+		if (input == null || input.isNull()) {
+			return DataResult.error(() -> "Not a string: null");
+		} else if (input.isString()) {
+			return DataResult.success(input.asString());
+		} else if (this.compressed && input.isNumber()) {
+			return DataResult.success(String.valueOf(input.asDouble()));
+		}
+		return DataResult.error(() -> "Not a string: " + input);
+	}
+
+	@Override
+	public JsonValue createString(final String value) {
+		return Json.value(value);
+	}
+
+	@Override
+	public DataResult mergeToList(final JsonValue list, final JsonValue value) {
+		if (list == null || list.isNull()) {
+			return DataResult.success(new JsonArray().add(value));
+		} else if (list.isArray()) {
+			return DataResult.success(new JsonArray().addAll(list.asArray()).add(value));
+		}
+		return DataResult.error(() -> "mergeToList called with not a list: " + list, list);
+	}
+
+	@Override
+	public DataResult mergeToList(final JsonValue list, final List values) {
+		if (list == null || list.isNull()) {
+			final JsonArray result = new JsonArray();
+			values.forEach(result::add);
+			return DataResult.success(result);
+		} else if (list.isArray()) {
+			final JsonArray result = (JsonArray) list.asArray().shallowCopy();
+			values.forEach(result::add);
+			return DataResult.success(result);
+		}
+		return DataResult.error(() -> "mergeToList called with not a list: " + list, list);
+	}
+
+	@Override
+	public DataResult mergeToMap(final JsonValue map, final JsonValue key, final JsonValue value) {
+		if (!(map == null || map.isObject() || map.isNull())) {
+			return DataResult.error(() -> "mergeToMap called with not a map: " + map, map);
+		} else if (!(key.isString() || (this.compressed && isPrimitiveLike(key)))) {
+			final String msg = "key is not a string: " + key;
+			return map != null ? DataResult.error(() -> msg, map) : DataResult.error(() -> msg);
+		}
+		if (map == null || map.isNull()) {
+			return DataResult.success(new JsonObject().add(asPrimitiveString(key), value));
+		}
+		return DataResult.success(new JsonObject().addAll(map.asObject()).add(asPrimitiveString(key), value));
+	}
+
+	@Override
+	public DataResult mergeToMap(final JsonValue map, MapLike values) {
+		if (!(map == null || map.isObject() || map.isNull())) {
+			return DataResult.error(() -> "mergeToMap called with not a map: " + map, map);
+		}
+		final JsonObject output = new JsonObject();
+		if (map != null && map.isObject()) {
+			output.addAll(map.asObject());
+		}
+		final List missed = new ArrayList<>();
+		values.entries().forEach(entry -> {
+			final JsonValue key = entry.getFirst();
+			if (key.isString() || (this.compressed && isPrimitiveLike(key))) {
+				output.add(asPrimitiveString(key), entry.getSecond());
+			} else {
+				missed.add(key);
+			}
+		});
+		if (!missed.isEmpty()) {
+			return DataResult.error(() -> "some keys are not strings: " + missed, output);
+		}
+		return DataResult.success(output);
+	}
+
+	@Override
+	public DataResult>> getMapValues(final JsonValue input) {
+		if (input == null || !input.isObject()) {
+			return DataResult.error(() -> "Not an XJS object: " + input);
+		}
+		final Stream.Builder> builder = Stream.builder();
+		for (final JsonObject.Member member : input.asObject()) {
+			final JsonValue value = member.getValue();
+			builder.add(Pair.of(Json.value(member.getKey()), value.isNull() ? null : value));
+		}
+		return DataResult.success(builder.build());
+	}
+
+	@Override
+	public DataResult>> getMapEntries(final JsonValue input) {
+		if (input == null || !input.isObject()) {
+			return DataResult.error(() -> "Not an XJS object: " + input);
+		}
+		return DataResult.success(c -> {
+			for (final JsonObject.Member member : input.asObject()) {
+				final JsonValue value = member.getValue();
+				c.accept(Json.value(member.getKey()), value.isNull() ? null : value);
+			}
+		});
+	}
+
+	@Override
+	public DataResult> getMap(final JsonValue input) {
+		if (input == null || !input.isObject()) {
+			return DataResult.error(() -> "Not an XJS object: " + input);
+		}
+		return DataResult.success(new XJSMapLike(input.asObject()));
+	}
+
+	@Override
+	public JsonValue createMap(final Stream> map) {
+		final JsonObject result = new JsonObject();
+		map.forEach(p -> {
+			final JsonValue v = p.getSecond();
+			result.add(p.getFirst().asString(), v != null ? v : EMPTY);
+		});
+		return result;
+	}
+
+	@Override
+	public DataResult> getStream(final JsonValue input) {
+		if (input == null || !input.isArray()) {
+			return DataResult.error(() -> "Not an XJS array: " + input);
+		}
+		final Stream.Builder builder = Stream.builder();
+		for (final JsonValue value : input.asArray()) {
+			builder.add(value.isNull() ? null : value);
+		}
+		return DataResult.success(builder.build());
+	}
+
+	@Override
+	public DataResult>> getList(final JsonValue input) {
+		if (input == null || !input.isArray()) {
+			return DataResult.error(() -> "Not an XJS array: + " + input);
+		}
+		return DataResult.success(c -> {
+			for (final JsonValue value : input.asArray()) {
+				c.accept(value.isNull() ? null : value);
+			}
+		});
+	}
+
+	@Override
+	public JsonValue createList(final Stream input) {
+		final JsonArray result = new JsonArray();
+		input.forEach(v -> result.add(v != null ? v : EMPTY));
+		return result;
+	}
+
+	@Override
+	public JsonValue remove(final JsonValue input, final String key) {
+		if (input != null && input.isObject()) {
+			final JsonObject result = new JsonObject();
+			for (final JsonObject.Member member : input.asObject()) {
+				if (!member.getKey().equals(key)) {
+					result.add(member.getKey(), member.getValue());
+				}
+			}
+			return result;
+		}
+		return input;
+	}
+
+	@Override
+	public boolean compressMaps() {
+		return this.compressed;
+	}
+
+	@Override
+	public String toString() {
+		return "XJS";
+	}
+
+	private record XJSMapLike(JsonObject object) implements MapLike {
+
+		@Nullable
+		@Override
+		public JsonValue get(final JsonValue key) {
+			final JsonValue value = this.object.get(key.asString());
+			return value != null && value.isNull() ? null : value;
+		}
+
+		@Nullable
+		@Override
+		public JsonValue get(final String key) {
+			final JsonValue value = this.object.get(key);
+			return value != null && value.isNull() ? null : value;
+		}
+
+		@Override
+		public Stream> entries() {
+			final Stream.Builder> builder = Stream.builder();
+			for (final JsonObject.Member member : this.object) {
+				final JsonValue value = member.getValue();
+				builder.add(Pair.of(Json.value(member.getKey()), value.isNull() ? null : value));
+			}
+			return builder.build();
+		}
+
+		@Override
+		public String toString() {
+			return "XJSMapLike[" + this.object + "]";
+		}
+	}
+
+	private static boolean isPrimitiveLike(final JsonValue value) {
+		return value.isBoolean() || value.isString() || value.isNumber();
+	}
+
+	private static String asPrimitiveString(final JsonValue value) {
+		return value.isNumber() ? String.valueOf(value.asDouble()) : value.asString();
+	}
+}
diff --git a/src/main/java/net/frozenblock/lib/config/api/instance/xjs/XjsTypedEntrySerializer.java b/src/main/java/net/frozenblock/lib/config/api/instance/xjs/XjsTypedEntrySerializer.java
new file mode 100644
index 0000000..0936e9b
--- /dev/null
+++ b/src/main/java/net/frozenblock/lib/config/api/instance/xjs/XjsTypedEntrySerializer.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright (C) 2024 FrozenBlock
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see .
+ */
+
+package net.frozenblock.lib.config.api.instance.xjs;
+
+import com.mojang.datafixers.util.Pair;
+import com.mojang.serialization.Codec;
+import com.mojang.serialization.DataResult;
+import net.frozenblock.lib.FrozenLogUtils;
+import net.frozenblock.lib.FrozenSharedConstants;
+import net.frozenblock.lib.config.api.entry.TypedEntry;
+import net.frozenblock.lib.config.api.entry.TypedEntryType;
+import net.frozenblock.lib.config.api.registry.ConfigRegistry;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+import xjs.data.JsonValue;
+
+import java.util.Collection;
+
+public final class XjsTypedEntrySerializer {
+    private XjsTypedEntrySerializer() {}
+
+    @SuppressWarnings({"unchecked", "rawtypes"})
+    public static JsonValue toJsonValue(final TypedEntry src) throws NonSerializableObjectException {
+        if (src != null) {
+            TypedEntryType type = src.type();
+            if (type != null) {
+                Codec codec = type.codec();
+                if (codec != null) {
+                    var encoded = codec.encodeStart(XjsOps.INSTANCE, src.value());
+                    if (encoded != null && encoded.error().isEmpty()) {
+                        var optional = encoded.result();
+                        if (optional.isPresent()) {
+                            return (JsonValue) optional.get();
+                        }
+                    }
+                }
+            }
+        }
+        throw new NonSerializableObjectException("Failed to serialize typed entry " + src);
+    }
+
+    public static TypedEntry fromJsonValue(final String modId, final JsonValue value) throws NonSerializableObjectException {
+        TypedEntry modEntry = getFromRegistry(modId, value, ConfigRegistry.getTypedEntryTypesForMod(modId));
+        if (modEntry != null) {
+            return modEntry;
+        }
+        throw new NonSerializableObjectException("Failed to deserialize typed entry" + value);
+    }
+
+    @Nullable
+    @SuppressWarnings("unchecked")
+    private static  TypedEntry getFromRegistry(final String modId, final JsonValue value, final @NotNull Collection> registry) throws ClassCastException {
+        for (TypedEntryType entryType : registry) {
+            TypedEntryType newType = (TypedEntryType) entryType;
+            TypedEntry entry = getFromType(modId, value, newType);
+            if (entry != null) {
+                return entry;
+            }
+        }
+        return null;
+    }
+
+    @Nullable
+    private static  TypedEntry getFromType(String modId, JsonValue value, @NotNull TypedEntryType entryType) throws ClassCastException {
+        if (!entryType.modId().equals(modId))
+            return null;
+
+        var codec = entryType.codec();
+        DataResult> result = codec.decode(XjsOps.INSTANCE, value);
+        if (result.error().isPresent())
+            return null;
+
+        var optional = result.result();
+        if (optional.isEmpty()) return null;
+
+        Pair pair = optional.get();
+        T first = pair.getFirst();
+        TypedEntry entry = TypedEntry.create(entryType, first);
+        FrozenLogUtils.log("Built typed entry " + entry, FrozenSharedConstants.UNSTABLE_LOGGING);
+        return entry;
+    }
+}
\ No newline at end of file
diff --git a/src/main/java/net/frozenblock/lib/config/api/instance/xjs/XjsUtils.java b/src/main/java/net/frozenblock/lib/config/api/instance/xjs/XjsUtils.java
new file mode 100644
index 0000000..04721ed
--- /dev/null
+++ b/src/main/java/net/frozenblock/lib/config/api/instance/xjs/XjsUtils.java
@@ -0,0 +1,859 @@
+/*
+ * Copyright (C) 2024 FrozenBlock
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see .
+ */
+
+package net.frozenblock.lib.config.api.instance.xjs;
+
+import com.mojang.datafixers.util.Either;
+import com.mojang.serialization.Codec;
+import lombok.experimental.UtilityClass;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.mutable.MutableObject;
+import org.jetbrains.annotations.CheckReturnValue;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+import personthecat.fresult.Result;
+import personthecat.fresult.Void;
+import xjs.data.*;
+import xjs.data.comments.CommentType;
+import xjs.data.exception.SyntaxException;
+import xjs.data.serialization.JsonContext;
+import xjs.data.serialization.writer.JsonWriterOptions;
+
+import java.io.*;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.Optional;
+import java.util.function.Consumer;
+
+import static java.util.Optional.empty;
+import static net.frozenblock.lib.config.api.instance.xjs.InvalidEnumConstantException.f;
+
+/*
+ Source: https://github.com/PersonTheCat/CatLib
+ License: GNU GPL-3.0
+ */
+/**
+ * A collection of convenience methods for interacting with XJS objects. Unlike
+ * the original methods inside of {@link JsonObject}, most of the utilities in this
+ * class return values wrapped in {@link Optional}, instead of null.
+ * 

+ * In a future version of this library (via Exjson/xjs-core), JSON objects will + * support returning {@link Optional} out of the box, as well as the options to + * flatten arrays, support additional data types, and more. As a result, most + * of these utilities will eventually be deprecated. + *

+ */ +@Slf4j +@UtilityClass +@SuppressWarnings("unused") +public class XjsUtils { + + /** + * Reads a {@link JsonObject} from the given file. + * + * @param file The file containing the serialized JSON object. + * @return The deserialized object, or else {@link Optional#empty}. + */ + public static Optional readJson(final File file) { + return Result + .define(FileNotFoundException.class, Result::WARN) + .define(SyntaxException.class, e -> { throw new JsonFormatException(f(file.getPath(), e)); }) + .suppress(() -> Json.parse(file).asObject()) + .get(); + } + + /** + * Reads a {@link JsonObject} from the given input stream. + * + * @param is The stream containing the serialized JSON object. + * @return The deserialized object, or else {@link Optional#empty}. + */ + public static Optional readJson(final InputStream is) { + return Result + .define(IOException.class, Result::WARN) + .define(SyntaxException.class, r -> { throw new JsonFormatException("Reading data"); }) + .suppress(() -> Json.parse(is).asObject()) + .get(); + } + + /** + * Variant of {@link #readJson(File)} which ignores syntax errors + * and simply returns {@link Optional#empty} if any error occurs. + * + * @param file The file containing the serialized JSON object. + * @return The deserialized object, or else {@link Optional#empty}. + */ + public static Optional readSuppressing(final File file) { + return Result.suppress(() -> Json.parse(file).asObject()).get(Result::WARN); + } + + /** + * Variant of {@link #readSuppressing(File)} which reads directly + * from an {@link InputStream}. + * + * @param is The data containing the serialized JSON object. + * @return The deserialized object, or else {@link Optional#empty}. + */ + public static Optional readSuppressing(final InputStream is) { + return Result.suppress(() -> Json.parse(is).asObject()).get(Result::WARN); + } + + /** + * Reads any JSON data from the given string contents. + * + * @param contents The raw JSON data being parsed. + * @return The parsed JSON data, or else {@link Result#err} containing the exception. + */ + public static Result readValue(final String contents) { + return Result.of(() -> Json.parse(contents)).ifErr(Result::IGNORE); + } + + /** + * Reads an object from the given data when provided a codec. + * + * @param codec Instructions for deserializing the data. + * @param value The actual data being deserialized. + * @param The type of object being returned. + * @return The deserialized object, or else {@link Optional#empty}. + */ + public static Optional readOptional(final Codec codec, final JsonValue value) { + return codec.parse(XjsOps.INSTANCE, value).result(); + } + + /** + * Reads an object from the given data, or else throws an exception. + * + * @param codec Instructions for deserializing the data. + * @param value The actual data being deserialized. + * @param The type of object being returned. + * @return The deserialized object. + */ + public static T readThrowing(final Codec codec, final JsonValue value) { + return codec.parse(XjsOps.INSTANCE, value).getOrThrow(partial -> { + throw new JsonFormatException(partial); + }); + } + + /** + * Writes a regular {@link JsonObject} to the disk. The format of this output file + * is automatically determined by its extension. + *

+ * Any file extended with .json will be written in regular JSON + * format. All other extensions will implicitly be treated as XJS. + *

+ *

+ * No {@link IOException}s will be thrown by this method. Instead, they will be + * logged and simply returned for the caller to optionally throw. + *

+ *

+ * All other exceptions will be thrown by this method. + *

+ * + * @param json The JSON data being serialized. + * @param file The destination file containing these data. + * @return A result which potentially contains an error. + */ + public static Result writeJson(final JsonObject json, final File file) { + return Result.with(() -> new FileWriter(file), writer -> { json.write(file); }) + .ifErr(e -> log.error("Writing file", e)); + } + + /** + * Writes the input value as JSON, returning {@link Optional#empty} if any errors + * occur in the process. + * + * @param codec The codec responsible for the serialization. + * @param a The data being serialized. + * @param The type of data being serialized. + * @return The serialized data, or else {@link Optional#empty}. + */ + public static Optional writeSuppressing(final Codec codec, final @Nullable A a) { + if (a == null) return Optional.of(JsonLiteral.jsonNull()); + return codec.encodeStart(XjsOps.INSTANCE, a).result(); + } + + /** + * Writes the input value as JSON, or else throwing an exception if any errors + * occur in the process. + * + * @param codec The codec responsible for the serialization. + * @param a The data being serialized. + * @param The type of data being serialized. + * @return The serialized data. + */ + public static JsonValue writeThrowing(final Codec codec, final @Nullable A a) { + if (a == null) return JsonLiteral.jsonNull(); + return codec.encodeStart(XjsOps.INSTANCE, a).result() + .orElseThrow(() -> new JsonFormatException("Writing object: " + a)); + } + + /** + * Reads a file from the disk and updates it. + *

+ * For example, + *

+ *
{@code
+	 *   XJSTools.updateJson(file, json -> {
+	 *      json.set("hello", "world");
+	 *   });
+	 * }
+ *

+ * The output of this expression will be applied to the original file. + *

+ * @param file the file containing JSON data. + * @param f Instructions for updating the JSON data. + * @return A result which potentially contains an error. + */ + @CheckReturnValue + public static Result updateJson(final File file, final Consumer f) { + // If #readJson returned empty, it's because the file didn't exist. + final JsonObject json = readJson(file).orElseGet(JsonObject::new); + f.accept(json); + return writeJson(json, file); + } + + /** + * Gets the default formatting options, guaranteed to never print a `\r` character, + * which Minecraft does not print correctly in-game. + * + * @return The default formatting options without \r. + */ + public static JsonWriterOptions noCr() { + return JsonContext.getDefaultFormatting().setEol("\n"); + } + + /** + * Updates a single value in a JSON object based on a full, dotted path. + *

+ * For example, + *

+ *
+	 *   /update my_json path.to.field true
+	 * 
+ * @param json The JSON object containing this path. + * @param value The updated value to set at this path. + */ + public static void setValueFromPath(final JsonObject json, final JsonPath path, @Nullable final JsonValue value) { + if (path.isEmpty()) { + return; + } + final Either lastVal = path.get(path.size() - 1); + final JsonContainer parent = getLastContainer(json, path); + // This will ideally be handled by XJS in the future. + if (value != null && value.getLinesAbove() == -1 && condenseNewValue(path, parent)) { + value.setLinesAbove(0); + } + setEither(parent, lastVal, value); + } + + /** + * Determines whether to format an incoming value as condensed. + * + * @param path The path to the value being set. + * @param container The parent container for this new value. + * @return true, if the value should be condensed. + */ + private static boolean condenseNewValue(final JsonPath path, final JsonContainer container) { + if (container.isEmpty()) { + return true; + } + final int s = path.size() == 1 && container.isObject() ? 1 : 0; + for (int i = s; i < container.size(); i++) { + if (container.getReference(i).getOnly().getLinesAbove() == 0) { + return true; + } + } + return false; + } + + /** + * Gets a single value in a JSON object based on a full, dotted path. + * + * @param json The JSON object containing this path. + * @param path The JSON path. + * @return The value at this location, or else {@link Optional#empty}. + */ + public static Optional getValueFromPath(final JsonObject json, final JsonPath path) { + if (path.isEmpty()) { + return empty(); + } + final Either lastVal = path.get(path.size() - 1); + return getEither(getLastContainer(json, path), lastVal); + } + + /** + * Retrieves the last JsonObject or JsonArray represented by this path. + *

+ * For example, a path of + *

+ *
+	 *   object1.array2.object3.value4
+	 * 
+ *

+ * will return object3 when passed into this method. + *

+ *

+ * If no object or array exists at this location, a new container will be created at this + * location and returned by the method. + *

+ * @param json The JSON object containing this path. + * @param path The JSON path. + * @return The value at this location, the original json, or else a new container. + */ + public static JsonContainer getLastContainer(final JsonObject json, final JsonPath path) { + if (path.isEmpty()) { + return json; + } + JsonContainer current = json; + for (int i = 0; i < path.size() - 1; i++) { + final Either val = path.get(i); + final Either peek = path.get(i + 1); + + if (val.right().isPresent()) { // Index + current = getOrTryNew(current.asArray(), val.right().get(), peek); + } else if (peek.left().isPresent()) { // Key -> key -> object + current = current.asObject() + .getOptional(val.left().orElseThrow(), JsonValue::asObject) + .orElseGet(Json::object); + } else { // Key -> index -> array + current = current.asObject() + .getOptional(val.left().orElseThrow(), JsonValue::asArray) + .orElseGet(Json::array); + } + } + return current; + } + + /** + * Gets the index of the last available element in this path, or else -1. + * + *

For example, when given the following JSON object:

+ *
+	 *   a:{b:[]}
+	 * 
+ *

And the following path:

+ *
+	 *   a.b[0].c
+	 * 
+ *

An index of 1 (pointing to b) will be returned.

+ * + * @param json The JSON object containing the data being inspected. + * @param path The path to the expected data, which may or may not exist. + * @return The index to the last matching element, or else -1. + */ + public static int getLastAvailable(final JsonObject json, final JsonPath path) { + final MutableObject current = new MutableObject<>(json); + int index = -1; + + for (final Either component : path) { + component.ifLeft(key -> { + final JsonValue value = current.getValue(); + if (value.isObject()) { + current.setValue(value.asObject().get(key)); + } else { + current.setValue(null); + } + }).ifRight(i -> { + final JsonValue value = current.getValue(); + if (value.isArray() && i < value.asArray().size()) { + current.setValue(value.asArray().get(i)); + } else { + current.setValue(null); + } + }); + if (current.getValue() == null) { + return index; + } + index++; + } + return index; + } + + /** + * Attempts to resolve the closest matching path in the given JSON data. + * + *

Essentially, this method accepts the canonicalized path of an expected value for the + * data being represented. It will account for the possibility that object arrays may be + * expressed as singletons and return the actual path, should any be used.

+ * + * @param json The object being inspected. + * @param path The canonicalized path to the expected value + * @return The actual path to the value, or else the canonical path. + */ + public static JsonPath getClosestMatch(final JsonObject json, final JsonPath path) { + final MutableObject current = new MutableObject<>(json); + final JsonPath.JsonPathBuilder builder = JsonPath.builder(); + + for (int i = 0; i < path.size(); i++) { + path.get(i).ifLeft(key -> { + JsonValue value = current.getValue(); + while (value.isArray() && !value.asArray().isEmpty()) { + builder.index(0); + value = value.asArray().get(0); + } + if (value.isObject() && value.asObject().has(key)) { + current.setValue(value.asObject().get(key)); + builder.key(key); + } else { + current.setValue(null); + } + }).ifRight(index -> { + final JsonValue value = current.getValue(); + if (value.isArray() && value.asArray().size() > index) { + current.setValue(value.asArray().get(index)); + builder.index(index); + } else if (!(value.isObject() && index == 0)) { + current.setValue(null); + } + }); + if (current.getValue() == null) { + return builder.build().append(path, i); + } + } + return builder.build(); + } + + /** + * Filters values from the given JSON object according to a list of expected paths. + * + * @param json The JSON object and source being transformed. + * @param paths The paths expected to stay in the output. + * @return A transformed object containing only the expected paths. + */ + public static JsonObject filter(final JsonObject json, final Collection paths) { + return filter(json, paths, false); + } + + /** + * Filters values from the given JSON object according to a list of expected paths. + * + * @param json The JSON object and source being transformed. + * @param paths The paths expected to stay in the output. + * @param blacklist Whether to optionally blacklist these paths. + * @return A transformed object containing only the expected paths. + */ + public static JsonObject filter(final JsonObject json, final Collection paths, final boolean blacklist) { + final JsonObject clone = (JsonObject) json.deepCopy(); + // Flag each path as used so anything else will get removed. + paths.forEach(path -> path.getValue(clone)); + return skip(clone, blacklist); + } + + /** + * Generates a new {@link JsonObject} containing only the values that were or were not + * used in the original. + * + * @param json The original JSON object being transformed. + * @param used true to skip used values, false to skip unused. + * @return A new JSON object with these values trimmed out. + */ + public static JsonObject skip(final JsonObject json, final boolean used) { + final JsonObject generated = (JsonObject) new JsonObject().setDefaultMetadata(json); + final StringBuilder skipped = new StringBuilder(); + + for (final JsonObject.Member member : json) { + final JsonValue value = member.getOnly(); + final String name = member.getKey(); + + if (member.getReference().isAccessed() != used) { + if (skipped.length() > 0) { + value.prependComment("Skipped " + skipped); + skipped.setLength(0); + } + if (value.isObject()) { + generated.add(name, skip(value.asObject(), used)); + } else if (value.isArray()) { + generated.add(name, skip(value.asArray(), used)); + } else { + generated.add(name, value); + } + } else if (skipped.length() == 0) { + skipped.append(name); + } else { + skipped.append(", ").append(name); + } + } + if (skipped.length() > 0) { + generated.prependComment(CommentType.INTERIOR, "Skipped " + skipped); + } + return generated; + } + + /** + * Generates a new {@link JsonArray} containing only the values that were or were not + * used in the original. + * + * @param json The original JSON array being transformed. + * @param used true to skip used values, false to skip unused. + * @return A new JSON array with these values trimmed out. + */ + public static JsonArray skip(final JsonArray json, final boolean used) { + final JsonArray generated = (JsonArray) new JsonArray().setDefaultMetadata(json); + int lastIndex = 0; + int index = 0; + + for (final JsonReference reference : json.references()) { + final JsonValue value = reference.getOnly(); + if (reference.isAccessed() != used) { + if (index == lastIndex + 1) { + value.prependComment("Skipped " + (index - 1)); + } else if (index > lastIndex) { + value.prependComment("Skipped " + lastIndex + " ~ " + (index - 1)); + } + if (value.isObject()) { + generated.add(skip(value.asObject(), used)); + } else if (value.isArray()) { + generated.add(skip(value.asArray(), used)); + } else { + generated.add(value); + } + lastIndex = index + 1; + } + index++; + } + if (index == lastIndex + 1) { + generated.prependComment(CommentType.INTERIOR, "Skipped " + (index - 1)); + } else if (index > lastIndex) { + generated.prependComment(CommentType.INTERIOR, "Skipped " + lastIndex + " ~ " + (index - 1)); + } + return generated; + } + + /** + * Retrieves a list of paths adjacent to the input path. This can be used to provide + * command suggestions as the user is walking through this container. + *

+ * For example, when given the following JSON object: + *

+ *
+	 *   a: [
+	 *     {
+	 *       b: { b1: true }
+	 *       c: { c1: false }
+	 *     }
+	 *   ]
+	 * 
+ *

+ * and the following incomplete command: + *

+ *
+	 *   /update my_json a[0]
+	 * 
+ *

+ * the following paths will be returned: + *

+ *
    + *
  • a[0].b
  • + *
  • a[0].c
  • + *
+ * @param json The JSON data containing these paths. + * @param path The JSON path. + * @return A list of all adjacent paths. + */ + public static List getPaths(final JsonObject json, final JsonPath path) { + final JsonValue container = Result.of(() -> getLastContainer(json, path)) + .get(Result::WARN) + .orElse(json); + int end = path.size() - 1; + if (end < 0) { + return getNeighbors("", container); + } + final Optional v = getEither(container, path.get(end)) + .filter(value -> value.isObject() || value.isArray()); + if (v.isPresent()) { + end++; // The full path is a valid container -> use it. + } + final String dir = JsonPath.serialize(path.subList(0, end)); + return getNeighbors(dir, v.orElse(container)); + } + + /** + * Retrieves a list of paths in the given container. + * + * @param dir The path to this container, as a string. + * @param container The {@link JsonObject} or {@link JsonArray} at this location. + * @return A formatted list of all members at this location. + */ + private static List getNeighbors(final String dir, final JsonValue container) { + final List neighbors = new ArrayList<>(); + if (container.isObject()) { + for (JsonObject.Member member : container.asObject()) { + final String name = member.getKey(); + neighbors.add(dir.isEmpty() ? name : f("{}.{}", dir, name)); + } + } else if (container.isArray()) { + for (int i = 0; i < container.asArray().size(); i++) { + neighbors.add(f("{}[{}]", dir, i)); + } + } + return neighbors; + } + + /** + * Attempts to retrieve an object or an array. Creates a new one, if absent. + * + * @throws IndexOutOfBoundsException If index > array.size() + * @param array The JSON array containing the researched data. + * @param index The index of the data in the array. + * @param type The path element at this index, indicating either a key or an index. + * @return Either a JSON object or array, whichever is at this location. + */ + private static JsonContainer getOrTryNew(final JsonArray array, final int index, final Either type) { + if (index == array.size()) { // The value must be added. + type.ifLeft(s -> array.add(new JsonObject())) + .ifRight(i -> array.add(new JsonArray())); + } // if index >= newSize -> index out of bounds + return array.get(index).asContainer(); + } + + /** + * Attempts to retrieve either an object or an array from a JSON container. + *

+ * If this value is a string, it will be treated as a key. If the value is a + * number, it will be treated as an index. + *

+ * @param container Either a JSON object or array + * @param either The accessor for the value at this location. + */ + private static Optional getEither(final JsonValue container, final Either either) { + if (either.left().isPresent()) { + return nullable(container.asObject().get(either.left().get())); + } else if (either.right().isPresent()) { + final JsonArray array = container.asArray(); + final int index = either.right().get(); + return index < array.size() ? Optional.of(array.get(index)) : empty(); + } + throw new UnreachableException(); + } + + /** + * Attempts to set a value in a container which may either be an object or an array. + * + * @param container Either a JSON object or array. + * @param either The accessor for this value, either a key or an index. + * @param value The value to set at this location. + */ + private static void setEither(final JsonValue container, final Either either, @Nullable final JsonValue value) { + if (either.left().isPresent()) { + if (value == null) { + container.asObject().remove(either.left().get()); + } else if (value.hasComments()) { + container.asObject().set(either.left().get(), value); + } else { + final String key = either.left().get(); + final JsonObject object = container.asObject(); + object.set(key, value); + } + } else if (either.right().isPresent()) { // Just to stop the linting. + if (value == null) { + container.asArray().remove(either.right().get()); + } else if (value.hasComments()) { + container.asArray().set(either.right().get(), value); + } else { + final int index = either.right().get(); + final JsonArray array = container.asArray(); + setOrAdd(array, index, value); + } + } + } + + /** + * Adds a value to an array by name. The value will be coerced into an array, if needed. + *

+ * For example, when adding a string to the following JSON field: + *

+ *
+	 *   field: hello
+	 * 
+ *

+ * the field will be updated as follows: + *

+ *
+	 *   field: [
+	 *     hello
+	 *     world
+	 *   ]
+	 * 
+ * @param json The JSON object containing these data. + * @param field The key for updating an array. + * @param value The value being added to the array. + * @return The original json passed in. + */ + public static JsonObject addToArray(final JsonObject json, final String field, final JsonValue value) { + JsonValue array = json.get(field); + if (array == null) { + array = new JsonArray(); + json.add(field, array); + } else if (!array.isArray()) { + array = new JsonArray().add(array); + json.set(field, array); + } + array.asArray().add(value); + return json; + } + + /** + * Sets the value at the given index, or else if index == array.size(), adds it. + * + * @param array The array being added into. + * @param index The index of the value being set. + * @param value The value being set. + * @return array, for method chaining. + * @throws IndexOutOfBoundsException If index < 0 || index > size + */ + public static JsonArray setOrAdd(final JsonArray array, final int index, final JsonValue value) { + if (index == array.size()) { + return array.add(value); + } + return array.set(index, value); + } + + /** + * Returns a list of {@link JsonObject}s from the given source. + *

+ * Note that the values in this array will be coerced into {@link JsonObject}s. + *

+ *

+ * These objects can be stored in any number of dimensions, but will be coerced + * into a single dimensional array. For example, each of the following values will + * yield single dimensional object arrays: + *

+ *
    + *
  • array: [{},{},{}]
  • + *
  • array: [[{}],[[{}]]]
  • + *
  • array: {}
  • + *
+ * @param json The JSON parent containing the array. + * @param field The field where this array is stored. + * @return The JSON array in the form of a regular list. + */ + public static List getObjectArray(final JsonObject json, final String field) { + final List array = new ArrayList<>(); + json.getOptional(field).map(JsonValue::intoArray) + .ifPresent(a -> flatten(array, a)); + return array; + } + + /** + * Recursively flattens object arrays into a single dimension. + * + * @param array The list of JSON objects being accumulated into. + * @param source The original JSON array data source. + */ + private static void flatten(final List array, final JsonArray source) { + for (final JsonValue value: source) { + if (value.isArray()) { + flatten(array, value.asArray()); + } else if (value.isObject()) { + array.add(value.asObject()); + } else { + throw new JsonFormatException(f("Expected an array or object: {}", value)); + } + } + } + + /** + * Variant of {@link #getObjectArray} which does not coerce values into objects. + *

+ * Note that any non-object values in this array will not be returned. + *

+ *

+ * For example, when given the following JSON array: + *

+ *
+	 *   array: [{},{},true,[[{}]]]
+	 * 
+ *

+ * This array will be returned: + *

+ *
+	 *   [{},{},{}]
+	 * 
+ * @param json The JSON object containing the array. + * @param field The key where this array is stored. + * @return A list of all {@link JsonObject}s at this location. + */ + public static List getRegularObjects(final JsonObject json, final String field) { + final List list = new ArrayList<>(); + final JsonArray array = json.getOptional(field) + .map(JsonValue::intoArray) + .orElseGet(JsonArray::new); + flattenRegularObjects(list, array); + return list; + } + + /** + * Variant of {@link #flatten} which does not coerce values into objects. + * + * @param array The list of JSON objects being accumulated into. + * @param source The original JSON array data source. + */ + private static void flattenRegularObjects(final List array, final JsonArray source) { + for (final JsonValue value: source) { + if (value.isArray()) { + flattenRegularObjects(array, value.asArray()); + } else if (value.isObject()) { + array.add(value.asObject()); + } + } + } + + /** + * Gets an array for the given key, or else adds a new array into the object and returns it. + * + * @param json The JSON object being inspected. + * @param field The name of the array being queried. + * @return The existing or new array. + */ + public static JsonArray getOrCreateArray(final JsonObject json, final String field) { + if (json.get(field) instanceof JsonArray array) { + return array; + } + final JsonArray array = Json.array(); + json.set(field, array); + return array; + } + + /** + * Gets and object for the given key, or else adds a new object into the container and returns it. + * + * @param json The JSON object being inspected. + * @param field The name of the object being queried. + * @return The existing or new object. + */ + public static JsonObject getOrCreateObject(final JsonObject json, final String field) { + if (json.get(field) instanceof JsonObject object) { + return object; + } + final JsonObject object = Json.object(); + json.set(field, object); + return object; + } + + /** + * Shorthand for calling Optional#ofNullable. + * + * @param val The value being wrapped. + * @param The type of value being wrapped. + * @return val, wrapped in {@link Optional}. + */ + @NotNull + private static Optional nullable(final @Nullable T val) { + return Optional.ofNullable(val); + } +} diff --git a/src/main/java/net/frozenblock/lib/config/api/registry/ConfigEvent.java b/src/main/java/net/frozenblock/lib/config/api/registry/ConfigEvent.java new file mode 100644 index 0000000..54c8d0e --- /dev/null +++ b/src/main/java/net/frozenblock/lib/config/api/registry/ConfigEvent.java @@ -0,0 +1,42 @@ +package net.frozenblock.lib.config.api.registry; + +import net.frozenblock.lib.config.api.instance.Config; +import net.neoforged.api.distmarker.Dist; +import net.neoforged.api.distmarker.OnlyIn; +import net.neoforged.bus.api.Event; + +public class ConfigEvent extends Event { + public final Config config; + public ConfigEvent(final Config config) { + this.config = config; + } + + public static class Load extends ConfigEvent { + public Load(Config config) { + super(config); + } + + @OnlyIn(Dist.CLIENT) + public static class Client extends Load { + + public Client(Config config) { + super(config); + } + } + } + + public static class Save extends ConfigEvent { + + public Save(Config config) { + super(config); + } + + @OnlyIn(Dist.CLIENT) + public static class Client extends Save { + + public Client(Config config) { + super(config); + } + } + } +} diff --git a/src/main/java/net/frozenblock/lib/config/api/registry/ConfigRegistry.java b/src/main/java/net/frozenblock/lib/config/api/registry/ConfigRegistry.java new file mode 100644 index 0000000..e1ff370 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/config/api/registry/ConfigRegistry.java @@ -0,0 +1,127 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.config.api.registry; + +import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap; +import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; +import it.unimi.dsi.fastutil.objects.ObjectArrayList; +import net.frozenblock.lib.config.api.entry.TypedEntryType; +import net.frozenblock.lib.config.api.instance.Config; +import net.frozenblock.lib.config.api.instance.ConfigModification; +import net.frozenblock.lib.config.api.sync.network.ConfigSyncData; +import org.jetbrains.annotations.*; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Map; + +public class ConfigRegistry { + private static final List> CONFIG_REGISTRY = new ObjectArrayList<>(); + private static final Map>> MOD_CONFIG_REGISTRY = new Object2ObjectOpenHashMap<>(); + + private static final Map>> MOD_TYPED_ENTRY_REGISTRY = new Object2ObjectOpenHashMap<>(); + private static final List> TYPED_ENTRY_REGISTRY = new ObjectArrayList<>(); + + private static final Map, Map, Integer>> MODIFICATION_REGISTRY = new Object2ObjectOpenHashMap<>(); + + private static final Map, @Nullable ConfigSyncData> CONFIG_SYNC_DATA = new Object2ObjectOpenHashMap<>(); + + @NotNull + @Contract("_ -> param1") + public static Config register(Config config) { + if (CONFIG_REGISTRY.contains(config)) { + throw new IllegalStateException("Config already registered."); + } + MOD_CONFIG_REGISTRY.computeIfAbsent(config.modId(), key -> new ArrayList<>()).add(config); + CONFIG_REGISTRY.add(config); + return config; + } + + public static boolean contains(Config config) { + return CONFIG_REGISTRY.contains(config); + } + + public static Collection> getConfigsForMod(String modId) { + return Map.copyOf(MOD_CONFIG_REGISTRY).getOrDefault(modId, new ArrayList<>()); + } + + @Unmodifiable + @Contract(pure = true) + public static Collection> getAllConfigs() { + return List.copyOf(CONFIG_REGISTRY); + } + + @NotNull + @Contract("_ -> param1") + public static TypedEntryType register(TypedEntryType entry) { + if (TYPED_ENTRY_REGISTRY.contains(entry)) { + throw new IllegalStateException("Typed entry already registered."); + } + MOD_TYPED_ENTRY_REGISTRY.computeIfAbsent(entry.modId(), key -> new ArrayList<>()).add(entry); + TYPED_ENTRY_REGISTRY.add(entry); + return entry; + } + + public static boolean contains(TypedEntryType entry) { + return TYPED_ENTRY_REGISTRY.contains(entry); + } + + public static Collection> getTypedEntryTypesForMod(String modId) { + return Map.copyOf(MOD_TYPED_ENTRY_REGISTRY).getOrDefault(modId, new ArrayList<>()); + } + + @Unmodifiable + @Contract(pure = true) + public static Collection> getAllTypedEntryTypes() { + return List.copyOf(TYPED_ENTRY_REGISTRY); + } + + public static ConfigModification register(Config config, ConfigModification modification, int priority) { + if (!contains(config)) throw new IllegalStateException("Config " + config + " not in registry!"); + MODIFICATION_REGISTRY.computeIfAbsent(config, a -> new Object2IntOpenHashMap<>()).put(modification, priority); + return modification; + } + + public static ConfigModification register(Config config, ConfigModification modification) { + return register(config, modification, 1000); + } + + public static Map, Integer> getModificationsForConfig(Config config) { + return (Map, Integer>) (Map) MODIFICATION_REGISTRY.getOrDefault(config, new Object2IntOpenHashMap<>()); + } + + @ApiStatus.Internal + @Nullable + public static ConfigSyncData setSyncData(Config config, @Nullable ConfigSyncData data) { + if (!contains(config)) throw new IllegalStateException("Config " + config + " not in registry!"); + CONFIG_SYNC_DATA.put(config, data); + return data; + } + + @ApiStatus.Internal + @Nullable + public static ConfigSyncData getSyncData(Config config) { + return (ConfigSyncData) CONFIG_SYNC_DATA.get(config); + } + + @ApiStatus.Internal + public static boolean containsSyncData(Config config) { + return CONFIG_SYNC_DATA.containsKey(config); + } +} diff --git a/src/main/java/net/frozenblock/lib/config/api/sync/SyncBehavior.java b/src/main/java/net/frozenblock/lib/config/api/sync/SyncBehavior.java new file mode 100644 index 0000000..b7d7451 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/config/api/sync/SyncBehavior.java @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.config.api.sync; + +import net.frozenblock.lib.config.frozenlib_config.FrozenLibConfig; + +/** + * Used to provide the behavior for a config entry when syncing is in action. + * See {@link FrozenLibConfig} for an example. + * @since 1.5.2 + */ +public enum SyncBehavior { + + /** + * The default behavior for all entries. + * Operators of the server and LAN hosts will be able to set the config values, and all others will see that value update on their client. + * Non-operator/host clients will not be able to modify these values, as they will be locked in the GUI (currently we provide native support for Cloth Config only,) and display the reason why it can't be modified. + * The values synced to the client will not save to their config, as this is simply a temporary modification. + */ + SYNCABLE(true), + + /** + * The config entry will not sync, and will be modifiable to anyone's client. + * This should be used for client-only options, for instance, whether or not you want a specific particle to spawn. + */ + UNSYNCABLE(false), + + /** + * The config entry will be locked whilst connected to the server or LAN world, but will respect the client's value as it will not sync. + * Anyone will be able to leave, change the value, then rejoin and have their own option enabled. + * This is recommended to be used for client-only options that may cause bugs or other issues if changed during gameplay. + * Keep in mind that operators/hosts will be able to modify this entry at any time for themselves. + */ + LOCK_WHEN_SYNCED(false); + + private final boolean canSync; + + SyncBehavior(boolean canSync) { + this.canSync = canSync; + } + + public boolean canSync() { + return this.canSync; + } +} diff --git a/src/main/java/net/frozenblock/lib/config/api/sync/annotation/EntrySyncData.java b/src/main/java/net/frozenblock/lib/config/api/sync/annotation/EntrySyncData.java new file mode 100644 index 0000000..598db06 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/config/api/sync/annotation/EntrySyncData.java @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.config.api.sync.annotation; + +import net.frozenblock.lib.config.api.sync.SyncBehavior; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Provides the attributes of a syncable config entry. + * See {@link net.frozenblock.lib.config.frozenlib_config.FrozenLibConfig} and {@link net.frozenblock.lib.config.frozenlib_config.gui.FrozenLibConfigGui} for an example. + * @since 1.5.2 + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +public @interface EntrySyncData { + + /** + * Used to identify config entries so their attributes can impact GUIs. + * Without this, it isn't possible to find the field needed in order to access their unique attributes for syncing. + */ + String value() default ""; + + /** + * Used to determine the behavior of a config entry on a server/LAN world. + */ + SyncBehavior behavior() default SyncBehavior.SYNCABLE; + +} diff --git a/src/main/java/net/frozenblock/lib/config/api/sync/annotation/UnsyncableConfig.java b/src/main/java/net/frozenblock/lib/config/api/sync/annotation/UnsyncableConfig.java new file mode 100644 index 0000000..af3f4dd --- /dev/null +++ b/src/main/java/net/frozenblock/lib/config/api/sync/annotation/UnsyncableConfig.java @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.config.api.sync.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Used to mark a config as unsyncable. + * Without this annotation, the config will sync if it allows modifications. + * @since 1.5 + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface UnsyncableConfig { +} diff --git a/src/main/java/net/frozenblock/lib/config/api/sync/network/ConfigByteBufUtil.java b/src/main/java/net/frozenblock/lib/config/api/sync/network/ConfigByteBufUtil.java new file mode 100644 index 0000000..1eca7a0 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/config/api/sync/network/ConfigByteBufUtil.java @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.config.api.sync.network; + +import blue.endless.jankson.Jankson; +import blue.endless.jankson.JsonElement; +import blue.endless.jankson.JsonObject; +import blue.endless.jankson.api.SyntaxError; +import net.frozenblock.lib.config.api.instance.ConfigSerialization; +import net.minecraft.network.FriendlyByteBuf; +import org.jetbrains.annotations.NotNull; + +/** + * @since 1.5 + */ +public final class ConfigByteBufUtil { + private ConfigByteBufUtil() {} + + public static T readJankson(@NotNull FriendlyByteBuf buf, String modId, String className) throws SyntaxError, ClassNotFoundException { + Jankson jankson = ConfigSerialization.createJankson(modId); + Class clazz = (Class) Class.forName(className); + JsonObject json = jankson.load(buf.readUtf()); + return jankson.fromJson(json, clazz); + } + + public static void writeJankson(@NotNull FriendlyByteBuf buf, String modId, T data) { + Jankson jankson = ConfigSerialization.createJankson(modId); + JsonElement element = jankson.toJson(data); + buf.writeUtf(element.toJson()); + } +} diff --git a/src/main/java/net/frozenblock/lib/config/api/sync/network/ConfigSyncData.java b/src/main/java/net/frozenblock/lib/config/api/sync/network/ConfigSyncData.java new file mode 100644 index 0000000..98a994e --- /dev/null +++ b/src/main/java/net/frozenblock/lib/config/api/sync/network/ConfigSyncData.java @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.config.api.sync.network; + +/** + * @since 1.5 + */ +public record ConfigSyncData(T instance) { +} diff --git a/src/main/java/net/frozenblock/lib/config/clothconfig/FrozenClothConfig.java b/src/main/java/net/frozenblock/lib/config/clothconfig/FrozenClothConfig.java new file mode 100644 index 0000000..692469c --- /dev/null +++ b/src/main/java/net/frozenblock/lib/config/clothconfig/FrozenClothConfig.java @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.config.clothconfig; + +import me.shedaniel.clothconfig2.api.AbstractConfigListEntry; +import me.shedaniel.clothconfig2.api.ConfigCategory; +import me.shedaniel.clothconfig2.api.ConfigEntryBuilder; +import me.shedaniel.clothconfig2.gui.widget.DynamicEntryListWidget; +import net.frozenblock.lib.config.api.instance.Config; +import net.frozenblock.lib.config.clothconfig.impl.DisableableWidgetInterface; +import net.minecraft.network.chat.Component; +import net.neoforged.api.distmarker.Dist; +import net.neoforged.api.distmarker.OnlyIn; +import org.jetbrains.annotations.NotNull; + +import java.util.Arrays; + +@OnlyIn(Dist.CLIENT) +public final class FrozenClothConfig { + private FrozenClothConfig() {} + + /** + * Creates a subcategory in the parent config category with the specified key and adds entries to it. + * + * @param entryBuilder the ConfigEntryBuilder instance + * @param parentCategory the parent config category + * @param key the key for the subcategory + * @param expanded if the subcategory is expanded or not + * @param tooltip the tooltip for the subcategory + * @param entries the entries to be added to the subcategory + * @return the newly created subcategory + */ + @SuppressWarnings("rawtypes") + public static ConfigCategory createSubCategory(@NotNull ConfigEntryBuilder entryBuilder, @NotNull ConfigCategory parentCategory, @NotNull Component key, boolean expanded, Component tooltip, @NotNull AbstractConfigListEntry... entries) { + // Create the subcategory + var subCategory = entryBuilder.startSubCategory(key, Arrays.stream(entries).toList()); + + // Set the expanded status + subCategory.setExpanded(expanded); + // If the tooltip is not null, set the tooltip for the subcategory + if (tooltip != null) { + subCategory.setTooltip(tooltip); + } + + // Add the subcategory to the parent category and return it + return parentCategory.addEntry(entryBuilder.startSubCategory(key, Arrays.stream(entries).toList()) + .setExpanded(expanded) + .setTooltip(tooltip) + .build() + ); + } + + /** + * Creates an entry that will interact with config syncing + * + * @param entry The config entry to be used + * @param clazz The class of the config file being accessed + * @param identifier The identifier of the field used for the config (Use {@link net.frozenblock.lib.config.api.sync.annotation.EntrySyncData} for this) + * @param configInstance The main instance of the config (See {@link net.frozenblock.lib.config.frozenlib_config.FrozenLibConfig#INSTANCE} for an example) + * @since 1.5 + */ + public static > T syncedEntry(T entry, Class clazz, String identifier, Config configInstance) { + ((DisableableWidgetInterface) entry).frozenLib$addSyncData(clazz, identifier, configInstance); + return entry; + } +} diff --git a/src/main/java/net/frozenblock/lib/config/clothconfig/impl/DisableableWidgetInterface.java b/src/main/java/net/frozenblock/lib/config/clothconfig/impl/DisableableWidgetInterface.java new file mode 100644 index 0000000..f3cafa1 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/config/clothconfig/impl/DisableableWidgetInterface.java @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.config.clothconfig.impl; + +import net.frozenblock.lib.config.api.instance.Config; +import net.frozenblock.lib.config.api.instance.ConfigModification; +import net.neoforged.api.distmarker.Dist; +import net.neoforged.api.distmarker.OnlyIn; + +@OnlyIn(Dist.CLIENT) +public interface DisableableWidgetInterface { + + void frozenLib$addSyncData(Class clazz, String identifier, Config configInstance); + + boolean frozenLib$isSyncable(); + + boolean frozenLib$hasValidData(); + + ConfigModification.EntryPermissionType frozenLib$getEntryPermissionType(); +} diff --git a/src/main/java/net/frozenblock/lib/config/clothconfig/mixin/client/AbstractConfigEntryMixin.java b/src/main/java/net/frozenblock/lib/config/clothconfig/mixin/client/AbstractConfigEntryMixin.java new file mode 100644 index 0000000..c9148bf --- /dev/null +++ b/src/main/java/net/frozenblock/lib/config/clothconfig/mixin/client/AbstractConfigEntryMixin.java @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.config.clothconfig.mixin.client; + +import me.shedaniel.clothconfig2.api.AbstractConfigEntry; +import net.frozenblock.lib.config.clothconfig.impl.DisableableWidgetInterface; +import net.neoforged.api.distmarker.Dist; +import net.neoforged.api.distmarker.OnlyIn; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@OnlyIn(Dist.CLIENT) +@Mixin(AbstractConfigEntry.class) +public class AbstractConfigEntryMixin { + + @Inject(method = "save", at = @At("HEAD"), cancellable = true, remap = false) + public void frozenLib$save(CallbackInfo info) { + if (!((DisableableWidgetInterface) this).frozenLib$getEntryPermissionType().canModify) { + info.cancel(); + } + } +} diff --git a/src/main/java/net/frozenblock/lib/config/clothconfig/mixin/client/DynamicEntryListWidgetEntryMixin.java b/src/main/java/net/frozenblock/lib/config/clothconfig/mixin/client/DynamicEntryListWidgetEntryMixin.java new file mode 100644 index 0000000..f6a6fc9 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/config/clothconfig/mixin/client/DynamicEntryListWidgetEntryMixin.java @@ -0,0 +1,101 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.config.clothconfig.mixin.client; + +import me.shedaniel.clothconfig2.api.DisableableWidget; +import me.shedaniel.clothconfig2.api.Requirement; +import me.shedaniel.clothconfig2.gui.widget.DynamicEntryListWidget; +import net.frozenblock.lib.FrozenLogUtils; +import net.frozenblock.lib.config.api.instance.Config; +import net.frozenblock.lib.config.api.instance.ConfigModification; +import net.frozenblock.lib.config.api.sync.annotation.EntrySyncData; +import net.frozenblock.lib.config.clothconfig.impl.DisableableWidgetInterface; +import net.frozenblock.lib.config.impl.network.ConfigSyncModification; +import net.neoforged.api.distmarker.Dist; +import net.neoforged.api.distmarker.OnlyIn; +import org.jetbrains.annotations.NotNull; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Unique; + +import java.lang.reflect.Field; + +@OnlyIn(Dist.CLIENT) +@Mixin(DynamicEntryListWidget.Entry.class) +public abstract class DynamicEntryListWidgetEntryMixin implements DisableableWidget, DisableableWidgetInterface { + + @Unique + private ConfigModification.EntryPermissionType frozenLib$entryPermissionType = ConfigModification.EntryPermissionType.CAN_MODIFY; + + @Unique + private boolean frozenLib$isSyncable = true; + + @Unique + private boolean frozenLib$hasValidData = false; + + @Unique + @Override + public void frozenLib$addSyncData(@NotNull Class clazz, @NotNull String identifier, Config configInstance) { + if (identifier.isEmpty()) { + new Exception("Cannot process sync value with empty identifier!").printStackTrace(); + return; + } + Field field = null; + for (Field fieldToCheck : clazz.getDeclaredFields()) { + EntrySyncData entrySyncData = fieldToCheck.getAnnotation(EntrySyncData.class); + if (entrySyncData != null && !entrySyncData.value().isEmpty() && entrySyncData.value().equals(identifier)) { + if (field != null) FrozenLogUtils.logError("Multiple fields in " + clazz.getName() + " contain identifier " + identifier + "!", true, null); + field = fieldToCheck; + } + } + final Field finalField = field; + if (finalField == null) { + new Exception("No such field with identifier " + identifier + " exists in " + clazz.getName() + "!").printStackTrace(); + return; + } + Requirement nonSyncRequirement = () -> { + this.frozenLib$entryPermissionType = ConfigSyncModification.canModifyField(finalField, configInstance); + this.frozenLib$isSyncable = ConfigSyncModification.isSyncable(finalField); + return this.frozenLib$entryPermissionType.canModify; + }; + if (this.getRequirement() != null) { + this.setRequirement(Requirement.all(this.getRequirement(), nonSyncRequirement)); + + } else { + this.setRequirement(nonSyncRequirement); + } + this.frozenLib$hasValidData = true; + } + + @Unique + @Override + public boolean frozenLib$isSyncable() { + return this.frozenLib$isSyncable; + } + + @Unique + @Override + public boolean frozenLib$hasValidData() { + return this.frozenLib$hasValidData; + } + + @Unique + @Override + public ConfigModification.EntryPermissionType frozenLib$getEntryPermissionType() { + return this.frozenLib$entryPermissionType; + } +} diff --git a/src/main/java/net/frozenblock/lib/config/clothconfig/mixin/client/TooltipListEntryMixin.java b/src/main/java/net/frozenblock/lib/config/clothconfig/mixin/client/TooltipListEntryMixin.java new file mode 100644 index 0000000..e6a3d18 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/config/clothconfig/mixin/client/TooltipListEntryMixin.java @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.config.clothconfig.mixin.client; + +import com.llamalad7.mixinextras.injector.ModifyReturnValue; +import me.shedaniel.clothconfig2.gui.entries.TooltipListEntry; +import net.frozenblock.lib.config.api.instance.ConfigModification; +import net.frozenblock.lib.config.clothconfig.impl.DisableableWidgetInterface; +import net.frozenblock.lib.config.impl.network.ConfigSyncPacket; +import net.frozenblock.lib.networking.FrozenClientNetworking; +import net.frozenblock.lib.networking.FrozenNetworking; +import net.minecraft.client.Minecraft; +import net.minecraft.network.chat.Component; +import net.neoforged.api.distmarker.Dist; +import net.neoforged.api.distmarker.OnlyIn; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; + +import java.util.Arrays; +import java.util.Optional; +import java.util.stream.Stream; + +@OnlyIn(Dist.CLIENT) +@Mixin(TooltipListEntry.class) +public class TooltipListEntryMixin { + + @ModifyReturnValue(method = "getTooltip()Ljava/util/Optional;", at = @At("RETURN"), remap = false) + public Optional frozenLib$getTooltip(Optional list) { + DisableableWidgetInterface disableableWidgetInterface = (DisableableWidgetInterface) this; + if (!disableableWidgetInterface.frozenLib$getEntryPermissionType().canModify) { + Optional optionalComponent = FrozenClientNetworking.connectedToLan() ? + disableableWidgetInterface.frozenLib$getEntryPermissionType().lanTooltip + : disableableWidgetInterface.frozenLib$getEntryPermissionType().tooltip; + + return optionalComponent.isPresent() ? + Optional.of(optionalComponent.orElseThrow().toFlatList().toArray(new Component[0])) + : + Optional.of(ConfigModification.EntryPermissionType.LOCKED_FOR_UNKNOWN_REASON.tooltip.orElseThrow().toFlatList().toArray(new Component[0])); + } else if ( + disableableWidgetInterface.frozenLib$hasValidData() + && disableableWidgetInterface.frozenLib$isSyncable() + && FrozenNetworking.isMultiplayer() + && ConfigSyncPacket.hasPermissionsToSendSync(Minecraft.getInstance().player, false) + ) { + Component entrySyncNotice = Component.translatable("tooltip.frozenlib.entry_sync_notice"); + return list.isEmpty() ? + Optional.of(entrySyncNotice.toFlatList().toArray(new Component[0])) + : + Optional.of(Stream.concat(Arrays.stream(list.get()), Stream.of(entrySyncNotice)).toArray(Component[]::new)); + } + return list; + } + +} diff --git a/src/main/java/net/frozenblock/lib/config/clothconfig/mixin/plugin/FrozenLibClothConfigMixinPlugin.java b/src/main/java/net/frozenblock/lib/config/clothconfig/mixin/plugin/FrozenLibClothConfigMixinPlugin.java new file mode 100644 index 0000000..4097c7f --- /dev/null +++ b/src/main/java/net/frozenblock/lib/config/clothconfig/mixin/plugin/FrozenLibClothConfigMixinPlugin.java @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.config.clothconfig.mixin.plugin; + + +import net.neoforged.fml.ModList; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.objectweb.asm.tree.ClassNode; +import org.spongepowered.asm.mixin.extensibility.IMixinConfigPlugin; +import org.spongepowered.asm.mixin.extensibility.IMixinInfo; + +import java.util.List; +import java.util.Set; + +public class FrozenLibClothConfigMixinPlugin implements IMixinConfigPlugin { + + @Override + public void onLoad(String mixinPackage) { + + } + + @Override + @Nullable + public String getRefMapperConfig() { + return null; + } + + @Override + public boolean shouldApplyMixin(String targetClassName, @NotNull String mixinClassName) { + if(ModList.get() == null) return false; + return ModList.get().isLoaded("cloth-config"); + } + + @Override + public void acceptTargets(Set myTargets, Set otherTargets) { + + } + + @Override + @Nullable + public List getMixins() { + return null; + } + + @Override + public void preApply(String targetClassName, ClassNode targetClass, String mixinClassName, IMixinInfo mixinInfo) { + + } + + @Override + public void postApply(String targetClassName, ClassNode targetClass, String mixinClassName, IMixinInfo mixinInfo) { + + } +} diff --git a/src/main/java/net/frozenblock/lib/config/frozenlib_config/FrozenLibConfig.java b/src/main/java/net/frozenblock/lib/config/frozenlib_config/FrozenLibConfig.java new file mode 100644 index 0000000..ea8c9e1 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/config/frozenlib_config/FrozenLibConfig.java @@ -0,0 +1,101 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.config.frozenlib_config; + +import blue.endless.jankson.Comment; +import me.shedaniel.autoconfig.annotation.ConfigEntry; +import net.frozenblock.lib.FrozenSharedConstants; +import net.frozenblock.lib.config.api.instance.Config; +import net.frozenblock.lib.config.api.instance.json.JsonConfig; +import net.frozenblock.lib.config.api.instance.json.JsonType; +import net.frozenblock.lib.config.api.registry.ConfigRegistry; +import net.frozenblock.lib.config.api.sync.SyncBehavior; +import net.frozenblock.lib.config.api.sync.annotation.EntrySyncData; +import org.quiltmc.qsl.frozenblock.misc.datafixerupper.api.QuiltDataFixerBuilder; +import org.quiltmc.qsl.frozenblock.misc.datafixerupper.api.QuiltDataFixes; + +import java.util.List; + +public class FrozenLibConfig { + + public static final Config INSTANCE = ConfigRegistry.register( + new JsonConfig<>( + FrozenSharedConstants.MOD_ID, + FrozenLibConfig.class, + JsonType.JSON5_UNQUOTED_KEYS, + true, + QuiltDataFixes.buildFixer(new QuiltDataFixerBuilder(0)), + 0 + ) { + @Override + public void onSave() throws Exception { + super.onSave(); + this.onSync(null); + } + + @Override + public void onSync(FrozenLibConfig syncInstance) { + var config = this.config(); + USE_WIND_ON_NON_FROZEN_SERVERS = config.useWindOnNonFrozenServers; + } + } + ); + + public static volatile boolean USE_WIND_ON_NON_FROZEN_SERVERS = true; + + @Comment("Mods may override any of these options, but the config file will not change.") + + @EntrySyncData(value = "useWindOnNonFrozenServers", behavior = SyncBehavior.UNSYNCABLE) + public boolean useWindOnNonFrozenServers = true; + + @EntrySyncData("saveItemCooldowns") + public boolean saveItemCooldowns = false; + + @EntrySyncData(value = "removeExperimentalWarning", behavior = SyncBehavior.UNSYNCABLE) + public boolean removeExperimentalWarning = false; + + @EntrySyncData("wardenSpawnTrackerCommand") + public boolean wardenSpawnTrackerCommand = false; + + @ConfigEntry.Gui.CollapsibleObject + public final DataFixerConfig dataFixer = new DataFixerConfig(); + + public static class DataFixerConfig { + + @Comment("Mods can only add to this list. User settings will always apply.") + @EntrySyncData("disabledDataFixTypes") + public List disabledDataFixTypes = List.of( + "world_gen_settings" + ); + } + + public static FrozenLibConfig get(boolean real) { + if (real) + return INSTANCE.instance(); + + return INSTANCE.config(); + } + + public static FrozenLibConfig get() { + return get(false); + } + + public static FrozenLibConfig getWithSync() { + return INSTANCE.configWithSync(); + } +} diff --git a/src/main/java/net/frozenblock/lib/config/frozenlib_config/gui/FrozenLibConfigGui.java b/src/main/java/net/frozenblock/lib/config/frozenlib_config/gui/FrozenLibConfigGui.java new file mode 100644 index 0000000..08a022f --- /dev/null +++ b/src/main/java/net/frozenblock/lib/config/frozenlib_config/gui/FrozenLibConfigGui.java @@ -0,0 +1,131 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.config.frozenlib_config.gui; + +import me.shedaniel.clothconfig2.api.ConfigBuilder; +import me.shedaniel.clothconfig2.api.ConfigCategory; +import me.shedaniel.clothconfig2.api.ConfigEntryBuilder; +import net.frozenblock.lib.FrozenSharedConstants; +import net.frozenblock.lib.config.api.instance.Config; +import net.frozenblock.lib.config.clothconfig.FrozenClothConfig; +import net.frozenblock.lib.config.frozenlib_config.FrozenLibConfig; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.network.chat.Component; +import net.neoforged.api.distmarker.Dist; +import net.neoforged.api.distmarker.OnlyIn; +import org.jetbrains.annotations.NotNull; + +@OnlyIn(Dist.CLIENT) +public final class FrozenLibConfigGui { + + private static void setupEntries(@NotNull ConfigCategory category, @NotNull ConfigEntryBuilder entryBuilder) { + var config = FrozenLibConfig.get(true); + var modifiedConfig = FrozenLibConfig.getWithSync(); + Config configInstance = FrozenLibConfig.INSTANCE; + var defaultConfig = FrozenLibConfig.INSTANCE.defaultInstance(); + var dataFixer = config.dataFixer; + category.setBackground(FrozenSharedConstants.id("config.png")); + + var useWindOnNonFrozenServers = category.addEntry( + FrozenClothConfig.syncedEntry( + entryBuilder.startBooleanToggle(text("use_wind_on_non_frozenlib_servers"), modifiedConfig.useWindOnNonFrozenServers) + .setDefaultValue(defaultConfig.useWindOnNonFrozenServers) + .setSaveConsumer(newValue -> config.useWindOnNonFrozenServers = newValue) + .setTooltip(tooltip("use_wind_on_non_frozenlib_servers")) + .build(), + config.getClass(), + "useWindOnNonFrozenServers", + configInstance + ) + ); + + var saveItemCooldowns = category.addEntry( + FrozenClothConfig.syncedEntry( + entryBuilder.startBooleanToggle(text("save_item_cooldowns"), modifiedConfig.saveItemCooldowns) + .setDefaultValue(defaultConfig.saveItemCooldowns) + .setSaveConsumer(newValue -> config.saveItemCooldowns = newValue) + .setTooltip(tooltip("save_item_cooldowns")) + .build(), + config.getClass(), + "saveItemCooldowns", + configInstance + ) + ); + + var removeExperimentalWarning = category.addEntry( + FrozenClothConfig.syncedEntry( + entryBuilder.startBooleanToggle(text("remove_experimental_warning"), modifiedConfig.removeExperimentalWarning) + .setDefaultValue(defaultConfig.removeExperimentalWarning) + .setSaveConsumer(newValue -> config.removeExperimentalWarning = newValue) + .setTooltip(tooltip("remove_experimental_warning")) + .build(), + config.getClass(), + "removeExperimentalWarning", + configInstance + ) + ); + + var wardenSpawnTrackerCommand = category.addEntry( + FrozenClothConfig.syncedEntry( + entryBuilder.startBooleanToggle(text("warden_spawn_tracker_command"), modifiedConfig.wardenSpawnTrackerCommand) + .setDefaultValue(defaultConfig.wardenSpawnTrackerCommand) + .setSaveConsumer(newValue -> config.wardenSpawnTrackerCommand = newValue) + .setTooltip(tooltip("warden_spawn_tracker_command")) + .build(), + config.getClass(), + "wardenSpawnTrackerCommand", + configInstance + ) + ); + + var disabledDataFixTypes = FrozenClothConfig.syncedEntry( + entryBuilder.startStrList(text("disabled_datafix_types"), modifiedConfig.dataFixer.disabledDataFixTypes) + .setDefaultValue(defaultConfig.dataFixer.disabledDataFixTypes) + .setSaveConsumer(newValue -> dataFixer.disabledDataFixTypes = newValue) + .setTooltip(tooltip("disabled_datafix_types")) + .requireRestart() + .build(), + dataFixer.getClass(), + "disabledDataFixTypes", + configInstance + ); + + var datafixerCategory = FrozenClothConfig.createSubCategory(entryBuilder, category, text("datafixer"), + false, + tooltip("datafixer"), + disabledDataFixTypes + ); + } + + public static Screen buildScreen(Screen parent) { + var configBuilder = ConfigBuilder.create().setParentScreen(parent).setTitle(text("component.title")); + configBuilder.setSavingRunnable(FrozenLibConfig.INSTANCE::save); + var config = configBuilder.getOrCreateCategory(text("config")); + ConfigEntryBuilder entryBuilder = configBuilder.entryBuilder(); + setupEntries(config, entryBuilder); + return configBuilder.build(); + } + + public static Component text(String key) { + return Component.translatable("option." + FrozenSharedConstants.MOD_ID + "." + key); + } + + public static Component tooltip(String key) { + return Component.translatable("tooltip." + FrozenSharedConstants.MOD_ID + "." + key); + } +} diff --git a/src/main/java/net/frozenblock/lib/config/impl/ConfigCommand.java b/src/main/java/net/frozenblock/lib/config/impl/ConfigCommand.java new file mode 100644 index 0000000..7859b47 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/config/impl/ConfigCommand.java @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.config.impl; + +import com.mojang.brigadier.CommandDispatcher; +import com.mojang.brigadier.arguments.StringArgumentType; +import net.frozenblock.lib.config.api.instance.Config; +import net.frozenblock.lib.config.api.registry.ConfigRegistry; +import net.frozenblock.lib.config.impl.network.ConfigSyncPacket; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.commands.Commands; +import net.minecraft.network.chat.Component; +import net.minecraft.server.level.ServerPlayer; + +import java.util.Collection; +import java.util.Collections; + +public final class ConfigCommand { + private ConfigCommand() {} + + public static void register(CommandDispatcher dispatcher) { + dispatcher.register(Commands.literal("frozenlib_config") + .then(Commands.literal("reload") + .then(Commands.argument("modId", StringArgumentType.string()) + .executes(context -> reloadConfigs(context.getSource(), StringArgumentType.getString(context, "modId"))) + ) + ) + ); + } + + private static int reloadConfigs(CommandSourceStack source, String modId) { + Collection> configs = ConfigRegistry.getConfigsForMod(modId); + for (Config config : configs) { + config.load(); + } + for (ServerPlayer player : Collections.unmodifiableCollection(source.getServer().getPlayerList().getPlayers())) { + ConfigSyncPacket.sendS2C(player, configs); + } + + if (configs.size() == 1) + source.sendSuccess(() -> Component.translatable("commands.frozenlib_config.reload.single", modId), true); + else + source.sendSuccess(() -> Component.translatable("commands.frozenlib_config.reload.multiple", configs.size(), modId), true); + return configs.size(); + } +} diff --git a/src/main/java/net/frozenblock/lib/config/impl/entry/TypedEntryImpl.java b/src/main/java/net/frozenblock/lib/config/impl/entry/TypedEntryImpl.java new file mode 100644 index 0000000..512127c --- /dev/null +++ b/src/main/java/net/frozenblock/lib/config/impl/entry/TypedEntryImpl.java @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.config.impl.entry; + +import net.frozenblock.lib.config.api.entry.TypedEntry; +import net.frozenblock.lib.config.api.entry.TypedEntryType; + +/** + * @since 1.7 + */ +public class TypedEntryImpl implements TypedEntry { + + private final TypedEntryType type; + private T value; + + public TypedEntryImpl(TypedEntryType type, T value) { + this.type = type; + this.value = value; + } + + @Override + public TypedEntryType type() { + return this.type; + } + + @Override + public T value() { + return this.value; + } + + @Override + public void setValue(T value) { + this.value = value; + } +} diff --git a/src/main/java/net/frozenblock/lib/config/impl/network/ConfigSyncModification.java b/src/main/java/net/frozenblock/lib/config/impl/network/ConfigSyncModification.java new file mode 100644 index 0000000..6f18f8b --- /dev/null +++ b/src/main/java/net/frozenblock/lib/config/impl/network/ConfigSyncModification.java @@ -0,0 +1,84 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.config.impl.network; + +import net.frozenblock.lib.config.api.instance.Config; +import net.frozenblock.lib.config.api.instance.ConfigModification; +import net.frozenblock.lib.config.api.sync.SyncBehavior; +import net.frozenblock.lib.config.api.sync.annotation.EntrySyncData; +import net.frozenblock.lib.config.api.sync.network.ConfigSyncData; +import net.frozenblock.lib.networking.FrozenNetworking; +import net.minecraft.client.Minecraft; +import net.neoforged.api.distmarker.Dist; +import net.neoforged.api.distmarker.OnlyIn; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.lang.reflect.Field; +import java.util.function.Consumer; + +/** + * @since 1.5 + */ +public record ConfigSyncModification(Config config, DataSupplier dataSupplier) implements Consumer { + + @Override + public void accept(T destination) { + try { + ConfigSyncData syncData = dataSupplier.get(config); + if (syncData == null || !FrozenNetworking.connectedToServer()) { + new Exception("Attempted to sync config " + config.path() + " for mod " + config.modId() + " outside a server!").printStackTrace(); + return; + } + T source = syncData.instance(); + config.setSynced(true); + ConfigModification.copyInto(source, destination, true); + } catch (NullPointerException ignored) {} + } + + @FunctionalInterface + public interface DataSupplier { + ConfigSyncData get(Config config); + } + + public static boolean isSyncable(@NotNull Field field) { + EntrySyncData entrySyncData = field.getAnnotation(EntrySyncData.class); + return entrySyncData == null || entrySyncData.behavior().canSync(); + } + + public static boolean isLockedWhenSynced(@NotNull Field field) { + EntrySyncData entrySyncData = field.getAnnotation(EntrySyncData.class); + return entrySyncData != null && entrySyncData.behavior() == SyncBehavior.LOCK_WHEN_SYNCED; + } + + @OnlyIn(Dist.CLIENT) + public static ConfigModification.EntryPermissionType canModifyField(@Nullable Field field, @Nullable Config config) { + if (config != null && field != null && config.supportsSync()) { + boolean isOperator = ConfigSyncPacket.hasPermissionsToSendSync(Minecraft.getInstance().player, false); + if (!config.isSynced() || isOperator) { + return ConfigModification.EntryPermissionType.CAN_MODIFY; + } else if (isSyncable(field)) { + return ConfigModification.EntryPermissionType.LOCKED_DUE_TO_SYNC; + } else if (isLockedWhenSynced(field)) { + return ConfigModification.EntryPermissionType.LOCKED_DUE_TO_SERVER; + } + } + return ConfigModification.EntryPermissionType.CAN_MODIFY; + } + +} diff --git a/src/main/java/net/frozenblock/lib/config/impl/network/ConfigSyncPacket.java b/src/main/java/net/frozenblock/lib/config/impl/network/ConfigSyncPacket.java new file mode 100644 index 0000000..03a8fd0 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/config/impl/network/ConfigSyncPacket.java @@ -0,0 +1,169 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.config.impl.network; + +import blue.endless.jankson.api.SyntaxError; +import net.frozenblock.lib.FrozenLogUtils; +import net.frozenblock.lib.FrozenSharedConstants; +import net.frozenblock.lib.config.api.instance.Config; +import net.frozenblock.lib.config.api.instance.ConfigModification; +import net.frozenblock.lib.config.api.registry.ConfigRegistry; +import net.frozenblock.lib.config.api.sync.network.ConfigByteBufUtil; +import net.frozenblock.lib.config.api.sync.network.ConfigSyncData; +import net.frozenblock.lib.networking.FrozenClientNetworking; +import net.frozenblock.lib.networking.FrozenNetworking; +import net.frozenblock.lib.networking.PlayerLookup; +import net.minecraft.client.Minecraft; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.entity.player.Player; +import net.neoforged.api.distmarker.Dist; +import net.neoforged.api.distmarker.OnlyIn; +import net.neoforged.fml.loading.FMLLoader; +import net.neoforged.neoforge.network.PacketDistributor; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.List; + +/** + * @since 1.5 + */ +public record ConfigSyncPacket( + String modId, + String className, + T configData +) implements CustomPacketPayload { + + public static final int PERMISSION_LEVEL = 2; + + public static final Type> PACKET_TYPE = new Type<>( + FrozenSharedConstants.id("config_sync_packet") + ); + public static final StreamCodec> CODEC = StreamCodec.ofMember(ConfigSyncPacket::write, ConfigSyncPacket::create); + + @Nullable + public static ConfigSyncPacket create(@NotNull FriendlyByteBuf buf) { + String modId = buf.readUtf(); + String className = buf.readUtf(); + try { + T configData = ConfigByteBufUtil.readJankson(buf, modId, className); + return new ConfigSyncPacket<>(modId, className, configData); + } catch (SyntaxError | ClassNotFoundException e) { + FrozenLogUtils.logError("Failed to read config data from packet.", true, e); + return null; + } + } + + public void write(@NotNull FriendlyByteBuf buf) { + buf.writeUtf(modId); + buf.writeUtf(className); + ConfigByteBufUtil.writeJankson(buf, modId, configData); + } + + public static void receive(@NotNull ConfigSyncPacket packet, @Nullable MinecraftServer server) { + String modId = packet.modId(); + String className = packet.className(); + for (Config raw : ConfigRegistry.getConfigsForMod(modId)) { + String configClassName = raw.configClass().getName(); + if (!configClassName.equals(className)) continue; + Config config = (Config) raw; + if (server != null) { + // C2S logic + ConfigModification.copyInto(packet.configData(), config.instance()); + if (!FrozenNetworking.connectedToIntegratedServer()) + config.save(); + for (ServerPlayer player : PlayerLookup.all(server)) { + sendS2C(player, List.of(config)); + } + } else { + // S2C logic + boolean shouldAddModification = !ConfigRegistry.containsSyncData(config); + ConfigRegistry.setSyncData(config, new ConfigSyncData<>(packet.configData())); + if (shouldAddModification) { + ConfigRegistry.register( + config, + new ConfigModification<>( + new ConfigSyncModification<>(config, ConfigRegistry::getSyncData) + ), + Integer.MIN_VALUE // make sure it's the first modification + ); + } + } + config.onSync(packet.configData()); + break; + } + } + + public static void sendS2C(ServerPlayer player, @NotNull Iterable> configs) { + if (FrozenNetworking.isLocalPlayer(player)) + return; + + for (Config config : configs) { + if (!config.supportsSync()) continue; + ConfigSyncPacket packet = new ConfigSyncPacket<>(config.modId(), config.configClass().getName(), config.config()); + PacketDistributor.sendToPlayer(player, packet); + } + } + + public static void sendS2C(ServerPlayer player) { + sendS2C(player, ConfigRegistry.getAllConfigs()); + } + + public static boolean hasPermissionsToSendSync(@Nullable Player player, boolean serverSide) { + if (FMLLoader.getDist() == Dist.DEDICATED_SERVER) + return player.hasPermissions(PERMISSION_LEVEL); + + if (FrozenClientNetworking.notConnected()) + return false; + + boolean isHost = serverSide && FrozenNetworking.isLocalPlayer(player); + return FrozenNetworking.connectedToIntegratedServer() || isHost || player.hasPermissions(PERMISSION_LEVEL); + } + + @OnlyIn(Dist.CLIENT) + public static void sendC2S(@NotNull Iterable> configs) { + //if (!ClientPlayNetworking.canSend(PACKET_TYPE)) return; TODO: CHECK THIS + + for (Config config : configs) { + if (!config.supportsSync()) continue; + ConfigSyncPacket packet = new ConfigSyncPacket<>(config.modId(), config.configClass().getName(), config.instance()); + PacketDistributor.sendToServer(packet); + } + } + + @OnlyIn(Dist.CLIENT) + public static void sendC2S() { + sendC2S(ConfigRegistry.getAllConfigs()); + } + + @OnlyIn(Dist.CLIENT) + public static void trySendC2S(Config config) { + if(hasPermissionsToSendSync(Minecraft.getInstance().player, false)) + sendC2S(List.of(config)); + } + + @Override + @NotNull + public Type type() { + return PACKET_TYPE; + } +} diff --git a/src/main/java/net/frozenblock/lib/core/impl/DataPackReloadMarker.java b/src/main/java/net/frozenblock/lib/core/impl/DataPackReloadMarker.java new file mode 100644 index 0000000..02459b1 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/core/impl/DataPackReloadMarker.java @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.core.impl; + +import net.frozenblock.lib.worldgen.surface.impl.OptimizedBiomeTagConditionSource; +import net.minecraft.core.registries.Registries; +import net.minecraft.server.packs.resources.PreparableReloadListener; +import net.minecraft.server.packs.resources.ResourceManager; +import net.minecraft.server.packs.resources.ResourceManagerReloadListener; +import net.minecraft.server.packs.resources.SimplePreparableReloadListener; +import net.minecraft.util.profiling.ProfilerFiller; +import net.neoforged.bus.api.SubscribeEvent; +import net.neoforged.neoforge.event.AddReloadListenerEvent; +import net.neoforged.neoforge.event.tick.ServerTickEvent; +import org.jetbrains.annotations.NotNull; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Executor; + +public class DataPackReloadMarker { + + private static boolean RELOADED = false; + + @SubscribeEvent + public static void addResourceListeners(AddReloadListenerEvent event) { + event.addListener((ResourceManagerReloadListener) resourceManager -> markReloaded()); + } + + @SubscribeEvent + public static void startServerTick(ServerTickEvent.Pre event) { + if(markedReloaded()) { + OptimizedBiomeTagConditionSource.optimizeAll(event.getServer().registryAccess().registryOrThrow(Registries.BIOME)); + } + } + + @SubscribeEvent + public static void endServerTick(ServerTickEvent.Post event) { + if (markedReloaded()) { + unmarkReloaded(); + } + } + + public static void markReloaded() { + RELOADED = true; + } + + public static void unmarkReloaded() { + RELOADED = false; + } + + public static boolean markedReloaded() { + return RELOADED; + } +} diff --git a/src/main/java/net/frozenblock/lib/core/mixin/BootstrapMixin.java b/src/main/java/net/frozenblock/lib/core/mixin/BootstrapMixin.java new file mode 100644 index 0000000..db7a510 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/core/mixin/BootstrapMixin.java @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.core.mixin; + +import net.frozenblock.lib.FrozenBools; +import net.minecraft.server.Bootstrap; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(Bootstrap.class) +public class BootstrapMixin { + + @Inject( + method = "bootStrap", + at = @At( + value = "INVOKE", + target = "Ljava/util/concurrent/atomic/AtomicLong;set(J)V" + ) + ) + private static void frozenLib$finishBootStrap(CallbackInfo info) { + FrozenBools.isInitialized = true; + } + +} diff --git a/src/main/java/net/frozenblock/lib/core/mixin/DecoderExceptionMixin.java b/src/main/java/net/frozenblock/lib/core/mixin/DecoderExceptionMixin.java new file mode 100644 index 0000000..4c3782b --- /dev/null +++ b/src/main/java/net/frozenblock/lib/core/mixin/DecoderExceptionMixin.java @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.core.mixin; + +import io.netty.handler.codec.DecoderException; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(DecoderException.class) +public class DecoderExceptionMixin { + + @Inject(method = "(Ljava/lang/Throwable;)V", at = @At("TAIL")) + private void init(Throwable cause, CallbackInfo ci) { + cause.printStackTrace(); + } +} diff --git a/src/main/java/net/frozenblock/lib/core/mixin/WardenSpawnTrackerCommandMixin.java b/src/main/java/net/frozenblock/lib/core/mixin/WardenSpawnTrackerCommandMixin.java new file mode 100644 index 0000000..e0075ff --- /dev/null +++ b/src/main/java/net/frozenblock/lib/core/mixin/WardenSpawnTrackerCommandMixin.java @@ -0,0 +1,123 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.core.mixin; + +import com.llamalad7.mixinextras.injector.wrapoperation.Operation; +import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; +import com.mojang.brigadier.CommandDispatcher; +import com.mojang.brigadier.arguments.IntegerArgumentType; +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import com.mojang.brigadier.tree.LiteralCommandNode; +import net.frozenblock.lib.config.frozenlib_config.FrozenLibConfig; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.commands.Commands; +import net.minecraft.commands.arguments.EntityArgument; +import net.minecraft.network.chat.Component; +import net.minecraft.server.commands.WardenSpawnTrackerCommand; +import net.minecraft.world.entity.player.Player; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; + +import java.util.Collection; +import java.util.List; +import java.util.function.Supplier; + +@Mixin(WardenSpawnTrackerCommand.class) +public abstract class WardenSpawnTrackerCommandMixin { + + @Shadow + private static int resetTracker(CommandSourceStack source, Collection targets) { + return 0; + } + + @Shadow + private static int setWarningLevel(CommandSourceStack source, Collection targets, int warningLevel) { + return 0; + } + + @WrapOperation( + method = "register", + at = @At( + value = "INVOKE", + target = "Lcom/mojang/brigadier/CommandDispatcher;register(Lcom/mojang/brigadier/builder/LiteralArgumentBuilder;)Lcom/mojang/brigadier/tree/LiteralCommandNode;", + remap = false + ) + ) + private static LiteralCommandNode frozenLib$register(CommandDispatcher dispatcher, LiteralArgumentBuilder builder, Operation> operation) { + if (FrozenLibConfig.get().wardenSpawnTrackerCommand) { + return dispatcher.register( + Commands.literal("warden_spawn_tracker") + .requires(player -> player.hasPermission(2)) + .then( + Commands.literal("clear") + .executes(context -> resetTracker(context.getSource(), List.of(context.getSource().getPlayerOrException()))) + .then( + Commands.argument("targets", EntityArgument.players()).executes(context -> resetTracker(context.getSource(), EntityArgument.getPlayers(context, "targets"))) + ) + ) + .then( + Commands.literal("set") + .then( + Commands.argument("targets", EntityArgument.players()) + .then( + Commands.argument("warning_level", IntegerArgumentType.integer(0, 4)) + .executes( + context -> setWarningLevel( + context.getSource(), EntityArgument.getPlayers(context, "targets"), IntegerArgumentType.getInteger(context, "warning_level") + ) + ) + ) + ) + ) + ); + } else return operation.call(dispatcher, builder); + } + + @WrapOperation( + method = "setWarningLevel", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/commands/CommandSourceStack;sendSuccess(Ljava/util/function/Supplier;Z)V", + ordinal = 0 + ) + ) + private static void frozenLib$modifySetWarningLevel(CommandSourceStack source, Supplier supplier, boolean broadcastToOps, Operation operation, CommandSourceStack source1, Collection targets, int warningLevel) { + if (FrozenLibConfig.get().wardenSpawnTrackerCommand) { + source.sendSuccess( + () -> Component.translatable("commands.warden_spawn_tracker.set.success.single", warningLevel, targets.iterator().next().getDisplayName()), true + ); + } else operation.call(source, supplier, broadcastToOps); + } + + @WrapOperation( + method = "setWarningLevel", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/commands/CommandSourceStack;sendSuccess(Ljava/util/function/Supplier;Z)V", + ordinal = 1 + ) + ) + private static void frozenLib$modifySetWarningLevelMultiple(CommandSourceStack source, Supplier supplier, boolean broadcastToOps, Operation operation, CommandSourceStack source1, Collection targets, int warningLevel) { + if (FrozenLibConfig.get().wardenSpawnTrackerCommand) { + source.sendSuccess( + () -> Component.translatable("commands.warden_spawn_tracker.set.success.multiple", warningLevel, targets.size()), true + ); + } else operation.call(source, supplier, broadcastToOps); + } +} diff --git a/src/main/java/net/frozenblock/lib/core/mixin/client/NoExperimentalMixin.java b/src/main/java/net/frozenblock/lib/core/mixin/client/NoExperimentalMixin.java new file mode 100644 index 0000000..cfd5e18 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/core/mixin/client/NoExperimentalMixin.java @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.core.mixin.client; + +import net.frozenblock.lib.config.frozenlib_config.FrozenLibConfig; +import net.minecraft.client.gui.screens.worldselection.WorldOpenFlows; +import net.minecraft.world.level.storage.LevelStorageSource; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.ModifyVariable; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(WorldOpenFlows.class) +public class NoExperimentalMixin { + @Inject( + method = "askForBackup(Lnet/minecraft/world/level/storage/LevelStorageSource$LevelStorageAccess;ZLjava/lang/Runnable;Ljava/lang/Runnable;)V", + at = @At("HEAD"), + cancellable = true, + require = 0 + ) + private void frozenLib$preventBackupScreenAndProceed(LevelStorageSource.LevelStorageAccess levelStorageAccess, boolean bl, Runnable runnable, Runnable runnable2, CallbackInfo info) { + if (FrozenLibConfig.get().removeExperimentalWarning) { + info.cancel(); + runnable.run(); + } + } + + @ModifyVariable(method = "confirmWorldCreation", at = @At("HEAD"), argsOnly = true, ordinal = 0, require = 0) + private static boolean frozenLib$skipCreationWarning(boolean original) { + if (FrozenLibConfig.get().removeExperimentalWarning) { + return true; + } + return original; + } + +} diff --git a/src/main/java/net/frozenblock/lib/datafix/api/BlockStateRenameFix.java b/src/main/java/net/frozenblock/lib/datafix/api/BlockStateRenameFix.java new file mode 100644 index 0000000..4bdd717 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/datafix/api/BlockStateRenameFix.java @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.datafix.api; + +import com.mojang.datafixers.DSL; +import com.mojang.datafixers.DataFix; +import com.mojang.datafixers.TypeRewriteRule; +import com.mojang.datafixers.schemas.Schema; +import com.mojang.serialization.Dynamic; +import net.minecraft.util.datafix.fixes.References; + +import java.util.Optional; + +public class BlockStateRenameFix extends DataFix { + + private final String name; + private final String blockId; + private final String oldState; + private final String defaultValue; + private final String newState; + + public BlockStateRenameFix(Schema outputSchema, String name, String blockId, String oldState, String defaultValue, String newState) { + super(outputSchema, false); + this.name = name; + this.blockId = blockId; + this.oldState = oldState; + this.defaultValue = defaultValue; + this.newState = newState; + } + + private Dynamic fix(Dynamic dynamic) { + Optional optional = dynamic.get("Name").asString().result(); + return optional.equals(Optional.of(this.blockId)) ? dynamic.update("Properties", dynamicx -> { + String string = dynamicx.get(this.oldState).asString(this.defaultValue); + return dynamicx.remove(this.oldState).set(this.newState, dynamicx.createString(string)); + }) : dynamic; + } + + @Override + protected TypeRewriteRule makeRule() { + return this.fixTypeEverywhereTyped( + this.name, this.getInputSchema().getType(References.BLOCK_STATE), typed -> typed.update(DSL.remainderFinder(), this::fix) + ); + } +} diff --git a/src/main/java/net/frozenblock/lib/datafix/api/FrozenEntityRenameFix.java b/src/main/java/net/frozenblock/lib/datafix/api/FrozenEntityRenameFix.java new file mode 100644 index 0000000..a2898ad --- /dev/null +++ b/src/main/java/net/frozenblock/lib/datafix/api/FrozenEntityRenameFix.java @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.datafix.api; + +import com.mojang.datafixers.DSL; +import com.mojang.datafixers.DataFix; +import com.mojang.datafixers.TypeRewriteRule; +import com.mojang.datafixers.schemas.Schema; +import com.mojang.datafixers.types.Type; +import com.mojang.datafixers.util.Pair; +import net.minecraft.util.datafix.fixes.References; +import net.minecraft.util.datafix.schemas.NamespacedSchema; + +import java.util.Objects; +import java.util.function.Function; + +public abstract class FrozenEntityRenameFix extends DataFix { + + private final String name; + + public FrozenEntityRenameFix(Schema outputSchema, String name) { + super(outputSchema, false); + this.name = name; + } + + @Override + public TypeRewriteRule makeRule() { + Type> type = DSL.named(References.ENTITY_NAME.typeName(), NamespacedSchema.namespacedString()); + if (!Objects.equals(this.getInputSchema().getType(References.ENTITY_NAME), type)) { + throw new IllegalStateException("Unexpected entity name type."); + } else { + return this.fixTypeEverywhere(this.name, type, dynamicOps -> pair -> pair.mapSecond(this::fixEntity)); + } + } + + protected abstract String fixEntity(String string); + + public static DataFix create(Schema outputSchema, String name, Function function) { + return new FrozenEntityRenameFix(outputSchema, name) { + @Override + protected String fixEntity(String string) { + return function.apply(string); + } + }; + } +} diff --git a/src/main/java/net/frozenblock/lib/datagen/api/FrozenBiomeTagProvider.java b/src/main/java/net/frozenblock/lib/datagen/api/FrozenBiomeTagProvider.java new file mode 100644 index 0000000..eb02ee9 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/datagen/api/FrozenBiomeTagProvider.java @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.datagen.api; + +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.world.level.biome.Biome; + +import java.util.concurrent.CompletableFuture; + +public abstract class FrozenBiomeTagProvider extends TagsProvider { + protected FrozenBiomeTagProvider(PackOutput output, CompletableFuture lookupProvider, String modId) { + super(output, Registries.BIOME, lookupProvider, modId, null); + } + +} diff --git a/src/main/java/net/frozenblock/lib/entity/api/EntityUtils.java b/src/main/java/net/frozenblock/lib/entity/api/EntityUtils.java new file mode 100644 index 0000000..3531aad --- /dev/null +++ b/src/main/java/net/frozenblock/lib/entity/api/EntityUtils.java @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.entity.api; + +import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import net.minecraft.core.Direction; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.phys.Vec3; +import org.jetbrains.annotations.NotNull; + +public class EntityUtils { + private static final Map> ENTITIES_PER_LEVEL = new Object2ObjectOpenHashMap<>(); + + public static void populateEntitiesPerLevel(@NotNull ServerLevel level) { + clearEntitiesPerLevel(level); + Iterable entityIterable = level.getEntities().getAll(); //TODO: Check if this is correct + ArrayList entityList = new ArrayList<>(); + entityIterable.forEach(entityList::add); + ENTITIES_PER_LEVEL.put(level, List.copyOf(entityList)); + } + + public static void clearEntitiesPerLevel(ServerLevel level) { + ENTITIES_PER_LEVEL.remove(level); + } + + public static List getEntitiesPerLevel(ServerLevel level) { + return ENTITIES_PER_LEVEL.computeIfAbsent(level, serverLevel -> new ArrayList<>()); + } + + public static Optional getMovementDirectionHorizontal(@NotNull Entity entity) { + Direction direction = null; + Vec3 deltaMovement = entity.getDeltaMovement(); + if (deltaMovement.horizontalDistance() > 0) { + double nonNegX = Math.abs(deltaMovement.x); + double nonNegZ = Math.abs(deltaMovement.z); + if (nonNegX > nonNegZ) { + direction = deltaMovement.x > 0 ? Direction.EAST : Direction.WEST; + } else if (nonNegZ > 0) { + direction = deltaMovement.z > 0 ? Direction.SOUTH : Direction.NORTH; + } + } + return Optional.ofNullable(direction); + } + +} \ No newline at end of file diff --git a/src/main/java/net/frozenblock/lib/entity/api/NoFlopAbstractFish.java b/src/main/java/net/frozenblock/lib/entity/api/NoFlopAbstractFish.java new file mode 100644 index 0000000..a4ad90b --- /dev/null +++ b/src/main/java/net/frozenblock/lib/entity/api/NoFlopAbstractFish.java @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.entity.api; + +import net.minecraft.sounds.SoundEvent; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.animal.AbstractFish; +import net.minecraft.world.level.Level; + +/** + * This is the same as {@link AbstractFish} but the entity will not flop when on land. + */ +public abstract class NoFlopAbstractFish extends AbstractFish { + + public NoFlopAbstractFish(EntityType entityType, Level level) { + super(entityType, level); + } + + @Override + protected SoundEvent getFlopSound() { + return null; + } + + /** + * Acts as a form of access widener. + */ + public boolean canRandomSwim() { + return super.canRandomSwim(); + } +} \ No newline at end of file diff --git a/src/main/java/net/frozenblock/lib/entity/api/SilentTicker.java b/src/main/java/net/frozenblock/lib/entity/api/SilentTicker.java new file mode 100644 index 0000000..e3fd020 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/entity/api/SilentTicker.java @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.entity.api; + +import net.minecraft.core.BlockPos; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.Marker; +import net.minecraft.world.level.Level; +import net.minecraft.world.phys.Vec3; + +public abstract class SilentTicker extends Marker { + private int ticks; + + public SilentTicker(EntityType entityType, Level level) { + super(entityType, level); + } + + @Override + public void tick() { + this.ticks += 1; + this.tick(this.level(), this.getPosition(1F), this.blockPosition(), this.ticks); + } + + @Override + public void readAdditionalSaveData(CompoundTag compound) { + super.readAdditionalSaveData(compound); + this.ticks = compound.getInt("frozenlib_ticks"); + } + + @Override + public void addAdditionalSaveData(CompoundTag compound) { + super.addAdditionalSaveData(compound); + compound.putInt("frozenlib_ticks", this.ticks); + } + + public abstract void tick(Level level, Vec3 vec3, BlockPos pos, int ticks); + + public int getTicks() { + return this.ticks; + } + +} \ No newline at end of file diff --git a/src/main/java/net/frozenblock/lib/entity/api/WolfVariantBiomeRegistry.java b/src/main/java/net/frozenblock/lib/entity/api/WolfVariantBiomeRegistry.java new file mode 100644 index 0000000..f36d72e --- /dev/null +++ b/src/main/java/net/frozenblock/lib/entity/api/WolfVariantBiomeRegistry.java @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.entity.api; + +import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import net.frozenblock.lib.math.api.AdvancedMath; +import net.minecraft.core.Registry; +import net.minecraft.core.RegistryAccess; +import net.minecraft.core.registries.Registries; +import net.minecraft.resources.ResourceKey; +import net.minecraft.world.entity.animal.WolfVariant; +import net.minecraft.world.level.biome.Biome; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public class WolfVariantBiomeRegistry { + private static final Map, List>> WOLF_VARIANT_FROM_BIOME = new Object2ObjectOpenHashMap<>(); + + public static void register(@NotNull ResourceKey biome, @NotNull ResourceKey wolfVariant) { + List> variantList = WOLF_VARIANT_FROM_BIOME.getOrDefault(biome, null); + if (variantList == null) { + variantList = new ArrayList<>(); + WOLF_VARIANT_FROM_BIOME.put(biome, variantList); + } + + variantList.add(wolfVariant); + } + + @NotNull + public static Optional> get(ResourceKey biome) { + return Optional.ofNullable(getVariantOrNull(biome)); + } + + @NotNull + public static Optional get(@NotNull RegistryAccess registryManager, ResourceKey biome) { + Registry registry = registryManager.registryOrThrow(Registries.WOLF_VARIANT); + return registry.getOptional(getVariantOrNull(biome)); + } + + @Nullable + private static ResourceKey getVariantOrNull(ResourceKey biome) { + List> variantList = WOLF_VARIANT_FROM_BIOME.getOrDefault(biome, null); + if (variantList != null && !variantList.isEmpty()) { + int size = variantList.size(); + return variantList.get(AdvancedMath.random().nextInt(size)); + } + return null; + } + +} \ No newline at end of file diff --git a/src/main/java/net/frozenblock/lib/entity/api/behavior/BreatheAir.java b/src/main/java/net/frozenblock/lib/entity/api/behavior/BreatheAir.java new file mode 100644 index 0000000..7b68337 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/entity/api/behavior/BreatheAir.java @@ -0,0 +1,90 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.entity.api.behavior; + +import com.google.common.collect.ImmutableMap; +import net.minecraft.core.BlockPos; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.util.Mth; +import net.minecraft.world.entity.MoverType; +import net.minecraft.world.entity.PathfinderMob; +import net.minecraft.world.entity.ai.behavior.Behavior; +import net.minecraft.world.level.LevelReader; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.pathfinder.PathComputationType; +import net.minecraft.world.phys.Vec3; +import org.jetbrains.annotations.NotNull; + +public class BreatheAir extends Behavior { + public BreatheAir() { + super(ImmutableMap.of()); + } + + @Override + public boolean checkExtraStartConditions(ServerLevel level, E owner) { + return super.checkExtraStartConditions(level, owner) && owner.getAirSupply() < 140; + } + + @Override + public boolean canStillUse(ServerLevel level, E owner, long gameTime) { + return this.checkExtraStartConditions(level, owner); + } + + @Override + public void start(ServerLevel level, E entity, long gameTime) { + this.findAirPosition(entity); + } + + private void findAirPosition(@NotNull E entity) { + Iterable iterable = BlockPos.betweenClosed( + Mth.floor(entity.getX() - 1.0), + entity.getBlockY(), + Mth.floor(entity.getZ() - 1.0), + Mth.floor(entity.getX() + 1.0), + Mth.floor(entity.getY() + 8.0), + Mth.floor(entity.getZ() + 1.0) + ); + BlockPos blockPos = null; + + for (BlockPos blockPos2 : iterable) { + if (this.givesAir(entity.level(), blockPos2)) { + blockPos = blockPos2; + break; + } + } + + if (blockPos == null) { + blockPos = BlockPos.containing(entity.getX(), entity.getY() + 8.0, entity.getZ()); + } + + entity.getNavigation().moveTo(blockPos.getX(), blockPos.getY() + 1, blockPos.getZ(), 1.0); + } + + @Override + public void tick(ServerLevel level, E entity, long gameTime) { + this.findAirPosition(entity); + entity.moveRelative(0.02F, new Vec3(entity.xxa, entity.yya, entity.zza)); + entity.move(MoverType.SELF, entity.getDeltaMovement()); + } + + private boolean givesAir(@NotNull LevelReader level, @NotNull BlockPos pos) { + BlockState blockState = level.getBlockState(pos); + return (level.getFluidState(pos).isEmpty() || blockState.is(Blocks.BUBBLE_COLUMN)) && blockState.isPathfindable(PathComputationType.LAND); + } +} \ No newline at end of file diff --git a/src/main/java/net/frozenblock/lib/entity/api/behavior/MoveToBlockBehavior.java b/src/main/java/net/frozenblock/lib/entity/api/behavior/MoveToBlockBehavior.java new file mode 100644 index 0000000..5e21dcb --- /dev/null +++ b/src/main/java/net/frozenblock/lib/entity/api/behavior/MoveToBlockBehavior.java @@ -0,0 +1,139 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.entity.api.behavior; + +import com.google.common.collect.ImmutableMap; +import net.frozenblock.lib.entity.impl.behavior.FrozenBehavior; +import net.minecraft.core.BlockPos; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.entity.PathfinderMob; +import net.minecraft.world.entity.ai.behavior.Behavior; +import net.minecraft.world.entity.ai.goal.MoveToBlockGoal; +import net.minecraft.world.level.LevelReader; + +/** + * {@link MoveToBlockGoal} as a behavior. + */ +public abstract class MoveToBlockBehavior extends Behavior { + public static final int DURATION = 1200; + protected final E mob; + public final double speedModifier; + protected int tryTicks; + protected BlockPos blockPos = BlockPos.ZERO; + private boolean reachedTarget; + private final int searchRange; + private final int verticalSearchRange; + protected int verticalSearchStart; + + public MoveToBlockBehavior(E mob, double speedModifier, int searchRange) { + this(mob, speedModifier, searchRange, 1); + } + + public MoveToBlockBehavior(E mob, double speedModifier, int searchRange, int verticalSearchRange) { + super(ImmutableMap.of(), DURATION); + this.mob = mob; + this.speedModifier = speedModifier; + this.searchRange = searchRange; + this.verticalSearchStart = 0; + this.verticalSearchRange = verticalSearchRange; + } + + @Override + public boolean checkExtraStartConditions(ServerLevel level, E owner) { + return this.findNearestBlock(); + } + + @Override + public boolean canStillUse(ServerLevel level, E entity, long gameTime) { + return this.tryTicks >= -((FrozenBehavior) this).getDuration() && this.tryTicks <= DURATION && this.isValidTarget(level, this.blockPos); + } + + @Override + public void start(ServerLevel level, E entity, long gameTime) { + this.moveMobToBlock(); + this.tryTicks = 0; + } + + protected void moveMobToBlock() { + this.mob + .getNavigation() + .moveTo(this.blockPos.getX() + 0.5, this.blockPos.getY() + 1, this.blockPos.getZ() + 0.5, this.speedModifier); + } + + public double acceptedDistance() { + return 1.0; + } + + protected BlockPos getMoveToTarget() { + return this.blockPos.above(); + } + + @Override + protected void tick(ServerLevel level, E owner, long gameTime) { + BlockPos blockPos = this.getMoveToTarget(); + if (!blockPos.closerToCenterThan(owner.position(), this.acceptedDistance())) { + this.reachedTarget = false; + ++this.tryTicks; + if (this.shouldRecalculatePath()) { + this.mob + .getNavigation() + .moveTo(blockPos.getX() + 0.5, blockPos.getY(), blockPos.getZ() + 0.5, this.speedModifier); + } + } else { + this.reachedTarget = true; + --this.tryTicks; + } + } + + public boolean shouldRecalculatePath() { + return this.tryTicks % 40 == 0; + } + + protected boolean isReachedTarget() { + return this.reachedTarget; + } + + /** + * Searches and sets new destination block and returns true if a suitable block (specified in {@link #isValidTarget(LevelReader, BlockPos)}) can be found. + */ + protected boolean findNearestBlock() { + BlockPos blockPos = this.mob.blockPosition(); + BlockPos.MutableBlockPos mutableBlockPos = new BlockPos.MutableBlockPos(); + + for(int k = this.verticalSearchStart; k <= this.verticalSearchRange; k = k > 0 ? -k : 1 - k) { + for(int l = 0; l < this.searchRange; ++l) { + for(int m = 0; m <= l; m = m > 0 ? -m : 1 - m) { + for(int n = m < l && m > -l ? l : 0; n <= l; n = n > 0 ? -n : 1 - n) { + mutableBlockPos.setWithOffset(blockPos, m, k - 1, n); + if (this.mob.isWithinRestriction(mutableBlockPos) && this.isValidTarget(this.mob.level(), mutableBlockPos)) { + this.blockPos = mutableBlockPos; + return true; + } + } + } + } + } + + return false; + } + + /** + * Return true to set given position as destination. + */ + public abstract boolean isValidTarget(LevelReader level, BlockPos pos); +} \ No newline at end of file diff --git a/src/main/java/net/frozenblock/lib/entity/api/behavior/SmootherSwimmingMoveControl.java b/src/main/java/net/frozenblock/lib/entity/api/behavior/SmootherSwimmingMoveControl.java new file mode 100644 index 0000000..49315f0 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/entity/api/behavior/SmootherSwimmingMoveControl.java @@ -0,0 +1,85 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.entity.api.behavior; + +import net.minecraft.util.Mth; +import net.minecraft.world.entity.Mob; +import net.minecraft.world.entity.ai.attributes.Attributes; +import net.minecraft.world.entity.ai.control.MoveControl; + +public class SmootherSwimmingMoveControl extends MoveControl { + private final float maxTurnX; + private final float maxTurnY; + private final float inWaterSpeedModifier; + private final float outsideWaterSpeedModifier; + private final boolean applyGravity; + + public SmootherSwimmingMoveControl(Mob mob, float maxTurnX, float maxTurnY, float inWaterSpeedModifier, float outsideWaterSpeedModifier, boolean applyGravity) { + super(mob); + this.maxTurnX = maxTurnX; + this.maxTurnY = maxTurnY; + this.inWaterSpeedModifier = inWaterSpeedModifier; + this.outsideWaterSpeedModifier = outsideWaterSpeedModifier; + this.applyGravity = applyGravity; + } + + @Override + public void tick() { + if (this.applyGravity && this.mob.isInWater()) { + this.mob.setDeltaMovement(this.mob.getDeltaMovement().add(0.0, 0.005, 0.0)); + } + + if (this.operation == MoveControl.Operation.MOVE_TO && !this.mob.getNavigation().isDone()) { + double d = this.wantedX - this.mob.getX(); + double e = this.wantedY - this.mob.getY(); + double f = this.wantedZ - this.mob.getZ(); + double g = d * d + e * e + f * f; + if (g < 2.5000003E-7F) { + this.mob.setZza(0.0F); + } else { + float h = (float)(Mth.atan2(f, d) * 180.0F / (float)Math.PI) - 90.0F; + this.mob.setYRot(this.rotlerp(this.mob.getYRot(), h, (float)this.maxTurnY)); + this.mob.yBodyRot = this.mob.getYRot(); + this.mob.yHeadRot = this.mob.getYRot(); + float i = (float)(this.speedModifier * this.mob.getAttributeValue(Attributes.MOVEMENT_SPEED)); + if (this.mob.isInWater()) { + this.mob.setSpeed(i * this.inWaterSpeedModifier); + double j = Math.sqrt(d * d + f * f); + if (Math.abs(e) > 1.0E-5F || Math.abs(j) > 1.0E-5F) { + float k = -((float)(Mth.atan2(e, j) * 180.0F / (float)Math.PI)); + k = Mth.clamp(Mth.wrapDegrees(k), (float)(-this.maxTurnX), (float)this.maxTurnX); + this.mob.setXRot(this.rotlerp(this.mob.getXRot(), k, 5.0F)); + } + + float k = Mth.cos(this.mob.getXRot() * (float) (Math.PI / 180.0)); + float l = Mth.sin(this.mob.getXRot() * (float) (Math.PI / 180.0)); + this.mob.zza = k * i; + this.mob.yya = -l * i; + } else { + this.mob.setSpeed(i * this.outsideWaterSpeedModifier); + } + + } + } else { + this.mob.setSpeed(0.0F); + this.mob.setXxa(0.0F); + this.mob.setYya(0.0F); + this.mob.setZza(0.0F); + } + } +} \ No newline at end of file diff --git a/src/main/java/net/frozenblock/lib/entity/api/command/ScaleEntityCommand.java b/src/main/java/net/frozenblock/lib/entity/api/command/ScaleEntityCommand.java new file mode 100644 index 0000000..961a368 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/entity/api/command/ScaleEntityCommand.java @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.entity.api.command; + +import com.mojang.brigadier.CommandDispatcher; +import com.mojang.brigadier.arguments.DoubleArgumentType; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.commands.Commands; +import net.minecraft.commands.arguments.EntityArgument; +import net.minecraft.network.chat.Component; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.entity.ai.attributes.AttributeInstance; +import net.minecraft.world.entity.ai.attributes.Attributes; +import org.jetbrains.annotations.NotNull; + +public class ScaleEntityCommand { + + public static void register(@NotNull CommandDispatcher dispatcher) { + dispatcher.register(Commands.literal("scale").requires(source -> source.hasPermission(2)) + .then(Commands.argument("targets", EntityArgument.entities()) + .then(Commands.argument("scale", DoubleArgumentType.doubleArg()) + .executes(context -> scale(context.getSource(), EntityArgument.getEntities(context, "targets"), DoubleArgumentType.getDouble(context, "scale"))) + ) + ) + ); + } + + private static int scale(CommandSourceStack source, @NotNull Collection entities, double scale) { + StringBuilder entityString = new StringBuilder(); + int entityAmount = 0; + List affectedEntities = new ArrayList<>(); + for (Entity entity : entities) { + if (entity instanceof LivingEntity livingEntity) { + AttributeInstance attributeInstance = livingEntity.getAttribute(Attributes.SCALE); + if (attributeInstance != null) { + attributeInstance.setBaseValue(scale); + entityAmount += 1; + affectedEntities.add(livingEntity); + } + } + } + + boolean oneEntity = affectedEntities.size() == 1; + for (Entity entity : affectedEntities) { + entityString.append(entity.getDisplayName().getString()).append(oneEntity ? "" : ", "); + } + + if (entityAmount > 0) { + source.sendSuccess(() -> Component.translatable(oneEntity ? "commands.scale.entity.success" : "commands.scale.entity.success.multiple", entityString.toString(), scale), true); + return 1; + } else { + source.sendFailure(Component.translatable("commands.scale.entity.failure")); + return 0; + } + } + +} \ No newline at end of file diff --git a/src/main/java/net/frozenblock/lib/entity/api/rendering/EntityTextureOverride.java b/src/main/java/net/frozenblock/lib/entity/api/rendering/EntityTextureOverride.java index b5ed529..055f9dc 100644 --- a/src/main/java/net/frozenblock/lib/entity/api/rendering/EntityTextureOverride.java +++ b/src/main/java/net/frozenblock/lib/entity/api/rendering/EntityTextureOverride.java @@ -45,7 +45,7 @@ public static EntityTextureOverride register(Resourc } public static EntityTextureOverride register(ResourceLocation key, EntityType type, ResourceLocation texture, Condition condition) { - return Registry.register(FrozenClientRegistry.ENTITY_TEXTURE_OVERRIDES, key, new EntityTextureOverride<>(type, texture, condition)); + return Registry.register(FrozenClientRegistry.ENTITY_TEXTURE_OVERRIDE, key, new EntityTextureOverride<>(type, texture, condition)); } diff --git a/src/main/java/net/frozenblock/lib/entity/api/rendering/FrozenRenderType.java b/src/main/java/net/frozenblock/lib/entity/api/rendering/FrozenRenderType.java new file mode 100644 index 0000000..a891268 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/entity/api/rendering/FrozenRenderType.java @@ -0,0 +1,275 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.entity.api.rendering; + +import com.mojang.blaze3d.vertex.DefaultVertexFormat; +import com.mojang.blaze3d.vertex.VertexFormat; +import java.util.function.BiFunction; +import net.frozenblock.lib.FrozenSharedConstants; +import net.minecraft.Util; +import net.minecraft.client.renderer.RenderStateShard; +import net.minecraft.client.renderer.RenderType; +import net.minecraft.resources.ResourceLocation; +import net.neoforged.api.distmarker.Dist; +import net.neoforged.api.distmarker.OnlyIn; +import org.lwjgl.opengl.GL11C; +import org.lwjgl.opengl.GL20C; +import static net.minecraft.client.renderer.RenderType.create; + +@OnlyIn(Dist.CLIENT) +public final class FrozenRenderType { + private FrozenRenderType() {} + + public static final BiFunction ENTITY_TRANSLUCENT_EMISSIVE_FIXED = Util.memoize( + ((identifier, affectsOutline) -> { + RenderType.CompositeState multiPhaseParameters = RenderType.CompositeState.builder() + .setShaderState(RenderStateShard.RENDERTYPE_ENTITY_TRANSLUCENT_EMISSIVE_SHADER) + .setTextureState(new RenderStateShard.TextureStateShard(identifier, false, false)) + .setTransparencyState(RenderStateShard.TRANSLUCENT_TRANSPARENCY) + .setCullState(RenderStateShard.NO_CULL) + .setWriteMaskState(RenderStateShard.COLOR_DEPTH_WRITE) + .setOverlayState(RenderStateShard.OVERLAY) + .createCompositeState(affectsOutline); + return create( + FrozenSharedConstants.string("entity_translucent_emissive_fixed"), + DefaultVertexFormat.NEW_ENTITY, + VertexFormat.Mode.QUADS, + 256, + true, + true, + multiPhaseParameters + ); + }) + ); + + public static final BiFunction ENTITY_TRANSLUCENT_EMISSIVE_FIXED_CULL = Util.memoize( + ((identifier, affectsOutline) -> { + RenderType.CompositeState multiPhaseParameters = RenderType.CompositeState.builder() + .setShaderState(RenderStateShard.RENDERTYPE_ENTITY_TRANSLUCENT_EMISSIVE_SHADER) + .setTextureState(new RenderStateShard.TextureStateShard(identifier, false, false)) + .setTransparencyState(RenderStateShard.TRANSLUCENT_TRANSPARENCY) + .setCullState(RenderStateShard.CULL) + .setWriteMaskState(RenderStateShard.COLOR_DEPTH_WRITE) + .setOverlayState(RenderStateShard.OVERLAY) + .createCompositeState(affectsOutline); + return create( + FrozenSharedConstants.string("entity_translucent_emissive_fixed_cull"), + DefaultVertexFormat.NEW_ENTITY, + VertexFormat.Mode.QUADS, + 256, + true, + true, + multiPhaseParameters + ); + }) + ); + + public static final BiFunction ENTITY_TRANSLUCENT_EMISSIVE_ALWAYS_RENDER = Util.memoize( + ((texture, affectsOutline) -> create( + FrozenSharedConstants.string("entity_translucent_emissive_always_render"), + DefaultVertexFormat.POSITION_COLOR_TEX_LIGHTMAP, + VertexFormat.Mode.QUADS, + 256, + false, + true, + RenderType.CompositeState.builder() + .setShaderState(RenderStateShard.RENDERTYPE_ENTITY_TRANSLUCENT_EMISSIVE_SHADER) + .setTextureState(new RenderStateShard.TextureStateShard(texture, false, false)) + .setTransparencyState(RenderStateShard.TRANSLUCENT_TRANSPARENCY) + .setCullState(RenderStateShard.NO_CULL) + .setWriteMaskState(RenderStateShard.COLOR_DEPTH_WRITE) + .setDepthTestState(RenderStateShard.NO_DEPTH_TEST) + //.setLayeringState(RenderStateShard.POLYGON_OFFSET_LAYERING) + //.setOutputState(RenderStateShard.TRANSLUCENT_TARGET) + .createCompositeState(affectsOutline) + )) + ); + + public static final BiFunction ENTITY_TRANSLUCENT_EMISSIVE_ALWAYS_RENDER_CULL = Util.memoize( + ((texture, affectsOutline) -> create( + FrozenSharedConstants.string("entity_translucent_emissive_always_render_cull"), + DefaultVertexFormat.POSITION_COLOR_TEX_LIGHTMAP, + VertexFormat.Mode.QUADS, + 256, + false, + true, + RenderType.CompositeState.builder() + .setShaderState(RenderStateShard.RENDERTYPE_ENTITY_TRANSLUCENT_EMISSIVE_SHADER) + .setTextureState(new RenderStateShard.TextureStateShard(texture, false, false)) + .setTransparencyState(RenderStateShard.TRANSLUCENT_TRANSPARENCY) + .setCullState(RenderStateShard.CULL) + .setWriteMaskState(RenderStateShard.COLOR_DEPTH_WRITE) + .setDepthTestState(RenderStateShard.NO_DEPTH_TEST) + //.setLayeringState(RenderStateShard.POLYGON_OFFSET_LAYERING) + //.setOutputState(RenderStateShard.TRANSLUCENT_TARGET) + .createCompositeState(affectsOutline) + )) + ); + + public static final BiFunction APPARITION_OUTER = Util.memoize( + ((texture, affectsOutline) -> create( + FrozenSharedConstants.string("apparition_outer"), + DefaultVertexFormat.NEW_ENTITY, + VertexFormat.Mode.QUADS, + 1536, + false, + true, + RenderType.CompositeState.builder() + .setShaderState(RenderType.RENDERTYPE_EYES_SHADER) + .setTextureState(new RenderStateShard.TextureStateShard(texture, false, false)) + .setTransparencyState(RenderType.TRANSLUCENT_TRANSPARENCY) + .setWriteMaskState(RenderType.COLOR_WRITE) + .setCullState(RenderStateShard.NO_CULL) + .createCompositeState(false) + )) + ); + + public static final BiFunction APPARITION_OUTER_CULL = Util.memoize( + ((texture, affectsOutline) -> create( + FrozenSharedConstants.string("apparition_outer_cull"), + DefaultVertexFormat.NEW_ENTITY, + VertexFormat.Mode.QUADS, + 1536, + false, + true, + RenderType.CompositeState.builder() + .setShaderState(RenderType.RENDERTYPE_EYES_SHADER) + .setTextureState(new RenderStateShard.TextureStateShard(texture, false, false)) + .setTransparencyState(RenderType.TRANSLUCENT_TRANSPARENCY) + .setWriteMaskState(RenderType.COLOR_WRITE) + .createCompositeState(false) + )) + ); + + public static final BiFunction ENTITY_TRANSLUCENT_EMISSIVE_CULL = Util.memoize( + ((identifier, affectsOutline) -> { + RenderType.CompositeState multiPhaseParameters = RenderType.CompositeState.builder() + .setShaderState(RenderStateShard.RENDERTYPE_ENTITY_TRANSLUCENT_EMISSIVE_SHADER) + .setTextureState(new RenderStateShard.TextureStateShard(identifier, false, false)) + .setTransparencyState(RenderStateShard.TRANSLUCENT_TRANSPARENCY) + .setCullState(RenderStateShard.CULL) + .setWriteMaskState(RenderStateShard.COLOR_WRITE) + .setOverlayState(RenderStateShard.OVERLAY) + .setDepthTestState(RenderStateShard.NO_DEPTH_TEST) + .createCompositeState(affectsOutline); + return create( + FrozenSharedConstants.string("entity_translucent_emissive_cull"), + DefaultVertexFormat.NEW_ENTITY, + VertexFormat.Mode.QUADS, + 1536, + true, + true, + multiPhaseParameters + ); + }) + ); + + public static RenderType entityTranslucentEmissiveFixed(ResourceLocation resourceLocation) { + return ENTITY_TRANSLUCENT_EMISSIVE_FIXED.apply(resourceLocation, true); + } + + public static RenderType entityTranslucentEmissiveFixedCull(ResourceLocation resourceLocation) { + return ENTITY_TRANSLUCENT_EMISSIVE_FIXED_CULL.apply(resourceLocation, true); + } + + public static RenderType entityTranslucentEmissiveFixedNoOutline(ResourceLocation resourceLocation) { + return ENTITY_TRANSLUCENT_EMISSIVE_FIXED.apply(resourceLocation, false); + } + + public static RenderType entityTranslucentEmissiveAlwaysRender(ResourceLocation resourceLocation) { + return ENTITY_TRANSLUCENT_EMISSIVE_ALWAYS_RENDER.apply(resourceLocation, false); + } + + public static RenderType entityTranslucentEmissiveAlwaysRenderCull(ResourceLocation resourceLocation) { + return ENTITY_TRANSLUCENT_EMISSIVE_ALWAYS_RENDER_CULL.apply(resourceLocation, false); + } + + public static RenderType apparitionOuter(ResourceLocation resourceLocation) { + return APPARITION_OUTER.apply(resourceLocation, false); + } + + public static RenderType apparitionOuterCull(ResourceLocation resourceLocation) { + return APPARITION_OUTER_CULL.apply(resourceLocation, false); + } + + public static RenderType entityTranslucentEmissiveCull(ResourceLocation resourceLocation) { + return ENTITY_TRANSLUCENT_EMISSIVE_CULL.apply(resourceLocation, true); + } + + /*@Nullable + public static ShaderInstance renderTypeTranslucentCutoutShader; + + public static final RenderStateShard.ShaderStateShard RENDERTYPE_TRANSLUCENT_CUTOUT_SHADER = new RenderStateShard.ShaderStateShard( + WilderWildClient::getRenderTypeTranslucentCutoutShader + ); + + @Nullable + public static ShaderInstance getRenderTypeTranslucentCutoutShader() { + return renderTypeTranslucentCutoutShader; + } + + public static final RenderType TRANSLUCENT_CUTOUT = create( + "translucent_cutout_wilderwild", DefaultVertexFormat.BLOCK, VertexFormat.Mode.QUADS, 2097152, true, true, RenderType.translucentState(RENDERTYPE_TRANSLUCENT_CUTOUT_SHADER) + ); + + public static RenderType translucentCutout() { + return TRANSLUCENT_CUTOUT; + }*/ + + public static RenderType dynamicLightStencil() { + return DYNAMIC_LIGHT_STENCIL; + } + + public static RenderType dynamicLightColor() { + return DYNAMIC_LIGHT_COLOR; + } + + public static final RenderStateShard.OutputStateShard STENCIL_SETUP_AND_LEAK = new RenderStateShard.OutputStateShard("stencil_setup_and_leak", () -> { + GL11C.glClear(GL11C.GL_STENCIL_BUFFER_BIT); + GL11C.glColorMask(false, false, false, false); + GL11C.glEnable(GL11C.GL_DEPTH_TEST); + GL11C.glDepthMask(true); + GL11C.glEnable(GL11C.GL_STENCIL_TEST); + GL11C.glDepthMask(false); + GL11C.glEnable(34383); + GL11C.glDisable(2884); + GL11C.glStencilFunc(519, 0, 255); + GL20C.glStencilOpSeparate(1029, 7680, 34055, 7680); + GL20C.glStencilOpSeparate(1028, 7680, 34056, 7680); + }, () -> { + GL11C.glDisable(34383); + GL11C.glEnable(2884); + GL11C.glColorMask(true, true, true, true); + }); + + public static final RenderStateShard.OutputStateShard STENCIL_RENDER_AND_CLEAR = new RenderStateShard.OutputStateShard("stencil_render_and_clear", () -> { + GL11C.glStencilFunc(514, 1, 255); + GL11C.glDepthFunc(516); + GL11C.glCullFace(1028); + GL20C.glStencilOpSeparate(1028, 7680, 7680, 7680); + }, () -> { + GL11C.glDepthFunc(515); + GL11C.glCullFace(1029); + GL11C.glDepthMask(true); + GL11C.glDisable(2960); + }); + + private static final RenderType DYNAMIC_LIGHT_STENCIL = RenderType.create("dynamic_light_stencil", DefaultVertexFormat.POSITION_COLOR, VertexFormat.Mode.TRIANGLES, 256, RenderType.CompositeState.builder().setShaderState(RenderStateShard.POSITION_COLOR_SHADER).setOutputState(STENCIL_SETUP_AND_LEAK).createCompositeState(false)); + + private static final RenderType DYNAMIC_LIGHT_COLOR = RenderType.create("dynamic_light_color", DefaultVertexFormat.POSITION_COLOR, VertexFormat.Mode.TRIANGLES, 256, RenderType.CompositeState.builder().setShaderState(RenderStateShard.POSITION_COLOR_SHADER).setOutputState(STENCIL_RENDER_AND_CLEAR).setTransparencyState(RenderStateShard.TRANSLUCENT_TRANSPARENCY).createCompositeState(false)); +} \ No newline at end of file diff --git a/src/main/java/net/frozenblock/lib/entity/api/spawnplacement/FrozenSpawnPlacementTypes.java b/src/main/java/net/frozenblock/lib/entity/api/spawnplacement/FrozenSpawnPlacementTypes.java new file mode 100644 index 0000000..d109cc6 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/entity/api/spawnplacement/FrozenSpawnPlacementTypes.java @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.entity.api.spawnplacement; + +import net.minecraft.core.BlockPos; +import net.minecraft.tags.BlockTags; +import net.minecraft.tags.FluidTags; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.SpawnPlacementType; +import net.minecraft.world.level.LevelReader; +import net.minecraft.world.level.NaturalSpawner; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.pathfinder.PathComputationType; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public class FrozenSpawnPlacementTypes { + public static final SpawnPlacementType ON_GROUND_OR_ON_LAVA_SURFACE = new SpawnPlacementType() { + @Override + public boolean isSpawnPositionOk(LevelReader levelReader, BlockPos blockPos, @Nullable EntityType entityType) { + if (entityType != null && levelReader.getWorldBorder().isWithinBounds(blockPos)) { + BlockPos belowPos = blockPos.below(); + BlockState belowState = levelReader.getBlockState(belowPos); + if (!belowState.isValidSpawn(levelReader, belowPos, entityType) && !belowState.getFluidState().is(FluidTags.LAVA) && !belowState.is(Blocks.MAGMA_BLOCK)) { + return false; + } else { + return this.isValidEmptySpawnBlock(levelReader, blockPos, entityType); + } + } + return false; + } + + private boolean isValidEmptySpawnBlock(@NotNull LevelReader levelReader, BlockPos blockPos, EntityType entityType) { + BlockState blockState = levelReader.getBlockState(blockPos); + boolean isSafeBurning = blockState.is(BlockTags.FIRE); + return isSafeBurning || NaturalSpawner.isValidEmptySpawnBlock(levelReader, blockPos, blockState, blockState.getFluidState(), entityType); + } + + @NotNull + @Override + public BlockPos adjustSpawnPosition(@NotNull LevelReader levelReader, @NotNull BlockPos blockPos) { + BlockPos belowPos = blockPos.below(); + return levelReader.getBlockState(belowPos).isPathfindable(PathComputationType.LAND) ? belowPos : blockPos; + } + }; + +} \ No newline at end of file diff --git a/src/main/java/net/frozenblock/lib/entity/behavior/api/FrozenBehaviorUtils.java b/src/main/java/net/frozenblock/lib/entity/behavior/api/FrozenBehaviorUtils.java new file mode 100644 index 0000000..494b3eb --- /dev/null +++ b/src/main/java/net/frozenblock/lib/entity/behavior/api/FrozenBehaviorUtils.java @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.entity.behavior.api; + +import lombok.experimental.UtilityClass; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.entity.ai.behavior.BehaviorControl; +import net.minecraft.world.entity.ai.behavior.OneShot; + +@UtilityClass +public class FrozenBehaviorUtils { + + public static OneShot getOneShot(BehaviorControl control) { + if (!(control instanceof OneShot oneShot)) { + throw new IllegalStateException("Behavior control is not a OneShot"); + } else { + return oneShot; + } + } +} \ No newline at end of file diff --git a/src/main/java/net/frozenblock/lib/entity/impl/EntityStepOnBlockInterface.java b/src/main/java/net/frozenblock/lib/entity/impl/EntityStepOnBlockInterface.java new file mode 100644 index 0000000..de167fe --- /dev/null +++ b/src/main/java/net/frozenblock/lib/entity/impl/EntityStepOnBlockInterface.java @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.entity.impl; + +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.state.BlockState; + +public interface EntityStepOnBlockInterface { + + void frozenLib$onSteppedOnBlock(Level level, BlockPos blockPos, BlockState blockState); + +} \ No newline at end of file diff --git a/src/main/java/net/frozenblock/lib/entity/impl/FrozenStartTrackingEntityInterface.java b/src/main/java/net/frozenblock/lib/entity/impl/FrozenStartTrackingEntityInterface.java new file mode 100644 index 0000000..8b1d39d --- /dev/null +++ b/src/main/java/net/frozenblock/lib/entity/impl/FrozenStartTrackingEntityInterface.java @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.entity.impl; + +import net.minecraft.server.level.ServerPlayer; + +public interface FrozenStartTrackingEntityInterface { + + void frozenLib$playerStartsTracking(ServerPlayer serverPlayer); + +} \ No newline at end of file diff --git a/src/main/java/net/frozenblock/lib/entity/impl/behavior/FrozenBehavior.java b/src/main/java/net/frozenblock/lib/entity/impl/behavior/FrozenBehavior.java new file mode 100644 index 0000000..fb52dde --- /dev/null +++ b/src/main/java/net/frozenblock/lib/entity/impl/behavior/FrozenBehavior.java @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.entity.impl.behavior; + +public interface FrozenBehavior { + + int getDuration(); +} \ No newline at end of file diff --git a/src/main/java/net/frozenblock/lib/entity/impl/client/rendering/CubeInvertInterface.java b/src/main/java/net/frozenblock/lib/entity/impl/client/rendering/CubeInvertInterface.java new file mode 100644 index 0000000..bfb47d6 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/entity/impl/client/rendering/CubeInvertInterface.java @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.entity.impl.client.rendering; + +public interface CubeInvertInterface { + + void frozenLib$setInverted(boolean inverted); + +} \ No newline at end of file diff --git a/src/main/java/net/frozenblock/lib/entity/impl/client/rendering/ModelPartInvertInterface.java b/src/main/java/net/frozenblock/lib/entity/impl/client/rendering/ModelPartInvertInterface.java new file mode 100644 index 0000000..4c997d5 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/entity/impl/client/rendering/ModelPartInvertInterface.java @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.entity.impl.client.rendering; + +public interface ModelPartInvertInterface { + + void frozenLib$setInverted(boolean inverted); + +} \ No newline at end of file diff --git a/src/main/java/net/frozenblock/lib/entity/mixin/AbstractFishMixin.java b/src/main/java/net/frozenblock/lib/entity/mixin/AbstractFishMixin.java new file mode 100644 index 0000000..c779a12 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/entity/mixin/AbstractFishMixin.java @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.entity.mixin; + +import net.frozenblock.lib.entity.api.NoFlopAbstractFish; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.animal.AbstractFish; +import net.minecraft.world.entity.animal.WaterAnimal; +import net.minecraft.world.level.Level; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(AbstractFish.class) +public abstract class AbstractFishMixin extends WaterAnimal { + + private AbstractFishMixin(EntityType entityType, Level level) { + super(entityType, level); + } + + @Inject(method = "aiStep", at = @At("HEAD"), cancellable = true) + private void frozenLib$noFlop(CallbackInfo ci) { + AbstractFish fish = AbstractFish.class.cast(this); + if (fish instanceof NoFlopAbstractFish) { + super.aiStep(); + ci.cancel(); + } + } +} \ No newline at end of file diff --git a/src/main/java/net/frozenblock/lib/entity/mixin/EntityMixin.java b/src/main/java/net/frozenblock/lib/entity/mixin/EntityMixin.java new file mode 100644 index 0000000..df3f065 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/entity/mixin/EntityMixin.java @@ -0,0 +1,73 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.entity.mixin; + +import com.llamalad7.mixinextras.sugar.Local; +import net.frozenblock.lib.entity.impl.EntityStepOnBlockInterface; +import net.frozenblock.lib.entity.impl.FrozenStartTrackingEntityInterface; +import net.frozenblock.lib.screenshake.impl.EntityScreenShakeInterface; +import net.frozenblock.lib.sound.impl.EntityLoopingFadingDistanceSoundInterface; +import net.frozenblock.lib.sound.impl.EntityLoopingSoundInterface; +import net.frozenblock.lib.spotting_icons.impl.EntitySpottingIconInterface; +import net.minecraft.core.BlockPos; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.MoverType; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.phys.Vec3; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(Entity.class) +public abstract class EntityMixin implements FrozenStartTrackingEntityInterface, EntityStepOnBlockInterface { + + @Shadow + public abstract Level level(); + + @Unique + @Override + public void frozenLib$playerStartsTracking(ServerPlayer serverPlayer) { + Entity entity = Entity.class.cast(this); + ((EntityLoopingSoundInterface)entity).frozenLib$getSoundManager().syncWithPlayer(serverPlayer); + ((EntityLoopingFadingDistanceSoundInterface)entity).frozenLib$getFadingSoundManager().syncWithPlayer(serverPlayer); + ((EntitySpottingIconInterface)entity).getSpottingIconManager().sendIconPacket(serverPlayer); + ((EntityScreenShakeInterface)entity).getScreenShakeManager().syncWithPlayer(serverPlayer); + } + + @Inject( + method = "move", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/world/level/block/Block;stepOn(Lnet/minecraft/world/level/Level;Lnet/minecraft/core/BlockPos;Lnet/minecraft/world/level/block/state/BlockState;Lnet/minecraft/world/entity/Entity;)V", + shift = At.Shift.AFTER + ) + ) + public void frozenLib$runSteppedOn(MoverType type, Vec3 pos, CallbackInfo ci, @Local BlockPos blockPos, @Local BlockState blockState) { + this.frozenLib$onSteppedOnBlock(this.level(), blockPos, blockState); + } + + @Override + public void frozenLib$onSteppedOnBlock(Level level, BlockPos blockPos, BlockState blockState) { + + } +} \ No newline at end of file diff --git a/src/main/java/net/frozenblock/lib/entity/mixin/ServerEntityMixin.java b/src/main/java/net/frozenblock/lib/entity/mixin/ServerEntityMixin.java new file mode 100644 index 0000000..7e0a449 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/entity/mixin/ServerEntityMixin.java @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.entity.mixin; + +import net.frozenblock.lib.entity.impl.FrozenStartTrackingEntityInterface; +import net.minecraft.server.level.ServerEntity; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.entity.Entity; +import org.spongepowered.asm.mixin.Final; +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(ServerEntity.class) +public class ServerEntityMixin { + + @Shadow @Final + private Entity entity; + + @Inject(method = "addPairing", at = @At("TAIL")) + public void frozenLib$additionalStartTracking(ServerPlayer player, CallbackInfo info) { + ((FrozenStartTrackingEntityInterface)this.entity).frozenLib$playerStartsTracking(player); + } + +} \ No newline at end of file diff --git a/src/main/java/net/frozenblock/lib/entity/mixin/WolfVariantsMixin.java b/src/main/java/net/frozenblock/lib/entity/mixin/WolfVariantsMixin.java new file mode 100644 index 0000000..47f0cef --- /dev/null +++ b/src/main/java/net/frozenblock/lib/entity/mixin/WolfVariantsMixin.java @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.entity.mixin; + +import com.llamalad7.mixinextras.sugar.Local; +import java.util.Optional; +import net.frozenblock.lib.entity.api.WolfVariantBiomeRegistry; +import net.minecraft.core.Holder; +import net.minecraft.core.Registry; +import net.minecraft.core.RegistryAccess; +import net.minecraft.resources.ResourceKey; +import net.minecraft.world.entity.animal.WolfVariant; +import net.minecraft.world.entity.animal.WolfVariants; +import net.minecraft.world.level.biome.Biome; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +@Mixin(WolfVariants.class) +public class WolfVariantsMixin { + + @Inject( + method = "getSpawnVariant", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/core/Registry;holders()Ljava/util/stream/Stream;", + shift = At.Shift.BEFORE + ), + cancellable = true + ) + private static void frozenLib$checkForNewBiomes( + RegistryAccess registryManager, Holder holder, CallbackInfoReturnable> info, + @Local Registry registry + ) { + Optional> optionalBiome = holder.unwrapKey(); + if (optionalBiome.isPresent()) { + ResourceKey biomeKey = optionalBiome.get(); + Optional> optionalVariant = WolfVariantBiomeRegistry.get(biomeKey); + optionalVariant.ifPresent(wolfVariantResourceKey -> info.setReturnValue(registry.getHolderOrThrow(wolfVariantResourceKey))); + } + } + +} \ No newline at end of file diff --git a/src/main/java/net/frozenblock/lib/entity/mixin/behavior/BehaviorMixin.java b/src/main/java/net/frozenblock/lib/entity/mixin/behavior/BehaviorMixin.java new file mode 100644 index 0000000..282246e --- /dev/null +++ b/src/main/java/net/frozenblock/lib/entity/mixin/behavior/BehaviorMixin.java @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.entity.mixin.behavior; + +import com.llamalad7.mixinextras.sugar.Local; +import net.frozenblock.lib.entity.impl.behavior.FrozenBehavior; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.entity.ai.behavior.Behavior; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +@Mixin(Behavior.class) +public class BehaviorMixin implements FrozenBehavior { + + @Unique + private int frozenLib$duration; + + @Inject(method = "tryStart", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/world/entity/ai/behavior/Behavior;start(Lnet/minecraft/server/level/ServerLevel;Lnet/minecraft/world/entity/LivingEntity;J)V", + shift = At.Shift.BEFORE + ) + ) + private void frozenLib$tryStart(ServerLevel level, E owner, long gameTime, CallbackInfoReturnable info, @Local int i) { + this.frozenLib$duration = i; + } + + @Unique + @Override + public int getDuration() { + return this.frozenLib$duration; + } +} \ No newline at end of file diff --git a/src/main/java/net/frozenblock/lib/entity/mixin/client/rendering/LivingEntityRendererMixin.java b/src/main/java/net/frozenblock/lib/entity/mixin/client/rendering/LivingEntityRendererMixin.java new file mode 100644 index 0000000..c9671c8 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/entity/mixin/client/rendering/LivingEntityRendererMixin.java @@ -0,0 +1,111 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.entity.mixin.client.rendering; + +import net.frozenblock.lib.registry.api.client.FrozenClientRegistry; +import net.minecraft.client.model.EntityModel; +import net.minecraft.client.renderer.RenderType; +import net.minecraft.client.renderer.entity.EntityRenderer; +import net.minecraft.client.renderer.entity.EntityRendererProvider; +import net.minecraft.client.renderer.entity.LivingEntityRenderer; +import net.minecraft.world.entity.LivingEntity; +import net.neoforged.api.distmarker.Dist; +import net.neoforged.api.distmarker.OnlyIn; +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.CallbackInfoReturnable; + +@OnlyIn(Dist.CLIENT) +@Mixin(LivingEntityRenderer.class) +public abstract class LivingEntityRendererMixin> extends EntityRenderer { + + @Shadow + protected M model; + + protected LivingEntityRendererMixin(EntityRendererProvider.Context context) { + super(context); + } + + @Inject( + method = "getRenderType", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/client/model/EntityModel;renderType(Lnet/minecraft/resources/ResourceLocation;)Lnet/minecraft/client/renderer/RenderType;" + ), + cancellable = true + ) + private void frozenLib$getEasterEgg(T livingEntity, boolean bodyVisible, boolean translucent, boolean glowing, CallbackInfoReturnable cir) { + FrozenClientRegistry.ENTITY_TEXTURE_OVERRIDE.forEach(override -> { + if (override.type() == livingEntity.getType()) { + var texture = override.texture(); + if (texture != null) { + if (override.condition().condition(livingEntity)) { + cir.setReturnValue(this.model.renderType(texture)); + } + } + } + }); + } + + @Inject( + method = "getRenderType", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/client/renderer/RenderType;itemEntityTranslucentCull(Lnet/minecraft/resources/ResourceLocation;)Lnet/minecraft/client/renderer/RenderType;" + ), + cancellable = true + ) + private void frozenLib$getItemEasterEgg(T livingEntity, boolean bodyVisible, boolean translucent, boolean glowing, CallbackInfoReturnable cir) { + FrozenClientRegistry.ENTITY_TEXTURE_OVERRIDE.forEach(override -> { + if (override.type() == livingEntity.getType()) { + var texture = override.texture(); + if (texture != null) { + if (override.condition().condition(livingEntity)) { + cir.setReturnValue(RenderType.itemEntityTranslucentCull(texture)); + } + } + } + }); + } + + @Inject( + method = "getRenderType", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/client/renderer/RenderType;outline(Lnet/minecraft/resources/ResourceLocation;)Lnet/minecraft/client/renderer/RenderType;", + shift = At.Shift.BEFORE + ), + cancellable = true) + private void frozenLib$getOutlineEasterEgg(T livingEntity, boolean bodyVisible, boolean translucent, boolean glowing, CallbackInfoReturnable cir) { + if (glowing) { + FrozenClientRegistry.ENTITY_TEXTURE_OVERRIDE.forEach(override -> { + if (override.type() == livingEntity.getType()) { + var texture = override.texture(); + if (texture != null) { + if (override.condition().condition(livingEntity)) { + cir.setReturnValue(RenderType.outline(texture)); + } + } + } + }); + } + } + +} \ No newline at end of file diff --git a/src/main/java/net/frozenblock/lib/entity/mixin/client/rendering/ModelPartCubeMixin.java b/src/main/java/net/frozenblock/lib/entity/mixin/client/rendering/ModelPartCubeMixin.java new file mode 100644 index 0000000..516d931 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/entity/mixin/client/rendering/ModelPartCubeMixin.java @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.entity.mixin.client.rendering; + +import com.llamalad7.mixinextras.injector.ModifyExpressionValue; +import java.util.ArrayList; +import java.util.List; +import net.frozenblock.lib.entity.impl.client.rendering.CubeInvertInterface; +import net.minecraft.client.model.geom.ModelPart; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; + +@Mixin(ModelPart.Cube.class) +public class ModelPartCubeMixin implements CubeInvertInterface { + + @Unique + private boolean frozenLib$inverted = false; + + @ModifyExpressionValue( + method = "compile", + at = @At( + value = "FIELD", + target = "Lnet/minecraft/client/model/geom/ModelPart$Polygon;vertices:[Lnet/minecraft/client/model/geom/ModelPart$Vertex;" + ) + ) + public ModelPart.Vertex[] frozenLib$invertModelPart(ModelPart.Vertex[] original) { + if (this.frozenLib$inverted) { + List vertices = new ArrayList<>(); + for (int i = 0; i < original.length; ++i) { + vertices.add(original[(original.length - 1) - i]); + } + return vertices.toArray(new ModelPart.Vertex[0]); + } + return original; + } + + @Override + public void frozenLib$setInverted(boolean inverted) { + this.frozenLib$inverted = inverted; + } +} \ No newline at end of file diff --git a/src/main/java/net/frozenblock/lib/entity/mixin/client/rendering/ModelPartMixin.java b/src/main/java/net/frozenblock/lib/entity/mixin/client/rendering/ModelPartMixin.java new file mode 100644 index 0000000..f66a609 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/entity/mixin/client/rendering/ModelPartMixin.java @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.entity.mixin.client.rendering; + +import java.util.List; +import net.frozenblock.lib.entity.impl.client.rendering.CubeInvertInterface; +import net.frozenblock.lib.entity.impl.client.rendering.ModelPartInvertInterface; +import net.minecraft.client.model.geom.ModelPart; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; + +@Mixin(ModelPart.class) +public class ModelPartMixin implements ModelPartInvertInterface { + + @Shadow + @Final + private List cubes; + + @Override + public void frozenLib$setInverted(boolean inverted) { + this.cubes.forEach(cube -> { + if (cube instanceof CubeInvertInterface cubeInvert) { + cubeInvert.frozenLib$setInverted(inverted); + } + }); + } +} \ No newline at end of file diff --git a/src/main/java/net/frozenblock/lib/entrypoint/api/ClientEventEntrypoint.java b/src/main/java/net/frozenblock/lib/entrypoint/api/ClientEventEntrypoint.java new file mode 100644 index 0000000..a5f8f81 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/entrypoint/api/ClientEventEntrypoint.java @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.entrypoint.api; + +/** + * A client-sided event callback that can be used as an entrypoint. + *

+ * Defined with the {@code frozenlib:client_events} key in {@code fabric.mod.json}. + */ +public interface ClientEventEntrypoint { +} diff --git a/src/main/java/net/frozenblock/lib/entrypoint/api/CommonEventEntrypoint.java b/src/main/java/net/frozenblock/lib/entrypoint/api/CommonEventEntrypoint.java new file mode 100644 index 0000000..113f7b8 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/entrypoint/api/CommonEventEntrypoint.java @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.entrypoint.api; + +/** + * A common event callback that can be used as an entrypoint. + *

+ * Defined with the {@code frozenlib:events} key in {@code fabric.mod.json}. + */ +public interface CommonEventEntrypoint { +} diff --git a/src/main/java/net/frozenblock/lib/entrypoint/api/DevelopmentCheck.java b/src/main/java/net/frozenblock/lib/entrypoint/api/DevelopmentCheck.java new file mode 100644 index 0000000..0f1729c --- /dev/null +++ b/src/main/java/net/frozenblock/lib/entrypoint/api/DevelopmentCheck.java @@ -0,0 +1,9 @@ +package net.frozenblock.lib.entrypoint.api; + +import net.neoforged.fml.loading.FMLEnvironment; + +public interface DevelopmentCheck { + static boolean isDevelopment() { + return FMLEnvironment.production; + } +} diff --git a/src/main/java/net/frozenblock/lib/entrypoint/api/FrozenClientEntrypoint.java b/src/main/java/net/frozenblock/lib/entrypoint/api/FrozenClientEntrypoint.java new file mode 100644 index 0000000..f47a01a --- /dev/null +++ b/src/main/java/net/frozenblock/lib/entrypoint/api/FrozenClientEntrypoint.java @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.entrypoint.api; + +import net.neoforged.bus.api.Event; + +public class FrozenClientEntrypoint extends Event implements DevelopmentCheck { +} diff --git a/src/main/java/net/frozenblock/lib/entrypoint/api/FrozenMainEntrypoint.java b/src/main/java/net/frozenblock/lib/entrypoint/api/FrozenMainEntrypoint.java new file mode 100644 index 0000000..867988b --- /dev/null +++ b/src/main/java/net/frozenblock/lib/entrypoint/api/FrozenMainEntrypoint.java @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.entrypoint.api; + +import net.neoforged.bus.api.Event; + +/** + * Event called on mod init (Common side)*/ +public class FrozenMainEntrypoint extends Event implements DevelopmentCheck { + +} diff --git a/src/main/java/net/frozenblock/lib/entrypoint/api/FrozenModInitializer.java b/src/main/java/net/frozenblock/lib/entrypoint/api/FrozenModInitializer.java new file mode 100644 index 0000000..1d6d1aa --- /dev/null +++ b/src/main/java/net/frozenblock/lib/entrypoint/api/FrozenModInitializer.java @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.entrypoint.api; + + +import net.frozenblock.lib.FrozenSharedConstants; +import net.minecraft.core.Registry; +import net.minecraft.resources.ResourceLocation; +import net.neoforged.bus.api.IEventBus; +import net.neoforged.fml.ModContainer; +import net.neoforged.neoforge.common.NeoForge; + +public abstract class FrozenModInitializer { + + private final String modId; + private final ModContainer container; + + protected FrozenModInitializer(String modId, IEventBus modEventBus, ModContainer modContainer, Object eventListener) { + this.modId = modId; + this.container = modContainer; + if(eventListener == null) { + FrozenSharedConstants.LOGGER.warn("EventListener for mod " + modId + " is null"); + } else { + NeoForge.EVENT_BUS.register(eventListener); + } + modEventBus.register(this); + + this.onInitialize(modId, modEventBus, modContainer); + } + + public abstract void onInitialize(String modId, IEventBus eventBus, ModContainer container); + + + public ResourceLocation id(String path) { + return ResourceLocation.fromNamespaceAndPath(this.modId, path); + } + + public T register(Registry registry, String path, T value) { + return Registry.register(registry, this.id(path), value); + } +} diff --git a/src/main/java/net/frozenblock/lib/entrypoint/api/ServerEventEntrypoint.java b/src/main/java/net/frozenblock/lib/entrypoint/api/ServerEventEntrypoint.java new file mode 100644 index 0000000..2a3ea14 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/entrypoint/api/ServerEventEntrypoint.java @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.entrypoint.api; + +/** + * A dedicated-server-sided event callback that can be used as an entrypoint. + *

+ * Defined with the {@code frozenlib:server_events} key in {@code fabric.mod.json}. + */ +public interface ServerEventEntrypoint { +} diff --git a/src/main/java/net/frozenblock/lib/event/api/ClientEvent.java b/src/main/java/net/frozenblock/lib/event/api/ClientEvent.java new file mode 100644 index 0000000..d569bfd --- /dev/null +++ b/src/main/java/net/frozenblock/lib/event/api/ClientEvent.java @@ -0,0 +1,42 @@ +package net.frozenblock.lib.event.api; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.multiplayer.ClientLevel; +import net.neoforged.bus.api.Event; +import net.neoforged.fml.event.IModBusEvent; + +public class ClientEvent extends Event implements IModBusEvent { + public final T source; + + private ClientEvent(T source) { + this.source = source; + } + + public static class Started extends ClientEvent { + public Started(Minecraft minecraft) { + super(minecraft); + } + } + + /** + * Use GameShuttingDownEvent instead + * */ + @Deprecated + public static class Stopping extends ClientEvent { + public Stopping(Minecraft minecraft) { + super(minecraft); + } + } + + public static class LevelEnd extends ClientEvent { + public LevelEnd(ClientLevel source) { + super(source); + } + } + + public static class LevelStart extends ClientEvent { + public LevelStart(ClientLevel source) { + super(source); + } + } +} diff --git a/src/main/java/net/frozenblock/lib/event/api/ClientTickEvent.java b/src/main/java/net/frozenblock/lib/event/api/ClientTickEvent.java new file mode 100644 index 0000000..9fb78bd --- /dev/null +++ b/src/main/java/net/frozenblock/lib/event/api/ClientTickEvent.java @@ -0,0 +1,27 @@ +package net.frozenblock.lib.event.api; + +import net.minecraft.client.Minecraft; +import net.neoforged.bus.api.Event; + +public class ClientTickEvent extends Event { + public final Minecraft minecraft; + + private ClientTickEvent(final Minecraft minecraft) { + this.minecraft = minecraft; + } + + public static class Start extends ClientTickEvent { + + public Start(Minecraft minecraft) { + super(minecraft); + } + } + + public static class End extends ClientTickEvent { + + public End(Minecraft minecraft) { + super(minecraft); + } + } + +} diff --git a/src/main/java/net/frozenblock/lib/event/api/FrozenVeinEvent.java b/src/main/java/net/frozenblock/lib/event/api/FrozenVeinEvent.java new file mode 100644 index 0000000..9ba7c01 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/event/api/FrozenVeinEvent.java @@ -0,0 +1,19 @@ +package net.frozenblock.lib.event.api; + +import net.frozenblock.lib.mobcategory.impl.FrozenMobCategory; +import net.frozenblock.lib.worldgen.vein.impl.FrozenVeinType; +import net.neoforged.bus.api.Event; + +import java.util.ArrayList; + +public class FrozenVeinEvent extends Event { + private final ArrayList context; + + public FrozenVeinEvent (final ArrayList context) { + this.context = context; + } + + public void registerCategory(final FrozenVeinType category) { + context.add(category); + } +} diff --git a/src/main/java/net/frozenblock/lib/event/api/GravityModificationEvent.java b/src/main/java/net/frozenblock/lib/event/api/GravityModificationEvent.java new file mode 100644 index 0000000..c158129 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/event/api/GravityModificationEvent.java @@ -0,0 +1,12 @@ +package net.frozenblock.lib.event.api; + +import net.frozenblock.lib.gravity.api.GravityContext; +import net.neoforged.bus.api.Event; + +public class GravityModificationEvent extends Event { + public final GravityContext context; + + public GravityModificationEvent(final GravityContext context) { + this.context = context; + } +} diff --git a/src/main/java/net/frozenblock/lib/event/api/MobCategoryEvent.java b/src/main/java/net/frozenblock/lib/event/api/MobCategoryEvent.java new file mode 100644 index 0000000..9ca352b --- /dev/null +++ b/src/main/java/net/frozenblock/lib/event/api/MobCategoryEvent.java @@ -0,0 +1,18 @@ +package net.frozenblock.lib.event.api; + +import net.frozenblock.lib.mobcategory.impl.FrozenMobCategory; +import net.neoforged.bus.api.Event; + +import java.util.ArrayList; + +public class MobCategoryEvent extends Event { + private final ArrayList context; + + public MobCategoryEvent(final ArrayList context) { + this.context = context; + } + + public void registerCategory(final FrozenMobCategory category) { + context.add(category); + } +} diff --git a/src/main/java/net/frozenblock/lib/event/api/PlayerJoinEvent.java b/src/main/java/net/frozenblock/lib/event/api/PlayerJoinEvent.java new file mode 100644 index 0000000..ee482c5 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/event/api/PlayerJoinEvent.java @@ -0,0 +1,39 @@ +package net.frozenblock.lib.event.api; + +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.neoforged.bus.api.Event; + +public class PlayerJoinEvent extends Event { + + public final MinecraftServer server; + public final ServerPlayer player; + + private PlayerJoinEvent(final MinecraftServer server, final ServerPlayer player) { + this.server = server; + this.player = player; + } + + /** + * The event that is triggered when a player joins the server. + */ + public static class Server extends PlayerJoinEvent { + + public Server(MinecraftServer server, ServerPlayer player) { + super(server, player); + } + } + + /** + * The event that is triggered when a player joins a world. + */ + public static class Level extends PlayerJoinEvent { + public final ServerLevel level; + + public Level(MinecraftServer server, ServerLevel level, ServerPlayer player) { + super(server, player); + this.level = level; + } + } +} diff --git a/src/main/java/net/frozenblock/lib/event/api/RegistryFreezeEvent.java b/src/main/java/net/frozenblock/lib/event/api/RegistryFreezeEvent.java new file mode 100644 index 0000000..b240242 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/event/api/RegistryFreezeEvent.java @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.event.api; + +import net.minecraft.core.Registry; +import net.neoforged.bus.api.Event; +import org.jetbrains.annotations.Nullable; + +/** + * {@link Registry} freeze events + */ +public class RegistryFreezeEvent extends Event { + + @Nullable public final Registry registry; + public final boolean allRegistries; + + private RegistryFreezeEvent(@Nullable Registry registry, boolean allRegistries) { + this.registry = registry; + this.allRegistries = allRegistries; + } + + /** + * Invoked when a {@link Registry} is about to be frozen + *

+ * The registry will not be frozen when this is invoked. + */ + public static class Start extends RegistryFreezeEvent { + + public Start(@Nullable Registry registry, boolean allRegistries) { + super(registry, allRegistries); + } + } + + /** + * Invoked when a {@link Registry} is already frozen + *

+ * The registry will be frozen when this is invoked. + */ + public static class End extends RegistryFreezeEvent { + + public End(@Nullable Registry registry, boolean allRegistries) { + super(registry, allRegistries); + } + } + +} diff --git a/src/main/java/net/frozenblock/lib/event/impl/EventType.java b/src/main/java/net/frozenblock/lib/event/impl/EventType.java new file mode 100644 index 0000000..2b95924 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/event/impl/EventType.java @@ -0,0 +1,91 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.event.impl; + +import net.frozenblock.lib.entrypoint.api.ClientEventEntrypoint; +import net.frozenblock.lib.entrypoint.api.CommonEventEntrypoint; +import net.frozenblock.lib.entrypoint.api.ServerEventEntrypoint; +import org.jetbrains.annotations.ApiStatus; + +import java.util.List; + +/** + * Enum class representing the different environments for events to take place on + */ +@ApiStatus.Internal +public enum EventType { + + /** + * Represents a client-sided event. + */ + CLIENT("frozenlib:client_events", ClientEventEntrypoint.class), + + /** + * Represents a common event. + */ + COMMON("frozenlib:events", CommonEventEntrypoint.class), + + /** + * Represents a server-sided event. + */ + SERVER("frozenlib:server_events", ServerEventEntrypoint.class); + + /** + * A list of all the possible event types. + */ + public static final List VALUES = List.of(values()); + + /** + * The entrypoint string for the event type. + */ + private final String entrypoint; + + /** + * The listener class for the event type. + */ + private final Class listener; + + /** + * Constructor for the EventType enum. + * + * @param entrypoint The entrypoint string for the event type. + * @param listener The listener class for the event type. + */ + EventType(String entrypoint, Class listener) { + this.entrypoint = entrypoint; + this.listener = listener; + } + + /** + * Returns the entrypoint string for the event type. + * + * @return The entrypoint string. + */ + public String entrypoint() { + return this.entrypoint; + } + + /** + * Returns the listener class for the event type. + * + * @return The listener class. + */ + public Class listener() { + return this.listener; + } +} diff --git a/src/main/java/net/frozenblock/lib/event/mixin/BuiltInRegistriesMixin.java b/src/main/java/net/frozenblock/lib/event/mixin/BuiltInRegistriesMixin.java new file mode 100644 index 0000000..b8c9a0b --- /dev/null +++ b/src/main/java/net/frozenblock/lib/event/mixin/BuiltInRegistriesMixin.java @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.event.mixin; + +import net.frozenblock.lib.event.api.RegistryFreezeEvent; +import net.minecraft.core.registries.BuiltInRegistries; +import net.neoforged.neoforge.common.NeoForge; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(BuiltInRegistries.class) +public class BuiltInRegistriesMixin { + + @Inject(method = "freeze", at = @At("HEAD")) + private static void frozenLib$freezeBuiltinsStart(CallbackInfo ci) { + NeoForge.EVENT_BUS.post(new RegistryFreezeEvent.Start(null, true)); + } + + @Inject(method = "freeze", at = @At("TAIL")) + private static void frozenLib$freezeBuiltinsEnd(CallbackInfo ci) { + NeoForge.EVENT_BUS.post(new RegistryFreezeEvent.End(null, true)); + } +} diff --git a/src/main/java/net/frozenblock/lib/event/mixin/ClientLevelMixin.java b/src/main/java/net/frozenblock/lib/event/mixin/ClientLevelMixin.java new file mode 100644 index 0000000..52caa56 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/event/mixin/ClientLevelMixin.java @@ -0,0 +1,22 @@ +package net.frozenblock.lib.event.mixin; + +import net.frozenblock.lib.event.api.ClientEvent; +import net.minecraft.client.multiplayer.ClientLevel; +import net.neoforged.neoforge.common.NeoForge; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(ClientLevel.class) +public class ClientLevelMixin { + @Inject(method = "tickEntities", at = @At("TAIL")) + public void tickWorldAfterBlockEntities(CallbackInfo ci) { + NeoForge.EVENT_BUS.post(new ClientEvent.LevelEnd((ClientLevel) (Object)this)); + } + + @Inject(method = "tickEntities", at = @At("HEAD")) + private void startWorldTick(CallbackInfo ci) { + NeoForge.EVENT_BUS.post(new ClientEvent.LevelStart((ClientLevel) (Object)this)); + } +} diff --git a/src/main/java/net/frozenblock/lib/event/mixin/ConnectionMixin.java b/src/main/java/net/frozenblock/lib/event/mixin/ConnectionMixin.java new file mode 100644 index 0000000..6b78109 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/event/mixin/ConnectionMixin.java @@ -0,0 +1,28 @@ +package net.frozenblock.lib.event.mixin; + +import io.netty.channel.ChannelHandlerContext; +import net.frozenblock.lib.FrozenClient; +import net.frozenblock.lib.networking.FrozenClientNetworking; +import net.minecraft.network.Connection; +import net.neoforged.api.distmarker.Dist; +import net.neoforged.api.distmarker.OnlyIn; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@OnlyIn(Dist.CLIENT) +@Mixin(Connection.class) +public class ConnectionMixin { + @Inject(at = @At("HEAD"), method = "channelInactive") + private void disconnectAddon(ChannelHandlerContext channelHandlerContext, CallbackInfo ci) { + FrozenClient.disconnect(); + FrozenClientNetworking.registerClientReceivers(); + } + + @Inject(method = "handleDisconnection", at = @At(value = "INVOKE", target = "Lnet/minecraft/network/PacketListener;onDisconnect(Lnet/minecraft/network/DisconnectionDetails;)V")) + private void disconnectAddon(CallbackInfo ci) { + FrozenClient.disconnect(); + FrozenClientNetworking.registerClientReceivers(); + } +} diff --git a/src/main/java/net/frozenblock/lib/event/mixin/MappedRegistryMixin.java b/src/main/java/net/frozenblock/lib/event/mixin/MappedRegistryMixin.java new file mode 100644 index 0000000..5c32c9c --- /dev/null +++ b/src/main/java/net/frozenblock/lib/event/mixin/MappedRegistryMixin.java @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.event.mixin; + +import net.frozenblock.lib.event.api.RegistryFreezeEvent; +import net.minecraft.core.MappedRegistry; +import net.minecraft.core.Registry; +import net.neoforged.neoforge.common.NeoForge; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +@Mixin(MappedRegistry.class) +public class MappedRegistryMixin { + + @Inject(method = "freeze", at = @At("HEAD")) + private void frozenLib$freezeStart(CallbackInfoReturnable> cir) { + NeoForge.EVENT_BUS.post(new RegistryFreezeEvent.Start(MappedRegistry.class.cast(this), false)); + } + + @Inject(method = "freeze", at = @At("TAIL")) + private void frozenLib$freezeEnd(CallbackInfoReturnable> cir) { + NeoForge.EVENT_BUS.post(new RegistryFreezeEvent.End(MappedRegistry.class.cast(this), false)); + } +} diff --git a/src/main/java/net/frozenblock/lib/event/mixin/MinecraftMixin.java b/src/main/java/net/frozenblock/lib/event/mixin/MinecraftMixin.java new file mode 100644 index 0000000..5f15b83 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/event/mixin/MinecraftMixin.java @@ -0,0 +1,29 @@ +package net.frozenblock.lib.event.mixin; + +import net.frozenblock.lib.event.api.ClientEvent; +import net.frozenblock.lib.event.api.ClientTickEvent; +import net.minecraft.client.Minecraft; +import net.neoforged.neoforge.common.NeoForge; +import org.spongepowered.asm.mixin.Debug; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(Minecraft.class) +public class MinecraftMixin { + @Inject(at = @At("HEAD"), method = "tick") + private void startClientTick(CallbackInfo info) { + NeoForge.EVENT_BUS.post(new ClientTickEvent.Start((Minecraft) (Object)this)); + } + + @Inject(at = @At("RETURN"), method = "tick") + private void endClientTick(CallbackInfo info) { + NeoForge.EVENT_BUS.post(new ClientTickEvent.End((Minecraft) (Object)this)); + } + + @Inject(at = @At(value = "FIELD", target = "Lnet/minecraft/client/Minecraft;gameThread:Ljava/lang/Thread;", shift = At.Shift.AFTER, ordinal = 0), method = "run") + private void clientStarted(CallbackInfo ci) { + NeoForge.EVENT_BUS.post(new ClientEvent.Started((Minecraft) (Object)this)); + } +} diff --git a/src/main/java/net/frozenblock/lib/event/mixin/PlayerListMixin.java b/src/main/java/net/frozenblock/lib/event/mixin/PlayerListMixin.java new file mode 100644 index 0000000..956783c --- /dev/null +++ b/src/main/java/net/frozenblock/lib/event/mixin/PlayerListMixin.java @@ -0,0 +1,28 @@ +package net.frozenblock.lib.event.mixin; + +import net.frozenblock.lib.FrozenMain; +import net.frozenblock.lib.event.api.PlayerJoinEvent; +import net.minecraft.network.Connection; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.server.network.CommonListenerCookie; +import net.minecraft.server.players.PlayerList; +import net.neoforged.neoforge.common.NeoForge; +import org.spongepowered.asm.mixin.Final; +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(PlayerList.class) +public class PlayerListMixin { + @Shadow + @Final + private MinecraftServer server; + + @Inject(method = "placeNewPlayer", at = @At("TAIL")) + public void frozenLib$onPlayerJoined(Connection connection, ServerPlayer player, CommonListenerCookie commonListenerCookie, CallbackInfo ci) { + NeoForge.EVENT_BUS.post(new PlayerJoinEvent.Server(this.server, player)); + } +} diff --git a/src/main/java/net/frozenblock/lib/event/mixin/ServerLevelMixin.java b/src/main/java/net/frozenblock/lib/event/mixin/ServerLevelMixin.java new file mode 100644 index 0000000..66d553e --- /dev/null +++ b/src/main/java/net/frozenblock/lib/event/mixin/ServerLevelMixin.java @@ -0,0 +1,26 @@ +package net.frozenblock.lib.event.mixin; + +import net.frozenblock.lib.FrozenMain; +import net.frozenblock.lib.event.api.PlayerJoinEvent; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.neoforged.neoforge.common.NeoForge; +import org.spongepowered.asm.mixin.Final; +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(ServerLevel.class) +public class ServerLevelMixin { + @Shadow + @Final + private MinecraftServer server; + + @Inject(method = "addPlayer", at = @At("TAIL")) + private void frozenLib$addPlayer(ServerPlayer player, CallbackInfo info) { + NeoForge.EVENT_BUS.post(new PlayerJoinEvent.Level(this.server, ServerLevel.class.cast(this), player)); + } +} diff --git a/src/main/java/net/frozenblock/lib/feature_flag/api/FrozenFeatureFlags.java b/src/main/java/net/frozenblock/lib/feature_flag/api/FrozenFeatureFlags.java new file mode 100644 index 0000000..d6bce84 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/feature_flag/api/FrozenFeatureFlags.java @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.feature_flag.api; + +import com.google.common.base.Preconditions; +import lombok.experimental.UtilityClass; +import net.minecraft.world.flag.FeatureFlagRegistry; +import net.minecraft.world.flag.FeatureFlags; + +@UtilityClass +public class FrozenFeatureFlags { + + public static FeatureFlagRegistry.Builder builder; + + public static void rebuild() { + Preconditions.checkArgument(builder != null, new NullPointerException("Feature flags rebuilt before builder exists")); + FeatureFlags.REGISTRY = builder.build(); + FeatureFlags.CODEC = FeatureFlags.REGISTRY.codec(); + } +} diff --git a/src/main/java/net/frozenblock/lib/feature_flag/mixin/FeatureFlagBuilderMixin.java b/src/main/java/net/frozenblock/lib/feature_flag/mixin/FeatureFlagBuilderMixin.java new file mode 100644 index 0000000..4b59302 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/feature_flag/mixin/FeatureFlagBuilderMixin.java @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.feature_flag.mixin; + +import com.llamalad7.mixinextras.injector.ModifyExpressionValue; +import net.minecraft.world.flag.FeatureFlagRegistry; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; + +@Mixin(value = FeatureFlagRegistry.Builder.class, priority = 1001) +public class FeatureFlagBuilderMixin { + + @ModifyExpressionValue( + method = "create", + at = @At( + value = "CONSTANT", + args = { + "intValue=64" + } + ) + ) + private int frozenLib$increaseMax(int constant) { + return Math.max(constant, 512); + } +} diff --git a/src/main/java/net/frozenblock/lib/feature_flag/mixin/FeatureFlagsMixin.java b/src/main/java/net/frozenblock/lib/feature_flag/mixin/FeatureFlagsMixin.java new file mode 100644 index 0000000..d813a62 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/feature_flag/mixin/FeatureFlagsMixin.java @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.feature_flag.mixin; + +import com.llamalad7.mixinextras.sugar.Local; +import net.frozenblock.lib.feature_flag.api.FrozenFeatureFlags; +import net.minecraft.world.flag.FeatureFlagRegistry; +import net.minecraft.world.flag.FeatureFlags; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(FeatureFlags.class) +public class FeatureFlagsMixin { + + @Inject( + method = "", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/world/flag/FeatureFlagRegistry$Builder;createVanilla(Ljava/lang/String;)Lnet/minecraft/world/flag/FeatureFlag;", + ordinal = 0 + ) + ) + private static void frozenLib$save(CallbackInfo info, @Local FeatureFlagRegistry.Builder builder) { + FrozenFeatureFlags.builder = builder; + } +} diff --git a/src/main/java/net/frozenblock/lib/file/nbt/NbtFileUtils.java b/src/main/java/net/frozenblock/lib/file/nbt/NbtFileUtils.java new file mode 100644 index 0000000..dd5894a --- /dev/null +++ b/src/main/java/net/frozenblock/lib/file/nbt/NbtFileUtils.java @@ -0,0 +1,80 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.file.nbt; + +import com.mojang.logging.LogUtils; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.NbtIo; +import net.neoforged.fml.loading.FMLPaths; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; + +import java.io.File; +import java.io.IOException; + +public class NbtFileUtils { + private static final Logger LOGGER = LogUtils.getLogger(); + public static final File CONFIG_PATH = FMLPaths.CONFIGDIR.get().toFile(); + + public static void saveToConfigFile(CompoundTag compoundTag, String fileName) { + CONFIG_PATH.mkdirs(); + saveToFile(compoundTag, new File(CONFIG_PATH, withNBTExtension(fileName))); + } + + public static void saveToFile(CompoundTag compoundTag, @NotNull File file, String fileName) { + file.mkdirs(); + File destFile = new File(file, withNBTExtension(fileName)); + saveToFile(compoundTag, destFile); + } + + public static void saveToFile(CompoundTag compoundTag, @NotNull File file) { + file.mkdirs(); + try { + NbtIo.writeCompressed(compoundTag, file.toPath()); + } catch (IOException iOException) { + LOGGER.error("Could not save data {}", file, iOException); + } + } + + @Nullable + public static CompoundTag readFromConfigFile(String fileName) { + return readFromFile(new File(CONFIG_PATH, withNBTExtension(fileName))); + } + + @Nullable + public static CompoundTag readFromFile(File file, String fileName) { + return readFromFile(new File(file, withNBTExtension(fileName))); + } + + @Nullable + public static CompoundTag readFromFile(File file) { + CompoundTag compoundTag = null; + try { + compoundTag = NbtIo.read(file.toPath()); + } catch (IOException iOException) { + LOGGER.error("Could not read data {}", file, iOException); + } + return compoundTag; + } + + public static String withNBTExtension(String string) { + return string + ".nbt"; + } + +} diff --git a/src/main/java/net/frozenblock/lib/gametest/api/PositionType.java b/src/main/java/net/frozenblock/lib/gametest/api/PositionType.java new file mode 100644 index 0000000..349e87e --- /dev/null +++ b/src/main/java/net/frozenblock/lib/gametest/api/PositionType.java @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.gametest.api; + +/** + * @since 1.3.8 + */ +public enum PositionType { + /** + * Converts absolute positions to relative + */ + RELATIVE, + /** + * Converts relative positions to absolute + */ + ABSOLUTE +} diff --git a/src/main/java/net/frozenblock/lib/gametest/api/TrackedPosition.java b/src/main/java/net/frozenblock/lib/gametest/api/TrackedPosition.java new file mode 100644 index 0000000..d3beb56 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/gametest/api/TrackedPosition.java @@ -0,0 +1,136 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.gametest.api; + +import net.minecraft.core.BlockPos; +import net.minecraft.core.Position; +import net.minecraft.core.Vec3i; +import net.minecraft.gametest.framework.GameTestHelper; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.phys.Vec3; +import org.joml.Vector3dc; +import org.joml.Vector3fc; +import org.joml.Vector3ic; + +/** + * + * @param type The {@link PositionType} linked to {@link #pos}. + * @param pos The position + * @param opposite The position in the opposite position type + * @param The type of position being tracked + * @since 1.3.8 + */ +public record TrackedPosition(PositionType type, T pos, T opposite) { + + /** + * Creates a new {@link TrackedPosition} with the given position as its relative value. + */ + public static TrackedPosition createRelative(GameTestHelper helper, BlockPos pos) { + return new TrackedPosition<>(PositionType.RELATIVE, pos, helper.absolutePos(pos)); + } + + /** + * Creates a new {@link TrackedPosition} with the given position as its relative value. + *

+ * Asserts the given block is at the position. + */ + public static TrackedPosition createRelative(GameTestHelper helper, Block block, BlockPos pos) { + return createRelative(helper, pos).assertBlockPresent(helper, block); + } + + /** + * Creates a new {@link TrackedPosition} with the given position as its absolute value. + */ + public static TrackedPosition createAbsolute(GameTestHelper helper, BlockPos pos) { + return new TrackedPosition<>(PositionType.ABSOLUTE, pos, helper.relativePos(pos)); + } + + /** + * Creates a new {@link TrackedPosition} with the given position as its absolute value. + *

+ * Asserts the given block is at the relative position. + */ + public static TrackedPosition createAbsolute(GameTestHelper helper, Block block, BlockPos pos) { + return createAbsolute(helper, pos).assertBlockPresent(helper, block); + } + + /** + * Creates a new {@link TrackedPosition} with the given position as its relative value. + */ + public static TrackedPosition createRelative(GameTestHelper helper, Vec3 pos) { + return new TrackedPosition<>(PositionType.RELATIVE, pos, helper.absoluteVec(pos)); + } + + /** + * Creates a new {@link TrackedPosition} with the given position as its relative value. + *

+ * Asserts the given block is at the position. + */ + public static TrackedPosition createRelative(GameTestHelper helper, Block block, Vec3 pos) { + return createRelative(helper, pos).assertBlockPresent(helper, block); + } + + /** + * Creates a new {@link TrackedPosition} with the given position as its absolute value. + */ + public static TrackedPosition createAbsolute(GameTestHelper helper, Vec3 pos) { + return new TrackedPosition<>(PositionType.ABSOLUTE, pos, helper.relativeVec(pos)); + } + + /** + * Creates a new {@link TrackedPosition} with the given position as its absolute value. + *

+ * Asserts the given block is at the relative position. + */ + public static TrackedPosition createAbsolute(GameTestHelper helper, Block block, Vec3 pos) { + return createAbsolute(helper, pos).assertBlockPresent(helper, block); + } + + public T absolute() { + return switch (this.type()) { + case RELATIVE -> this.opposite(); + case ABSOLUTE -> this.pos(); + }; + } + + public T relative() { + return switch (this.type()) { + case RELATIVE -> this.pos(); + case ABSOLUTE -> this.opposite(); + }; + } + + public TrackedPosition assertBlockPresent(GameTestHelper helper, Block block) throws IllegalStateException { + T relative = this.relative(); + if (relative instanceof Position position) { // covers Vec3 + helper.assertBlockPresent(block, BlockPos.containing(position)); + } else if (relative instanceof BlockPos blockPos) { + helper.assertBlockPresent(block, blockPos); + } else if (relative instanceof Vec3i vec3i) { + helper.assertBlockPresent(block, new BlockPos(vec3i)); + } else if (relative instanceof Vector3ic vec3i) { + helper.assertBlockPresent(block, new BlockPos(vec3i.x(), vec3i.y(), vec3i.z())); + } else if (relative instanceof Vector3fc vec3f) { + helper.assertBlockPresent(block, BlockPos.containing(vec3f.x(), vec3f.y(), vec3f.z())); + } else if (relative instanceof Vector3dc vec3d) { + helper.assertBlockPresent(block, BlockPos.containing(vec3d.x(), vec3d.y(), vec3d.z())); + } else + throw new IllegalStateException("Invalid position type: " + relative.getClass().getName()); + return this; + } +} diff --git a/src/main/java/net/frozenblock/lib/gravity/api/GravityAPI.java b/src/main/java/net/frozenblock/lib/gravity/api/GravityAPI.java new file mode 100644 index 0000000..b288d6c --- /dev/null +++ b/src/main/java/net/frozenblock/lib/gravity/api/GravityAPI.java @@ -0,0 +1,90 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.gravity.api; + +import net.frozenblock.lib.event.api.GravityModificationEvent; +import net.minecraft.resources.ResourceKey; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.level.Level; +import net.minecraft.world.phys.Vec3; +import net.neoforged.bus.api.SubscribeEvent; +import net.neoforged.neoforge.common.NeoForge; +import org.jetbrains.annotations.NotNull; + +import java.util.*; + +public final class GravityAPI { + private GravityAPI() {} + + public static final Vec3 DEFAULT_GRAVITY = new Vec3(0.0, 1.0, 0.0); + + private static final Map, List>> GRAVITY_BELTS = new HashMap<>(); + + public static void register(ResourceKey dimension, GravityBelt gravityBelt) { + getAllBelts(dimension).add(gravityBelt); + } + + @NotNull + public static List> getAllBelts(ResourceKey dimension) { + return GRAVITY_BELTS.computeIfAbsent(dimension, dimension1 -> new ArrayList<>()); + } + + public static List> getAllBelts(Level level) { + return getAllBelts(level.dimension()); + } + + @SubscribeEvent + public static void register(GravityModificationEvent event) { + if (GRAVITY_BELTS.containsKey(event.context.dimension)) { + Optional> optionalGravityBelt = getAffectingGravityBelt(GRAVITY_BELTS.get(event.context.dimension), event.context.y); + if (optionalGravityBelt.isPresent()) { + GravityBelt belt = optionalGravityBelt.get(); + event.context.gravity = belt.getGravity(null, event.context.y); + } + } + } + + public static Vec3 calculateGravity(ResourceKey dimension, double y) { + GravityContext context = new GravityContext(dimension, y, null); + NeoForge.EVENT_BUS.post(new GravityModificationEvent(context)); + return context.gravity; + } + + public static Vec3 calculateGravity(Level level, double y) { + return calculateGravity(level.dimension(), y); + } + + public static Vec3 calculateGravity(Entity entity) { + ResourceKey dimension = entity.level().dimension(); + double y = entity.getY(); + GravityContext context = new GravityContext(dimension, y, entity); + NeoForge.EVENT_BUS.post(new GravityModificationEvent(context)); + return context.gravity; + } + + public static Optional> getAffectingGravityBelt(List> belts, double y) { + Optional> optionalGravityBelt = Optional.empty(); + for (GravityBelt belt : belts) { + if (belt.affectsPosition(y)) { + optionalGravityBelt = Optional.of(belt); + break; + } + } + return optionalGravityBelt; + } +} diff --git a/src/main/java/net/frozenblock/lib/gravity/api/GravityBelt.java b/src/main/java/net/frozenblock/lib/gravity/api/GravityBelt.java new file mode 100644 index 0000000..6211e2f --- /dev/null +++ b/src/main/java/net/frozenblock/lib/gravity/api/GravityBelt.java @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.gravity.api; + +import com.mojang.serialization.Codec; +import com.mojang.serialization.codecs.RecordCodecBuilder; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.phys.Vec3; +import org.jetbrains.annotations.Nullable; + +public record GravityBelt(double minY, double maxY, boolean renderBottom, boolean renderTop, + T function) { + public GravityBelt(double minY, double maxY, T function) { + this(minY, maxY, false, false, function); + } + + public boolean affectsPosition(double y) { + return y >= minY && y < maxY; + } + + public Vec3 getGravity(@Nullable Entity entity, double y) { + if (this.affectsPosition(y)) { + return this.function.get(entity, y, this.minY, this.maxY); + } + return GravityAPI.DEFAULT_GRAVITY; + } + + public static > Codec> codec(Codec gravityFunction) { + return RecordCodecBuilder.create(instance -> + instance.group( + Codec.DOUBLE.fieldOf("minY").forGetter(GravityBelt::minY), + Codec.DOUBLE.fieldOf("maxY").forGetter(GravityBelt::maxY), + gravityFunction.fieldOf("gravityFunction").forGetter(GravityBelt::function) + ).apply(instance, GravityBelt::new) + ); + } + + @Nullable + public static > Codec> codec(T gravityFunction) { + Codec codec = gravityFunction.codec(); + if (codec == null) return null; + return codec(codec); + } +} diff --git a/src/main/java/net/frozenblock/lib/gravity/api/GravityContext.java b/src/main/java/net/frozenblock/lib/gravity/api/GravityContext.java new file mode 100644 index 0000000..8b1cc9b --- /dev/null +++ b/src/main/java/net/frozenblock/lib/gravity/api/GravityContext.java @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.gravity.api; + +import net.minecraft.resources.ResourceKey; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.level.Level; +import net.minecraft.world.phys.Vec3; +import org.jetbrains.annotations.Nullable; + +public class GravityContext { + + public final ResourceKey dimension; + + public final double y; + + /** + * A mutable property that will determine the outputting gravity + */ + public Vec3 gravity; + + @Nullable + public final Entity entity; + + public GravityContext(ResourceKey dimension, double y, @Nullable Entity entity) { + this(dimension, y, GravityAPI.DEFAULT_GRAVITY, entity); + } + + public GravityContext(ResourceKey dimension, double y, Vec3 gravity, @Nullable Entity entity) { + this.dimension = dimension; + this.y = y; + this.gravity = gravity; + this.entity = entity; + } +} diff --git a/src/main/java/net/frozenblock/lib/gravity/api/GravityFunction.java b/src/main/java/net/frozenblock/lib/gravity/api/GravityFunction.java new file mode 100644 index 0000000..a18468d --- /dev/null +++ b/src/main/java/net/frozenblock/lib/gravity/api/GravityFunction.java @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.gravity.api; + +import net.minecraft.world.entity.Entity; +import net.minecraft.world.phys.Vec3; +import org.jetbrains.annotations.Nullable; + +@FunctionalInterface +public interface GravityFunction { + /*** + * @param entity The optional entity being tracked + * @param y The current y position + * @param minY The minimum Y position of the gravity belt + * @param maxY The maximum Y position of the gravity belt + * @return The gravity value + */ + Vec3 get(@Nullable Entity entity, double y, double minY, double maxY); +} diff --git a/src/main/java/net/frozenblock/lib/gravity/api/SerializableGravityFunction.java b/src/main/java/net/frozenblock/lib/gravity/api/SerializableGravityFunction.java new file mode 100644 index 0000000..f20d448 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/gravity/api/SerializableGravityFunction.java @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.gravity.api; + +import com.mojang.serialization.Codec; + +public interface SerializableGravityFunction extends GravityFunction { + Codec codec(); +} diff --git a/src/main/java/net/frozenblock/lib/gravity/api/functions/AbsoluteGravityFunction.java b/src/main/java/net/frozenblock/lib/gravity/api/functions/AbsoluteGravityFunction.java new file mode 100644 index 0000000..c35ed26 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/gravity/api/functions/AbsoluteGravityFunction.java @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.gravity.api.functions; + +import com.mojang.serialization.Codec; +import com.mojang.serialization.codecs.RecordCodecBuilder; +import net.frozenblock.lib.gravity.api.GravityBelt; +import net.frozenblock.lib.gravity.api.SerializableGravityFunction; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.phys.Vec3; +import org.jetbrains.annotations.Nullable; + +public record AbsoluteGravityFunction(Vec3 gravity) implements SerializableGravityFunction { + + public static final Codec CODEC = RecordCodecBuilder.create(instance -> + instance.group( + Vec3.CODEC.fieldOf("gravity").forGetter(AbsoluteGravityFunction::gravity) + ).apply(instance, AbsoluteGravityFunction::new) + ); + + public static final Codec> BELT_CODEC = GravityBelt.codec(CODEC); + + @Override + public Vec3 get(@Nullable Entity entity, double y, double minY, double maxY) { + return gravity(); + } + + @Override + public Codec codec() { + return CODEC; + } +} diff --git a/src/main/java/net/frozenblock/lib/gravity/api/functions/InterpolatedGravityFunction.java b/src/main/java/net/frozenblock/lib/gravity/api/functions/InterpolatedGravityFunction.java new file mode 100644 index 0000000..ce1bf63 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/gravity/api/functions/InterpolatedGravityFunction.java @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.gravity.api.functions; + +import com.mojang.serialization.Codec; +import com.mojang.serialization.codecs.RecordCodecBuilder; +import net.frozenblock.lib.gravity.api.GravityBelt; +import net.frozenblock.lib.gravity.api.SerializableGravityFunction; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.phys.Vec3; +import org.jetbrains.annotations.Nullable; + +public record InterpolatedGravityFunction( + Vec3 gravity + //double minLerpGravity, + //double maxLerpGravity, + //double minLerpY, + //double maxLerpY +) implements SerializableGravityFunction { + + public static final Codec CODEC = RecordCodecBuilder.create(instance -> + instance.group( + Vec3.CODEC.fieldOf("gravity").forGetter(InterpolatedGravityFunction::gravity) + //Codec.DOUBLE.fieldOf("minLerpGravity").forGetter(InterpolatedGravityFunction::minLerpGravity), + //Codec.DOUBLE.fieldOf("maxLerpGravity").forGetter(InterpolatedGravityFunction::maxLerpY), + //Codec.DOUBLE.fieldOf("minLerpY").forGetter(InterpolatedGravityFunction::minLerpY), + //Codec.DOUBLE.fieldOf("maxLerpY").forGetter(InterpolatedGravityFunction::maxLerpY) + ).apply(instance, InterpolatedGravityFunction::new) + ); + + public static final Codec> BELT_CODEC = GravityBelt.codec(CODEC); + + @Override + public Vec3 get(@Nullable Entity entity, double y, double minY, double maxY) { + double normalizedY = (y - minY) / (maxY - minY); + //double normalizedY = (y - minLerpY) / (maxLerpY - minLerpY); + + return gravity.scale(normalizedY); + /*if (normalizedY < 0.5) { + return Mth.clamp(Mth.lerp(normalizedY, minLerpGravity, gravity), minLerpGravity, gravity); + } + if (normalizedY < 1.0) return Mth.lerp(normalizedY, gravity, maxLerpGravity); + return maxLerpGravity;*/ + } + + @Override + public Codec codec() { + return CODEC; + } +} diff --git a/src/main/java/net/frozenblock/lib/gravity/impl/EntityGravityInterface.java b/src/main/java/net/frozenblock/lib/gravity/impl/EntityGravityInterface.java new file mode 100644 index 0000000..599c953 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/gravity/impl/EntityGravityInterface.java @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.gravity.impl; + +import net.minecraft.world.phys.Vec3; + +public interface EntityGravityInterface { + + double frozenLib$getGravity(); + + Vec3 frozenLib$getEffectiveGravity(); +} diff --git a/src/main/java/net/frozenblock/lib/gravity/impl/GravityRenderingImpl.java b/src/main/java/net/frozenblock/lib/gravity/impl/GravityRenderingImpl.java new file mode 100644 index 0000000..f45e0bc --- /dev/null +++ b/src/main/java/net/frozenblock/lib/gravity/impl/GravityRenderingImpl.java @@ -0,0 +1,110 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.gravity.impl; + +import com.mojang.blaze3d.systems.RenderSystem; +import com.mojang.blaze3d.vertex.*; +import com.mojang.math.Axis; +import net.frozenblock.lib.gravity.api.GravityAPI; +import net.frozenblock.lib.gravity.api.GravityBelt; +import net.minecraft.client.Camera; +import net.minecraft.client.multiplayer.ClientLevel; +import net.minecraft.client.renderer.GameRenderer; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.util.Mth; +import org.jetbrains.annotations.ApiStatus; +import org.joml.Matrix4f; + +@ApiStatus.Internal +public final class GravityRenderingImpl { + private GravityRenderingImpl() {} + + private static final ResourceLocation FORCEFIELD_LOCATION = ResourceLocation.withDefaultNamespace("textures/misc/forcefield.png"); + + public static void renderGravityBelts(ClientLevel level, Camera camera, PoseStack poseStack) { + // not working properly + if (true) return; + RenderSystem.defaultBlendFunc(); + double y = camera.getPosition().y(); + for (GravityBelt gravityBelt : GravityAPI.getAllBelts(level)) { + poseStack.pushPose(); + poseStack.mulPose(Axis.YP.rotationDegrees(-90F)); + if (gravityBelt.renderTop()) { + float distance = (float) (gravityBelt.maxY() - y); + float alpha = Mth.lerp(Mth.clamp(Math.abs(distance), 0, 32) / 32F, 1F, 0F); + if (alpha > 0) { + RenderSystem.setShaderColor(0.25f, 0.45f, 1.0f, 1.0f); + poseStack.pushPose(); + poseStack.translate(0, distance, 0); + //poseStack.mulPose(Axis.XP.rotationDegrees((rotation - xRot) * 360F)); + Matrix4f matrix4f3 = poseStack.last().pose(); + + float k = 130; + RenderSystem.setShader(GameRenderer::getPositionTexShader); + RenderSystem.setShaderTexture(0, FORCEFIELD_LOCATION); + var firstBuffer = Tesselator.getInstance().begin(VertexFormat.Mode.QUADS, DefaultVertexFormat.POSITION_TEX); + firstBuffer.addVertex(matrix4f3, -k, 0F, -k).setUv(0.0f, 0.0f); + firstBuffer.addVertex(matrix4f3, k, 0F, -k).setUv(1.0f, 0.0f); + firstBuffer.addVertex(matrix4f3, k, 0F, k).setUv(1.0f, 1.0f); + firstBuffer.addVertex(matrix4f3, -k, 0F, k).setUv(0.0f, 1.0f); + BufferUploader.drawWithShader(firstBuffer.build()); + + var secondBuffer = Tesselator.getInstance().begin(VertexFormat.Mode.QUADS, DefaultVertexFormat.POSITION_TEX); + secondBuffer.addVertex(matrix4f3, -k, 0F, k).setUv(0.0f, 0.0f); + secondBuffer.addVertex(matrix4f3, k, 0F, k).setUv(1.0f, 0.0f); + secondBuffer.addVertex(matrix4f3, k, 0F, -k).setUv(1.0f, 1.0f); + secondBuffer.addVertex(matrix4f3, -k, 0F, -k).setUv(0.0f, 1.0f); + BufferUploader.drawWithShader(secondBuffer.build()); + poseStack.popPose(); + } + } + + if (gravityBelt.renderBottom()) { + float distance = (float) (gravityBelt.minY() - y); + float alpha = Mth.lerp(Mth.clamp(Math.abs(distance), 0, 32) / 32F, 1F, 0F); + if (alpha > 0) { + RenderSystem.setShaderColor(0.25f, 0.45f, 1.0f, 1.0f); + poseStack.pushPose(); + poseStack.translate(0, distance, 0); + //poseStack.mulPose(Axis.XP.rotationDegrees((rotation - xRot) * 360F)); + Matrix4f matrix4f3 = poseStack.last().pose(); + + float k = 130; + RenderSystem.setShader(GameRenderer::getPositionTexShader); + RenderSystem.setShaderTexture(0, FORCEFIELD_LOCATION); + var firstBuffer = Tesselator.getInstance().begin(VertexFormat.Mode.QUADS, DefaultVertexFormat.POSITION_TEX); + firstBuffer.addVertex(matrix4f3, -k, 0F, -k).setUv(0.0f, 0.0f); + firstBuffer.addVertex(matrix4f3, k, 0F, -k).setUv(1.0f, 0.0f); + firstBuffer.addVertex(matrix4f3, k, 0F, k).setUv(1.0f, 1.0f); + firstBuffer.addVertex(matrix4f3, -k, 0F, k).setUv(0.0f, 1.0f); + BufferUploader.drawWithShader(firstBuffer.build()); + + var secondBuffer = Tesselator.getInstance().begin(VertexFormat.Mode.QUADS, DefaultVertexFormat.POSITION_TEX); + secondBuffer.addVertex(matrix4f3, -k, 0F, k).setUv(0.0f, 0.0f); + secondBuffer.addVertex(matrix4f3, k, 0F, k).setUv(1.0f, 0.0f); + secondBuffer.addVertex(matrix4f3, k, 0F, -k).setUv(1.0f, 1.0f); + secondBuffer.addVertex(matrix4f3, -k, 0F, -k).setUv(0.0f, 1.0f); + BufferUploader.drawWithShader(secondBuffer.build()); + poseStack.popPose(); + } + } + poseStack.popPose(); + } + RenderSystem.setShaderColor(1.0f, 1.0f, 1.0f, 1.0f); + } +} diff --git a/src/main/java/net/frozenblock/lib/gravity/mixin/BoatMixin.java b/src/main/java/net/frozenblock/lib/gravity/mixin/BoatMixin.java new file mode 100644 index 0000000..c58a037 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/gravity/mixin/BoatMixin.java @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.gravity.mixin; + +import net.frozenblock.lib.gravity.impl.EntityGravityInterface; +import net.minecraft.world.entity.vehicle.Boat; +import net.minecraft.world.phys.Vec3; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.ModifyArgs; +import org.spongepowered.asm.mixin.injection.invoke.arg.Args; + +@Mixin(Boat.class) +public abstract class BoatMixin implements EntityGravityInterface { + + @ModifyArgs(method = "floatBoat", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/world/entity/vehicle/Boat;setDeltaMovement(DDD)V", + ordinal = 0 + ) + ) + private void frozenLib$useGravity(Args args) { + double x = args.get(0); + double y = (double) args.get(1) + this.frozenLib$getGravity(); + double z = args.get(2); + + Vec3 newVec = new Vec3(x, y, z).subtract(this.frozenLib$getEffectiveGravity()); + args.set(0, newVec.x); + args.set(1, newVec.y); + args.set(2, newVec.z); + } +} diff --git a/src/main/java/net/frozenblock/lib/gravity/mixin/EntityMixin.java b/src/main/java/net/frozenblock/lib/gravity/mixin/EntityMixin.java new file mode 100644 index 0000000..027acf4 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/gravity/mixin/EntityMixin.java @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.gravity.mixin; + +import com.llamalad7.mixinextras.injector.wrapoperation.Operation; +import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; +import com.llamalad7.mixinextras.sugar.Local; +import net.frozenblock.lib.gravity.api.GravityAPI; +import net.frozenblock.lib.gravity.impl.EntityGravityInterface; +import net.minecraft.core.BlockPos; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.phys.Vec3; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(Entity.class) +public abstract class EntityMixin implements EntityGravityInterface { + + @Shadow + public float fallDistance; + + @Inject(method = "checkFallDamage", at = @At("TAIL")) + private void frozenLib$checkFallDamage(double y, boolean onGround, BlockState state, BlockPos pos, CallbackInfo info) { + Vec3 gravity = GravityAPI.calculateGravity(Entity.class.cast(this)); + double gravityDistance = gravity.length(); + this.fallDistance *= (float) gravityDistance; + } + + @WrapOperation( + method = "applyGravity", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/world/phys/Vec3;add(DDD)Lnet/minecraft/world/phys/Vec3;" + ) + ) + public Vec3 frozenLib$applyGravity(Vec3 instance, double x, double y, double z, Operation original, @Local(ordinal = 0) double originalGravity) { + Vec3 gravityVec = GravityAPI.calculateGravity(Entity.class.cast(this)).scale(originalGravity); + Vec3 directional = new Vec3(x, y + originalGravity, z).subtract(gravityVec); + + return original.call(instance, directional.x, directional.y, directional.z); + } + + @Unique + @Override + public double frozenLib$getGravity() { + return 0.04D; + } + + @Unique + @Override + public Vec3 frozenLib$getEffectiveGravity() { + Entity entity = Entity.class.cast(this); + return GravityAPI.calculateGravity(entity).scale(this.frozenLib$getGravity()); + } +} diff --git a/src/main/java/net/frozenblock/lib/gravity/mixin/LivingEntityMixin.java b/src/main/java/net/frozenblock/lib/gravity/mixin/LivingEntityMixin.java new file mode 100644 index 0000000..686a4f5 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/gravity/mixin/LivingEntityMixin.java @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.gravity.mixin; + +import com.llamalad7.mixinextras.injector.ModifyExpressionValue; +import com.llamalad7.mixinextras.injector.wrapoperation.Operation; +import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; +import com.llamalad7.mixinextras.sugar.Local; +import net.frozenblock.lib.gravity.api.GravityAPI; +import net.frozenblock.lib.gravity.impl.EntityGravityInterface; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.phys.Vec3; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; + +@Mixin(LivingEntity.class) +public abstract class LivingEntityMixin implements EntityGravityInterface { + + // TODO: convert to directional + @ModifyExpressionValue( + method = "travel", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/world/entity/LivingEntity;getGravity()D" + ) + ) + private double frozenLib$useGravity(double original) { + LivingEntity entity = LivingEntity.class.cast(this); + return original * GravityAPI.calculateGravity(entity).length(); + } + + @WrapOperation( + method = "travel", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/world/entity/LivingEntity;setDeltaMovement(DDD)V", + ordinal = 3 + ) + ) + private void frozenLib$newGravity(LivingEntity instance, double x, double y, double z, Operation original, @Local(ordinal = 0) double originalGravity) { + LivingEntity entity = LivingEntity.class.cast(this); + Vec3 gravityVec = GravityAPI.calculateGravity(entity).scale(originalGravity); + Vec3 directional = new Vec3(x, y + originalGravity, z).subtract(gravityVec); + + original.call(instance, directional.x, directional.y, directional.z); + } +} diff --git a/src/main/java/net/frozenblock/lib/gravity/mixin/ThrowableProjectileMixin.java b/src/main/java/net/frozenblock/lib/gravity/mixin/ThrowableProjectileMixin.java new file mode 100644 index 0000000..52bb696 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/gravity/mixin/ThrowableProjectileMixin.java @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.gravity.mixin; + +import net.frozenblock.lib.gravity.impl.EntityGravityInterface; +import net.minecraft.world.entity.projectile.ThrowableProjectile; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.Unique; + +@Mixin(ThrowableProjectile.class) +public abstract class ThrowableProjectileMixin implements EntityGravityInterface { + + @Shadow + protected abstract double getDefaultGravity(); + + @Unique + @Override + public double frozenLib$getGravity() { + return this.getDefaultGravity(); + } +} diff --git a/src/main/java/net/frozenblock/lib/gravity/mixin/client/LevelRendererMixin.java b/src/main/java/net/frozenblock/lib/gravity/mixin/client/LevelRendererMixin.java new file mode 100644 index 0000000..397c7eb --- /dev/null +++ b/src/main/java/net/frozenblock/lib/gravity/mixin/client/LevelRendererMixin.java @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.gravity.mixin.client; + +import com.llamalad7.mixinextras.sugar.Local; +import com.mojang.blaze3d.vertex.PoseStack; +import net.frozenblock.lib.gravity.impl.GravityRenderingImpl; +import net.minecraft.client.Camera; +import net.minecraft.client.multiplayer.ClientLevel; +import net.minecraft.client.renderer.LevelRenderer; +import net.neoforged.api.distmarker.Dist; +import net.neoforged.api.distmarker.OnlyIn; +import org.jetbrains.annotations.Nullable; +import org.joml.Matrix4f; +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; + +@OnlyIn(Dist.CLIENT) +@Mixin(LevelRenderer.class) +public class LevelRendererMixin { + + @Shadow + @Nullable + private ClientLevel level; + + @Inject( + method = "renderSky", + at = @At( + value = "INVOKE", + target = "Lcom/mojang/blaze3d/systems/RenderSystem;depthMask(Z)V", + ordinal = 1, + shift = At.Shift.AFTER + ), + require = 0 + ) + private void frozenLib$renderSky( + Matrix4f matrix4f, Matrix4f matrix4f2, float f, Camera camera, boolean bl, Runnable runnable, CallbackInfo info, + @Local(ordinal = 0) PoseStack poseStack + ) { + GravityRenderingImpl.renderGravityBelts(level, camera, poseStack); + } +} diff --git a/src/main/java/net/frozenblock/lib/gravity/mixin/client/ParticleMixin.java b/src/main/java/net/frozenblock/lib/gravity/mixin/client/ParticleMixin.java new file mode 100644 index 0000000..fd45b81 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/gravity/mixin/client/ParticleMixin.java @@ -0,0 +1,90 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.gravity.mixin.client; + +import com.llamalad7.mixinextras.sugar.Share; +import com.llamalad7.mixinextras.sugar.ref.LocalDoubleRef; +import net.frozenblock.lib.gravity.api.GravityAPI; +import net.minecraft.client.multiplayer.ClientLevel; +import net.minecraft.client.particle.Particle; +import net.minecraft.world.phys.Vec3; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(Particle.class) +public class ParticleMixin { + + @Unique + private static final double BASE_GRAVITY = 0.04; + + @Shadow + protected double xd; + + @Shadow + protected double yd; + + @Shadow + protected double zd; + + @Shadow + @Final + protected ClientLevel level; + + @Shadow + public double y; + + @Shadow + protected float gravity; + + @Inject(method = "tick", at = @At("HEAD")) + private void frozenLib$storeY( + CallbackInfo ci, + @Share("oldX") LocalDoubleRef oldX, + @Share("oldY") LocalDoubleRef oldY, + @Share("oldZ") LocalDoubleRef oldZ + ) { + oldX.set(this.xd); + oldY.set(this.yd); + oldZ.set(this.zd); + } + + @Inject( + method = "tick", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/client/particle/Particle;move(DDD)V", + ordinal = 0 + ) + ) + private void frozenLib$useGravity( + CallbackInfo ci, + @Share("oldX") LocalDoubleRef oldX, + @Share("oldY") LocalDoubleRef oldY, + @Share("oldZ") LocalDoubleRef oldZ + ) { + Vec3 gravity = GravityAPI.calculateGravity(this.level, this.y).scale(this.gravity).scale(BASE_GRAVITY); + this.xd = oldX.get() - gravity.x; + this.yd = oldY.get() - gravity.y; + this.zd = oldZ.get() - gravity.z; + } +} diff --git a/src/main/java/net/frozenblock/lib/ingamedevtools/RegisterInGameDevTools.java b/src/main/java/net/frozenblock/lib/ingamedevtools/RegisterInGameDevTools.java new file mode 100644 index 0000000..450a661 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/ingamedevtools/RegisterInGameDevTools.java @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.ingamedevtools; + +import net.frozenblock.lib.FrozenSharedConstants; +import net.frozenblock.lib.ingamedevtools.item.Camera; +import net.frozenblock.lib.ingamedevtools.item.LootTableWhacker; +import net.minecraft.core.Registry; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.Item; +import net.neoforged.neoforge.registries.RegisterEvent; + +public class RegisterInGameDevTools { + public static final Item CAMERA = new Camera(new Item.Properties().stacksTo(1)); + public static final Item LOOT_TABLE_WHACKER = new LootTableWhacker(new Item.Properties().stacksTo(1)); + + public static void register(RegisterEvent.RegisterHelper registry) { + registry.register(FrozenSharedConstants.id("camera"), CAMERA); + registry.register(ResourceLocation.parse(FrozenSharedConstants.string("loot_table_whacker")), LOOT_TABLE_WHACKER); + } +} diff --git a/src/main/java/net/frozenblock/lib/ingamedevtools/item/Camera.java b/src/main/java/net/frozenblock/lib/ingamedevtools/item/Camera.java new file mode 100644 index 0000000..3369f5f --- /dev/null +++ b/src/main/java/net/frozenblock/lib/ingamedevtools/item/Camera.java @@ -0,0 +1,89 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.ingamedevtools.item; + +import net.frozenblock.lib.FrozenSharedConstants; +import net.minecraft.client.Minecraft; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.InteractionResultHolder; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.Level; +import org.jetbrains.annotations.NotNull; + +import java.io.File; +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.Date; + +public class Camera extends Item { + + public Camera(Properties settings) { + super(settings); + } + + private boolean canGo; + + @Override + public void inventoryTick(ItemStack itemStack, Level world, Entity entity, int i, boolean bl) { + if (entity instanceof Player player) { + if (player.getCooldowns().isOnCooldown(this) && player.getCooldowns().getCooldownPercent(this, 0) == 0.9F) { + if (world.isClientSide && canGo) { + FrozenSharedConstants.LOGGER.warn("PLAYER HAS ACCESS TO DEV CAMERA AND HAS JUST USED IT"); + Minecraft client = Minecraft.getInstance(); + File directory = getPanoramaFolderName(new File(client.gameDirectory, "panoramas")); + File directory1 = new File(directory, "screenshots"); + directory1.mkdir(); + directory1.mkdirs(); + client.grabPanoramixScreenshot(directory, 1024, 1024); + canGo = false; + } + } + } + } + + private static final DateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd_HH.mm.ss"); + + private static File getPanoramaFolderName(File directory) { + String string = DATE_FORMAT.format(new Date()); + int i = 1; + while (true) { + File file = new File(directory, string + (i == 1 ? "" : "_" + i)); + if (!file.exists()) { + return file; + } + ++i; + } + } + + @Override + public InteractionResultHolder use(Level world, @NotNull Player user, InteractionHand hand) { + ItemStack itemStack = user.getItemInHand(hand); + if (!user.getCooldowns().isOnCooldown(this)) { + user.getCooldowns().addCooldown(this, 10); + if (world.isClientSide) { + canGo = true; + } + return InteractionResultHolder.success(itemStack); + } + return InteractionResultHolder.fail(itemStack); + } + +} diff --git a/src/main/java/net/frozenblock/lib/ingamedevtools/item/LootTableWhacker.java b/src/main/java/net/frozenblock/lib/ingamedevtools/item/LootTableWhacker.java new file mode 100644 index 0000000..d97dbc3 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/ingamedevtools/item/LootTableWhacker.java @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.ingamedevtools.item; + +import net.frozenblock.lib.FrozenLogUtils; +import net.minecraft.core.BlockPos; +import net.minecraft.core.component.DataComponents; +import net.minecraft.core.registries.Registries; +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.MutableComponent; +import net.minecraft.resources.ResourceKey; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.context.UseOnContext; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.entity.BrushableBlockEntity; +import net.minecraft.world.level.block.entity.RandomizableContainerBlockEntity; +import net.minecraft.world.level.storage.loot.LootTable; +import org.jetbrains.annotations.NotNull; + +public class LootTableWhacker extends Item { + + public LootTableWhacker(Properties settings) { + super(settings); + } + + private static final MutableComponent FAIL_NO_NAME = Component.translatable("frozenlib.loot_table_whacker.fail.no_name"); + + @Override + public InteractionResult useOn(@NotNull UseOnContext context) { + Level level = context.getLevel(); + BlockPos blockPos = context.getClickedPos(); + ItemStack stack = context.getItemInHand(); + Player player = context.getPlayer(); + if (player == null) { + return InteractionResult.PASS; + } + if (stack.has(DataComponents.CUSTOM_NAME)) { + String id = stack.getHoverName().getString(); + ResourceLocation location = ResourceLocation.parse(id); + ResourceKey key = ResourceKey.create(Registries.LOOT_TABLE, location); + if (!level.isClientSide) { + if (level.getBlockEntity(blockPos) instanceof RandomizableContainerBlockEntity loot) { + loot.lootTable = key; + player.displayClientMessage(Component.translatable("frozenlib.loot_table_whacker.success", location.toString()), true); + FrozenLogUtils.log(location.toString(), true); + } else if (level.getBlockEntity(blockPos) instanceof BrushableBlockEntity loot) { + loot.lootTable = key; + player.displayClientMessage(Component.translatable("frozenlib.loot_table_whacker.success", location.toString()), true); + FrozenLogUtils.log(location.toString(), true); + } + } + return InteractionResult.SUCCESS; + } else { + player.displayClientMessage(FAIL_NO_NAME, true); + } + return InteractionResult.FAIL; + } + +} diff --git a/src/main/java/net/frozenblock/lib/integration/api/ModIntegration.java b/src/main/java/net/frozenblock/lib/integration/api/ModIntegration.java new file mode 100644 index 0000000..e5f91b5 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/integration/api/ModIntegration.java @@ -0,0 +1,127 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.integration.api; + +import net.minecraft.client.Minecraft; +import net.minecraft.core.HolderLookup; +import net.minecraft.core.Registry; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.core.registries.Registries; +import net.minecraft.data.registries.VanillaRegistries; +import net.minecraft.resources.ResourceKey; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.tags.TagKey; +import net.minecraft.world.item.Item; +import net.minecraft.world.level.biome.Biome; +import net.minecraft.world.level.block.Block; +import net.neoforged.api.distmarker.Dist; +import net.neoforged.api.distmarker.OnlyIn; +import net.neoforged.fml.ModList; + +public abstract class ModIntegration { + + private final String modID; + private final String modRegistryID; + private final boolean isModLoaded; + + public ModIntegration(String modID, String modRegistryID) { + this.modID = modID; + this.modRegistryID = modRegistryID; + this.isModLoaded = ModList.get().isLoaded(this.modID); + } + + public ModIntegration(String modID) { + this(modID, modID); + } + + public String getID() { + return this.modID; + } + + public ResourceLocation id(String path) { + return ResourceLocation.fromNamespaceAndPath(this.modRegistryID, path); + } + + public Block getBlock(String path) { + return BuiltInRegistries.BLOCK.get(id(path)); + } + + public Item getItem(String path) { + return BuiltInRegistries.ITEM.get(id(path)); + } + + public ResourceKey getBiomeKey(String path) { + return ResourceKey.create(Registries.BIOME, id(path)); + } + + public TagKey getBlockTag(String path) { + var key = id(path); + var registry = BuiltInRegistries.BLOCK; + return getTag(registry, key); + } + + public TagKey getItemTag(String path) { + var key = id(path); + var registry = BuiltInRegistries.ITEM; + return getTag(registry, key); + } + + public TagKey getBiomeTag(String path) { + var key = id(path); + var registry = VanillaRegistries.createLookup().lookupOrThrow(Registries.BIOME); + return getTag(registry, key); + } + + public TagKey getTag(Registry registry, ResourceLocation key) { + return registry.getTagNames() + .filter(tag -> tag.location().equals(key)) + .findAny() + .orElse(TagKey.create(registry.key(), key)); + } + + @SuppressWarnings("unchecked") + public TagKey getTag(HolderLookup.RegistryLookup lookup, ResourceLocation key) { + return lookup.listTagIds() + .filter(tag -> tag.location().equals(key)) + .findAny() + .orElse(TagKey.create((ResourceKey>) lookup.key(), key)); + } + + public boolean modLoaded() { + return this.isModLoaded; + } + + /** + * Runs prior to registries freezing in order to allow for the registering of things. + */ + public void initPreFreeze() { + } + + /** + * Runs prior to registries freezing in order to allow for registering things. + */ + @OnlyIn(Dist.CLIENT) + public void clientInitPreFreeze() { + } + + public abstract void init(); + + @OnlyIn(Dist.CLIENT) + public void clientInit() { + } +} diff --git a/src/main/java/net/frozenblock/lib/integration/api/ModIntegrationSupplier.java b/src/main/java/net/frozenblock/lib/integration/api/ModIntegrationSupplier.java new file mode 100644 index 0000000..55d77d8 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/integration/api/ModIntegrationSupplier.java @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.integration.api; + +import net.frozenblock.lib.integration.impl.EmptyModIntegration; +import net.neoforged.fml.ModList; + +import java.util.Optional; +import java.util.function.Supplier; + +public class ModIntegrationSupplier { + protected final String modID; + protected final boolean isModLoaded; + protected final Optional optionalIntegration; + protected final T unloadedModIntegration; + + public ModIntegrationSupplier(Supplier modIntegrationSupplier, String modID) { + this.modID = modID; + this.isModLoaded = ModList.get().isLoaded(this.modID); + this.optionalIntegration = this.modLoaded() ? Optional.of(modIntegrationSupplier.get()) : Optional.empty(); + this.unloadedModIntegration = (T) new EmptyModIntegration(modID); + } + + public ModIntegrationSupplier(Supplier modIntegrationSupplier, Supplier unloadedModIntegrationSupplier, String modID) { + this.modID = modID; + this.isModLoaded = ModList.get().isLoaded(this.modID); + this.optionalIntegration = this.modLoaded() ? Optional.of(modIntegrationSupplier.get()) : Optional.empty(); + this.unloadedModIntegration = unloadedModIntegrationSupplier.get(); + } + + public T getIntegration() { + return this.optionalIntegration.orElse(this.unloadedModIntegration); + } + + public Optional get() { + return this.optionalIntegration; + } + + public boolean modLoaded() { + return isModLoaded; + } +} diff --git a/src/main/java/net/frozenblock/lib/integration/api/ModIntegrations.java b/src/main/java/net/frozenblock/lib/integration/api/ModIntegrations.java new file mode 100644 index 0000000..def3ca7 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/integration/api/ModIntegrations.java @@ -0,0 +1,86 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.integration.api; + +import lombok.experimental.UtilityClass; +import net.frozenblock.lib.registry.api.FrozenRegistry; +import net.minecraft.core.Registry; +import net.minecraft.resources.ResourceLocation; +import net.neoforged.api.distmarker.Dist; +import net.neoforged.fml.loading.FMLLoader; + +import java.util.List; +import java.util.function.Supplier; + +@UtilityClass +public class ModIntegrations { + + /** + * Registers a mod integration class + * + * @param integration The mod integration class to register + * @param srcModID The id of the mod registering the mod integration + * @param modID The id of the mod being integrated + * @return A {@link ModIntegrationSupplier}. + */ + public static ModIntegrationSupplier register(Supplier integration, String srcModID, String modID) { + return Registry.register(FrozenRegistry.MOD_INTEGRATION, ResourceLocation.fromNamespaceAndPath(srcModID, modID), new ModIntegrationSupplier<>(integration, modID)); + } + + /** + * Registers a mod integration class + * + * @param integration The mod integration class to register + * @param unloadedIntegration The integration to use when the mod is unloaded + * @param srcModID The id of the mod registering the mod integration + * @param modID The id of the mod being integrated + * @return A {@link ModIntegrationSupplier}. + */ + public static ModIntegrationSupplier register(Supplier integration, Supplier unloadedIntegration, String srcModID, String modID) { + return Registry.register(FrozenRegistry.MOD_INTEGRATION, ResourceLocation.fromNamespaceAndPath(srcModID, modID), new ModIntegrationSupplier<>(integration, unloadedIntegration, modID)); + } + + public static List> getIntegrationSuppliers() { + return FrozenRegistry.MOD_INTEGRATION.stream().toList(); + } + + /** + * Runs prior to registries freezing in order to allow for the registering of things. + */ + public static void initializePreFreeze() { + for (var integration : FrozenRegistry.MOD_INTEGRATION) { + integration.getIntegration().initPreFreeze(); + if (FMLLoader.getDist() == Dist.CLIENT) { + integration.getIntegration().clientInitPreFreeze(); + } + } + } + + /** + * Initialize all mod integrations. + */ + public static void initialize() { + for (var integration : FrozenRegistry.MOD_INTEGRATION) { + integration.getIntegration().init(); + if (FMLLoader.getDist() == Dist.CLIENT) { + integration.getIntegration().clientInit(); + } + } + } + +} diff --git a/src/main/java/net/frozenblock/lib/integration/impl/EmptyModIntegration.java b/src/main/java/net/frozenblock/lib/integration/impl/EmptyModIntegration.java new file mode 100644 index 0000000..f160694 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/integration/impl/EmptyModIntegration.java @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.integration.impl; + +import net.frozenblock.lib.integration.api.ModIntegration; +import org.jetbrains.annotations.ApiStatus; + +/** + * An empty mod integration used if a mod is not loaded + */ +@ApiStatus.Internal +public class EmptyModIntegration extends ModIntegration { + public EmptyModIntegration(String modID) { + super(modID); + } + + @Override + public void init() { + + } +} diff --git a/src/main/java/net/frozenblock/lib/item/api/CooldownChange.java b/src/main/java/net/frozenblock/lib/item/api/CooldownChange.java new file mode 100644 index 0000000..0a13852 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/item/api/CooldownChange.java @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.item.api; + +import lombok.experimental.UtilityClass; +import net.frozenblock.lib.item.impl.CooldownInterface; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemCooldowns; +import org.jetbrains.annotations.NotNull; + +@UtilityClass +public class CooldownChange { + + public static void changeCooldown(@NotNull Player player, Item item, int additionalCooldown, int min) { + ItemCooldowns manager = player.getCooldowns(); + ItemCooldowns.CooldownInstance entry = manager.cooldowns.get(item); + if (entry != null) { + int between = entry.endTime - entry.startTime; + if ((between + additionalCooldown) > min) { + ((CooldownInterface)player.getCooldowns()).frozenLib$changeCooldown(item, additionalCooldown); + } + } + } + +} diff --git a/src/main/java/net/frozenblock/lib/item/api/HeavyItemDamageRegistry.java b/src/main/java/net/frozenblock/lib/item/api/HeavyItemDamageRegistry.java new file mode 100644 index 0000000..c304108 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/item/api/HeavyItemDamageRegistry.java @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.item.api; + +import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; +import net.minecraft.util.Mth; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import org.jetbrains.annotations.NotNull; + +public class HeavyItemDamageRegistry { + + private static final Object2ObjectOpenHashMap HEAVY_ITEM_DAMAGE = new Object2ObjectOpenHashMap<>(); + + public static void register(Item item, float startDamage, float maxStackDamage) { + HEAVY_ITEM_DAMAGE.put(item, new HeavyItemDamage(startDamage, maxStackDamage)); + } + + public static float getDamage(@NotNull ItemStack stack) { + Item item = stack.getItem(); + if (HEAVY_ITEM_DAMAGE.containsKey(item)) { + HeavyItemDamage heavyItemDamage = HEAVY_ITEM_DAMAGE.get(item); + float maxStackSize = stack.getMaxStackSize(); + return Mth.lerp(Math.min(maxStackSize, (float)stack.getCount() / maxStackSize), heavyItemDamage.startDamage, heavyItemDamage.maxStackDamage); + } + return 1.0F; + } + + private static class HeavyItemDamage { + private final float startDamage; + private final float maxStackDamage; + + private HeavyItemDamage(float startDamage, float maxStackDamage) { + this.startDamage = startDamage; + this.maxStackDamage = maxStackDamage; + } + } +} diff --git a/src/main/java/net/frozenblock/lib/item/api/ItemBlockStateTagUtils.java b/src/main/java/net/frozenblock/lib/item/api/ItemBlockStateTagUtils.java new file mode 100644 index 0000000..bd940aa --- /dev/null +++ b/src/main/java/net/frozenblock/lib/item/api/ItemBlockStateTagUtils.java @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.item.api; + +import net.minecraft.core.component.DataComponents; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.Tag; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.component.BlockItemStateProperties; +import net.minecraft.world.level.block.state.properties.BooleanProperty; +import net.minecraft.world.level.block.state.properties.Property; +import org.jetbrains.annotations.NotNull; + +public class ItemBlockStateTagUtils { + + public static > T getProperty(@NotNull ItemStack stack, Property property, T defaultValue) { + BlockItemStateProperties blockItemStateProperties = stack.getOrDefault(DataComponents.BLOCK_STATE, BlockItemStateProperties.EMPTY); + if (!blockItemStateProperties.isEmpty()) { + var properties = blockItemStateProperties.properties(); + String stringValue = property.getName(); + if (properties.containsKey(stringValue)) { + return property.getValue(properties.get(stringValue)).orElse(defaultValue); + } + } + return defaultValue; + } + + public static boolean getBoolProperty(@NotNull ItemStack stack, BooleanProperty property, boolean orElse) { + BlockItemStateProperties blockItemStateProperties = stack.getOrDefault(DataComponents.BLOCK_STATE, BlockItemStateProperties.EMPTY); + if (!blockItemStateProperties.isEmpty()) { + var properties = blockItemStateProperties.properties(); + String stringValue = property.getName(); + if (properties.containsKey(stringValue)) { + return properties.get(stringValue).equals("true"); + } + } + return orElse; + } + + public static > void setProperty(@NotNull ItemStack stack, @NotNull Property property, T value) { + BlockItemStateProperties blockItemStateProperties = stack.getOrDefault(DataComponents.BLOCK_STATE, BlockItemStateProperties.EMPTY); + stack.set(DataComponents.BLOCK_STATE, blockItemStateProperties.with(property, value)); + } + + @NotNull + private static CompoundTag getOrCreateBlockStateTag(@NotNull CompoundTag compoundTag) { + CompoundTag blockStateTag; + if (compoundTag.contains("BlockStateTag", Tag.TAG_COMPOUND)) { + blockStateTag = compoundTag.getCompound("BlockStateTag"); + } else { + blockStateTag = new CompoundTag(); + compoundTag.put("BlockStateTag", blockStateTag); + } + return blockStateTag; + } +} diff --git a/src/main/java/net/frozenblock/lib/item/api/PrickOnUseBlockItem.java b/src/main/java/net/frozenblock/lib/item/api/PrickOnUseBlockItem.java new file mode 100644 index 0000000..7009d19 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/item/api/PrickOnUseBlockItem.java @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.item.api; + +import net.minecraft.core.component.DataComponents; +import net.minecraft.resources.ResourceKey; +import net.minecraft.sounds.SoundEvent; +import net.minecraft.world.damagesource.DamageType; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.item.BlockItem; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.Block; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public class PrickOnUseBlockItem extends BlockItem { + public final float damage; + public final SoundEvent hurtSound; + public final ResourceKey damageType; + + public PrickOnUseBlockItem(Block block, Properties properties, float damage, @Nullable SoundEvent sound, ResourceKey damageType) { + super(block, properties); + this.damage = damage; + this.hurtSound = sound; + this.damageType = damageType; + } + + @Override + @NotNull + public ItemStack finishUsingItem(ItemStack stack, Level world, LivingEntity user) { + if (this.components().has(DataComponents.FOOD)) { + user.hurt(world.damageSources().source(this.damageType),this.damage); + if (this.hurtSound != null && !user.isSilent()) { + user.playSound(this.hurtSound, 0.5F, 0.9F + (world.random.nextFloat() * 0.2F)); + } + return user.eat(world, stack); + } + return stack; + } + +} diff --git a/src/main/java/net/frozenblock/lib/item/api/PrickOnUseItem.java b/src/main/java/net/frozenblock/lib/item/api/PrickOnUseItem.java new file mode 100644 index 0000000..536f53d --- /dev/null +++ b/src/main/java/net/frozenblock/lib/item/api/PrickOnUseItem.java @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.item.api; + +import net.minecraft.core.component.DataComponents; +import net.minecraft.resources.ResourceKey; +import net.minecraft.sounds.SoundEvent; +import net.minecraft.world.damagesource.DamageType; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.Level; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public class PrickOnUseItem extends Item { + public final float damage; + public final SoundEvent hurtSound; + public final ResourceKey damageType; + + public PrickOnUseItem(Properties properties, float damage, @Nullable SoundEvent sound, ResourceKey damageType) { + super(properties); + this.damage = damage; + this.hurtSound = sound; + this.damageType = damageType; + } + + @Override + @NotNull + public ItemStack finishUsingItem(ItemStack stack, Level world, LivingEntity user) { + if (this.components().has(DataComponents.FOOD)) { + user.hurt(world.damageSources().source(this.damageType),this.damage); + if (this.hurtSound != null && !user.isSilent()) { + user.playSound(this.hurtSound, 0.5F, 0.9F + (world.random.nextFloat() * 0.2F)); + } + return user.eat(world, stack); + } + return stack; + } + +} diff --git a/src/main/java/net/frozenblock/lib/item/api/axe/AxeBehaviors.java b/src/main/java/net/frozenblock/lib/item/api/axe/AxeBehaviors.java new file mode 100644 index 0000000..1a80b75 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/item/api/axe/AxeBehaviors.java @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.item.api.axe; + +import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +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.state.BlockState; +import org.jetbrains.annotations.Nullable; + +import java.util.Map; + +public class AxeBehaviors { + private static final Map AXE_BEHAVIORS = new Object2ObjectOpenHashMap<>(); + + public static void register(Block block, AxeBehavior axeBehavior) { + AXE_BEHAVIORS.put(block, axeBehavior); + } + + @Nullable + public static AxeBehavior get(Block block) { + return AXE_BEHAVIORS.getOrDefault(block, null); + } + + public interface AxeBehavior { + boolean meetsRequirements(LevelReader level, BlockPos pos, Direction direction, BlockState state); + + BlockState getOutputBlockState(BlockState state); + + void onSuccess(Level level, BlockPos pos, Direction direction, BlockState state, BlockState oldState); + } + +} diff --git a/src/main/java/net/frozenblock/lib/item/api/bonemeal/BonemealBehaviors.java b/src/main/java/net/frozenblock/lib/item/api/bonemeal/BonemealBehaviors.java new file mode 100644 index 0000000..6c0331f --- /dev/null +++ b/src/main/java/net/frozenblock/lib/item/api/bonemeal/BonemealBehaviors.java @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.item.api.bonemeal; + +import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; +import net.minecraft.core.BlockPos; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.util.RandomSource; +import net.minecraft.world.level.LevelReader; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.state.BlockState; +import org.jetbrains.annotations.Nullable; + +import java.util.Map; + +public class BonemealBehaviors { + private static final Map BONEMEAL_BEHAVIORS = new Object2ObjectOpenHashMap<>(); + + public static void register(Block block, BonemealBehavior bonemealBehavior) { + BONEMEAL_BEHAVIORS.put(block, bonemealBehavior); + } + + @Nullable + public static BonemealBehavior get(Block block) { + return BONEMEAL_BEHAVIORS.getOrDefault(block, null); + } + + public interface BonemealBehavior { + boolean meetsRequirements(LevelReader level, BlockPos pos, BlockState state); + + default boolean isBonemealSuccess(LevelReader level, RandomSource random, BlockPos pos, BlockState state) { + return true; + } + + void performBonemeal(ServerLevel level, RandomSource random, BlockPos pos, BlockState state); + } + +} diff --git a/src/main/java/net/frozenblock/lib/item/api/removable/RemovableDataComponents.java b/src/main/java/net/frozenblock/lib/item/api/removable/RemovableDataComponents.java new file mode 100644 index 0000000..fa45a9a --- /dev/null +++ b/src/main/java/net/frozenblock/lib/item/api/removable/RemovableDataComponents.java @@ -0,0 +1,96 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.item.api.removable; + +import net.frozenblock.lib.FrozenLogUtils; +import net.minecraft.core.Holder; +import net.minecraft.core.component.DataComponentType; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.resources.ResourceKey; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.level.Level; + +import java.util.LinkedHashMap; +import java.util.Set; + +public class RemovableDataComponents { + + private static final LinkedHashMap>, RemovableDataComponent> REMOVABLE_DATA_COMPONENTS = new LinkedHashMap<>(); + + public static void register(DataComponentType component, RemovalPredicate removalPredicate, boolean removeOnStackMerge) { + ResourceKey> key = BuiltInRegistries.DATA_COMPONENT_TYPE.getResourceKey(component).orElseThrow(); + Holder> holder = BuiltInRegistries.DATA_COMPONENT_TYPE.getHolderOrThrow(key); + + REMOVABLE_DATA_COMPONENTS.put(holder, new RemovableDataComponent(holder, removalPredicate, removeOnStackMerge)); + } + + public static boolean canRemoveComponent(DataComponentType component, Level level, Entity entity, int slot, boolean selected) { + ResourceKey> key = BuiltInRegistries.DATA_COMPONENT_TYPE.getResourceKey(component).orElseThrow(); + Holder> holder = BuiltInRegistries.DATA_COMPONENT_TYPE.getHolderOrThrow(key); + RemovableDataComponent removableDataComponent = REMOVABLE_DATA_COMPONENTS.get(holder); + if (removableDataComponent != null) { + return removableDataComponent.shouldRemove(level, entity, slot, selected); + } else { + FrozenLogUtils.logError("Unable to find RemovableDataComponent for DataComponent " + key.location() + "!", true, null); + FrozenLogUtils.logError("Please make sure " + key.location() + " is registered in RemovableDataComponents.class!", true, null); + return false; + } + } + + public static boolean shouldRemoveComponentOnStackMerge(DataComponentType component) { + ResourceKey> key = BuiltInRegistries.DATA_COMPONENT_TYPE.getResourceKey(component).orElseThrow(); + Holder> holder = BuiltInRegistries.DATA_COMPONENT_TYPE.getHolderOrThrow(key); + RemovableDataComponent removableDataComponent = REMOVABLE_DATA_COMPONENTS.get(holder); + if (removableDataComponent != null) { + return removableDataComponent.shouldRemoveOnStackMerge(); + } else { + FrozenLogUtils.logError("Unable to find RemovableDataComponent data for DataComponent " + key.location() + "!", true, null); + FrozenLogUtils.logError("Please make sure " + key.location() + " is registered in RemovableDataComponents.class!", true, null); + return true; + } + } + + public static Set>> keys() { + return REMOVABLE_DATA_COMPONENTS.keySet(); + } + + public static class RemovableDataComponent implements RemovalPredicate { + private final Holder> component; + private final RemovalPredicate predicate; + private final boolean removeOnStackMerge; + + public RemovableDataComponent(Holder> component, RemovalPredicate predicate, boolean removeOnStackMerge) { + this.component = component; + this.predicate = predicate; + this.removeOnStackMerge = removeOnStackMerge; + } + + public Holder> getComponent() { + return this.component; + } + + @Override + public boolean shouldRemove(Level level, Entity entity, int slot, boolean selected) { + return this.predicate.shouldRemove(level, entity, slot, selected); + } + + public boolean shouldRemoveOnStackMerge() { + return this.removeOnStackMerge; + } + } +} diff --git a/src/main/java/net/frozenblock/lib/item/api/removable/RemovableItemTags.java b/src/main/java/net/frozenblock/lib/item/api/removable/RemovableItemTags.java new file mode 100644 index 0000000..592c314 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/item/api/removable/RemovableItemTags.java @@ -0,0 +1,89 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.item.api.removable; + +import net.frozenblock.lib.FrozenLogUtils; +import net.minecraft.core.component.DataComponents; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.level.Level; + +import java.util.LinkedHashMap; +import java.util.Set; + +/** + * Targets {@link DataComponents#CUSTOM_DATA } + */ +public class RemovableItemTags { + + private static final LinkedHashMap REMOVABLE_ITEM_TAGS = new LinkedHashMap<>(); + + public static void register(String tagKey, RemovalPredicate removalPredicate, boolean removeOnStackMerge) { + REMOVABLE_ITEM_TAGS.put(tagKey, new RemovableItemTag(tagKey, removalPredicate, removeOnStackMerge)); + } + + public static boolean canRemoveTag(String tagKey, Level level, Entity entity, int slot, boolean selected) { + RemovableItemTag removableItemTag = REMOVABLE_ITEM_TAGS.get(tagKey); + if (removableItemTag != null) { + return removableItemTag.shouldRemove(level, entity, slot, selected); + } else { + FrozenLogUtils.logError("Unable to find RemovableItemTag data for TagKey " + tagKey + "!", true, null); + FrozenLogUtils.logError("Please make sure " + tagKey + " is registered in RemovableItemTags.class!", true, null); + return false; + } + } + + public static boolean shouldRemoveTagOnStackMerge(String tagKey) { + RemovableItemTag removableItemTag = REMOVABLE_ITEM_TAGS.get(tagKey); + if (removableItemTag != null) { + return removableItemTag.shouldRemoveOnStackMerge(); + } else { + FrozenLogUtils.logError("Unable to find RemovableItemTag data for TagKey " + tagKey + "!", true, null); + FrozenLogUtils.logError("Please make sure " + tagKey + " is registered in RemovableItemTags.class!", true, null); + return true; + } + } + + public static Set keys() { + return REMOVABLE_ITEM_TAGS.keySet(); + } + + public static class RemovableItemTag implements RemovalPredicate { + private final String tagKey; + private final RemovalPredicate predicate; + private final boolean removeOnStackMerge; + + public RemovableItemTag(String tagKey, RemovalPredicate predicate, boolean removeOnStackMerge) { + this.tagKey = tagKey; + this.predicate = predicate; + this.removeOnStackMerge = removeOnStackMerge; + } + + public String getTagKey() { + return this.tagKey; + } + + @Override + public boolean shouldRemove(Level level, Entity entity, int slot, boolean selected) { + return this.predicate.shouldRemove(level, entity, slot, selected); + } + + public boolean shouldRemoveOnStackMerge() { + return this.removeOnStackMerge; + } + } +} diff --git a/src/main/java/net/frozenblock/lib/item/api/removable/RemovalPredicate.java b/src/main/java/net/frozenblock/lib/item/api/removable/RemovalPredicate.java new file mode 100644 index 0000000..7fde67c --- /dev/null +++ b/src/main/java/net/frozenblock/lib/item/api/removable/RemovalPredicate.java @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.item.api.removable; + +import net.minecraft.world.entity.Entity; +import net.minecraft.world.level.Level; + +@FunctionalInterface +public interface RemovalPredicate { + boolean shouldRemove(Level level, Entity entity, int slot, boolean selected); +} diff --git a/src/main/java/net/frozenblock/lib/item/api/shovel/ShovelBehaviors.java b/src/main/java/net/frozenblock/lib/item/api/shovel/ShovelBehaviors.java new file mode 100644 index 0000000..c16e485 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/item/api/shovel/ShovelBehaviors.java @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.item.api.shovel; + +import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +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.state.BlockState; +import org.jetbrains.annotations.Nullable; + +import java.util.Map; + +public class ShovelBehaviors { + private static final Map SHOVEL_BEHAVIORS = new Object2ObjectOpenHashMap<>(); + + public static void register(Block block, ShovelBehavior shovelBehavior) { + SHOVEL_BEHAVIORS.put(block, shovelBehavior); + } + + @Nullable + public static ShovelBehavior get(Block block) { + return SHOVEL_BEHAVIORS.getOrDefault(block, null); + } + + public interface ShovelBehavior { + boolean meetsRequirements(LevelReader level, BlockPos pos, Direction direction, BlockState state); + + BlockState getOutputBlockState(BlockState state); + + void onSuccess(Level level, BlockPos pos, Direction direction, BlockState state, BlockState oldState); + } + +} diff --git a/src/main/java/net/frozenblock/lib/item/impl/CooldownInterface.java b/src/main/java/net/frozenblock/lib/item/impl/CooldownInterface.java new file mode 100644 index 0000000..9fb7617 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/item/impl/CooldownInterface.java @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.item.impl; + +import net.minecraft.world.item.Item; + +public interface CooldownInterface { + + void frozenLib$changeCooldown(Item item, int additional); + + void frozenLib$onCooldownChanged(Item item, int additional); + +} diff --git a/src/main/java/net/frozenblock/lib/item/impl/ItemStackExtension.java b/src/main/java/net/frozenblock/lib/item/impl/ItemStackExtension.java new file mode 100644 index 0000000..f117ab1 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/item/impl/ItemStackExtension.java @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.item.impl; + +public interface ItemStackExtension { + + boolean frozenLib$canRemoveTags(); + + void frozenLib$setCanRemoveTags(boolean canRemoveTags); + +} diff --git a/src/main/java/net/frozenblock/lib/item/impl/SaveableItemCooldowns.java b/src/main/java/net/frozenblock/lib/item/impl/SaveableItemCooldowns.java new file mode 100644 index 0000000..4b32c10 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/item/impl/SaveableItemCooldowns.java @@ -0,0 +1,118 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.item.impl; + +import com.mojang.serialization.Codec; +import com.mojang.serialization.Dynamic; +import com.mojang.serialization.codecs.RecordCodecBuilder; +import net.frozenblock.lib.FrozenSharedConstants; +import net.frozenblock.lib.config.frozenlib_config.FrozenLibConfig; +import net.frozenblock.lib.item.impl.network.CooldownTickCountPacket; +import net.frozenblock.lib.item.impl.network.ForcedCooldownPacket; +import net.frozenblock.lib.tag.api.FrozenItemTags; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.NbtOps; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.server.packs.repository.Pack; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemCooldowns; +import net.neoforged.neoforge.network.PacketDistributor; +import org.jetbrains.annotations.NotNull; +import org.slf4j.Logger; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +public class SaveableItemCooldowns { + + @NotNull + public static List makeSaveableCooldownInstanceList(@NotNull ServerPlayer player) { + ArrayList saveableCooldownInstances = new ArrayList<>(); + int tickCount = player.getCooldowns().tickCount; + player.getCooldowns().cooldowns.forEach( + ((item, cooldownInstance) -> { + if (item.builtInRegistryHolder().is(FrozenItemTags.ALWAYS_SAVE_COOLDOWNS) || FrozenLibConfig.get().saveItemCooldowns) { + saveableCooldownInstances.add(SaveableCooldownInstance.makeFromCooldownInstance(item, cooldownInstance, tickCount)); + } + }) + ); + return saveableCooldownInstances; + } + + public static void saveCooldowns(@NotNull CompoundTag tag, @NotNull ServerPlayer player) { + Logger logger = FrozenSharedConstants.LOGGER; + SaveableCooldownInstance.CODEC.listOf() + .encodeStart(NbtOps.INSTANCE, makeSaveableCooldownInstanceList(player)) + .resultOrPartial(logger::error) + .ifPresent((savedItemCooldownsNbt) -> tag.put("FrozenLibSavedItemCooldowns", savedItemCooldownsNbt)); + } + + @NotNull + public static List readCooldowns(@NotNull CompoundTag tag) { + ArrayList saveableCooldownInstances = new ArrayList<>(); + if (tag.contains("FrozenLibSavedItemCooldowns", 9)) { + Logger logger = FrozenSharedConstants.LOGGER; + SaveableCooldownInstance.CODEC.listOf().parse(new Dynamic<>(NbtOps.INSTANCE, tag.getList("FrozenLibSavedItemCooldowns", 10))) + .resultOrPartial(logger::error) + .ifPresent(saveableCooldownInstances::addAll); + } + return saveableCooldownInstances; + } + + public static void setCooldowns(@NotNull List saveableCooldownInstances, @NotNull ServerPlayer player) { + if (!player.level().isClientSide) { + ItemCooldowns itemCooldowns = player.getCooldowns(); + int tickCount = itemCooldowns.tickCount; + + PacketDistributor.sendToPlayer(player, new CooldownTickCountPacket(tickCount)); + + for (SaveableCooldownInstance saveableCooldownInstance : saveableCooldownInstances) { + int cooldownLeft = saveableCooldownInstance.cooldownLeft(); + int startTime = tickCount - (saveableCooldownInstance.totalCooldownTime() - cooldownLeft); + int endTime = tickCount + cooldownLeft; + Optional optionalItem = BuiltInRegistries.ITEM.getOptional(saveableCooldownInstance.itemResourceLocation()); + if (optionalItem.isPresent()) { + Item item = optionalItem.get(); + itemCooldowns.cooldowns.put(item, new ItemCooldowns.CooldownInstance(startTime, endTime)); + PacketDistributor.sendToPlayer(player, new ForcedCooldownPacket(item, startTime, endTime)); + } + } + } + } + + public record SaveableCooldownInstance(ResourceLocation itemResourceLocation, int cooldownLeft, int totalCooldownTime) { + + public static final Codec CODEC = RecordCodecBuilder.create(instance -> instance.group( + ResourceLocation.CODEC.fieldOf("ItemResourceLocation").forGetter(SaveableCooldownInstance::itemResourceLocation), + Codec.INT.fieldOf("CooldownLeft").orElse(0).forGetter(SaveableCooldownInstance::cooldownLeft), + Codec.INT.fieldOf("TotalCooldownTime").orElse(0).forGetter(SaveableCooldownInstance::totalCooldownTime) + ).apply(instance, SaveableCooldownInstance::new)); + + + @NotNull + public static SaveableCooldownInstance makeFromCooldownInstance(@NotNull Item item, @NotNull ItemCooldowns.CooldownInstance cooldownInstance, int tickCount) { + ResourceLocation resourceLocation = BuiltInRegistries.ITEM.getKey(item); + int cooldownLeft = cooldownInstance.endTime - tickCount; + int totalCooldownTime = cooldownInstance.endTime - cooldownInstance.startTime; + return new SaveableCooldownInstance(resourceLocation, cooldownLeft, totalCooldownTime); + } + } +} diff --git a/src/main/java/net/frozenblock/lib/item/impl/network/CooldownChangePacket.java b/src/main/java/net/frozenblock/lib/item/impl/network/CooldownChangePacket.java new file mode 100644 index 0000000..790a80f --- /dev/null +++ b/src/main/java/net/frozenblock/lib/item/impl/network/CooldownChangePacket.java @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.item.impl.network; + +import net.frozenblock.lib.FrozenSharedConstants; +import net.minecraft.core.registries.Registries; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.ByteBufCodecs; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.world.item.Item; +import org.jetbrains.annotations.NotNull; + +public record CooldownChangePacket( + Item item, + int additional +) implements CustomPacketPayload { + + public static final Type PACKET_TYPE = new Type<>( + FrozenSharedConstants.id("cooldown_change_packet") + ); + public static final StreamCodec CODEC = StreamCodec.ofMember(CooldownChangePacket::write, CooldownChangePacket::new); + + public CooldownChangePacket(RegistryFriendlyByteBuf buf) { + this(ByteBufCodecs.registry(Registries.ITEM).decode(buf), buf.readVarInt()); + } + + public void write(RegistryFriendlyByteBuf buf) { + ByteBufCodecs.registry(Registries.ITEM).encode(buf, this.item()); + buf.writeVarInt(this.additional()); + } + + @Override + @NotNull + public Type type() { + return PACKET_TYPE; + } +} diff --git a/src/main/java/net/frozenblock/lib/item/impl/network/CooldownTickCountPacket.java b/src/main/java/net/frozenblock/lib/item/impl/network/CooldownTickCountPacket.java new file mode 100644 index 0000000..924d036 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/item/impl/network/CooldownTickCountPacket.java @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.item.impl.network; + +import net.frozenblock.lib.FrozenSharedConstants; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.codec.ByteBufCodecs; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import org.jetbrains.annotations.NotNull; + +public record CooldownTickCountPacket(int count) implements CustomPacketPayload { + + public static final Type PACKET_TYPE = new Type<>( + FrozenSharedConstants.id("cooldown_tick_count_packet") + ); + public static final StreamCodec CODEC = ByteBufCodecs.VAR_INT.map(CooldownTickCountPacket::new, CooldownTickCountPacket::count).cast(); + + @Override + @NotNull + public Type type() { + return PACKET_TYPE; + } +} diff --git a/src/main/java/net/frozenblock/lib/item/impl/network/ForcedCooldownPacket.java b/src/main/java/net/frozenblock/lib/item/impl/network/ForcedCooldownPacket.java new file mode 100644 index 0000000..7cb0b01 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/item/impl/network/ForcedCooldownPacket.java @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.item.impl.network; + +import net.frozenblock.lib.FrozenSharedConstants; +import net.minecraft.core.registries.Registries; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.ByteBufCodecs; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.world.item.Item; +import org.jetbrains.annotations.NotNull; + +public record ForcedCooldownPacket( + Item item, + int startTime, + int endTime +) implements CustomPacketPayload { + + public static final Type PACKET_TYPE = new Type<>( + FrozenSharedConstants.id("forced_cooldown_packet") + ); + public static final StreamCodec CODEC = StreamCodec.ofMember(ForcedCooldownPacket::write, ForcedCooldownPacket::new); + + public ForcedCooldownPacket(RegistryFriendlyByteBuf buf) { + this(ByteBufCodecs.registry(Registries.ITEM).decode(buf), buf.readVarInt(), buf.readVarInt()); + } + + public void write(RegistryFriendlyByteBuf buf) { + ByteBufCodecs.registry(Registries.ITEM).encode(buf, this.item()); + buf.writeVarInt(this.startTime()); + buf.writeVarInt(this.endTime()); + } + + @Override + @NotNull + public Type type() { + return PACKET_TYPE; + } +} diff --git a/src/main/java/net/frozenblock/lib/item/mixin/AbstractContainerMenuMixin.java b/src/main/java/net/frozenblock/lib/item/mixin/AbstractContainerMenuMixin.java new file mode 100644 index 0000000..ed03ac8 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/item/mixin/AbstractContainerMenuMixin.java @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.item.mixin; + +import com.llamalad7.mixinextras.sugar.Local; +import net.frozenblock.lib.item.impl.ItemStackExtension; +import net.minecraft.world.inventory.AbstractContainerMenu; +import net.minecraft.world.item.ItemStack; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +@Mixin(AbstractContainerMenu.class) +public class AbstractContainerMenuMixin { + + @Inject( + method = "moveItemStackTo", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/world/item/ItemStack;isSameItemSameComponents(Lnet/minecraft/world/item/ItemStack;Lnet/minecraft/world/item/ItemStack;)Z" + ) + ) + private void frozenLib$triggerSlotListeners( + ItemStack stack, int startIndex, int endIndex, boolean reverseDirection, CallbackInfoReturnable info, + @Local(ordinal = 1) ItemStack itemStack + ) { + ItemStackExtension.class.cast(stack).frozenLib$setCanRemoveTags(true); + ItemStackExtension.class.cast(itemStack).frozenLib$setCanRemoveTags(true); + } +} diff --git a/src/main/java/net/frozenblock/lib/item/mixin/ItemCooldownsMixin.java b/src/main/java/net/frozenblock/lib/item/mixin/ItemCooldownsMixin.java new file mode 100644 index 0000000..fe72bfe --- /dev/null +++ b/src/main/java/net/frozenblock/lib/item/mixin/ItemCooldownsMixin.java @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.item.mixin; + +import net.frozenblock.lib.item.impl.CooldownInterface; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemCooldowns; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.Unique; + +import java.util.Map; + +@Mixin(ItemCooldowns.class) +public class ItemCooldownsMixin implements CooldownInterface { + + @Final + @Shadow + public Map cooldowns; + + @Unique + @Override + public void frozenLib$changeCooldown(Item item, int additional) { + this.cooldowns.computeIfPresent(item, (item1, cooldown) -> { + this.frozenLib$onCooldownChanged(item, additional); + return new ItemCooldowns.CooldownInstance(cooldown.startTime, cooldown.endTime + additional); + }); + } + + @Unique + @Override + public void frozenLib$onCooldownChanged(Item item, int additional) { + } + +} diff --git a/src/main/java/net/frozenblock/lib/item/mixin/ItemStackMixin.java b/src/main/java/net/frozenblock/lib/item/mixin/ItemStackMixin.java new file mode 100644 index 0000000..931fd72 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/item/mixin/ItemStackMixin.java @@ -0,0 +1,114 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.item.mixin; + +import net.frozenblock.lib.item.api.removable.RemovableDataComponents; +import net.frozenblock.lib.item.api.removable.RemovableItemTags; +import net.frozenblock.lib.item.impl.ItemStackExtension; +import net.minecraft.core.Holder; +import net.minecraft.core.component.DataComponentType; +import net.minecraft.core.component.DataComponents; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.component.CustomData; +import net.minecraft.world.level.Level; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +@Mixin(ItemStack.class) +public final class ItemStackMixin implements ItemStackExtension { + + @Unique + private boolean frozenLib$canRemoveTags = false; + + @Inject(at = @At("TAIL"), method = "inventoryTick") + public void frozenLib$removeTags(Level level, Entity entity, int slot, boolean selected, CallbackInfo info) { + ItemStack stack = ItemStack.class.cast(this); + + // Removable Item Tags + + CustomData.update(DataComponents.CUSTOM_DATA, stack, compound -> { + for (String key : RemovableItemTags.keys()) { + if (RemovableItemTags.canRemoveTag(key, level, entity, slot, selected)) { + compound.remove(key); + } + } + }); + + // Removable Data Components + + for (Holder> component : RemovableDataComponents.keys()) { + DataComponentType value = component.value(); + if (RemovableDataComponents.canRemoveComponent(value, level, entity, slot, selected)) { + stack.remove(value); + } + } + + } + + @Inject(method = "isSameItemSameComponents", at = @At("HEAD")) + private static void frozenLib$removeTagsAndCompare(ItemStack left, ItemStack right, CallbackInfoReturnable info) { + var extendedLeft = ItemStackExtension.class.cast(left); + var extendedRight = ItemStackExtension.class.cast(right); + + + if (extendedLeft.frozenLib$canRemoveTags()) { + frozenLib$fixEmptyTags(left); + extendedLeft.frozenLib$setCanRemoveTags(false); + } + + if (extendedRight.frozenLib$canRemoveTags()) { + frozenLib$fixEmptyTags(right); + extendedRight.frozenLib$setCanRemoveTags(false); + } + } + + @Unique + private static void frozenLib$fixEmptyTags(ItemStack stack) { + CustomData.update(DataComponents.CUSTOM_DATA, stack, compound -> { + for (String key : RemovableItemTags.keys()) { + if (RemovableItemTags.shouldRemoveTagOnStackMerge(key)) { + compound.remove(key); + } + } + }); + + for (Holder> holder : RemovableDataComponents.keys()) { + var value = holder.value(); + if (RemovableDataComponents.shouldRemoveComponentOnStackMerge(value)) { + stack.remove(value); + } + } + } + + @Unique + @Override + public boolean frozenLib$canRemoveTags() { + return this.frozenLib$canRemoveTags; + } + + @Unique + @Override + public void frozenLib$setCanRemoveTags(boolean canRemoveTags) { + this.frozenLib$canRemoveTags = canRemoveTags; + } +} diff --git a/src/main/java/net/frozenblock/lib/item/mixin/LivingEntityMixin.java b/src/main/java/net/frozenblock/lib/item/mixin/LivingEntityMixin.java new file mode 100644 index 0000000..cc96efb --- /dev/null +++ b/src/main/java/net/frozenblock/lib/item/mixin/LivingEntityMixin.java @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.item.mixin; + +import com.llamalad7.mixinextras.injector.v2.WrapWithCondition; +import net.frozenblock.lib.tag.api.FrozenItemTags; +import net.minecraft.core.Holder; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.level.gameevent.GameEvent; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; + +@Mixin(LivingEntity.class) +public abstract class LivingEntityMixin { + + @WrapWithCondition(method = "startUsingItem", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/world/entity/LivingEntity;gameEvent(Lnet/minecraft/core/Holder;)V" + ) + ) + private boolean preventStartingGameEvent(LivingEntity entity, Holder event) { + return !entity.getUseItem().is(FrozenItemTags.NO_USE_GAME_EVENTS); + } + + @WrapWithCondition( + method = "stopUsingItem", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/world/entity/LivingEntity;gameEvent(Lnet/minecraft/core/Holder;)V" + ) + ) + private boolean preventStoppingGameEvent(LivingEntity entity, Holder event) { + return !entity.getUseItem().is(FrozenItemTags.NO_USE_GAME_EVENTS); + } + +} diff --git a/src/main/java/net/frozenblock/lib/item/mixin/ServerItemCooldownsMixin.java b/src/main/java/net/frozenblock/lib/item/mixin/ServerItemCooldownsMixin.java new file mode 100644 index 0000000..956b413 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/item/mixin/ServerItemCooldownsMixin.java @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.item.mixin; + +import net.frozenblock.lib.item.impl.CooldownInterface; +import net.frozenblock.lib.item.impl.network.CooldownChangePacket; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemCooldowns; +import net.minecraft.world.item.ServerItemCooldowns; +import net.neoforged.neoforge.network.PacketDistributor; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.Unique; + +@Mixin(ServerItemCooldowns.class) +public class ServerItemCooldownsMixin extends ItemCooldowns implements CooldownInterface { + @Shadow + @Final + private ServerPlayer player; + + @Unique + @Override + public void frozenLib$changeCooldown(Item item, int additional) { + this.cooldowns.computeIfPresent(item, (item1, cooldown) -> { + this.frozenLib$onCooldownChanged(item, additional); + return new CooldownInstance(cooldown.startTime, cooldown.endTime + additional); + }); + } + + @Unique + @Override + public void frozenLib$onCooldownChanged(Item item, int additional) { + PacketDistributor.sendToPlayer(this.player, new CooldownChangePacket(item, additional)); + } + +} diff --git a/src/main/java/net/frozenblock/lib/item/mixin/ServerPlayerMixin.java b/src/main/java/net/frozenblock/lib/item/mixin/ServerPlayerMixin.java new file mode 100644 index 0000000..68bc013 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/item/mixin/ServerPlayerMixin.java @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.item.mixin; + +import net.frozenblock.lib.item.impl.SaveableItemCooldowns; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.server.network.ServerGamePacketListenerImpl; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.level.portal.DimensionTransition; +import org.jetbrains.annotations.Nullable; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +import java.util.List; +import java.util.Optional; + +@Mixin(ServerPlayer.class) +public class ServerPlayerMixin { + + @Shadow + public ServerGamePacketListenerImpl connection; + @Shadow + private boolean isChangingDimension; + + @Unique + public Optional> frozenLib$savedItemCooldowns = Optional.empty(); + @Unique @Nullable + private CompoundTag frozenLib$savedCooldownTag; + + @Inject(method = "readAdditionalSaveData", at = @At(value = "TAIL")) + public void frozenLib$readAdditionalSaveData(CompoundTag compound, CallbackInfo info) { + this.frozenLib$savedItemCooldowns = Optional.of(SaveableItemCooldowns.readCooldowns(compound)); + } + + @Inject(method = "addAdditionalSaveData", at = @At(value = "TAIL")) + public void frozenLib$addAdditionalSaveData(CompoundTag compound, CallbackInfo info) { + SaveableItemCooldowns.saveCooldowns(compound, ServerPlayer.class.cast(this)); + } + + @Inject(method = "tick", at = @At(value = "TAIL")) + public void tick(CallbackInfo info) { + if (this.frozenLib$savedItemCooldowns.isPresent() && this.connection != null && this.connection.isAcceptingMessages() && !this.isChangingDimension) { + SaveableItemCooldowns.setCooldowns(this.frozenLib$savedItemCooldowns.get(), ServerPlayer.class.cast(this)); + this.frozenLib$savedItemCooldowns = Optional.empty(); + } + } + + @Inject(method = "changeDimension", at = @At(value = "HEAD")) + public void frozenLib$changeDimensionSaveCooldowns(DimensionTransition dimensionTransition, CallbackInfoReturnable cir) { + CompoundTag tempTag = new CompoundTag(); + SaveableItemCooldowns.saveCooldowns(tempTag, ServerPlayer.class.cast(this)); + this.frozenLib$savedCooldownTag = tempTag; + } + + @Inject(method = "changeDimension", at = @At(value = "RETURN")) + public void frozenLib$changeDimensionLoadCooldowns(DimensionTransition dimensionTransition, CallbackInfoReturnable cir) { + if (this.frozenLib$savedCooldownTag != null) { + this.frozenLib$savedItemCooldowns = Optional.of(SaveableItemCooldowns.readCooldowns(this.frozenLib$savedCooldownTag)); + } + } + +} diff --git a/src/main/java/net/frozenblock/lib/item/mixin/axe/AxeItemMixin.java b/src/main/java/net/frozenblock/lib/item/mixin/axe/AxeItemMixin.java new file mode 100644 index 0000000..8dc73f8 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/item/mixin/axe/AxeItemMixin.java @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.item.mixin.axe; + +import com.llamalad7.mixinextras.injector.wrapoperation.Operation; +import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; +import net.frozenblock.lib.item.api.axe.AxeBehaviors; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.AxeItem; +import net.minecraft.world.item.context.UseOnContext; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.state.BlockState; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; + +import java.util.Optional; + +@Mixin(AxeItem.class) +public class AxeItemMixin { + + @WrapOperation( + method = "useOn", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/world/item/AxeItem;evaluateNewBlockState(Lnet/minecraft/world/level/Level;Lnet/minecraft/core/BlockPos;Lnet/minecraft/world/entity/player/Player;Lnet/minecraft/world/level/block/state/BlockState;Lnet/minecraft/world/item/context/UseOnContext;)Ljava/util/Optional;", + ordinal = 0 + ) + ) + public Optional frozenlib$runAxeBehavior( + AxeItem instance, + Level world, + BlockPos pos, + Player player, + BlockState state, + UseOnContext context0, + Operation> original, + UseOnContext context + ) { + BlockState blockState = world.getBlockState(pos); + Direction direction = context.getClickedFace(); + AxeBehaviors.AxeBehavior axeBehavior = AxeBehaviors.get(blockState.getBlock()); + if (axeBehavior != null && axeBehavior.meetsRequirements(world, pos, direction, state)) { + BlockState outputState = axeBehavior.getOutputBlockState(state); + if (outputState != null) { + axeBehavior.onSuccess(world, pos, direction, outputState, state); + return Optional.of(outputState); + } + } + + return original.call(instance, world, pos, player, state); + } + +} diff --git a/src/main/java/net/frozenblock/lib/item/mixin/bonemeal/BoneMealItemMixin.java b/src/main/java/net/frozenblock/lib/item/mixin/bonemeal/BoneMealItemMixin.java new file mode 100644 index 0000000..bf4ba4b --- /dev/null +++ b/src/main/java/net/frozenblock/lib/item/mixin/bonemeal/BoneMealItemMixin.java @@ -0,0 +1,86 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.item.mixin.bonemeal; + +import com.llamalad7.mixinextras.sugar.Local; +import net.frozenblock.lib.item.api.bonemeal.BonemealBehaviors; +import net.minecraft.core.BlockPos; +import net.minecraft.core.particles.ParticleTypes; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.util.ParticleUtils; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.BoneMealItem; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.LevelAccessor; +import net.minecraft.world.level.block.state.BlockState; +import org.jetbrains.annotations.Nullable; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +@Mixin(BoneMealItem.class) +public class BoneMealItemMixin { + + @Inject( + method = "applyBonemeal", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/world/level/block/state/BlockState;getBlock()Lnet/minecraft/world/level/block/Block;" + ), + cancellable = true + ) + private static void frozenLib$runBonemeal( + ItemStack stack, Level world, BlockPos pos, @Nullable Player player, CallbackInfoReturnable info, + @Local(ordinal = 0) BlockState blockState + ) { + BonemealBehaviors.BonemealBehavior bonemealBehavior = BonemealBehaviors.get(blockState.getBlock()); + if (bonemealBehavior != null && bonemealBehavior.meetsRequirements(world, pos, blockState)) { + if (world instanceof ServerLevel serverLevel) { + if (bonemealBehavior.isBonemealSuccess(world, world.random, pos, blockState)) { + bonemealBehavior.performBonemeal(serverLevel, world.random, pos, blockState); + } + + stack.shrink(1); + } + + info.setReturnValue(true); + } + } + + @Inject( + method = "addGrowthParticles", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/world/level/block/state/BlockState;getBlock()Lnet/minecraft/world/level/block/Block;" + ), + cancellable = true + ) + private static void frozenLib$addGrowthParticles( + LevelAccessor world, BlockPos pos, int count, CallbackInfo info, @Local(ordinal = 0) BlockState blockState + ) { + BonemealBehaviors.BonemealBehavior bonemealBehavior = BonemealBehaviors.get(blockState.getBlock()); + if (bonemealBehavior != null) { + ParticleUtils.spawnParticleInBlock(world, pos, count, ParticleTypes.HAPPY_VILLAGER); + info.cancel(); + } + } + +} diff --git a/src/main/java/net/frozenblock/lib/item/mixin/shovel/ShovelItemMixin.java b/src/main/java/net/frozenblock/lib/item/mixin/shovel/ShovelItemMixin.java new file mode 100644 index 0000000..f77d028 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/item/mixin/shovel/ShovelItemMixin.java @@ -0,0 +1,131 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.item.mixin.shovel; + +import com.llamalad7.mixinextras.injector.ModifyExpressionValue; +import com.llamalad7.mixinextras.sugar.Local; +import com.llamalad7.mixinextras.sugar.Share; +import com.llamalad7.mixinextras.sugar.ref.LocalBooleanRef; +import com.llamalad7.mixinextras.sugar.ref.LocalRef; +import net.frozenblock.lib.item.api.shovel.ShovelBehaviors; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.item.ShovelItem; +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.state.BlockState; +import net.neoforged.neoforge.common.ItemAbility; +import org.objectweb.asm.Opcodes; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.ModifyVariable; +import org.spongepowered.asm.mixin.injection.Redirect; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +@Mixin(ShovelItem.class) +public class ShovelItemMixin { + + @ModifyExpressionValue( + method = "useOn", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/world/item/context/UseOnContext;getClickedFace()Lnet/minecraft/core/Direction;", + ordinal = 0 + ) + ) + public Direction frozenlib$startShovelBehavior( + Direction original, + @Local Level level, @Local BlockPos pos, @Local BlockState blockState, + @Share("frozenLib$isCustomBehavior") LocalBooleanRef isCustomBehavior, + @Share("frozenLib$direction") LocalRef direction, + @Share("frozenLib$shovelBehavior") LocalRef shovelBehavior + ) { + direction.set(original); + ShovelBehaviors.ShovelBehavior possibleBehavior = ShovelBehaviors.get(blockState.getBlock()); + if (possibleBehavior != null && possibleBehavior.meetsRequirements(level, pos, original, blockState)) { + isCustomBehavior.set(true); + shovelBehavior.set(possibleBehavior); + return Direction.UP; + } + isCustomBehavior.set(false); + return original; + } + + @ModifyExpressionValue( + method = "useOn", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/world/level/block/state/BlockState;isAir()Z", + ordinal = 0 + ) + ) + public boolean frozenlib$removeOtherBehaviorsA( + boolean original, + @Share("frozenLi2b$isCustomBehavior") LocalBooleanRef isCustomBehavior + ) { + return !isCustomBehavior.get() && original; + } + + @ModifyExpressionValue( + method = "useOn", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/world/level/block/state/BlockState;getToolModifiedState(Lnet/minecraft/world/item/context/UseOnContext;Lnet/neoforged/neoforge/common/ItemAbility;Z)Lnet/minecraft/world/level/block/state/BlockState;", + ordinal = 1 + ) + ) + public BlockState frozenlib$removeOtherBehaviorsB( + BlockState original, + @Share("frozenLib$isCustomBehavior") LocalBooleanRef isCustomBehavior + ) { + if (isCustomBehavior.get()) { + return Blocks.AIR.defaultBlockState(); + } + return original; + } + + @Inject( + method = "useOn", + at = @At( + value = "JUMP", + opcode = Opcodes.IFNULL, + ordinal = 0 + ) + ) + public void frozenlib$runShovelBehavior( + UseOnContext context, CallbackInfoReturnable info, + @Local Level level, + @Local BlockPos pos, + @Local(ordinal = 0) BlockState blockState, + @Local(ordinal = 2) LocalRef blockState3, + @Share("frozenLib$direction") LocalRef direction, + @Share("frozenLib$shovelBehavior") LocalRef shovelBehavior + ) { + ShovelBehaviors.ShovelBehavior runBehavior = shovelBehavior.get(); + if (runBehavior != null) { + BlockState outputState = runBehavior.getOutputBlockState(blockState); + runBehavior.onSuccess(level, pos, direction.get(), outputState, blockState); + blockState3.set(outputState); + } + } + +} diff --git a/src/main/java/net/frozenblock/lib/jankson/JanksonDataBuilder.java b/src/main/java/net/frozenblock/lib/jankson/JanksonDataBuilder.java new file mode 100644 index 0000000..701eb5a --- /dev/null +++ b/src/main/java/net/frozenblock/lib/jankson/JanksonDataBuilder.java @@ -0,0 +1,40 @@ +package net.frozenblock.lib.jankson; + +import blue.endless.jankson.Jankson; +import com.mojang.datafixers.DataFixer; +import org.jetbrains.annotations.Nullable; + +public class JanksonDataBuilder extends Jankson.Builder { + + @Nullable + private DataFixer fixer; + @Nullable + private Integer version; + + + public static Jankson.Builder withFixer(final Jankson.Builder builder, @Nullable final DataFixer fixer) { + if(builder instanceof JanksonDataBuilder dataBuilder) { + dataBuilder.fixer = fixer; + } + return builder; + } + + public static Jankson.Builder withVersion(final Jankson.Builder builder, @Nullable final Integer version) { + if(builder instanceof JanksonDataBuilder dataBuilder) { + dataBuilder.version = version; + } + return builder; + } + + public static Jankson.Builder withBoth(final Jankson.Builder builder, @Nullable final DataFixer fixer, @Nullable final Integer version) { + return withVersion(withFixer(builder, fixer), version); + } + + public static @Nullable DataFixer getFixer(final Jankson.Builder builder) { + return builder instanceof JanksonDataBuilder dataBuilder ? dataBuilder.fixer : null; + } + + public static @Nullable Integer getVersion(final Jankson.Builder builder) { + return builder instanceof JanksonDataBuilder dataBuilder ? dataBuilder.version : null; + } +} diff --git a/src/main/java/net/frozenblock/lib/jankson/JanksonEntry.java b/src/main/java/net/frozenblock/lib/jankson/JanksonEntry.java new file mode 100644 index 0000000..a0c4d48 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/jankson/JanksonEntry.java @@ -0,0 +1,7 @@ +package net.frozenblock.lib.jankson; + +import blue.endless.jankson.JsonElement; +import org.jetbrains.annotations.Nullable; + +public record JanksonEntry(String key, @Nullable String comment, JsonElement element) { +} diff --git a/src/main/java/net/frozenblock/lib/jankson/JsonObjectDatafixer.java b/src/main/java/net/frozenblock/lib/jankson/JsonObjectDatafixer.java new file mode 100644 index 0000000..a61d140 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/jankson/JsonObjectDatafixer.java @@ -0,0 +1,8 @@ +package net.frozenblock.lib.jankson; + +import com.mojang.datafixers.DataFixer; +import org.jetbrains.annotations.NotNull; + +public interface JsonObjectDatafixer { + void dataFix(@NotNull DataFixer dataFixer, int newVersion); +} diff --git a/src/main/java/net/frozenblock/lib/jankson/SaveToggle.java b/src/main/java/net/frozenblock/lib/jankson/SaveToggle.java new file mode 100644 index 0000000..ba3c38c --- /dev/null +++ b/src/main/java/net/frozenblock/lib/jankson/SaveToggle.java @@ -0,0 +1,13 @@ +package net.frozenblock.lib.jankson; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.FIELD }) +public @interface SaveToggle { + + boolean value() default true; +} \ No newline at end of file diff --git a/src/main/java/net/frozenblock/lib/jankson/mixin/JanksonMixin.java b/src/main/java/net/frozenblock/lib/jankson/mixin/JanksonMixin.java new file mode 100644 index 0000000..0cb7964 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/jankson/mixin/JanksonMixin.java @@ -0,0 +1,42 @@ +package net.frozenblock.lib.jankson.mixin; + +import blue.endless.jankson.Jankson; +import blue.endless.jankson.JsonObject; +import com.mojang.datafixers.DataFixer; +import net.frozenblock.lib.jankson.JanksonDataBuilder; +import net.frozenblock.lib.jankson.JsonObjectDatafixer; +import org.jetbrains.annotations.Nullable; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +import java.io.InputStream; + +@Mixin(Jankson.class) +public class JanksonMixin { + @Shadow + private JsonObject root; + @Nullable + private DataFixer dataFixer; + @Nullable + private Integer version; + + @Inject(at = @At("RETURN"), method = "(Lblue/endless/jankson/Jankson$Builder;)V") + private void init(Jankson.Builder builder, CallbackInfo ci) { + dataFixer = JanksonDataBuilder.getFixer(builder); + version = JanksonDataBuilder.getVersion(builder); + } + + @Inject(at = @At("RETURN"), method = "load(Ljava/io/InputStream;)Lblue/endless/jankson/JsonObject;") + public void load(InputStream in, CallbackInfoReturnable cir) { + @Nullable DataFixer fixer = this.dataFixer; + @Nullable Integer newVersion = this.version; + if (fixer != null && newVersion != null) { + assert root != null; + ((JsonObjectDatafixer)root).dataFix(fixer, newVersion); + } + } +} diff --git a/src/main/java/net/frozenblock/lib/jankson/mixin/JsonObjectMixin.java b/src/main/java/net/frozenblock/lib/jankson/mixin/JsonObjectMixin.java new file mode 100644 index 0000000..9fe4103 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/jankson/mixin/JsonObjectMixin.java @@ -0,0 +1,68 @@ +package net.frozenblock.lib.jankson.mixin; + +import blue.endless.jankson.JsonElement; +import blue.endless.jankson.JsonObject; +import blue.endless.jankson.JsonPrimitive; +import com.mojang.datafixers.DSL; +import com.mojang.datafixers.DataFixer; +import com.mojang.serialization.Dynamic; +import net.frozenblock.lib.config.api.instance.json.JanksonOps; +import net.frozenblock.lib.jankson.JanksonEntry; +import net.frozenblock.lib.jankson.JsonObjectDatafixer; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.spongepowered.asm.mixin.Mixin; +import xjs.data.Json; + +import java.util.ArrayList; +import java.util.List; + +@Mixin(JsonObject.class) +public class JsonObjectMixin implements JsonObjectDatafixer { + + private static final DSL.TypeReference TYPE = () -> "JANKSON"; + + @Override + public void dataFix(@NotNull DataFixer dataFixer, int newVersion) { + boolean shouldAddVersion = true; + final JsonObject that = ((JsonObject)(Object)this); + String versionKey = "jankson:schema_version"; + for(String key : that.keySet()) { + if(versionKey.equals(key)) { + shouldAddVersion = false; + break; + } + } + + + if (shouldAddVersion) { + final List converter = new ArrayList<>(); + for(String key : that.keySet()) { + final JsonElement value = that.get(key); + @Nullable final String comment = that.getComment(key); + converter.add(new JanksonEntry(key, comment, value)); + } + that.clear(); + that.put(versionKey, JanksonOps.INSTANCE.createNumeric(newVersion), "The version of this JSON file\nDon't modify!"); + for(JanksonEntry entry : converter) { + that.put(entry.key(), entry.element(), entry.comment()); + } + } + + JsonElement versionEntry = null; + for(String key : that.keySet()) { + if(key.equals(versionKey)) { + versionEntry = that.get(key); + break; + } + } + assert versionEntry != null; + int version = ((JsonPrimitive)versionEntry).asInt(newVersion); + + for(String key : that.keySet()) { + Dynamic dynamic = new Dynamic<>(JanksonOps.INSTANCE, that.get(key)); + @Nullable final String comment = that.getComment(key); + that.put(key, dataFixer.update(TYPE, dynamic, version, newVersion).getValue(), comment); + } + } +} diff --git a/src/main/java/net/frozenblock/lib/liquid/render/api/LiquidRenderUtils.java b/src/main/java/net/frozenblock/lib/liquid/render/api/LiquidRenderUtils.java new file mode 100644 index 0000000..d1f7112 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/liquid/render/api/LiquidRenderUtils.java @@ -0,0 +1,317 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.liquid.render.api; + +import com.mojang.blaze3d.vertex.VertexConsumer; +import net.minecraft.client.renderer.LevelRenderer; +import net.minecraft.client.renderer.texture.TextureAtlasSprite; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.util.Mth; +import net.minecraft.world.level.BlockAndTintGetter; +import net.minecraft.world.level.BlockGetter; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.material.Fluid; +import net.minecraft.world.level.material.FluidState; +import net.minecraft.world.phys.shapes.Shapes; +import net.minecraft.world.phys.shapes.VoxelShape; + +public class LiquidRenderUtils { + + public static void tesselateWithSingleTexture(BlockAndTintGetter level, BlockPos pos, VertexConsumer vertexConsumer, BlockState blockState, FluidState fluidState, TextureAtlasSprite textureAtlasSprite) { + float ap; + float ao; + float ag; + float af; + float ae; + float ad; + float ac; + float ab; + float z; + float y; + float southWestHeight; + float southEastHeight; + float northWestHeight; + float northEastHeight; + float f = (0xFFFFFF >> 16 & 0xFF) / 255.0f; + float g = (0xFFFFFF >> 8 & 0xFF) / 255.0f; + float h = (0xFFFFFF & 0xFF) / 255.0f; + BlockState downBlockState = level.getBlockState(pos.relative(Direction.DOWN)); + FluidState downFluidState = downBlockState.getFluidState(); + BlockState upBlockState = level.getBlockState(pos.relative(Direction.UP)); + FluidState upFluidState = upBlockState.getFluidState(); + BlockState northBlockState = level.getBlockState(pos.relative(Direction.NORTH)); + FluidState northFluidState = northBlockState.getFluidState(); + BlockState southBlockState = level.getBlockState(pos.relative(Direction.SOUTH)); + FluidState southFluidState = southBlockState.getFluidState(); + BlockState westBlockState = level.getBlockState(pos.relative(Direction.WEST)); + FluidState westFluidState = westBlockState.getFluidState(); + BlockState eastBlockState = level.getBlockState(pos.relative(Direction.EAST)); + FluidState eastFluidState = eastBlockState.getFluidState(); + boolean shouldRenderUp = !isNeighborSameFluidAndBlock(fluidState, upFluidState, blockState, upBlockState); + boolean shouldRenderDown = shouldRenderFace(level, pos, fluidState, blockState, Direction.DOWN, downFluidState, downBlockState) && !isFaceOccludedByNeighbor(level, pos, Direction.DOWN, 0.8888889f, downBlockState, blockState); + boolean shouldRenderNorth = shouldRenderFace(level, pos, fluidState, blockState, Direction.NORTH, northFluidState, northBlockState); + boolean bl5 = shouldRenderFace(level, pos, fluidState, blockState, Direction.SOUTH, southFluidState, southBlockState); + boolean bl6 = shouldRenderFace(level, pos, fluidState, blockState, Direction.WEST, westFluidState, westBlockState); + boolean bl7 = shouldRenderFace(level, pos, fluidState, blockState, Direction.EAST, eastFluidState, eastBlockState); + if (!(shouldRenderUp || shouldRenderDown || bl7 || bl6 || shouldRenderNorth || bl5)) { + return; + } + float j = level.getShade(Direction.DOWN, true); + float k = level.getShade(Direction.UP, true); + float l = level.getShade(Direction.NORTH, true); + float m = level.getShade(Direction.WEST, true); + Fluid fluid = fluidState.getType(); + float n = getHeight(level, fluid, pos, blockState, fluidState); + if (n >= 1.0f) { + northEastHeight = 1.0f; + northWestHeight = 1.0f; + southEastHeight = 1.0f; + southWestHeight = 1.0f; + } else { + float s = getHeight(level, fluid, pos.north(), northBlockState, northFluidState); + float t = getHeight(level, fluid, pos.south(), southBlockState, southFluidState); + float u = getHeight(level, fluid, pos.east(), eastBlockState, eastFluidState); + float v = getHeight(level, fluid, pos.west(), westBlockState, westFluidState); + northEastHeight = calculateAverageHeight(level, fluid, n, s, u, pos.relative(Direction.NORTH).relative(Direction.EAST)); + northWestHeight = calculateAverageHeight(level, fluid, n, s, v, pos.relative(Direction.NORTH).relative(Direction.WEST)); + southEastHeight = calculateAverageHeight(level, fluid, n, t, u, pos.relative(Direction.SOUTH).relative(Direction.EAST)); + southWestHeight = calculateAverageHeight(level, fluid, n, t, v, pos.relative(Direction.SOUTH).relative(Direction.WEST)); + } + float d = pos.getX() & 0xF; + float e = pos.getY() & 0xF; + float w = pos.getZ() & 0xF; + y = shouldRenderDown ? 0.001f : 0.0f; + if (shouldRenderUp && !isFaceOccludedByNeighbor(level, pos, Direction.UP, Math.min(Math.min(northWestHeight, southWestHeight), Math.min(southEastHeight, northEastHeight)), upBlockState, blockState)) { + float ak; + float aj; + float ai; + float ah; + float aa; + northWestHeight -= 0.001f; + southWestHeight -= 0.001f; + southEastHeight -= 0.001f; + northEastHeight -= 0.001f; + z = textureAtlasSprite.getU(0.0F); + aa = textureAtlasSprite.getV(0.0F); + ab = z; + ac = textureAtlasSprite.getV(1.0F); + ad = textureAtlasSprite.getU(1.0F); + ae = ac; + af = ad; + ag = aa; + float al = (z + ab + ad + af) / 4.0f; + ah = (aa + ac + ae + ag) / 4.0f; + ai = textureAtlasSprite.contents().width() / (textureAtlasSprite.getU1() - textureAtlasSprite.getU0()); + aj = textureAtlasSprite.contents().height() / (textureAtlasSprite.getV1() - textureAtlasSprite.getV0()); + ak = 4.0f / Math.max(aj, ai); + z = Mth.lerp(ak, z, al); + ab = Mth.lerp(ak, ab, al); + ad = Mth.lerp(ak, ad, al); + af = Mth.lerp(ak, af, al); + aa = Mth.lerp(ak, aa, ah); + ac = Mth.lerp(ak, ac, ah); + ae = Mth.lerp(ak, ae, ah); + ag = Mth.lerp(ak, ag, ah); + int am = getLightColor(level, pos); + float an = k * f; + ao = k * g; + ap = k * h; + vertex(vertexConsumer, d + 0.0f, e + northWestHeight, w + 0.0f, an, ao, ap, z, aa, am); + vertex(vertexConsumer, d + 0.0f, e + southWestHeight, w + 1.0f, an, ao, ap, ab, ac, am); + vertex(vertexConsumer, d + 1.0f, e + southEastHeight, w + 1.0f, an, ao, ap, ad, ae, am); + vertex(vertexConsumer, d + 1.0f, e + northEastHeight, w + 0.0f, an, ao, ap, af, ag, am); + if (fluidState.shouldRenderBackwardUpFace(level, pos.above()) || !blockState.equals(downBlockState)) { + vertex(vertexConsumer, d + 0.0f, e + northWestHeight, w + 0.0f, an, ao, ap, z, aa, am); + vertex(vertexConsumer, d + 1.0f, e + northEastHeight, w + 0.0f, an, ao, ap, af, ag, am); + vertex(vertexConsumer, d + 1.0f, e + southEastHeight, w + 1.0f, an, ao, ap, ad, ae, am); + vertex(vertexConsumer, d + 0.0f, e + southWestHeight, w + 1.0f, an, ao, ap, ab, ac, am); + } + } + if (shouldRenderDown) { + z = textureAtlasSprite.getU0(); + ab = textureAtlasSprite.getU1(); + ad = textureAtlasSprite.getV0(); + af = textureAtlasSprite.getV1(); + int aq = getLightColor(level, pos.below()); + ac = j * f; + ae = j * g; + ag = j * h; + vertex(vertexConsumer, d, e + y, w + 1.0f, ac, ae, ag, z, af, aq); + vertex(vertexConsumer, d, e + y, w, ac, ae, ag, z, ad, aq); + vertex(vertexConsumer, d + 1.0f, e + y, w, ac, ae, ag, ab, ad, aq); + vertex(vertexConsumer, d + 1.0f, e + y, w + 1.0f, ac, ae, ag, ab, af, aq); + if (!downFluidState.equals(fluidState) && downBlockState.getBlock() != blockState.getBlock() && !downBlockState.canOcclude()) { + vertex(vertexConsumer, d, e + y, w + 1.0f, ac, ae, ag, z, af, aq); + vertex(vertexConsumer, d + 1.0f, e + y, w + 1.0f, ac, ae, ag, z, ad, aq); + vertex(vertexConsumer, d + 1.0f, e + y, w, ac, ae, ag, ab, ad, aq); + vertex(vertexConsumer, d, e + y, w, ac, ae, ag, ab, af, aq); + } + } + int ar = getLightColor(level, pos); + for (Direction direction : Direction.Plane.HORIZONTAL) { + float av; + float au; + float at; + float as; + float aa; + if (!(switch (direction) { + case NORTH -> { + af = northWestHeight; + aa = northEastHeight; + as = d; + at = d + 1.0f; + au = w + 0.001f; + av = w + 0.001f; + yield shouldRenderNorth; + } + case SOUTH -> { + af = southEastHeight; + aa = southWestHeight; + as = d + 1.0f; + at = d; + au = w + 1.0f - 0.001f; + av = w + 1.0f - 0.001f; + yield bl5; + } + case WEST -> { + af = southWestHeight; + aa = northWestHeight; + as = d + 0.001f; + at = d + 0.001f; + au = w + 1.0f; + av = w; + yield bl6; + } + default -> { + af = northEastHeight; + aa = southEastHeight; + as = d + 1.0f - 0.001f; + at = d + 1.0f - 0.001f; + au = w; + av = w + 1.0f; + yield bl7; + } + }) || isFaceOccludedByNeighbor(level, pos, direction, Math.max(af, aa), level.getBlockState(pos.relative(direction)), level.getBlockState(pos.relative(direction)))) continue; + ao = textureAtlasSprite.getU(0); + ap = textureAtlasSprite.getU(1); + float aw = textureAtlasSprite.getV(0); + float ax = textureAtlasSprite.getV(0); + float ay = textureAtlasSprite.getV(1); + float az = direction.getAxis() == Direction.Axis.Z ? l : m; + float ba = k * az * f; + float bb = k * az * g; + float bc = k * az * h; + vertex(vertexConsumer, as, e + af, au, ba, bb, bc, ao, aw, ar); + vertex(vertexConsumer, at, e + aa, av, ba, bb, bc, ap, ax, ar); + vertex(vertexConsumer, at, e + y, av, ba, bb, bc, ap, ay, ar); + vertex(vertexConsumer, as, e + y, au, ba, bb, bc, ao, ay, ar); + vertex(vertexConsumer, as, e + y, au, ba, bb, bc, ao, ay, ar); + vertex(vertexConsumer, at, e + y, av, ba, bb, bc, ap, ay, ar); + vertex(vertexConsumer, at, e + aa, av, ba, bb, bc, ap, ax, ar); + vertex(vertexConsumer, as, e + af, au, ba, bb, bc, ao, aw, ar); + } + } + + private static float calculateAverageHeight(BlockAndTintGetter world, Fluid fluid, float height, float adjacentHeightA, float adjacentHeightB, BlockPos fluidPos) { + if (adjacentHeightB >= 1.0f || adjacentHeightA >= 1.0f) { + return 1.0f; + } + float[] fs = new float[2]; + if (adjacentHeightB > 0.0f || adjacentHeightA > 0.0f) { + float f = getHeight(world, fluid, fluidPos); + if (f >= 1.0f) { + return 1.0f; + } + addWeightedHeight(fs, f); + } + addWeightedHeight(fs, height); + addWeightedHeight(fs, adjacentHeightB); + addWeightedHeight(fs, adjacentHeightA); + return fs[0] / fs[1]; + } + + public static void addWeightedHeight(float[] weights, float height) { + if (height >= 0.8f) { + weights[0] = weights[0] + height * 10.0f; + weights[1] = weights[1] + 10.0f; + } else if (height >= 0.0f) { + weights[0] = weights[0] + height; + weights[1] = weights[1] + 1.0f; + } + } + + public static float getHeight(BlockAndTintGetter world, Fluid fluid, BlockPos blockState) { + BlockState blockState2 = world.getBlockState(blockState); + return getHeight(world, fluid, blockState, blockState2, blockState2.getFluidState()); + } + + public static float getHeight(BlockAndTintGetter world, Fluid fluid, BlockPos pos, BlockState blockState, FluidState state) { + if (fluid.isSame(state.getType())) { + BlockState blockState2 = world.getBlockState(pos.above()); + if (fluid.isSame(blockState2.getFluidState().getType())) { + return 1.0f; + } + return state.getOwnHeight(); + } + if (!blockState.isSolid()) { + return 0.0f; + } + return -1.0f; + } + + //vertex + public static void vertex(VertexConsumer consumer, float x, float y, float z, float red, float green, float blue, float u, float v, int packedLight) { + consumer.addVertex(x, y, z).setColor(red, green, blue, 1.0f).setUv(u, v).setLight(packedLight).setNormal(0.0f, 1.0f, 0.0f); + } + + public static int getLightColor(BlockAndTintGetter level, BlockPos pos) { + int i = LevelRenderer.getLightColor(level, pos); + int j = LevelRenderer.getLightColor(level, pos.above()); + int k = i & 0xFF; + int l = j & 0xFF; + int m = i >> 16 & 0xFF; + int n = j >> 16 & 0xFF; + return (Math.max(k, l)) | (Math.max(m, n)) << 16; + } + + private static boolean isNeighborSameFluidAndBlock(FluidState firstState, FluidState secondState, BlockState firstBlock, BlockState secondBlock) { + return secondState.getType().isSame(firstState.getType()) && firstBlock.getBlock() == secondBlock.getBlock(); + } + + private static boolean isFaceOccludedByState(BlockGetter level, Direction face, float height, BlockPos pos, BlockState state, BlockState neighborState) { + if (neighborState.getBlock() == state.getBlock() && state.canOcclude()) { + VoxelShape voxelShape = Shapes.box(0.0, 0.0, 0.0, 1.0, height, 1.0); + VoxelShape voxelShape2 = state.getOcclusionShape(level, pos); + return Shapes.blockOccudes(voxelShape, voxelShape2, face); + } + return false; + } + + private static boolean isFaceOccludedByNeighbor(BlockGetter level, BlockPos pos, Direction side, float height, BlockState blockState, BlockState neighborState) { + return isFaceOccludedByState(level, side, height, pos.relative(side), blockState, neighborState); + } + + private static boolean isFaceOccludedBySelf(BlockGetter level, BlockPos pos, BlockState state, Direction face, BlockState neighborState) { + return isFaceOccludedByState(level, face.getOpposite(), 1.0f, pos, state, neighborState); + } + + public static boolean shouldRenderFace(BlockAndTintGetter level, BlockPos pos, FluidState fluidState, BlockState blockState, Direction side, FluidState neighborFluid, BlockState neighborState) { + return !isFaceOccludedBySelf(level, pos, blockState, side, neighborState) && !isNeighborSameFluidAndBlock(fluidState, neighborFluid, blockState, neighborState); + } +} diff --git a/src/main/java/net/frozenblock/lib/math/api/AdvancedMath.java b/src/main/java/net/frozenblock/lib/math/api/AdvancedMath.java new file mode 100644 index 0000000..1e4ec22 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/math/api/AdvancedMath.java @@ -0,0 +1,251 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.math.api; + +import lombok.experimental.UtilityClass; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.util.Mth; +import net.minecraft.util.RandomSource; +import net.minecraft.world.phys.Vec3; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * Adds more math operations. + * + * @author LiukRast (2021-2022) + * @author FrozenBlock (2022) + * @since 4.0 + */ +@UtilityClass +public class AdvancedMath { + + @Contract(" -> new") + @NotNull + public static RandomSource random() { + return RandomSource.create(); + } + + public static float range(final float min, final float max, + final float number) { + return (number * max) + min; + } + + public static double randomPosNeg() { + return random().nextDouble() * (random().nextDouble() >= 0.5 ? 1 : -1); + } + + + public static boolean squareBetween( + final int x, + final int z, + final int between1, + final int between2 + ) { + boolean cond1 = x > between1 && x < between2; + boolean cond2 = z > between1 && z < between2; + return cond1 && cond2; + } + + public static BlockPos offset(final BlockPos pos, final @NotNull Direction dir, final int a) { + return switch (dir) { + case WEST -> pos.west(a); + case EAST -> pos.east(a); + case SOUTH -> pos.south(a); + case NORTH -> pos.north(a); + case UP -> pos.above(a); + case DOWN -> pos.below(a); + }; + } + + public static BlockPos offset(final BlockPos pos, final Direction dir) { + return offset(pos, dir, 1); + } + + public static int waterToHollowedProperty(final int value) { + if (value > 8) { + return 8; + } else if (value < 0) { + return -1; + } else { + return value; + } + } + + public static int waterLevelReduce(final int value) { + if (value < 8) { + return value + 1; + } else { + return 8; + } + } + + public static double cutCos(double value, double offset, boolean inverse) { + double equation = Math.cos(value); + if (!inverse) { + return Math.max(equation, offset); + } else { + return Math.max(-equation, offset); + } + } + + public static int factorial(int n) { + if (n < 0) { + throw new IllegalArgumentException("Factorial of negative numbers is undefined"); + } + int result = 1; + for (int i = 2; i <= n; i++) { + result *= i; + } + return result; + } + + public static int permutations(int n, int r) { + if (n < 0 || r < 0 || r > n) { + throw new IllegalArgumentException("Invalid input: n must be non-negative, r must be non-negative and not greater than n"); + } + int result = 1; + for (int i = n; i > n - r; i--) { + result *= i; + } + return result; + } + + public static int combinations(int n, int r) { + if (n < 0 || r < 0 || r > n) { + throw new IllegalArgumentException("Invalid input: n must be non-negative, r must be non-negative and not greater than n"); + } + int numerator = 1; + for (int i = n; i > n - r; i--) { + numerator *= i; + } + int denominator = 1; + for (int i = r; i > 0; i--) { + denominator *= i; + } + return numerator / denominator; + } + + /** + * Solves a quadratic equation of the form ax^2 + bx + c = 0. + * + * @param a the coefficient of x^2 (must be non-zero) + * @param b the coefficient of x + * @param c the constant term + * @return an array containing the real roots of the equation, or null if no real roots exist + * @throws IllegalArgumentException if a is zero + */ + public static double @Nullable [] solveQuadraticEquation(double a, double b, double c) { + if (a == 0) { + throw new IllegalArgumentException("a cannot be zero"); + } + double discriminant = b * b - 4 * a * c; + if (discriminant < 0) { + return null; + } else if (discriminant == 0) { + double root = -b / (2 * a); + return new double[]{root}; + } else { + double root1 = (-b + Math.sqrt(discriminant)) / (2 * a); + double root2 = (-b - Math.sqrt(discriminant)) / (2 * a); + return new double[]{root1, root2}; + } + } + + /** + * Calculates the greatest common divisor (GCD) of two numbers using the Euclidean algorithm. + * + * @param a the first number + * @param b the second number + * @return the GCD of a and b + */ + public static int greatestCommonDivisor(int a, int b) { + if (a == 0 || b == 0) { + return Math.abs(a + b); // GCD(0, b) == b; GCD(a, 0) == a; GCD(0, 0) == 0 + } + + while (b != 0) { + int temp = b; + b = a % b; + a = temp; + } + + return Math.abs(a); + } + + /** + * @param axis The axis that should be used to determine a random direction. + * @return A random {@linkplain Direction} on a specific {@linkplain Direction.Axis}. + */ + @NotNull + public static Direction randomDir(@NotNull final Direction.Axis axis) { + double random = random().nextDouble(); + switch (axis) { + case X -> { + return random > 0.5 ? Direction.EAST : Direction.WEST; + } + case Y -> { + return random > 0.5 ? Direction.UP : Direction.DOWN; + } + default -> { + return random > 0.5 ? Direction.NORTH : Direction.SOUTH; + } + } + } + + @NotNull + public static Vec3 rotateAboutXZ(@NotNull Vec3 original, double distanceFrom, double angle) { + double calcAngle = angle * (Math.PI / 180D); + Vec3 offsetVec = original.add(distanceFrom, 0, distanceFrom); + double originX = original.x; + double originZ = original.z; + double distancedX = offsetVec.x; + double distancedZ = offsetVec.z; + double x = originX + (distancedX - originX) * Math.cos(calcAngle) - (distancedZ - originZ) * Math.sin(calcAngle); + double z = originZ + (distancedX - originX) * Math.sin(calcAngle) + (distancedZ - originZ) * Math.cos(calcAngle); + return new Vec3(x, original.y, z); + } + + @NotNull + public static Vec3 rotateAboutX(@NotNull Vec3 original, double distanceFrom, double angle) { + double calcAngle = angle * (Math.PI / 180D); + Vec3 offsetVec = original.add(distanceFrom, 0, 0); + double originX = original.x; + double originZ = original.z; + double distancedX = offsetVec.x; + double distancedZ = offsetVec.z; + double x = originX + (distancedX - originX) * Math.cos(calcAngle) - (distancedZ - originZ) * Math.sin(calcAngle); + double z = originZ + (distancedX - originX) * Math.sin(calcAngle) + (distancedZ - originZ) * Math.cos(calcAngle); + return new Vec3(x, original.y, z); + } + + @Contract(pure = true) + public static double getAngleFromOriginXZ(@NotNull Vec3 pos) { // https://stackoverflow.com/questions/35271222/getting-the-angle-from-a-direction-vector + double angleRad = Math.atan2(pos.x, pos.z); + double degrees = angleRad * Mth.RAD_TO_DEG; + return (360D + Math.round(degrees)) % 360D; + } + + + public static double getAngleBetweenXZ(@NotNull Vec3 posA, @NotNull Vec3 posB) { + double angle = Math.atan2(posA.x - posB.x, posA.z - posB.z); + return (360D + (angle * Mth.RAD_TO_DEG)) % 360D; + } +} \ No newline at end of file diff --git a/src/main/java/net/frozenblock/lib/math/api/AnimationAPI.java b/src/main/java/net/frozenblock/lib/math/api/AnimationAPI.java new file mode 100644 index 0000000..5e1c74b --- /dev/null +++ b/src/main/java/net/frozenblock/lib/math/api/AnimationAPI.java @@ -0,0 +1,568 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.math.api; + +import lombok.experimental.UtilityClass; +import org.jetbrains.annotations.ApiStatus; + +import java.awt.geom.Point2D; + +/** + * This class is used to make animations with easings. + *

+ * Defining a point A(x,y) and B(x,y) you can create an animation between those two points ( A.getY() won't affect the animation). + *

+ * Learn more at the README + * + * @author LiukRast (2021-2022) + * @since 4.0 + */ +@ApiStatus.Obsolete(since = "Minecraft 1.19") +@UtilityClass +public class AnimationAPI { + + public static double relativeX(Point2D a, Point2D b, double x) { + return (x - a.getX()) / (b.getX() - a.getX()); + } + + /** + * Generates a "random" number depending on another number. + * + * @deprecated Use seed() instead of this! + **/ + @Deprecated + public static double rawSeed(double seed) { + double f = Math.pow(Math.PI, 3); + double linear = (seed + f) * f; + double flat = Math.floor(linear); + return linear - flat; + } + + /** + * Executes {@link #rawSeed(double)} multiple times to make the number look more "random" + **/ + public static double seed(double seed) { + return rawSeed(rawSeed(rawSeed(seed))); + } + + /** + * Convert a 2D position with a seed in a resulting seed + **/ + public static double seed2D(Point2D seed2d, double seed) { + return rawSeed(seed2d.getX()) * rawSeed(seed2d.getX()) * rawSeed(seed); + } + + public static double legAnimation(double base, double range, double frequency, double limbAngle, double limbDistance, boolean inverted) { + double baseRange = 1.4; + double baseFrequency = 0.6662; + double wave = Math.sin(limbAngle * (baseFrequency * frequency)) * (baseRange * range) * limbDistance; + if (inverted) { + return base + wave; + } else { + return base - wave; + } + } + + public static double legAnimation(double base, double range, double frequency, double limbAngle, double limbDistance) { + return legAnimation(base, range, frequency, limbAngle, limbDistance, false); + } + + public static double legAnimation(double base, double limbAngle, double limbDistance, boolean inverted) { + return legAnimation(base, 1, 1, limbAngle, limbDistance, inverted); + } + + public static double legAnimation(double base, double limbAngle, double limbDistance) { + return legAnimation(base, limbAngle, limbDistance, false); + } + + + /** + * SINE EASING - Generated using Math.sin() + */ + public static double sineEaseIn(Point2D a, Point2D b, double x) { + if (x < a.getX()) { + return 0; // before animation defining the eq as 0 + } else if (x > b.getX()) { + return b.getY(); // after animation defining the eq as b's Y + } else { + return b.getY() * (1 - Math.cos(Math.PI * (relativeX(a, b, x) / 2))); + } + } + + public static double sineEaseOut(Point2D a, Point2D b, double x) { + if (x < a.getX()) { + return 0; // before animation defining the eq as 0 + } else if (x > b.getX()) { + return b.getY(); // after animation defining the eq as b's Y + } else { + return b.getY() * (Math.sin(Math.PI * (relativeX(a, b, x) / 2))); + } + } + + public static double sineEaseInOut(Point2D a, Point2D b, double x) { + if (x < a.getX()) { + return 0; // before animation defining the eq as 0 + } else if (x > b.getX()) { + return b.getY(); // after animation defining the eq as b's Y + } else { + return b.getY() * (0.5F - (Math.cos(Math.PI * relativeX(a, b, x)) / 2)); + } + } + // ------------------------------------------------------- + + /** + * POLYNOMIAL EASING - Generated by elevating x at a "c" value + */ + public static double polyEaseIn(Point2D a, Point2D b, double x, double c) { + if (c < 0) { + System.out.println("Animation API error - c must be >= 0"); + return Math.random(); + } + if (x < a.getX()) { + return 0; // before animation defining the eq as 0 + } else if (x > b.getX()) { + return b.getY(); // after animation defining the eq as b's Y + } else { + return b.getY() * (Math.pow(relativeX(a, b, x), c)); + } + } + + public static double polyEaseOut(Point2D a, Point2D b, double x, double c) { + if (c < 0) { + System.out.println("Animation API error - c must be >= 0"); + return Math.random(); + } + if (x < a.getX()) { + return 0; // before animation defining the eq as 0 + } else if (x > b.getX()) { + return b.getY(); // after animation defining the eq as b's Y + } else { + return b.getY() * (1 - Math.pow(-(relativeX(a, b, x) - 1), c)); + } + } + + public static double polyEaseInOut(Point2D a, Point2D b, double x, double c) { + if (c < 0) { + System.out.println("Animation API error - c must be >= 0"); + return Math.random(); + } + if (x < a.getX()) { + return 0; // before animation defining the eq as 0 + } else if (x > b.getX()) { + return b.getY(); // after animation defining the eq as b's Y + } else { + if (x < (b.getX() - a.getX()) / 2) { + return b.getY() * (Math.pow(2, c - 1) * Math.pow(relativeX(a, b, x), c)); + } else { + return b.getY() * (1 - Math.pow(2 - 2 * relativeX(a, b, x), c) / 2); + } + } + } + // ------------------------------------------------------- + + /** + * QUADRATIC EASING - Generated using Poly and assuming c = 2 + */ + public static double quadraticEaseIn(Point2D a, Point2D b, double x) { + return polyEaseIn(a, b, x, 2); + } + + public static double quadraticEaseOut(Point2D a, Point2D b, double x) { + return polyEaseOut(a, b, x, 2); + } + + public static double quadraticEaseInOut(Point2D a, Point2D b, double x) { + return polyEaseInOut(a, b, x, 2); + } + // ------------------------------------------------------- + + /** + * CUBIC EASING - Generated using Poly and assuming c = 3 + */ + public static double cubicEaseIn(Point2D a, Point2D b, double x) { + return polyEaseIn(a, b, x, 3); + } + + public static double cubicEaseOut(Point2D a, Point2D b, double x) { + return polyEaseOut(a, b, x, 3); + } + + public static double cubicEaseInOut(Point2D a, Point2D b, double x) { + return polyEaseInOut(a, b, x, 3); + } + // ------------------------------------------------------- + + /** + * QUARTIC EASING - Generated using Poly and assuming c = 4 + */ + public static double quarticEaseIn(Point2D a, Point2D b, double x) { + return polyEaseIn(a, b, x, 4); + } + + public static double quarticEaseOut(Point2D a, Point2D b, double x) { + return polyEaseOut(a, b, x, 4); + } + + public static double quarticEaseInOut(Point2D a, Point2D b, double x) { + return polyEaseInOut(a, b, x, 4); + } + // ------------------------------------------------------- + + /** + * QUINTIC EASING - Generated using Poly and assuming c = 5 + */ + public static double quinticEaseIn(Point2D a, Point2D b, double x) { + return polyEaseIn(a, b, x, 5); + } + + public static double quinticEaseOut(Point2D a, Point2D b, double x) { + return polyEaseOut(a, b, x, 5); + } + + public static double quinticEaseInOut(Point2D a, Point2D b, double x) { + return polyEaseInOut(a, b, x, 5); + } + // ------------------------------------------------------- + + /** + * EXPONENTIAL EASING - Generated by 2^x + */ + public static double expoEaseIn(Point2D a, Point2D b, double x) { + if (x < a.getX()) { + return 0; // before animation defining the eq as 0 + } else if (x > b.getX()) { + return b.getY(); // after animation defining the eq as b's Y + } else { + return b.getY() * Math.pow(2, (10 * relativeX(a, b, x)) - 10); + } + } + + public static double expoEaseOut(Point2D a, Point2D b, double x) { + if (x < a.getX()) { + return 0; // before animation defining the eq as 0 + } else if (x > b.getX()) { + return b.getY(); // after animation defining the eq as b's Y + } else { + return b.getY() * (1 - Math.pow(2, -10 * relativeX(a, b, x))); + } + } + + public static double expoEaseInOut(Point2D a, Point2D b, double x) { + if (x < a.getX()) { + return 0; // before animation defining the eq as 0 + } else if (x > b.getX()) { + return b.getY(); // after animation defining the eq as b's Y + } else { + if (x < (b.getX() - a.getX()) / 2) { + return b.getY() * Math.pow(2, (20 * relativeX(a, b, x)) - 10) / 2; + } else { + return b.getY() * (2 - Math.pow(2, 10 - (20 * relativeX(a, b, x)))) / 2; + } + } + } + // ------------------------------------------------------- + + /** + * CICRULAR EASING - Uses Roots and Powers to make curves + */ + public static double circEaseIn(Point2D a, Point2D b, double x, int roundness) { + if (roundness < 0) { + System.out.println("Animation API error - roundness must be >= 0"); + return Math.random(); + } + if (x < a.getX()) { + return 0; // before animation defining the eq as 0 + } else if (x > b.getX()) { + return b.getY(); // after animation defining the eq as b's Y + } else { + return b.getY() * (1 - Math.pow(1 - Math.pow(relativeX(a, b, x), roundness), 1 / roundness)); + } + } + + public static double circEaseOut(Point2D a, Point2D b, double x, int roundness) { + if (roundness < 0) { + System.out.println("Animation API error - roundness must be >= 0"); + return Math.random(); + } + if (x < a.getX()) { + return 0; // before animation defining the eq as 0 + } else if (x > b.getX()) { + return b.getY(); // after animation defining the eq as b's Y + } else { + return b.getY() * Math.pow(1 - Math.pow(relativeX(a, b, x) - 1, roundness), 1 / roundness); + } + } + + public static double circEaseInOut(Point2D a, Point2D b, double x, int roundness) { + if (roundness < 0) { + System.out.println("Animation API error - roundness must be >= 0"); + return Math.random(); + } + if (x < a.getX()) { + return 0; // before animation defining the eq as 0 + } else if (x > b.getX()) { + return b.getY(); // after animation defining the eq as b's Y + } else { + if (x < (b.getX() - a.getX()) / 2) { + return b.getY() * (1 - Math.pow(1 - Math.pow(2 * relativeX(a, b, x), roundness), 1 / roundness)) / 2; + } else { + return b.getY() * (Math.pow(1 - Math.pow(-2 * relativeX(a, b, x) + 2, roundness), 1 / roundness) + 1) / 2; + } + } + } + + public static double circEaseIn(Point2D a, Point2D b, double x) { + return circEaseIn(a, b, x, 2); + } + + public static double circEaseOut(Point2D a, Point2D b, double x) { + return circEaseOut(a, b, x, 2); + } + + public static double circEaseInOut(Point2D a, Point2D b, double x) { + return circEaseInOut(a, b, x, 2); + } + // ------------------------------------------------------- + + /** + * ELASTIC EASING - Generated by Cosine and a variable "c" of the curves intensity + */ + public static double elasticEaseIn(Point2D a, Point2D b, double x, int c) { + if (c < 0) { + System.out.println("Animation API error - c must be >= 0"); + return Math.random(); + } + if (x < a.getX()) { + return 0; // before animation defining the eq as 0 + } else if (x > b.getX()) { + return b.getY(); // after animation defining the eq as b's Y + } else { + return b.getY() * (Math.cos(2 * Math.PI * c * relativeX(a, b, x)) * relativeX(a, b, x)); + } + } + + public static double elasticEaseOut(Point2D a, Point2D b, double x, int c) { + if (c < 0) { + System.out.println("Animation API error - c must be >= 0"); + return Math.random(); + } + if (x < a.getX()) { + return 0; // before animation defining the eq as 0 + } else if (x > b.getX()) { + return b.getY(); // after animation defining the eq as b's Y + } else { + return b.getY() * (1 - (Math.cos(2 * Math.PI * c * relativeX(a, b, x)) * (1 - relativeX(a, b, x)))); + } + } + + public static double elasticEaseInOut(Point2D a, Point2D b, double x, int c) { + if (c < 0) { + System.out.println("Animation API error - c must be >= 0"); + return Math.random(); + } + if (x < a.getX()) { + return 0; // before animation defining the eq as 0 + } else if (x > b.getX()) { + return b.getY(); // after animation defining the eq as b's Y + } else { + return b.getY() * (relativeX(a, b, x) + (Math.sin(2 * Math.PI * c * relativeX(a, b, x)) * Math.sin(Math.PI * relativeX(a, b, x)))); + } + } + + // Same Equations but automaticly defines + public static double elasticEaseIn(Point2D a, Point2D b, double x) { + int c = (int) (b.getX() - a.getX()); + return elasticEaseIn(a, b, x, c); + } + + public static double elasticEaseOut(Point2D a, Point2D b, double x) { + int c = (int) (b.getX() - a.getX()); + return elasticEaseOut(a, b, x, c); + } + + public static double elasticEaseInOut(Point2D a, Point2D b, double x) { + int c = (int) (b.getX() - a.getX()); + return elasticEaseInOut(a, b, x, c); + } + // ------------------------------------------------------- + + /** + * BOUNCE EASING - Generated by an elastic absoluted + */ + public static double bounceEaseIn(Point2D a, Point2D b, double x, int c) { + if (c < 0) { + System.out.println("Animation API error - c must be >= 0"); + return Math.random(); + } + if (x < a.getX()) { + return 0; // before animation defining the eq as 0 + } else if (x > b.getX()) { + return b.getY(); // after animation defining the eq as b's Y + } else { + return b.getY() * Math.abs(Math.cos(2 * Math.PI * c * relativeX(a, b, x)) * relativeX(a, b, x)); + } + } + + public static double bounceEaseOut(Point2D a, Point2D b, double x, int c) { + if (c < 0) { + System.out.println("Animation API error - c must be >= 0"); + return Math.random(); + } + if (x < a.getX()) { + return 0; // before animation defining the eq as 0 + } else if (x > b.getX()) { + return b.getY(); // after animation defining the eq as b's Y + } else { + return b.getY() * (1 - Math.abs(Math.cos(2 * Math.PI * c * relativeX(a, b, x)) * (1 - relativeX(a, b, x)))); + } + } + + public static double bounceEaseInOut(Point2D a, Point2D b, double x, int c) { + if (c < 0) { + System.out.println("Animation API error - c must be >= 0"); + return Math.random(); + } + if (x < a.getX()) { + return 0; // before animation defining the eq as 0 + } else if (x > b.getX()) { + return b.getY(); // after animation defining the eq as b's Y + } else { + return b.getY() * (relativeX(a, b, x) + Math.abs(Math.sin(2 * Math.PI * c * relativeX(a, b, x)) * Math.sin(Math.PI * relativeX(a, b, x)))); + } + } + + // Same Equations but automatically defines c + public static double bounceEaseIn(Point2D a, Point2D b, double x) { + int c = (int) (b.getX() - a.getX()); + return elasticEaseIn(a, b, x, c); + } + + public static double bounceEaseOut(Point2D a, Point2D b, double x) { + int c = (int) (b.getX() - a.getX()); + return elasticEaseOut(a, b, x, c); + } + + public static double bounceEaseInOut(Point2D a, Point2D b, double x) { + int c = (int) (b.getX() - a.getX()); + return elasticEaseInOut(a, b, x, c); + } + // ------------------------------------------------------- + + /** + * BACK EASING - Generates a curve that comes back a little at the end (defined by an amount a >= 0) + */ + public static double backEaseIn(Point2D a, Point2D b, double x, double c1) { + double c2 = c1 + 1; + if (c1 < 0) { + System.out.println("Animation API error - c must be >= 0"); + return Math.random(); + } + if (x < a.getX()) { + return 0; // before animation defining the eq as 0 + } else if (x > b.getX()) { + return b.getY(); // after animation defining the eq as b's Y + } else { + return b.getY() * (c2 * Math.pow(relativeX(a, b, x), 3) - c1 * Math.pow(relativeX(a, b, x) - 1, 2)); + } + } + + public static double backEaseOut(Point2D a, Point2D b, double x, double c1) { + double c2 = c1 + 1; + if (c1 < 0) { + System.out.println("Animation API error - c must be >= 0"); + return Math.random(); + } + if (x < a.getX()) { + return 0; // before animation defining the eq as 0 + } else if (x > b.getX()) { + return b.getY(); // after animation defining the eq as b's Y + } else { + return b.getY() * (1 + c2 * Math.pow(relativeX(a, b, x) - 1, 3) + c1 * Math.pow(relativeX(a, b, x) - 1, 2)); + } + } + + public static double backEaseInOut(Point2D a, Point2D b, double x, double c1) { + double c2 = c1 + 1; + double c3 = c1 * 1.525F; + if (c1 < 0) { + System.out.println("Animation API error - c must be >= 0"); + return Math.random(); + } + if (x < a.getX()) { + return 0; // before animation defining the eq as 0 + } else if (x > b.getX()) { + return b.getY(); // after animation defining the eq as b's Y + } else { + if (x < (b.getX() - a.getX()) / 2) { + return b.getY() * (Math.pow(2 * relativeX(a, b, x), 2) * ((c3 + 1) * 2 * relativeX(a, b, x) - c3)) / 2; + } else { + return b.getY() * (Math.pow(2 * relativeX(a, b, x) - 2, 2) * ((c3 + 1) * (2 * relativeX(a, b, x) - 2) + c3) + 2) / 2; + } + } + } + + // Same method but automatically defines c1 + public static double backEaseIn(Point2D a, Point2D b, double x) { + return backEaseIn(a, b, x, 1.70158F); + } + + public static double backEaseOut(Point2D a, Point2D b, double x) { + return backEaseOut(a, b, x, 1.70158F); + } + + public static double backEaseInOut(Point2D a, Point2D b, double x) { + return backEaseInOut(a, b, x, 1.70158F); + } + // ------------------------------------------------------- + + /** + * LOOP SYSTEM + * Loop: defines A and B and always repeat between these two values + * Boomerang: creates a loop but instead of repeating it from start, it comes back and THEN loop + */ + public static double line(Point2D a, Point2D b, double x) { + return (relativeX(a, b, x) * (b.getY() - a.getY()) + a.getY()); + } + + public static double flat(Point2D a, Point2D b, double x) { + return (Math.floor(relativeX(a, b, x)) * (b.getY() - a.getY()) + a.getY()); + } + + public static double flat2(Point2D a, Point2D b, double x) { + return (2 * Math.floor(relativeX(a, b, x) / 2) * (b.getY() - a.getY()) + a.getY()); + } + + public static double inverse(Point2D a, Point2D b, double x) { + return (flat(a, b, x) + b.getY() - line(a, b, x)); + } + + // BOOMERANG + public static double boomerang(Point2D a, Point2D b, double x) { + return line(a, b, x) - flat2(a, b, x) + a.getY() < b.getY() ? (line(a, b, x) - flat2(a, b, x) + a.getY()) : inverse(a, b, x); + } + + // LOOP + public static double loop(Point2D a, Point2D b, double x) { + return (line(a, b, x) - flat(a, b, x) + a.getY()); + } + // ------------------------------------------------------- + + /* + * Animation API - LiukRast, ALL RIGHTS RESERVED (2021-2022) + */ +} diff --git a/src/main/java/net/frozenblock/lib/math/api/Conics.java b/src/main/java/net/frozenblock/lib/math/api/Conics.java new file mode 100644 index 0000000..39b8316 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/math/api/Conics.java @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.math.api; + +import lombok.experimental.UtilityClass; + +import java.awt.geom.Point2D; + +/** + * Allows defining conics via equations. + *

+ * Defining a point or 3D point allows you to define if it is inside or at the border of a conic. + * + * @author LiukRast (2021-2022) + * @since 4.0 + */ +@UtilityClass +public class Conics { + + public static boolean isCircle(Point2D center, double radius, Point2D actual) { + double curvex = Math.pow(actual.getX() - center.getX(), 2); + double curvey = Math.pow(actual.getY() - center.getY(), 2); + return curvex + curvey == Math.pow(radius, 2); + } + + public static boolean isInsideCircle(Point2D center, double radius, Point2D actual) { + double curvex = Math.pow(actual.getX() - center.getX(), 2); + double curvey = Math.pow(actual.getY() - center.getY(), 2); + return curvex + curvey <= Math.pow(radius, 2); + } + + public static boolean isEllipsoid(Point3D center, double a, double b, double c, Point3D actual) { + double curvex = Math.pow(actual.getX() - center.getX(), 2) / Math.pow(a, 2); + double curvey = Math.pow(actual.getY() - center.getY(), 2) / Math.pow(b, 2); + double curvez = Math.pow(actual.getZ() - center.getZ(), 2) / Math.pow(c, 2); + return curvex + curvey + curvez == 1; + } + + public static boolean isInsideEllipsoid(Point3D center, double a, double b, double c, Point3D actual) { + double curvex = Math.pow(actual.getX() - center.getX(), 2) / (Math.pow(a, 2)); + double curvey = Math.pow(actual.getY() - center.getY(), 2) / (Math.pow(b, 2)); + double curvez = Math.pow(actual.getZ() - center.getZ(), 2) / (Math.pow(c, 2)); + return curvex + curvey + curvez <= 1; + } +} diff --git a/src/main/java/net/frozenblock/lib/math/api/EasyNoiseSampler.java b/src/main/java/net/frozenblock/lib/math/api/EasyNoiseSampler.java new file mode 100644 index 0000000..f4e254a --- /dev/null +++ b/src/main/java/net/frozenblock/lib/math/api/EasyNoiseSampler.java @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.math.api; + +import lombok.experimental.UtilityClass; +import net.minecraft.core.Vec3i; +import net.minecraft.world.level.levelgen.LegacyRandomSource; +import net.minecraft.world.level.levelgen.SingleThreadedRandomSource; +import net.minecraft.world.level.levelgen.ThreadSafeLegacyRandomSource; +import net.minecraft.world.level.levelgen.XoroshiroRandomSource; +import net.minecraft.world.level.levelgen.synth.ImprovedNoise; +import net.minecraft.world.phys.Vec3; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; + +/** + * Adds easy-to-use noise sampling and random number generators + */ +@UtilityClass +public class EasyNoiseSampler { + @Contract("_ -> new") + public static @NotNull ImprovedNoise createCheckedNoise(long seed) { + return new ImprovedNoise(new LegacyRandomSource(seed)); + } + + @Contract("_ -> new") + public static @NotNull ImprovedNoise createLegacyThreadSafeNoise(long seed) { + return new ImprovedNoise(new ThreadSafeLegacyRandomSource(seed)); + } + + @Contract("_ -> new") + public static @NotNull ImprovedNoise createLocalNoise(long seed) { + return new ImprovedNoise(new SingleThreadedRandomSource(seed)); + } + + @Contract("_ -> new") + public static @NotNull ImprovedNoise createXoroNoise(long seed) { + return new ImprovedNoise(new XoroshiroRandomSource(seed)); + } + + public static double sample(ImprovedNoise sampler, Vec3i pos, double multiplier, boolean multiplyY, boolean useY) { + if (useY) { + if (multiplyY) { + return sampler.noise(pos.getX() * multiplier, pos.getY() * multiplier, pos.getZ() * multiplier); + } + return sampler.noise(pos.getX() * multiplier, pos.getY(), pos.getZ() * multiplier); + } + return sampler.noise(pos.getX() * multiplier, 64, pos.getZ() * multiplier); + } + + public static double sampleAbs(ImprovedNoise sampler, Vec3i pos, double multiplier, boolean multiplyY, boolean useY) { + return Math.abs(sample(sampler, pos, multiplier, multiplyY, useY)); + } + + public static double sample(ImprovedNoise sampler, Vec3 pos, double multiplier, boolean multiplyY, boolean useY) { + if (useY) { + if (multiplyY) { + return sampler.noise(pos.x() * multiplier, pos.y() * multiplier, pos.z() * multiplier); + } + return sampler.noise(pos.x() * multiplier, pos.y(), pos.z() * multiplier); + } + return sampler.noise(pos.x() * multiplier, 64, pos.z() * multiplier); + } +} diff --git a/src/main/java/net/frozenblock/lib/math/api/Point3D.java b/src/main/java/net/frozenblock/lib/math/api/Point3D.java new file mode 100644 index 0000000..a4e51c6 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/math/api/Point3D.java @@ -0,0 +1,203 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.math.api; + +import java.io.Serializable; + +/** + * Same as Java's Point2D, but with 3D coordinates. + *

+ * Allows you to create a 3D point with x, y and z. + * + * @author LiukRast (2021-2022) + * @since 4.0 + */ +public abstract class Point3D implements Cloneable { + + /** + * The {@code Float} class defines a point in 3D space specified in float + * precision. + */ + public static class Float extends Point3D implements Serializable { + public float x; + public float y; + public float z; + + public Float() { + } + + public Float(float x, float y, float z) { + this.x = x; + this.y = y; + this.z = z; + } + + public double getX() { + return x; + } + + public double getY() { + return y; + } + + public double getZ() { + return z; + } + + + public void setLocation(double x, double y, double z) { + this.x = (float) x; + this.y = (float) y; + this.z = (float) z; + } + + public void setLocation(float x, float y, float z) { + this.x = x; + this.y = y; + this.z = z; + } + + public String toString() { + return "Point3D.Float[" + x + ", " + y + ", " + z + "]"; + } + } + + public static class Double extends Point3D implements Serializable { + + public double x; + public double y; + public double z; + + + public Double() { + } + + public Double(double x, double y, double z) { + this.x = x; + this.y = y; + this.z = z; + } + + public double getX() { + return x; + } + + public double getY() { + return y; + } + + public double getZ() { + return y; + } + + public void setLocation(double x, double y, double z) { + this.x = x; + this.y = y; + this.z = z; + } + + public String toString() { + return "Point3D.Double[" + x + ", " + y + ", " + z + "]"; + } + } + + protected Point3D() { + } + + public abstract double getX(); + + public abstract double getY(); + + public abstract double getZ(); + + + public abstract void setLocation(double x, double y, double z); + + public void setLocation(Point3D p) { + setLocation(p.getX(), p.getY(), p.getZ()); + } + + public static double distanceSq(double x1, double y1, double z1, double x2, double y2, double z2) { + x1 -= x2; + y1 -= y2; + z1 -= z2; + return (x1 * x1 + y1 * y1 + z1 * z1); + } + + public static double distance(double x1, double y1, double z1, double x2, double y2, double z2) { + x1 -= x2; + y1 -= y2; + z1 -= z2; + return Math.sqrt(x1 * x1 + y1 * y1 + z1 * z1); + } + + public double distanceSq(double px, double py, double pz) { + px -= getX(); + py -= getY(); + pz -= getZ(); + return (px * px + py * py + pz * pz); + } + + public double distanceSq(Point3D pt) { + double px = pt.getX() - this.getX(); + double py = pt.getY() - this.getY(); + double pz = pt.getZ() - this.getZ(); + return (px * px + py * py + pz * pz); + } + + public double distance(double px, double py, double pz) { + px -= getX(); + py -= getY(); + pz -= getZ(); + return Math.sqrt(px * px + py * py + pz * pz); + } + + + public double distance(Point3D pt) { + double px = pt.getX() - this.getX(); + double py = pt.getY() - this.getY(); + double pz = pt.getZ() - this.getZ(); + return Math.sqrt(px * px + py * py + pz * pz); + } + + + public Object clone() { + try { + return super.clone(); + } catch (CloneNotSupportedException e) { + // this shouldn't happen, since we are Cloneable + throw new InternalError(e); + } + } + + + public int hashCode() { + long bits = java.lang.Double.doubleToLongBits(getX()); + bits ^= java.lang.Double.doubleToLongBits(getY()) * 31; + return (((int) bits) ^ ((int) (bits >> 32))); + } + + + public boolean equals(Object obj) { + if (obj instanceof Point3D p2d) { + return (getX() == p2d.getX()) && (getY() == p2d.getY()) && (getZ() == p2d.getZ()); + } + return super.equals(obj); + } +} + diff --git a/src/main/java/net/frozenblock/lib/menu/api/Panoramas.java b/src/main/java/net/frozenblock/lib/menu/api/Panoramas.java new file mode 100644 index 0000000..4aedd3d --- /dev/null +++ b/src/main/java/net/frozenblock/lib/menu/api/Panoramas.java @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.menu.api; + +import net.minecraft.resources.ResourceLocation; +import net.neoforged.api.distmarker.Dist; +import net.neoforged.api.distmarker.OnlyIn; + +import java.util.ArrayList; +import java.util.List; + +@OnlyIn(Dist.CLIENT) +public class Panoramas { + + private static final List PANORAMAS = new ArrayList<>(); + + public static void addPanorama(ResourceLocation location) { + PANORAMAS.add(location); + } + + public static List getPanoramas() { + return PANORAMAS; + } + +} diff --git a/src/main/java/net/frozenblock/lib/menu/api/SplashTextAPI.java b/src/main/java/net/frozenblock/lib/menu/api/SplashTextAPI.java new file mode 100644 index 0000000..33a3ddd --- /dev/null +++ b/src/main/java/net/frozenblock/lib/menu/api/SplashTextAPI.java @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.menu.api; + +import net.minecraft.resources.ResourceLocation; +import net.neoforged.api.distmarker.Dist; +import net.neoforged.api.distmarker.OnlyIn; + +import java.util.ArrayList; +import java.util.List; + +@OnlyIn(Dist.CLIENT) +public final class SplashTextAPI { + private SplashTextAPI() { + } + + private static final List SPLASH_FILES = new ArrayList<>(); + private static final List ADDITIONS = new ArrayList<>(); + private static final List REMOVALS = new ArrayList<>(); + + public static void addSplashLocation(ResourceLocation location) { + SPLASH_FILES.add(location); + } + + public static void add(String text) { + ADDITIONS.add(text); + } + + public static void remove(String text) { + REMOVALS.add(text); + } + + public static List getSplashFiles() { + return List.copyOf(SPLASH_FILES); + } + + public static List getAdditions() { + return List.copyOf(ADDITIONS); + } + + public static List getRemovals() { + return List.copyOf(REMOVALS); + } +} diff --git a/src/main/java/net/frozenblock/lib/menu/mixin/CubeMapMixin.java b/src/main/java/net/frozenblock/lib/menu/mixin/CubeMapMixin.java new file mode 100644 index 0000000..58e96ce --- /dev/null +++ b/src/main/java/net/frozenblock/lib/menu/mixin/CubeMapMixin.java @@ -0,0 +1,92 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.menu.mixin; + +import net.frozenblock.lib.FrozenLogUtils; +import net.frozenblock.lib.FrozenSharedConstants; +import net.frozenblock.lib.math.api.AdvancedMath; +import net.frozenblock.lib.menu.api.Panoramas; +import net.minecraft.Util; +import net.minecraft.client.Minecraft; +import net.minecraft.client.renderer.CubeMap; +import net.minecraft.resources.ResourceLocation; +import net.neoforged.api.distmarker.Dist; +import net.neoforged.api.distmarker.OnlyIn; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +import java.util.ArrayList; +import java.util.List; + +@OnlyIn(Dist.CLIENT) +@Mixin(CubeMap.class) +public class CubeMapMixin { + + @Shadow + @Final + private ResourceLocation[] images = new ResourceLocation[6]; + + @Unique + private boolean frozenLib$canReplacePanorama; + + @Inject(method = "", at = @At("TAIL")) + public void frozenLib$ensurePanoramaIsMain(ResourceLocation faces, CallbackInfo info) { + if (faces.equals(ResourceLocation.withDefaultNamespace("textures/gui/title/background/panorama"))) { + this.frozenLib$canReplacePanorama = true; + } + } + + @Inject(method = "render", at = @At("HEAD")) + public void frozenLib$render(Minecraft client, float x, float y, float alpha, CallbackInfo info) { + if (this.frozenLib$canReplacePanorama) { + this.frozenLib$canReplacePanorama = false; + List validPanoramas = new ArrayList<>(); + for (ResourceLocation panLocation : Panoramas.getPanoramas()) { + String namespace = panLocation.getNamespace(); + String path = panLocation.getPath(); + for (int i = 0; i < 6; ++i) { + // Panorama isn't valid if one of the six images isn't found; move on to the next ResourceLocation in the list. + if (Minecraft.getInstance().getResourceManager().getResource(ResourceLocation.fromNamespaceAndPath(namespace, path + "_" + i + ".png")).isEmpty()) { + FrozenLogUtils.logWarning("Unable to use panorama at " + namespace + ":" + path + ", proper resource pack may not be loaded!", FrozenSharedConstants.UNSTABLE_LOGGING); + break; + } + // Panorama is valid if all six images are found, add to valid panorama list. + if (i == 5) { + validPanoramas.add(panLocation); + } + } + } + if (!validPanoramas.isEmpty()) { + // Set panorama from a valid list. + this.frozenLib$replacePanoramaWith(Util.getRandom(validPanoramas, AdvancedMath.random())); + } + } + } + + @Unique + private void frozenLib$replacePanoramaWith(ResourceLocation faces) { + for (int i = 0; i < 6; i++) { + this.images[i] = faces.withPath(faces.getPath() + "_" + i + ".png"); + } + } +} diff --git a/src/main/java/net/frozenblock/lib/menu/mixin/SplashManagerMixin.java b/src/main/java/net/frozenblock/lib/menu/mixin/SplashManagerMixin.java new file mode 100644 index 0000000..d11ecaf --- /dev/null +++ b/src/main/java/net/frozenblock/lib/menu/mixin/SplashManagerMixin.java @@ -0,0 +1,92 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.menu.mixin; + +import com.llamalad7.mixinextras.injector.ModifyReturnValue; +import net.frozenblock.lib.menu.api.SplashTextAPI; +import net.minecraft.client.Minecraft; +import net.minecraft.client.resources.SplashManager; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.packs.resources.ResourceManager; +import net.minecraft.util.profiling.ProfilerFiller; +import net.neoforged.api.distmarker.Dist; +import net.neoforged.api.distmarker.OnlyIn; +import org.spongepowered.asm.mixin.Final; +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; + +import java.io.BufferedReader; +import java.io.IOException; +import java.util.List; + +@OnlyIn(Dist.CLIENT) +@Mixin(SplashManager.class) +public class SplashManagerMixin { + + @Shadow + @Final + private List splashes; + + @Inject( + method = "apply(Ljava/util/List;Lnet/minecraft/server/packs/resources/ResourceManager;Lnet/minecraft/util/profiling/ProfilerFiller;)V", + at = @At("RETURN") + ) + private void apply(List object, ResourceManager resourceManager, ProfilerFiller profiler, CallbackInfo ci) { + this.splashes.addAll(SplashTextAPI.getAdditions()); + + for (String removal : SplashTextAPI.getRemovals()) { + this.splashes.remove(removal); + } + } + + @ModifyReturnValue( + method = "prepare(Lnet/minecraft/server/packs/resources/ResourceManager;Lnet/minecraft/util/profiling/ProfilerFiller;)Ljava/util/List;", + at = @At("RETURN") + ) + public List addSplashFiles(List original, ResourceManager resourceManager, ProfilerFiller profiler) { + for (ResourceLocation splashLocation : SplashTextAPI.getSplashFiles()) { + try { + BufferedReader bufferedReader = Minecraft.getInstance().getResourceManager().openAsReader(splashLocation); + + List var4; + try { + var4 = bufferedReader.lines().map(String::trim).filter(splashText -> splashText.hashCode() != 125780783).toList(); + } catch (Throwable var7) { + try { + bufferedReader.close(); + } catch (Throwable var6) { + var7.addSuppressed(var6); + } + + throw var7; + } + + bufferedReader.close(); + + original.addAll(var4); + } catch (IOException ignored) { + + } + } + return original; + } + +} diff --git a/src/main/java/net/frozenblock/lib/mobcategory/api/FrozenMobCategories.java b/src/main/java/net/frozenblock/lib/mobcategory/api/FrozenMobCategories.java new file mode 100644 index 0000000..48c62d5 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/mobcategory/api/FrozenMobCategories.java @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.mobcategory.api; + +import lombok.experimental.UtilityClass; +import net.minecraft.world.entity.MobCategory; + +import java.util.LinkedHashMap; +import java.util.Map; + +@UtilityClass +public class FrozenMobCategories { + + private static final Map NEW_MOB_CATEROGIES = new LinkedHashMap<>(); + + public static void addMobCategory(String id, MobCategory category) { + NEW_MOB_CATEROGIES.put(id, category); + } + + public static MobCategory getCategory(String modId, String name) { + return NEW_MOB_CATEROGIES.get(modId.toUpperCase() + name.toUpperCase()); + } +} diff --git a/src/main/java/net/frozenblock/lib/mobcategory/api/entrypoint/FrozenMobCategoryEntrypoint.java b/src/main/java/net/frozenblock/lib/mobcategory/api/entrypoint/FrozenMobCategoryEntrypoint.java new file mode 100644 index 0000000..16bddaa --- /dev/null +++ b/src/main/java/net/frozenblock/lib/mobcategory/api/entrypoint/FrozenMobCategoryEntrypoint.java @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.mobcategory.api.entrypoint; + +import net.frozenblock.lib.mobcategory.impl.FrozenMobCategory; +import net.minecraft.resources.ResourceLocation; + +import java.util.ArrayList; + +public interface FrozenMobCategoryEntrypoint { + + void newCategories(ArrayList context); + + static FrozenMobCategory createCategory(ResourceLocation key, int max, boolean isFriendly, boolean isPersistent, int despawnDistance) { + return new FrozenMobCategory(key, max, isFriendly, isPersistent, despawnDistance); + } + +} + diff --git a/src/main/java/net/frozenblock/lib/mobcategory/impl/FrozenMobCategory.java b/src/main/java/net/frozenblock/lib/mobcategory/impl/FrozenMobCategory.java new file mode 100644 index 0000000..d68c641 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/mobcategory/impl/FrozenMobCategory.java @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.mobcategory.impl; + +import net.minecraft.resources.ResourceLocation; + +public record FrozenMobCategory(ResourceLocation key, int max, boolean isFriendly, boolean isPersistent, + int despawnDistance) { +} diff --git a/src/main/java/net/frozenblock/lib/mobcategory/mixin/MobCategoryMixin.java b/src/main/java/net/frozenblock/lib/mobcategory/mixin/MobCategoryMixin.java new file mode 100644 index 0000000..4e5eeb5 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/mobcategory/mixin/MobCategoryMixin.java @@ -0,0 +1,93 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.mobcategory.mixin; + +import net.frozenblock.lib.event.api.MobCategoryEvent; +import net.frozenblock.lib.mobcategory.api.FrozenMobCategories; +import net.frozenblock.lib.mobcategory.api.entrypoint.FrozenMobCategoryEntrypoint; +import net.frozenblock.lib.mobcategory.impl.FrozenMobCategory; +import net.minecraft.world.entity.MobCategory; +import net.neoforged.neoforge.common.NeoForge; +import org.objectweb.asm.Opcodes; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Mutable; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.gen.Invoker; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +import java.util.ArrayList; +import java.util.Arrays; + +@Mixin(MobCategory.class) +public class MobCategoryMixin { + + @SuppressWarnings("InvokerTarget") + @Invoker("") + private static MobCategory newType(String internalName, int internalId, String name, int max, boolean isFriendly, boolean isPersistent, int despawnDistance) { + throw new AssertionError("Mixin injection failed - FrozenLib MobCategoryMixin"); + } + + @SuppressWarnings("ShadowTarget") + @Shadow + @Final + @Mutable + private static MobCategory[] $VALUES; + + @Inject( + method = "", + at = @At( + value = "FIELD", + opcode = Opcodes.PUTSTATIC, + target = "Lnet/minecraft/world/entity/MobCategory;$VALUES:[Lnet/minecraft/world/entity/MobCategory;", + shift = At.Shift.AFTER + ) + ) + private static void addCustomCategories(CallbackInfo ci) { + var categories = new ArrayList<>(Arrays.asList($VALUES)); + var last = categories.get(categories.size() - 1); + int currentOrdinal = last.ordinal(); + + ArrayList internalIds = new ArrayList<>(); + for (MobCategory category : categories) { + internalIds.add(category.name()); + } + + ArrayList newCategories = new ArrayList<>(); + NeoForge.EVENT_BUS.post(new MobCategoryEvent(newCategories)); + + + for (FrozenMobCategory category : newCategories) { + var namespace = category.key().getNamespace(); + var path = category.key().getPath(); + StringBuilder internalId = new StringBuilder(namespace.toUpperCase()); + internalId.append(path.toUpperCase()); + if (internalIds.contains(internalId.toString())) { + throw new IllegalStateException("Cannot add duplicate MobCategory " + internalId + "!"); + } + currentOrdinal += 1; + var addedCategory = newType(internalId.toString(), currentOrdinal, namespace + path, category.max(), category.isFriendly(), category.isPersistent(), category.despawnDistance()); + categories.add(addedCategory); + FrozenMobCategories.addMobCategory(internalId.toString(), addedCategory); + } + + $VALUES = categories.toArray(new MobCategory[0]); + } +} diff --git a/src/main/java/net/frozenblock/lib/networking/FrozenByteBufCodecs.java b/src/main/java/net/frozenblock/lib/networking/FrozenByteBufCodecs.java new file mode 100644 index 0000000..7efc0b0 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/networking/FrozenByteBufCodecs.java @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.networking; + +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.world.phys.Vec3; +import org.jetbrains.annotations.NotNull; + +public class FrozenByteBufCodecs { + + public static final StreamCodec VEC3 = new StreamCodec<>() { + @NotNull + @Override + public Vec3 decode(@NotNull FriendlyByteBuf buf) { + return buf.readVec3(); + } + + @Override + public void encode(@NotNull FriendlyByteBuf buf, Vec3 vec) { + buf.writeVec3(vec); + } + }; +} diff --git a/src/main/java/net/frozenblock/lib/networking/FrozenClientNetworking.java b/src/main/java/net/frozenblock/lib/networking/FrozenClientNetworking.java new file mode 100644 index 0000000..d4d02b2 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/networking/FrozenClientNetworking.java @@ -0,0 +1,94 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.networking; + +import net.frozenblock.lib.config.api.instance.Config; +import net.frozenblock.lib.config.api.registry.ConfigRegistry; +import net.frozenblock.lib.config.impl.network.ConfigSyncPacket; +import net.frozenblock.lib.item.impl.CooldownInterface; +import net.frozenblock.lib.item.impl.network.CooldownChangePacket; +import net.frozenblock.lib.item.impl.network.CooldownTickCountPacket; +import net.frozenblock.lib.item.impl.network.ForcedCooldownPacket; +import net.frozenblock.lib.screenshake.api.client.ScreenShaker; +import net.frozenblock.lib.screenshake.impl.network.EntityScreenShakePacket; +import net.frozenblock.lib.screenshake.impl.network.RemoveEntityScreenShakePacket; +import net.frozenblock.lib.screenshake.impl.network.RemoveScreenShakePacket; +import net.frozenblock.lib.screenshake.impl.network.ScreenShakePacket; +import net.frozenblock.lib.sound.api.instances.RestrictedMovingSound; +import net.frozenblock.lib.sound.api.instances.RestrictedMovingSoundLoop; +import net.frozenblock.lib.sound.api.instances.RestrictedStartingSound; +import net.frozenblock.lib.sound.api.instances.distance_based.FadingDistanceSwitchingSound; +import net.frozenblock.lib.sound.api.instances.distance_based.RestrictedMovingFadingDistanceSwitchingSound; +import net.frozenblock.lib.sound.api.instances.distance_based.RestrictedMovingFadingDistanceSwitchingSoundLoop; +import net.frozenblock.lib.sound.api.networking.*; +import net.frozenblock.lib.sound.api.predicate.SoundPredicate; +import net.frozenblock.lib.spotting_icons.impl.EntitySpottingIconInterface; +import net.frozenblock.lib.spotting_icons.impl.SpottingIconPacket; +import net.frozenblock.lib.spotting_icons.impl.SpottingIconRemovePacket; +import net.frozenblock.lib.wind.api.ClientWindManager; +import net.frozenblock.lib.wind.api.WindDisturbance; +import net.frozenblock.lib.wind.api.WindDisturbanceLogic; +import net.frozenblock.lib.wind.impl.networking.WindDisturbancePacket; +import net.frozenblock.lib.wind.impl.networking.WindSyncPacket; +import net.minecraft.client.Minecraft; +import net.minecraft.client.multiplayer.ClientLevel; +import net.minecraft.client.multiplayer.ClientPacketListener; +import net.minecraft.client.multiplayer.ServerData; +import net.minecraft.client.player.LocalPlayer; +import net.minecraft.client.resources.sounds.EntityBoundSoundInstance; +import net.minecraft.client.sounds.SoundManager; +import net.minecraft.core.BlockPos; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.sounds.SoundSource; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemCooldowns; +import net.minecraft.world.phys.Vec3; +import net.neoforged.api.distmarker.Dist; +import net.neoforged.api.distmarker.OnlyIn; +import net.neoforged.bus.api.SubscribeEvent; + +import java.util.Optional; + +@OnlyIn(Dist.CLIENT) +public final class FrozenClientNetworking { + public static void registerClientReceivers() { + for (Config config : ConfigRegistry.getAllConfigs()) { + ConfigRegistry.setSyncData(config, null); + config.setSynced(false); + } + } + + public static boolean notConnected() { + Minecraft minecraft = Minecraft.getInstance(); + ClientPacketListener listener = minecraft.getConnection(); + if (listener == null) return true; + + LocalPlayer player = Minecraft.getInstance().player; + return player == null; + } + + public static boolean connectedToLan() { + if (notConnected()) return false; + ServerData serverData = Minecraft.getInstance().getCurrentServer(); + if (serverData == null) return false; + return serverData.isLan(); + } + +} diff --git a/src/main/java/net/frozenblock/lib/networking/FrozenNetworking.java b/src/main/java/net/frozenblock/lib/networking/FrozenNetworking.java new file mode 100644 index 0000000..73eb09c --- /dev/null +++ b/src/main/java/net/frozenblock/lib/networking/FrozenNetworking.java @@ -0,0 +1,264 @@ +package net.frozenblock.lib.networking; + +import net.frozenblock.lib.config.impl.network.ConfigSyncPacket; +import net.frozenblock.lib.item.impl.CooldownInterface; +import net.frozenblock.lib.item.impl.network.CooldownChangePacket; +import net.frozenblock.lib.item.impl.network.CooldownTickCountPacket; +import net.frozenblock.lib.item.impl.network.ForcedCooldownPacket; +import net.frozenblock.lib.screenshake.api.client.ScreenShaker; +import net.frozenblock.lib.screenshake.impl.network.EntityScreenShakePacket; +import net.frozenblock.lib.screenshake.impl.network.RemoveEntityScreenShakePacket; +import net.frozenblock.lib.screenshake.impl.network.ScreenShakePacket; +import net.frozenblock.lib.sound.api.instances.RestrictedMovingSound; +import net.frozenblock.lib.sound.api.instances.RestrictedMovingSoundLoop; +import net.frozenblock.lib.sound.api.instances.RestrictedStartingSound; +import net.frozenblock.lib.sound.api.instances.distance_based.FadingDistanceSwitchingSound; +import net.frozenblock.lib.sound.api.instances.distance_based.RestrictedMovingFadingDistanceSwitchingSound; +import net.frozenblock.lib.sound.api.instances.distance_based.RestrictedMovingFadingDistanceSwitchingSoundLoop; +import net.frozenblock.lib.sound.api.networking.*; +import net.frozenblock.lib.sound.api.predicate.SoundPredicate; +import net.frozenblock.lib.spotting_icons.impl.EntitySpottingIconInterface; +import net.frozenblock.lib.spotting_icons.impl.SpottingIconPacket; +import net.frozenblock.lib.spotting_icons.impl.SpottingIconRemovePacket; +import net.frozenblock.lib.wind.api.ClientWindManager; +import net.frozenblock.lib.wind.api.WindDisturbance; +import net.frozenblock.lib.wind.api.WindDisturbanceLogic; +import net.frozenblock.lib.wind.impl.networking.WindDisturbancePacket; +import net.frozenblock.lib.wind.impl.networking.WindSyncPacket; +import net.minecraft.client.Minecraft; +import net.minecraft.client.multiplayer.ClientLevel; +import net.minecraft.client.multiplayer.ClientPacketListener; +import net.minecraft.client.player.LocalPlayer; +import net.minecraft.client.resources.sounds.EntityBoundSoundInstance; +import net.minecraft.client.sounds.SoundManager; +import net.minecraft.core.BlockPos; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.sounds.SoundSource; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemCooldowns; +import net.minecraft.world.phys.Vec3; +import net.neoforged.api.distmarker.Dist; +import net.neoforged.bus.api.SubscribeEvent; +import net.neoforged.fml.loading.FMLLoader; +import net.neoforged.neoforge.network.event.RegisterPayloadHandlersEvent; +import net.neoforged.neoforge.network.handling.DirectionalPayloadHandler; +import net.neoforged.neoforge.network.registration.PayloadRegistrar; + +import java.util.Optional; + +public final class FrozenNetworking { + private FrozenNetworking() {} + + @SubscribeEvent + public static void registerNetworking(final RegisterPayloadHandlersEvent event) { + final PayloadRegistrar registry = event.registrar("1"); + + // BI + registry.playBidirectional(ConfigSyncPacket.PACKET_TYPE, ConfigSyncPacket.CODEC, new DirectionalPayloadHandler<>( + (packet, ctx) -> ConfigSyncPacket.receive(packet, null), + (packet, ctx) -> { + if (ConfigSyncPacket.hasPermissionsToSendSync(ctx.player(), true)) + ConfigSyncPacket.receive(packet, ctx.player().getServer()); + } + )); + + + // S2C + registry.playToClient(LocalPlayerSoundPacket.PACKET_TYPE, LocalPlayerSoundPacket.CODEC, (packet, ctx) -> { + LocalPlayer player = Minecraft.getInstance().player; + Minecraft.getInstance().getSoundManager().play(new EntityBoundSoundInstance(packet.sound().value(), SoundSource.PLAYERS, packet.volume(), packet.pitch(), player, Minecraft.getInstance().level.random.nextLong())); + }); + registry.playToClient(LocalSoundPacket.PACKET_TYPE, LocalSoundPacket.CODEC, (packet, ctx) -> { + ClientLevel level = Minecraft.getInstance().level; + Vec3 pos = packet.pos(); + level.playLocalSound(pos.x, pos.y, pos.z, packet.sound().value(), packet.category(), packet.volume(), packet.pitch(), packet.distanceDelay()); + }); + registry.playToClient(StartingMovingRestrictionSoundLoopPacket.PACKET_TYPE, StartingMovingRestrictionSoundLoopPacket.CODEC, (packet, ctx) -> { + ClientLevel level = Minecraft.getInstance().level; + Entity entity = level.getEntity(packet.id()); + if (entity != null) { + SoundPredicate.LoopPredicate predicate = SoundPredicate.getPredicate(packet.predicateId()); + Minecraft.getInstance().getSoundManager().play(new RestrictedStartingSound<>( + entity, packet.startingSound().value(), packet.category(), packet.volume(), packet.pitch(), predicate, packet.stopOnDeath(), + new RestrictedMovingSoundLoop<>( + entity, packet.sound().value(), packet.category(), packet.volume(), packet.pitch(), predicate, packet.stopOnDeath() + ) + )); + } + }); + registry.playToClient(MovingRestrictionSoundPacket.PACKET_TYPE, MovingRestrictionSoundPacket.CODEC, (packet, ctx) -> { + ClientLevel level = Minecraft.getInstance().level; + Entity entity = level.getEntity(packet.id()); + if (entity != null) { + SoundPredicate.LoopPredicate predicate = SoundPredicate.getPredicate(packet.predicateId()); + if (packet.looping()) + Minecraft.getInstance().getSoundManager().play(new RestrictedMovingSoundLoop<>(entity, packet.sound().value(), packet.category(), packet.volume(), packet.pitch(), predicate, packet.stopOnDeath())); + else + Minecraft.getInstance().getSoundManager().play(new RestrictedMovingSound<>(entity, packet.sound().value(), packet.category(), packet.volume(), packet.pitch(), predicate, packet.stopOnDeath())); + } + }); + registry.playToClient(FadingDistanceSwitchingSoundPacket.PACKET_TYPE, FadingDistanceSwitchingSoundPacket.CODEC, (packet, ctx) -> { + Minecraft.getInstance().getSoundManager().play(new FadingDistanceSwitchingSound(packet.closeSound().value(), packet.category(), packet.volume(), packet.pitch(), packet.fadeDist(), packet.maxDist(), packet.volume(), false, packet.pos())); + Minecraft.getInstance().getSoundManager().play(new FadingDistanceSwitchingSound(packet.farSound().value(), packet.category(), packet.volume(), packet.pitch(), packet.fadeDist(), packet.maxDist(), packet.volume(), true, packet.pos())); + }); + registry.playToClient(MovingFadingDistanceSwitchingRestrictionSoundPacket.PACKET_TYPE, MovingFadingDistanceSwitchingRestrictionSoundPacket.CODEC, (packet, ctx) -> { + SoundManager soundManager = Minecraft.getInstance().getSoundManager(); + ClientLevel level = Minecraft.getInstance().level; + Entity entity = level.getEntity(packet.id()); + if (entity != null) { + SoundPredicate.LoopPredicate predicate = SoundPredicate.getPredicate(packet.predicateId()); + if (packet.looping()) { + soundManager.play(new RestrictedMovingFadingDistanceSwitchingSoundLoop<>(entity, packet.closeSound().value(), packet.category(), packet.volume(), packet.pitch(), predicate, packet.stopOnDeath(), packet.fadeDist(), packet.maxDist(), packet.volume(), false)); + soundManager.play(new RestrictedMovingFadingDistanceSwitchingSoundLoop<>(entity, packet.farSound().value(), packet.category(), packet.volume(), packet.pitch(), predicate, packet.stopOnDeath(), packet.fadeDist(), packet.maxDist(), packet.volume(), true)); + } else { + soundManager.play(new RestrictedMovingFadingDistanceSwitchingSound<>(entity, packet.closeSound().value(), packet.category(), packet.volume(), packet.pitch(), predicate, packet.stopOnDeath(), packet.fadeDist(), packet.maxDist(), packet.volume(), false)); + soundManager.play(new RestrictedMovingFadingDistanceSwitchingSound<>(entity, packet.farSound().value(), packet.category(), packet.volume(), packet.pitch(), predicate, packet.stopOnDeath(), packet.fadeDist(), packet.maxDist(), packet.volume(), true)); + } + } + }); + registry.playToClient(CooldownChangePacket.PACKET_TYPE, CooldownChangePacket.CODEC, (packet, ctx) -> { + LocalPlayer player = Minecraft.getInstance().player; + Item item = packet.item(); + int additional = packet.additional(); + ((CooldownInterface)player.getCooldowns()).frozenLib$changeCooldown(item, additional); + }); + registry.playToClient(ForcedCooldownPacket.PACKET_TYPE, ForcedCooldownPacket.CODEC, (packet, ctx) -> { + LocalPlayer player = Minecraft.getInstance().player; + Item item = packet.item(); + int startTime = packet.startTime(); + int endTime = packet.endTime(); + player.getCooldowns().cooldowns.put(item, new ItemCooldowns.CooldownInstance(startTime, endTime)); + }); + registry.playToClient(CooldownTickCountPacket.PACKET_TYPE, CooldownTickCountPacket.CODEC, (packet, ctx) -> { + LocalPlayer player = Minecraft.getInstance().player; + if (player != null) { + player.getCooldowns().tickCount = packet.count(); + } + }); + registry.playToClient(ScreenShakePacket.PACKET_TYPE, ScreenShakePacket.CODEC, (packet, ctx) -> { + float intensity = packet.intensity(); + int duration = packet.duration(); + int fallOffStart = packet.falloffStart(); + Vec3 pos = packet.pos(); + float maxDistance = packet.maxDistance(); + int ticks = packet.ticks(); + + ClientLevel level = Minecraft.getInstance().level; + ScreenShaker.addShake(level, intensity, duration, fallOffStart, pos, maxDistance, ticks); + }); + registry.playToClient(EntityScreenShakePacket.PACKET_TYPE, EntityScreenShakePacket.CODEC, (packet, ctx) -> { + int id = packet.entityId(); + float intensity = packet.intensity(); + int duration = packet.duration(); + int fallOffStart = packet.falloffStart(); + float maxDistance = packet.maxDistance(); + int ticks = packet.ticks(); + + ClientLevel level = Minecraft.getInstance().level; + Entity entity = level.getEntity(id); + if (entity != null) { + ScreenShaker.addShake(entity, intensity, duration, fallOffStart, maxDistance, ticks); + } + }); + registry.playToClient(RemoveEntityScreenShakePacket.PACKET_TYPE, RemoveEntityScreenShakePacket.CODEC, (packet, ctx) -> { + int id = packet.entityId(); + + ClientLevel level = Minecraft.getInstance().level; + Entity entity = level.getEntity(id); + if (entity != null) { + ScreenShaker.SCREEN_SHAKES.removeIf(clientScreenShake -> clientScreenShake instanceof ScreenShaker.ClientEntityScreenShake entityScreenShake && entityScreenShake.getEntity() == entity); + } + }); + registry.playToClient(SpottingIconPacket.PACKET_TYPE, SpottingIconPacket.CODEC, (packet, ctx) -> { + int id = packet.entityId(); + ResourceLocation texture = packet.texture(); + float startFade = packet.startFade(); + float endFade = packet.endFade(); + ResourceLocation predicate = packet.restrictionID(); + + ClientLevel level = Minecraft.getInstance().level; + Entity entity = level.getEntity(id); + if (entity instanceof EntitySpottingIconInterface livingEntity) { + livingEntity.getSpottingIconManager().setIcon(texture, startFade, endFade, predicate); + } + }); + registry.playToClient(SpottingIconRemovePacket.PACKET_TYPE, SpottingIconRemovePacket.CODEC, (packet, ctx) -> { + int id = packet.entityId(); + + ClientLevel level = Minecraft.getInstance().level; + Entity entity = level.getEntity(id); + if (entity instanceof EntitySpottingIconInterface livingEntity) { + livingEntity.getSpottingIconManager().icon = null; + } + }); + registry.playToClient(WindSyncPacket.PACKET_TYPE, WindSyncPacket.CODEC, (packet, ctx) -> { + ClientWindManager.time = packet.windTime(); + ClientWindManager.setSeed(packet.seed()); + ClientWindManager.overrideWind = packet.override(); + ClientWindManager.commandWind = packet.commandWind(); + ClientWindManager.hasInitialized = true; + }); + registry.playToClient(WindDisturbancePacket.PACKET_TYPE, WindDisturbancePacket.CODEC, (packet, ctx) -> { + ClientLevel level = Minecraft.getInstance().level; + long posOrID = packet.posOrID(); + Optional disturbanceLogic = WindDisturbanceLogic.getWindDisturbanceLogic(packet.id()); + if (disturbanceLogic.isPresent()) { + WindDisturbanceLogic.SourceType sourceType = packet.disturbanceSourceType(); + Optional source = Optional.empty(); + if (sourceType == WindDisturbanceLogic.SourceType.ENTITY) { + source = Optional.ofNullable(level.getEntity((int) posOrID)); + } else if (sourceType == WindDisturbanceLogic.SourceType.BLOCK_ENTITY) { + source = Optional.ofNullable(level.getBlockEntity(BlockPos.of(posOrID))); + } + + ClientWindManager.addWindDisturbance( + new WindDisturbance( + source, + packet.origin(), + packet.affectedArea(), + disturbanceLogic.get() + ) + ); + } + }); + //C2S + } + + public static boolean isLocalPlayer(Player player) { + if (FMLLoader.getDist() == Dist.DEDICATED_SERVER) + return false; + + return Minecraft.getInstance().isLocalPlayer(player.getGameProfile().getId()); + } + + public static boolean connectedToIntegratedServer() { + if (FMLLoader.getDist() == Dist.DEDICATED_SERVER) + return false; + Minecraft minecraft = Minecraft.getInstance(); + return minecraft.hasSingleplayerServer(); + } + /** + * @return if the client is connected to any server + */ + public static boolean connectedToServer() { + if (FMLLoader.getDist() == Dist.DEDICATED_SERVER) + return false; + Minecraft minecraft = Minecraft.getInstance(); + ClientPacketListener listener = minecraft.getConnection(); + if (listener == null) + return false; + return listener.getConnection().isConnected(); + } + + /** + * @return if the current server is multiplayer (LAN/dedicated) or not (singleplayer) + */ + public static boolean isMultiplayer() { + if (FMLLoader.getDist() == Dist.DEDICATED_SERVER) + return true; + + return !Minecraft.getInstance().isSingleplayer(); + } +} diff --git a/src/main/java/net/frozenblock/lib/networking/PlayerLookup.java b/src/main/java/net/frozenblock/lib/networking/PlayerLookup.java new file mode 100644 index 0000000..5e42dd3 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/networking/PlayerLookup.java @@ -0,0 +1,13 @@ +package net.frozenblock.lib.networking; + +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerPlayer; + +import java.util.Collection; +import java.util.Collections; + +public class PlayerLookup { + public static Collection all(MinecraftServer server) { + return Collections.unmodifiableCollection(server.getPlayerList().getPlayers()); + } +} diff --git a/src/main/java/net/frozenblock/lib/particle/api/FrozenParticleTypes.java b/src/main/java/net/frozenblock/lib/particle/api/FrozenParticleTypes.java new file mode 100644 index 0000000..3f1c1da --- /dev/null +++ b/src/main/java/net/frozenblock/lib/particle/api/FrozenParticleTypes.java @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.particle.api; + +import com.mojang.serialization.MapCodec; +import lombok.experimental.UtilityClass; +import net.frozenblock.lib.FrozenSharedConstants; +import net.minecraft.core.particles.ParticleOptions; +import net.minecraft.core.particles.ParticleType; +import net.minecraft.core.particles.SimpleParticleType; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.neoforged.neoforge.registries.RegisterEvent; +import org.jetbrains.annotations.NotNull; + +import java.util.function.Function; + +@UtilityClass +public class FrozenParticleTypes { + public static final SimpleParticleType DEBUG_POS = new SimpleParticleType(false); + + public static void registerParticles(RegisterEvent.RegisterHelper> registry) { + FrozenSharedConstants.LOGGER.info("Registering Particles for " + FrozenSharedConstants.MOD_ID); + registry.register(FrozenSharedConstants.id("debug_pos"), DEBUG_POS); + } + + @NotNull + private static ParticleType generate( + boolean alwaysShow, + Function, MapCodec> codecGetter, + Function, StreamCodec> streamCodecGetter + ) { + return new ParticleType(alwaysShow) { + @Override + public MapCodec codec() { + return codecGetter.apply(this); + } + + @Override + public StreamCodec streamCodec() { + return streamCodecGetter.apply(this); + } + }; + } +} diff --git a/src/main/java/net/frozenblock/lib/particle/impl/DebugPosParticle.java b/src/main/java/net/frozenblock/lib/particle/impl/DebugPosParticle.java new file mode 100644 index 0000000..9ce724e --- /dev/null +++ b/src/main/java/net/frozenblock/lib/particle/impl/DebugPosParticle.java @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.particle.impl; + +import net.minecraft.client.multiplayer.ClientLevel; +import net.minecraft.client.particle.*; +import net.minecraft.core.particles.SimpleParticleType; +import net.neoforged.api.distmarker.Dist; +import net.neoforged.api.distmarker.OnlyIn; +import org.jetbrains.annotations.NotNull; + +@OnlyIn(Dist.CLIENT) +public class DebugPosParticle extends TextureSheetParticle { + + DebugPosParticle(@NotNull ClientLevel level, double x, double y, double z) { + super(level, x, y, z); + this.setSize(0.1F, 0.1F); + this.quadSize = 0.1F; + this.lifetime = 1; + this.hasPhysics = false; + this.gravity = 0.0F; + } + + @Override + public void tick() { + this.remove(); + } + + @Override + @NotNull + public ParticleRenderType getRenderType() { + return ParticleRenderType.PARTICLE_SHEET_OPAQUE; + } + + @OnlyIn(Dist.CLIENT) + public static class Provider implements ParticleProvider { + private final SpriteSet sprite; + + public Provider(SpriteSet sprites) { + this.sprite = sprites; + } + + @Override + @NotNull + public Particle createParticle(@NotNull SimpleParticleType defaultParticleType, @NotNull ClientLevel clientLevel, double x, double y, double z, double g, double h, double i) { + DebugPosParticle debugPosParticle = new DebugPosParticle(clientLevel, x, y, z); + debugPosParticle.pickSprite(this.sprite); + return debugPosParticle; + } + } +} diff --git a/src/main/java/net/frozenblock/lib/recipe/api/FrozenRecipeCodecs.java b/src/main/java/net/frozenblock/lib/recipe/api/FrozenRecipeCodecs.java new file mode 100644 index 0000000..d7a2943 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/recipe/api/FrozenRecipeCodecs.java @@ -0,0 +1,22 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.recipe.api; + +public class FrozenRecipeCodecs { + +} diff --git a/src/main/java/net/frozenblock/lib/recipe/api/FrozenRecipeProvider.java b/src/main/java/net/frozenblock/lib/recipe/api/FrozenRecipeProvider.java new file mode 100644 index 0000000..48370ae --- /dev/null +++ b/src/main/java/net/frozenblock/lib/recipe/api/FrozenRecipeProvider.java @@ -0,0 +1,108 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.recipe.api; + +import net.minecraft.core.HolderLookup; +import net.minecraft.data.PackOutput; +import net.minecraft.data.recipes.RecipeCategory; +import net.minecraft.data.recipes.RecipeOutput; +import net.minecraft.data.recipes.RecipeProvider; +import net.minecraft.data.recipes.ShapelessRecipeBuilder; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; +import net.minecraft.world.level.ItemLike; +import org.jetbrains.annotations.NotNull; + +import java.util.List; +import java.util.concurrent.CompletableFuture; + +public final class FrozenRecipeProvider extends RecipeProvider { + private FrozenRecipeProvider(PackOutput output, CompletableFuture registries) { + super(output, registries); + } + + public static void woodenButton(RecipeOutput recipeOutput, ItemLike button, ItemLike material) { + RecipeProvider.buttonBuilder(button, Ingredient.of(material)) + .unlockedBy("has_planks", RecipeProvider.has(material)) + .group("wooden_button").save(recipeOutput); + } + + public static void woodenDoor(RecipeOutput recipeOutput, ItemLike door, ItemLike material) { + RecipeProvider.doorBuilder(door, Ingredient.of(material)) + .unlockedBy("has_planks", RecipeProvider.has(material)) + .group("wooden_door").save(recipeOutput); + } + + public static void woodenFence(RecipeOutput recipeOutput, ItemLike fence, ItemLike material) { + RecipeProvider.fenceBuilder(fence, Ingredient.of(material)) + .unlockedBy("has_planks", RecipeProvider.has(material)) + .group("wooden_fence").save(recipeOutput); + } + + public static void woodenFenceGate(RecipeOutput recipeOutput, ItemLike fenceGate, ItemLike material) { + RecipeProvider.fenceGateBuilder(fenceGate, Ingredient.of(material)) + .unlockedBy("has_planks", RecipeProvider.has(material)) + .group("wooden_fence_gate").save(recipeOutput); + } + + + public static void woodenPressurePlace(RecipeOutput recipeOutput, ItemLike pressurePlate, ItemLike material) { + RecipeProvider.pressurePlateBuilder(RecipeCategory.REDSTONE, pressurePlate, Ingredient.of(material)) + .unlockedBy("has_planks", RecipeProvider.has(material)) + .group("wooden_pressure_plate").save(recipeOutput); + } + + public static void woodenSlab(RecipeOutput recipeOutput, ItemLike slab, ItemLike material) { + RecipeProvider.slabBuilder(RecipeCategory.BUILDING_BLOCKS, slab, Ingredient.of(material)) + .unlockedBy("has_planks", RecipeProvider.has(material)) + .group("wooden_slab").save(recipeOutput); + } + + public static void woodenStairs(RecipeOutput recipeOutput, ItemLike stairs, ItemLike material) { + RecipeProvider.stairBuilder(stairs, Ingredient.of(material)) + .unlockedBy("has_planks", RecipeProvider.has(material)) + .group("wooden_stairs").save(recipeOutput); + } + + public static void woodenTrapdoor(RecipeOutput recipeOutput, ItemLike trapdoor, ItemLike material) { + RecipeProvider.trapdoorBuilder(trapdoor, Ingredient.of(material)) + .unlockedBy("has_planks", RecipeProvider.has(material)) + .group("wooden_trapdoor").save(recipeOutput); + } + + public static void woodenSign(RecipeOutput recipeOutput, ItemLike sign, ItemLike material) { + RecipeProvider.signBuilder(sign, Ingredient.of(material)) + .unlockedBy("has_planks", RecipeProvider.has(material)) + .group("wooden_sign").save(recipeOutput); + } + + public static void colorWithDyes(RecipeOutput recipeOutput, @NotNull List dyes, List dyeableItems, String group, RecipeCategory recipeCategory, String modID) { + for(int i = 0; i < dyes.size(); ++i) { + Item item = dyes.get(i); + Item item2 = dyeableItems.get(i); + ShapelessRecipeBuilder.shapeless(recipeCategory, item2) + .requires(item) + .requires(Ingredient.of(dyeableItems.stream().filter(item2x -> !item2x.equals(item2)).map(ItemStack::new))) + .group(group) + .unlockedBy("has_needed_dye", RecipeProvider.has(item)) + .save(recipeOutput, ResourceLocation.fromNamespaceAndPath(modID, "dye_" + RecipeProvider.getItemName(item2))); + } + } +} diff --git a/src/main/java/net/frozenblock/lib/recipe/api/ShapedRecipeBuilderExtension.java b/src/main/java/net/frozenblock/lib/recipe/api/ShapedRecipeBuilderExtension.java new file mode 100644 index 0000000..df6f677 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/recipe/api/ShapedRecipeBuilderExtension.java @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.recipe.api; + +import net.minecraft.core.component.DataComponentPatch; +import net.minecraft.data.recipes.ShapedRecipeBuilder; +import org.jetbrains.annotations.Nullable; + +public interface ShapedRecipeBuilderExtension { + + ShapedRecipeBuilder frozenLib$patch(@Nullable DataComponentPatch tag); +} diff --git a/src/main/java/net/frozenblock/lib/recipe/api/ShapedRecipeUtil.java b/src/main/java/net/frozenblock/lib/recipe/api/ShapedRecipeUtil.java new file mode 100644 index 0000000..4161c48 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/recipe/api/ShapedRecipeUtil.java @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.recipe.api; + +import net.minecraft.core.component.DataComponentPatch; +import net.minecraft.data.recipes.ShapedRecipeBuilder; + +public final class ShapedRecipeUtil { + private ShapedRecipeUtil() {} + + public static ShapedRecipeBuilder withResultPatch(ShapedRecipeBuilder builder, DataComponentPatch patch) { + return ((ShapedRecipeBuilderExtension) builder).frozenLib$patch(patch); + } +} diff --git a/src/main/java/net/frozenblock/lib/recipe/mixin/ItemValueMixin.java b/src/main/java/net/frozenblock/lib/recipe/mixin/ItemValueMixin.java new file mode 100644 index 0000000..84dd0a9 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/recipe/mixin/ItemValueMixin.java @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.recipe.mixin; + +import com.mojang.datafixers.kinds.App; +import com.mojang.serialization.Codec; +import com.mojang.serialization.MapCodec; +import com.mojang.serialization.codecs.RecordCodecBuilder; +import net.minecraft.core.component.DataComponentPatch; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Redirect; + +import java.util.function.Function; + +@Mixin(Ingredient.ItemValue.class) +public class ItemValueMixin { + + @Redirect(method = "", + at = @At( + value = "INVOKE", + target = "Lcom/mojang/serialization/MapCodec;codec()Lcom/mojang/serialization/Codec;", + ordinal = 0 + ) + ) + private static Codec frozenLib$newCodec( + MapCodec map + ) { + return RecordCodecBuilder.create(instance -> + instance.group( + ItemStack.SIMPLE_ITEM_CODEC.fieldOf("item").forGetter(Ingredient.ItemValue::item), + DataComponentPatch.CODEC.optionalFieldOf("components", DataComponentPatch.EMPTY) + .forGetter(stack -> stack.item().getComponentsPatch()) + ).apply(instance, (item, patch) -> { + item.applyComponents(patch); + return new Ingredient.ItemValue(item); + }) + ); + } +} diff --git a/src/main/java/net/frozenblock/lib/recipe/mixin/ShapedRecipeBuilderMixin.java b/src/main/java/net/frozenblock/lib/recipe/mixin/ShapedRecipeBuilderMixin.java new file mode 100644 index 0000000..c38bb45 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/recipe/mixin/ShapedRecipeBuilderMixin.java @@ -0,0 +1,86 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.recipe.mixin; + +import com.llamalad7.mixinextras.injector.wrapoperation.Operation; +import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; +import net.frozenblock.lib.recipe.api.ShapedRecipeBuilderExtension; +import net.minecraft.advancements.AdvancementHolder; +import net.minecraft.core.component.DataComponentPatch; +import net.minecraft.data.recipes.RecipeOutput; +import net.minecraft.data.recipes.ShapedRecipeBuilder; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.crafting.Recipe; +import net.minecraft.world.item.crafting.ShapedRecipe; +import org.jetbrains.annotations.Nullable; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; + +@Mixin(ShapedRecipeBuilder.class) +public class ShapedRecipeBuilderMixin implements ShapedRecipeBuilderExtension { + + @Unique + @Nullable + private DataComponentPatch patch; + + @Unique + @Override + public ShapedRecipeBuilder frozenLib$patch(@Nullable DataComponentPatch patch) { + this.patch = patch; + return (ShapedRecipeBuilder) (Object) this; + } + + @WrapOperation( + method = "save", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/data/recipes/RecipeOutput;accept(Lnet/minecraft/resources/ResourceLocation;Lnet/minecraft/world/item/crafting/Recipe;Lnet/minecraft/advancements/AdvancementHolder;)V" + ) + ) + private void modifySave(RecipeOutput instance, ResourceLocation recipeId, Recipe recipe, AdvancementHolder holder, Operation operation) { + ((ShapedRecipeBuilderExtension) recipe).frozenLib$patch(this.patch); + operation.call(instance, recipeId, recipe, holder); + } + + /*@Mixin(ShapedRecipeBuilder.Result.class) + private static class ResultMixin implements ShapedRecipeBuilderExtension { + + @Unique + @Nullable + private CompoundTag tag; + + @Override + public ShapedRecipeBuilder frozenLib$tag(CompoundTag tag) { + this.tag = tag; + return null; + } + + @Override + public @Nullable CompoundTag frozenLib$getTag() { + return this.tag; + } + + @Inject(method = "serializeRecipeData", at = @At(value = "INVOKE", target = "Lcom/google/gson/JsonObject;add(Ljava/lang/String;Lcom/google/gson/JsonElement;)V", ordinal = 3), locals = LocalCapture.CAPTURE_FAILHARD) + private void addTagData(JsonObject json, CallbackInfo ci, JsonArray jsonArray, JsonObject jsonObject, JsonObject jsonObject2) { + if (this.tag != null) { + jsonObject2.add("tag", CompoundTag.CODEC.encodeStart(JsonOps.INSTANCE, this.tag).getOrThrow(false, str -> {})); + } + } + }*/ +} diff --git a/src/main/java/net/frozenblock/lib/recipe/mixin/ShapedRecipeMixin.java b/src/main/java/net/frozenblock/lib/recipe/mixin/ShapedRecipeMixin.java new file mode 100644 index 0000000..7bfd19a --- /dev/null +++ b/src/main/java/net/frozenblock/lib/recipe/mixin/ShapedRecipeMixin.java @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.recipe.mixin; + +import net.frozenblock.lib.recipe.api.ShapedRecipeBuilderExtension; +import net.minecraft.core.component.DataComponentPatch; +import net.minecraft.data.recipes.ShapedRecipeBuilder; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.ShapedRecipe; +import org.jetbrains.annotations.Nullable; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; + +@Mixin(ShapedRecipe.class) +public class ShapedRecipeMixin implements ShapedRecipeBuilderExtension { + + @Shadow + @Final + ItemStack result; + + @Override + public ShapedRecipeBuilder frozenLib$patch(@Nullable DataComponentPatch patch) { + if (patch != null) + this.result.applyComponents(patch); + return null; + } +} diff --git a/src/main/java/net/frozenblock/lib/registry/api/FrozenRegistry.java b/src/main/java/net/frozenblock/lib/registry/api/FrozenRegistry.java index edc8406..415b9a2 100644 --- a/src/main/java/net/frozenblock/lib/registry/api/FrozenRegistry.java +++ b/src/main/java/net/frozenblock/lib/registry/api/FrozenRegistry.java @@ -1,4 +1,102 @@ package net.frozenblock.lib.registry.api; +import com.mojang.serialization.Lifecycle; +import lombok.experimental.UtilityClass; +import net.frozenblock.lib.FrozenSharedConstants; +import net.frozenblock.lib.integration.api.ModIntegration; +import net.frozenblock.lib.integration.api.ModIntegrationSupplier; +import net.frozenblock.lib.sound.api.predicate.SoundPredicate; +import net.frozenblock.lib.spotting_icons.api.SpottingIconPredicate; +import net.frozenblock.lib.wind.api.WindDisturbanceLogic; +import net.minecraft.core.HolderLookup; +import net.minecraft.core.MappedRegistry; +import net.minecraft.core.Registry; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.data.registries.VanillaRegistries; +import net.minecraft.resources.ResourceKey; +import net.neoforged.bus.api.SubscribeEvent; +import net.neoforged.fml.common.EventBusSubscriber; +import net.neoforged.neoforge.registries.NewRegistryEvent; +import net.neoforged.neoforge.registries.RegistryBuilder; +import org.jetbrains.annotations.NotNull; + +@UtilityClass public class FrozenRegistry { + public static final ResourceKey>> MOD_INTEGRATION_REGISTRY = ResourceKey.createRegistryKey(FrozenSharedConstants.id("mod_integration")); + public static final ResourceKey>> SOUND_PREDICATE_REGISTRY = ResourceKey.createRegistryKey(FrozenSharedConstants.id("sound_predicate")); + public static final ResourceKey>> SOUND_PREDICATE_UNSYNCED_REGISTRY = ResourceKey.createRegistryKey(FrozenSharedConstants.id("sound_predicate_unsynced")); + public static final ResourceKey>> SPOTTING_ICON_PREDICATE_REGISTRY = ResourceKey.createRegistryKey(FrozenSharedConstants.id("spotting_icon_predicate")); + public static final ResourceKey>> WIND_DISTURBANCE_LOGIC_REGISTRY = ResourceKey.createRegistryKey(FrozenSharedConstants.id("wind_disturbance_logic")); + public static final ResourceKey>> WIND_DISTURBANCE_LOGIC_UNSYNCED_REGISTRY = ResourceKey.createRegistryKey(FrozenSharedConstants.id("wind_disturbance_logic_unsynced")); + + public static final MappedRegistry> MOD_INTEGRATION = createSimple(MOD_INTEGRATION_REGISTRY, null, + registry -> Registry.register(registry, FrozenSharedConstants.id("dummy"), new ModIntegrationSupplier<>(() -> new ModIntegration("dummy") { + @Override + public void init() {} + }, + "dummy" + )) + ); + + public static final MappedRegistry> SOUND_PREDICATE = createSimple(SOUND_PREDICATE_REGISTRY, true, + registry -> Registry.register(registry, FrozenSharedConstants.id("dummy"), new SoundPredicate<>(() -> entity -> false)) + ); + + public static final MappedRegistry> SOUND_PREDICATE_UNSYNCED = createSimple(SOUND_PREDICATE_UNSYNCED_REGISTRY, null, + registry -> Registry.register(registry, FrozenSharedConstants.id("dummy"), new SoundPredicate<>(() -> entity -> false)) + ); + + public static final MappedRegistry> SPOTTING_ICON_PREDICATE = createSimple(SPOTTING_ICON_PREDICATE_REGISTRY, true, + registry -> Registry.register(registry, FrozenSharedConstants.id("dummy"), new SpottingIconPredicate<>(entity -> false)) + ); + + public static final MappedRegistry> WIND_DISTURBANCE_LOGIC = createSimple(WIND_DISTURBANCE_LOGIC_REGISTRY, true, + registry -> Registry.register(registry, FrozenSharedConstants.id("dummy"), new WindDisturbanceLogic(WindDisturbanceLogic.defaultPredicate())) + ); + + public static final MappedRegistry> WIND_DISTURBANCE_LOGIC_UNSYNCED = createSimple(WIND_DISTURBANCE_LOGIC_UNSYNCED_REGISTRY, null, + registry -> Registry.register(registry, FrozenSharedConstants.id("dummy"), new WindDisturbanceLogic(WindDisturbanceLogic.defaultPredicate())) + ); + + @NotNull + public static HolderLookup.Provider vanillaRegistries() { + return VanillaRegistries.createLookup(); + } + + public static void initRegistry() { + } + + private static MappedRegistry createSimple(ResourceKey> key, Lifecycle lifecycle) { + return createSimple(key, lifecycle, null); + } + + private static MappedRegistry createSimple(ResourceKey> key, Lifecycle lifecycle, Boolean attribute) { + return createSimple(key, attribute, null); + } + + private static MappedRegistry createSimple(ResourceKey> key, Boolean attribute, BuiltInRegistries.RegistryBootstrap bootstrap) { + RegistryBuilder registryBuilder = new RegistryBuilder<>(key); + + if (attribute != null) { + registryBuilder.sync(attribute); + } + + final var registry = registryBuilder.create(); + + if (bootstrap != null) { + bootstrap.run(registry); + } + + return (MappedRegistry) registry; + } + + @SubscribeEvent + public static void registerRegistries(final NewRegistryEvent event) { + event.register(MOD_INTEGRATION); + event.register(SOUND_PREDICATE); + event.register(SOUND_PREDICATE_UNSYNCED); + event.register(SPOTTING_ICON_PREDICATE); + event.register(WIND_DISTURBANCE_LOGIC); + event.register(WIND_DISTURBANCE_LOGIC_UNSYNCED); + } } diff --git a/src/main/java/net/frozenblock/lib/registry/api/client/FrozenClientRegistry.java b/src/main/java/net/frozenblock/lib/registry/api/client/FrozenClientRegistry.java index fe89b41..1c7754a 100644 --- a/src/main/java/net/frozenblock/lib/registry/api/client/FrozenClientRegistry.java +++ b/src/main/java/net/frozenblock/lib/registry/api/client/FrozenClientRegistry.java @@ -2,15 +2,14 @@ import net.frozenblock.lib.FrozenSharedConstants; import net.frozenblock.lib.entity.api.rendering.EntityTextureOverride; -import net.minecraft.core.MappedRegistry; import net.minecraft.core.Registry; import net.minecraft.resources.ResourceKey; import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.entity.LivingEntity; import net.neoforged.api.distmarker.Dist; import net.neoforged.api.distmarker.OnlyIn; import net.neoforged.bus.api.SubscribeEvent; import net.neoforged.fml.common.EventBusSubscriber; -import net.neoforged.fml.common.Mod; import net.neoforged.neoforge.registries.NewRegistryEvent; import net.neoforged.neoforge.registries.RegistryBuilder; @@ -18,13 +17,13 @@ @EventBusSubscriber(modid = FrozenSharedConstants.MOD_ID, bus = EventBusSubscriber.Bus.MOD, value = Dist.CLIENT) public class FrozenClientRegistry { - public static final ResourceKey>> ENTITY_TEXTURE_OVERRIDE_KEY = ResourceKey.createRegistryKey(ResourceLocation.fromNamespaceAndPath("yourmodid", "spells")); - public static final Registry> ENTITY_TEXTURE_OVERRIDES = new RegistryBuilder<>(ENTITY_TEXTURE_OVERRIDE_KEY) + public static final ResourceKey> ENTITY_TEXTURE_OVERRIDE_KEY = ResourceKey.createRegistryKey(ResourceLocation.fromNamespaceAndPath("yourmodid", "spells")); + public static final Registry ENTITY_TEXTURE_OVERRIDE = new RegistryBuilder<>(ENTITY_TEXTURE_OVERRIDE_KEY) .sync(false) .create(); @SubscribeEvent - static void registerRegistries(NewRegistryEvent event) { - event.register(ENTITY_TEXTURE_OVERRIDES); + public static void registerRegistries(final NewRegistryEvent event) { + event.register(ENTITY_TEXTURE_OVERRIDE); } } diff --git a/src/main/java/net/frozenblock/lib/screenshake/api/ScreenShakeManager.java b/src/main/java/net/frozenblock/lib/screenshake/api/ScreenShakeManager.java new file mode 100644 index 0000000..2abcdbe --- /dev/null +++ b/src/main/java/net/frozenblock/lib/screenshake/api/ScreenShakeManager.java @@ -0,0 +1,216 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.screenshake.api; + +import com.mojang.serialization.Codec; +import com.mojang.serialization.DataResult; +import com.mojang.serialization.Dynamic; +import com.mojang.serialization.codecs.RecordCodecBuilder; +import net.frozenblock.lib.FrozenSharedConstants; +import net.frozenblock.lib.screenshake.impl.EntityScreenShakeInterface; +import net.frozenblock.lib.screenshake.impl.ScreenShakeManagerInterface; +import net.frozenblock.lib.screenshake.impl.ScreenShakeStorage; +import net.frozenblock.lib.screenshake.impl.network.EntityScreenShakePacket; +import net.frozenblock.lib.screenshake.impl.network.ScreenShakePacket; +import net.minecraft.core.BlockPos; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.NbtOps; +import net.minecraft.nbt.Tag; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.util.datafix.DataFixTypes; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.saveddata.SavedData; +import net.minecraft.world.phys.Vec3; +import net.neoforged.neoforge.network.PacketDistributor; +import org.jetbrains.annotations.NotNull; +import org.slf4j.Logger; + +import java.util.*; + +public class ScreenShakeManager { + private final ArrayList shakes = new ArrayList<>(); + private final ServerLevel level; + + public ScreenShakeManager(ServerLevel level) { + this.level = level; + } + + public void tick(@NotNull ServerLevel level) { + if (level.tickRateManager().runsNormally()) { + this.getShakes().removeIf(ScreenShake::shouldRemove); + for (ScreenShake shake : this.getShakes()) { + if (this.level.getChunkSource().hasChunk(shake.chunkPos.x, shake.chunkPos.z)) { + shake.ticks += 1; + Collection playersTrackingChunk = this.level.getChunkSource().chunkMap.getPlayers(shake.chunkPos, false); + for (ServerPlayer serverPlayer : playersTrackingChunk) { + if (!shake.trackingPlayers.contains(serverPlayer)) { + ScreenShakeManager.sendScreenShakePacketTo(serverPlayer, shake.getIntensity(), shake.getDuration(), shake.getDurationFalloffStart(), shake.getPos(), shake.getMaxDistance(), shake.getTicks()); + } + } + shake.trackingPlayers.clear(); + shake.trackingPlayers.addAll(playersTrackingChunk); + } + } + } + } + + public void addShake(float intensity, int duration, int falloffStart, Vec3 pos, float maxDistance, int ticks) { + this.getShakes().add(new ScreenShake(intensity, duration, falloffStart, pos, maxDistance, ticks)); + } + + public ArrayList getShakes() { + return this.shakes; + } + + public void load(@NotNull CompoundTag nbt) { + if (nbt.contains("ScreenShakes", 9)) { + this.getShakes().clear(); + DataResult> var10000 = ScreenShake.CODEC.listOf().parse(new Dynamic<>(NbtOps.INSTANCE, nbt.getList("ScreenShakes", 10))); + Logger logger = FrozenSharedConstants.LOGGER4; + Objects.requireNonNull(logger); + Optional> list = var10000.resultOrPartial(logger::error); + list.ifPresent(this.getShakes()::addAll); + } + } + + public void save(CompoundTag nbt) { + DataResult var10000 = ScreenShake.CODEC.listOf().encodeStart(NbtOps.INSTANCE, this.shakes); + Logger var10001 = FrozenSharedConstants.LOGGER4; + Objects.requireNonNull(var10001); + var10000.resultOrPartial(var10001::error).ifPresent((cursorsNbt) -> nbt.put("ScreenShakes", cursorsNbt)); + } + + public static class ScreenShake { + private final float intensity; + public final int duration; + private final int durationFalloffStart; + protected Vec3 pos; + public final float maxDistance; + public int ticks; + + public List trackingPlayers = new ArrayList<>(); + public final ChunkPos chunkPos; + + public static final Codec CODEC = RecordCodecBuilder.create((instance) -> instance.group( + Codec.FLOAT.fieldOf("Intensity").forGetter(ScreenShake::getIntensity), + Codec.INT.fieldOf("Duration").forGetter(ScreenShake::getDuration), + Codec.INT.fieldOf("FalloffStart").forGetter(ScreenShake::getDurationFalloffStart), + Vec3.CODEC.fieldOf("Position").forGetter(ScreenShake::getPos), + Codec.FLOAT.fieldOf("MaxDistance").forGetter(ScreenShake::getMaxDistance), + Codec.INT.fieldOf("Ticks").forGetter(ScreenShake::getTicks) + ).apply(instance, ScreenShake::new)); + + public ScreenShake(float intensity, int duration, int durationFalloffStart, Vec3 pos, float maxDistance, int ticks) { + this.intensity = intensity; + this.duration = duration; + this.durationFalloffStart = durationFalloffStart; + this.pos = pos; + this.maxDistance = maxDistance; + this.ticks = ticks; + this.chunkPos = new ChunkPos(BlockPos.containing(pos)); + } + + public boolean shouldRemove() { + return this.ticks > this.duration; + } + + public float getIntensity() { + return this.intensity; + } + + public int getDuration() { + return this.duration; + } + + public int getDurationFalloffStart() { + return this.durationFalloffStart; + } + + public Vec3 getPos() { + return this.pos; + } + + public float getMaxDistance() { + return this.maxDistance; + } + + public int getTicks() { + return this.ticks; + } + + } + + public static ScreenShakeManager getScreenShakeManager(ServerLevel level) { + return ((ScreenShakeManagerInterface)level).frozenLib$getScreenShakeManager(); + } + + public SavedData.Factory createData() { + return new SavedData.Factory<>(() -> new ScreenShakeStorage(this), (tag, provider) -> ScreenShakeStorage.load(tag, this), DataFixTypes.SAVED_DATA_RANDOM_SEQUENCES); + } + + public static void addScreenShake(Level level, float intensity, double x, double y, double z, float maxDistance) { + addScreenShake(level, intensity, 20, 5, x, y, z, maxDistance); + } + + public static void addScreenShake(Level level, float intensity, int duration, double x, double y, double z, float maxDistance) { + addScreenShake(level, intensity, duration, 1, x, y, z, maxDistance); + } + + public static void addScreenShake(Level level, float intensity, int duration, int falloffStart, double x, double y, double z, float maxDistance) { + addScreenShake(level, intensity, duration, falloffStart, x, y, z, maxDistance, 0); + } + + public static void addScreenShake(@NotNull Level level, float intensity, int duration, int falloffStart, double x, double y, double z, float maxDistance, int ticks) { + if (!level.isClientSide) { + ServerLevel serverLevel = (ServerLevel) level; + ScreenShakeManager.getScreenShakeManager(serverLevel).addShake(intensity, duration, falloffStart, new Vec3(x, y, z), maxDistance, ticks); + } + } + + public static void sendScreenShakePacketTo(ServerPlayer player, float intensity, int duration, int falloffStart, Vec3 pos, float maxDistance, int ticks) { + PacketDistributor.sendToPlayer(player, new ScreenShakePacket(intensity, duration, falloffStart, pos, maxDistance, ticks)); + } + + //With Entity + public static void addEntityScreenShake(Entity entity, float intensity, float maxDistance) { + addEntityScreenShake(entity, intensity, 5, 1, maxDistance); + } + + public static void addEntityScreenShake(Entity entity, float intensity, int duration, float maxDistance) { + addEntityScreenShake(entity, intensity, duration, 1, maxDistance); + } + + public static void addEntityScreenShake(Entity entity, float intensity, int duration, int falloffStart, float maxDistance) { + addEntityScreenShake(entity, intensity, duration, falloffStart, maxDistance, 0); + } + + public static void addEntityScreenShake(@NotNull Entity entity, float intensity, int duration, int falloffStart, float maxDistance, int ticks) { + if (!entity.level().isClientSide) { + EntityScreenShakePacket packet = new EntityScreenShakePacket(entity.getId(), intensity, duration, falloffStart, maxDistance, ticks); + PacketDistributor.sendToPlayersInDimension((ServerLevel)entity.level(), packet); + ((EntityScreenShakeInterface)entity).addScreenShake(intensity, duration, falloffStart, maxDistance, ticks); + } + } + + public static void sendEntityScreenShakeTo(ServerPlayer player, Entity entity, float intensity, int duration, int falloffStart, float maxDistance, int ticks) { + PacketDistributor.sendToPlayer(player, new EntityScreenShakePacket(entity.getId(), intensity, duration, falloffStart, maxDistance, ticks)); + } +} diff --git a/src/main/java/net/frozenblock/lib/screenshake/api/client/ScreenShaker.java b/src/main/java/net/frozenblock/lib/screenshake/api/client/ScreenShaker.java new file mode 100644 index 0000000..a35d6c0 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/screenshake/api/client/ScreenShaker.java @@ -0,0 +1,184 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.screenshake.api.client; + +import com.mojang.blaze3d.platform.Window; +import com.mojang.blaze3d.vertex.PoseStack; +import com.mojang.math.Axis; +import java.util.ArrayList; +import net.minecraft.client.Camera; +import net.minecraft.client.Minecraft; +import net.minecraft.client.multiplayer.ClientLevel; +import net.minecraft.util.Mth; +import net.minecraft.util.RandomSource; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.phys.Vec3; +import net.neoforged.api.distmarker.Dist; +import net.neoforged.api.distmarker.OnlyIn; +import org.jetbrains.annotations.NotNull; + +@OnlyIn(Dist.CLIENT) +public class ScreenShaker { + public static final ArrayList SCREEN_SHAKES = new ArrayList<>(); + + private static float prevYRot; + private static float yRot; + private static float prevXRot; + private static float xRot; + private static float prevZRot; + private static float zRot; + + public static void tick(@NotNull ClientLevel level) { + if (level.tickRateManager().runsNormally()) { + Minecraft client = Minecraft.getInstance(); + prevYRot = yRot; + prevXRot = xRot; + prevZRot = zRot; + if (!client.isMultiplayerServer() && client.isPaused()) { + yRot = 0F; + xRot = 0F; + zRot = 0F; + return; + } + Window window = client.getWindow(); + int windowWidth = window.getWidth(); + int windowHeight = window.getHeight(); + RandomSource randomSource = level.getRandom(); + + SCREEN_SHAKES.removeIf(clientScreenShake -> clientScreenShake.shouldRemove(level)); + float highestIntensity = 0; + float totalIntensity = 0; + int amount = 0; + for (ClientScreenShake screenShake : SCREEN_SHAKES) { + screenShake.tick(); + float shakeIntensity = screenShake.getIntensity(client.gameRenderer.getMainCamera().getPosition()); + if (shakeIntensity > 0) { + totalIntensity += shakeIntensity; + highestIntensity = Math.max(shakeIntensity, highestIntensity); + amount += 1; + } + screenShake.ticks += 1; + } + float intensity = (amount > 0 && totalIntensity != 0 && highestIntensity != 0) ? (highestIntensity + ((totalIntensity / amount) * 0.5F)) : 0F; + yRot = Mth.nextFloat(randomSource, -intensity, intensity) * ((float) windowWidth / (float) windowHeight); + xRot = Mth.nextFloat(randomSource, -intensity, intensity); + zRot = Mth.nextFloat(randomSource, -intensity, intensity); + } + } + + public static void shake(@NotNull PoseStack poseStack, float partialTicks) { + poseStack.mulPose(Axis.XP.rotationDegrees(prevXRot + partialTicks * xRot - prevXRot)); + poseStack.mulPose(Axis.YP.rotationDegrees(prevYRot + partialTicks * yRot - prevYRot)); + poseStack.mulPose(Axis.ZP.rotationDegrees(prevZRot + partialTicks * zRot - prevZRot)); + } + + @Deprecated + public static float cameraZ(float partialTicks) { + return Mth.lerp(partialTicks, prevZRot, zRot); + } + + @Deprecated + public static void cameraShake(@NotNull Camera camera, float partialTicks) { + camera.setRotation(camera.getYRot() + (Mth.lerp(partialTicks, prevYRot, yRot)), camera.getXRot() + (Mth.lerp(partialTicks, prevXRot, xRot))); + } + + public static void addShake(ClientLevel level, float intensity, int duration, int falloffStart, Vec3 pos, float maxDistance, int ticks) { + SCREEN_SHAKES.add(new ClientScreenShake(level, intensity, duration, falloffStart, pos, maxDistance, ticks)); + } + + public static void addShake(Entity entity, float intensity, int duration, int falloffStart, float maxDistance, int ticks) { + SCREEN_SHAKES.add(new ClientEntityScreenShake(entity, intensity, duration, falloffStart, maxDistance, ticks)); + } + + public static void clear() { + SCREEN_SHAKES.clear(); + } + + public static class ClientScreenShake { + public ClientLevel level; + private final float intensity; + public final int duration; + private final int durationFalloffStart; + protected Vec3 pos; + public final float maxDistance; + public int ticks; + + public ClientScreenShake(ClientLevel level, float intensity, int duration, int durationFalloffStart, Vec3 pos, float maxDistance, int ticks) { + this.level = level; + this.intensity = intensity; + this.duration = duration; + this.durationFalloffStart = durationFalloffStart; + this.pos = pos; + this.maxDistance = maxDistance; + this.ticks = ticks; + } + + public float getIntensity(@NotNull Vec3 playerPos) { + float distanceBasedIntensity = Math.max((float) (1F - (playerPos.distanceTo(this.pos) / this.maxDistance)), 0); + if (distanceBasedIntensity > 0) { + float timeFromFalloffStart = Math.max(this.ticks - this.durationFalloffStart, 0); //Starts counting up once it reaches falloff start + float falloffTime = this.duration - this.durationFalloffStart; //The amount of time the intensity falls off for before reaching 0 + float lerpedTimeFromFalloffStart = Mth.lerp((float)this.ticks / this.duration, 0, timeFromFalloffStart); + return (distanceBasedIntensity * ((falloffTime - lerpedTimeFromFalloffStart) / falloffTime)) * intensity; + } + return 0F; + } + + public void tick() { + + } + + public boolean shouldRemove(ClientLevel level) { + return this.ticks > this.duration || level != this.level; + } + } + + public static class ClientEntityScreenShake extends ClientScreenShake { + private final Entity entity; + + public ClientEntityScreenShake(@NotNull Entity entity, float intensity, int duration, int durationFalloffStart, float maxDistance, int ticks) { + super((ClientLevel) entity.level(), intensity, duration, durationFalloffStart, entity.position(), maxDistance, ticks); + this.entity = entity; + } + + @Override + public float getIntensity(@NotNull Vec3 playerPos) { + if (this.entity != null && !this.entity.isRemoved()) { + this.pos = this.entity.position(); + return super.getIntensity(playerPos); + } + return 0F; + } + + public Entity getEntity() { + return this.entity; + } + + @Override + public void tick() { + super.tick(); + this.level = (ClientLevel) this.entity.level(); + } + + @Override + public boolean shouldRemove(ClientLevel level) { + return super.shouldRemove(level) || entity == null || entity.isRemoved(); + } + } + +} diff --git a/src/main/java/net/frozenblock/lib/screenshake/api/command/ScreenShakeCommand.java b/src/main/java/net/frozenblock/lib/screenshake/api/command/ScreenShakeCommand.java new file mode 100644 index 0000000..8b2a4ed --- /dev/null +++ b/src/main/java/net/frozenblock/lib/screenshake/api/command/ScreenShakeCommand.java @@ -0,0 +1,142 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.screenshake.api.command; + +import com.mojang.brigadier.CommandDispatcher; +import com.mojang.brigadier.arguments.FloatArgumentType; +import com.mojang.brigadier.arguments.IntegerArgumentType; +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import net.frozenblock.lib.screenshake.api.ScreenShakeManager; +import net.frozenblock.lib.screenshake.impl.EntityScreenShakeInterface; +import net.frozenblock.lib.screenshake.impl.network.RemoveEntityScreenShakePacket; +import net.frozenblock.lib.screenshake.impl.network.RemoveScreenShakePacket; +import net.frozenblock.lib.screenshake.impl.network.ScreenShakePacket; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.commands.Commands; +import net.minecraft.commands.arguments.EntityArgument; +import net.minecraft.commands.arguments.coordinates.Vec3Argument; +import net.minecraft.network.chat.Component; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.phys.Vec3; +import net.neoforged.neoforge.network.PacketDistributor; + +public class ScreenShakeCommand { + + public static void register(CommandDispatcher dispatcher) { + LiteralArgumentBuilder literalArgumentBuilder = Commands.literal("screenshake").requires(source -> source.hasPermission(2)); + + literalArgumentBuilder.then(Commands.argument("pos", Vec3Argument.vec3()).executes(context -> shake(context.getSource(), Vec3Argument.getVec3(context, "pos"), 1F, 10, 5, 16F)) + .then(Commands.argument("intensity", FloatArgumentType.floatArg()).executes(context -> shake(context.getSource(), Vec3Argument.getVec3(context, "pos"), FloatArgumentType.getFloat(context, "intensity"), 10, 5, 16F)) + .then(Commands.argument("duration", IntegerArgumentType.integer()).executes(context -> shake(context.getSource(), Vec3Argument.getVec3(context, "pos"), FloatArgumentType.getFloat(context, "intensity"), IntegerArgumentType.getInteger(context, "duration"), 5, 16F)) + .then(Commands.argument("durationFalloffStart", IntegerArgumentType.integer()).executes(context -> shake(context.getSource(), Vec3Argument.getVec3(context, "pos"), FloatArgumentType.getFloat(context, "intensity"), IntegerArgumentType.getInteger(context, "duration"), IntegerArgumentType.getInteger(context, "durationFalloffStart"), 16F)) + .then(Commands.argument("maxDistance", FloatArgumentType.floatArg()).executes(context -> shake(context.getSource(), Vec3Argument.getVec3(context, "pos"), FloatArgumentType.getFloat(context, "intensity"), IntegerArgumentType.getInteger(context, "duration"), IntegerArgumentType.getInteger(context, "durationFalloffStart"), FloatArgumentType.getFloat(context, "maxDistance"))) + .then(Commands.argument("players", EntityArgument.players()).executes(context -> shake(context.getSource(), Vec3Argument.getVec3(context, "pos"), FloatArgumentType.getFloat(context, "intensity"), IntegerArgumentType.getInteger(context, "duration"), IntegerArgumentType.getInteger(context, "durationFalloffStart"), FloatArgumentType.getFloat(context, "maxDistance"), EntityArgument.getPlayers(context, "players"))))))))); + + literalArgumentBuilder.then(Commands.argument("entity", EntityArgument.entities()).executes(context -> shake(context.getSource(), EntityArgument.getEntities(context, "entity"), 1F, 10, 5, 16F)) + .then(Commands.argument("intensity", FloatArgumentType.floatArg()).executes(context -> shake(context.getSource(), EntityArgument.getEntities(context, "entity"), FloatArgumentType.getFloat(context, "intensity"), 10, 5, 16F)) + .then(Commands.argument("duration", IntegerArgumentType.integer()).executes(context -> shake(context.getSource(), EntityArgument.getEntities(context, "entity"), FloatArgumentType.getFloat(context, "intensity"), IntegerArgumentType.getInteger(context, "duration"), 5, 16F)) + .then(Commands.argument("durationFalloffStart", IntegerArgumentType.integer()).executes(context -> shake(context.getSource(), EntityArgument.getEntities(context, "entity"), FloatArgumentType.getFloat(context, "intensity"), IntegerArgumentType.getInteger(context, "duration"), IntegerArgumentType.getInteger(context, "durationFalloffStart"), 16F)) + .then(Commands.argument("maxDistance", FloatArgumentType.floatArg()).executes(context -> shake(context.getSource(), EntityArgument.getEntities(context, "entity"), FloatArgumentType.getFloat(context, "intensity"), IntegerArgumentType.getInteger(context, "duration"), IntegerArgumentType.getInteger(context, "durationFalloffStart"), FloatArgumentType.getFloat(context, "maxDistance")))))))); + + literalArgumentBuilder.then(Commands.literal("remove") + .then(Commands.literal("for").then(Commands.argument("players", EntityArgument.players()).executes(context -> removeShakesFor(context.getSource(), EntityArgument.getPlayers(context, "players"))))) + .then(Commands.literal("from").then(Commands.argument("entities", EntityArgument.entities()).executes(context -> removeShakesFrom(context.getSource(), EntityArgument.getEntities(context, "entity"))))) + ); + + dispatcher.register(literalArgumentBuilder); + } + + private static int shake(CommandSourceStack source, Vec3 vec3, float intensity, int duration, int durationFalloffStart, float maxDistance) { + vec3 = new Vec3(Math.round(vec3.x()), Math.round(vec3.y()), Math.round(vec3.z())); + ScreenShakeManager.addScreenShake(source.getLevel(), intensity, duration, durationFalloffStart, vec3.x(), vec3.y(), vec3.z(), maxDistance); + Vec3 finalVec = vec3; + source.sendSuccess(() -> Component.translatable("commands.screenshake.success", finalVec.x(), finalVec.y(), finalVec.z(), intensity, duration, durationFalloffStart, maxDistance), true); + return 1; + } + + private static int shake(CommandSourceStack source, Vec3 vec3, float intensity, int duration, int durationFalloffStart, float maxDistance, Collection entities) { + vec3 = new Vec3(Math.round(vec3.x()), Math.round(vec3.y()), Math.round(vec3.z())); + ScreenShakePacket packet = new ScreenShakePacket(intensity, duration, durationFalloffStart, vec3, maxDistance, 0); + StringBuilder playerString = new StringBuilder(); + for (ServerPlayer serverPlayer : entities) { + PacketDistributor.sendToPlayer(serverPlayer, packet); + playerString.append(serverPlayer.getDisplayName().getString()).append(", "); + } + Vec3 finalVec = vec3; + source.sendSuccess(() -> Component.translatable("commands.screenshake.player.success", playerString.toString(), finalVec.x(), finalVec.y(), finalVec.z(), intensity, duration, durationFalloffStart, maxDistance), true); + + return 1; + } + + private static int shake(CommandSourceStack source, Collection entities, float intensity, int duration, int durationFalloffStart, float maxDistance) { + StringBuilder entityString = new StringBuilder(); + for (Entity entity : entities) { + ScreenShakeManager.addEntityScreenShake(entity, intensity, duration, durationFalloffStart, maxDistance); + entityString.append(entity.getDisplayName().getString()).append(", "); + } + source.sendSuccess(() -> Component.translatable("commands.screenshake.entity.success", entityString.toString(), intensity, duration, durationFalloffStart, maxDistance), true); + return 1; + } + + private static int removeShakesFor(CommandSourceStack source, Collection entities) { + StringBuilder playerString = new StringBuilder(); + boolean onePlayer = entities.size() == 1; + CustomPacketPayload packet = new RemoveScreenShakePacket(); + for (ServerPlayer serverPlayer : entities) { + PacketDistributor.sendToPlayer(serverPlayer, packet); + playerString.append(serverPlayer.getDisplayName().getString()).append(onePlayer ? "" : ", "); + } + source.sendSuccess(() -> Component.translatable(onePlayer ? "commands.screenshake.remove.player.success" : "commands.screenshake.remove.player.success.multiple", playerString.toString()), true); + return 1; + } + + private static int removeShakesFrom(CommandSourceStack source, Collection entities) { + StringBuilder entityString = new StringBuilder(); + int entityAmount = 0; + List affectedEntities = new ArrayList<>(); + for (Entity entity : entities) { + if (!((EntityScreenShakeInterface)entity).getScreenShakeManager().getShakes().isEmpty()) { + CustomPacketPayload packet = new RemoveEntityScreenShakePacket(entity.getId()); + affectedEntities.add(entity); + ((EntityScreenShakeInterface)entity).getScreenShakeManager().getShakes().clear(); + PacketDistributor.sendToPlayersTrackingChunk(source.getLevel(), new ChunkPos(entity.blockPosition()), packet); + entityAmount += 1; + } + } + + boolean oneEntity = affectedEntities.size() == 1; + for (Entity entity : affectedEntities) { + entityString.append(entity.getDisplayName().getString()).append(oneEntity ? "" : ", "); + } + + if (entityAmount > 0) { + source.sendSuccess(() -> Component.translatable(oneEntity ? "commands.screenshake.remove.entity.success" : "commands.screenshake.remove.entity.success.multiple", entityString.toString()), true); + return 1; + } else { + source.sendFailure(Component.translatable("commands.screenshake.remove.entity.failure")); + return 0; + } + } + +} diff --git a/src/main/java/net/frozenblock/lib/screenshake/impl/EntityScreenShakeInterface.java b/src/main/java/net/frozenblock/lib/screenshake/impl/EntityScreenShakeInterface.java new file mode 100644 index 0000000..36e9e4a --- /dev/null +++ b/src/main/java/net/frozenblock/lib/screenshake/impl/EntityScreenShakeInterface.java @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.screenshake.impl; + +import org.jetbrains.annotations.ApiStatus; + +@ApiStatus.Internal +public interface EntityScreenShakeInterface { + + EntityScreenShakeManager getScreenShakeManager(); + + void addScreenShake(float intensity, int duration, int durationFalloffStart, float maxDistance, int ticks); + +} diff --git a/src/main/java/net/frozenblock/lib/screenshake/impl/EntityScreenShakeManager.java b/src/main/java/net/frozenblock/lib/screenshake/impl/EntityScreenShakeManager.java new file mode 100644 index 0000000..df4d803 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/screenshake/impl/EntityScreenShakeManager.java @@ -0,0 +1,132 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.screenshake.impl; + +import com.mojang.serialization.Codec; +import com.mojang.serialization.DataResult; +import com.mojang.serialization.Dynamic; +import com.mojang.serialization.codecs.RecordCodecBuilder; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import net.frozenblock.lib.FrozenSharedConstants; +import net.frozenblock.lib.screenshake.api.ScreenShakeManager; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.NbtOps; +import net.minecraft.nbt.Tag; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.entity.Entity; +import org.slf4j.Logger; + +public class EntityScreenShakeManager { + private final ArrayList shakes = new ArrayList<>(); + public Entity entity; + + public EntityScreenShakeManager(Entity entity) { + this.entity = entity; + } + + public void load(CompoundTag nbt) { + if (nbt.contains("ScreenShakes", 9)) { + this.shakes.clear(); + DataResult> var10000 = EntityScreenShake.CODEC.listOf().parse(new Dynamic<>(NbtOps.INSTANCE, nbt.getList("ScreenShakes", 10))); + Logger var10001 = FrozenSharedConstants.LOGGER4; + Objects.requireNonNull(var10001); + Optional> list = var10000.resultOrPartial(var10001::error); + list.ifPresent(this.shakes::addAll); + } + } + + public void save(CompoundTag nbt) { + DataResult var10000 = EntityScreenShake.CODEC.listOf().encodeStart(NbtOps.INSTANCE, this.shakes); + Logger var10001 = FrozenSharedConstants.LOGGER4; + Objects.requireNonNull(var10001); + var10000.resultOrPartial(var10001::error).ifPresent((cursorsNbt) -> nbt.put("ScreenShakes", cursorsNbt)); + } + + public void addShake(float intensity, int duration, int durationFalloffStart, float maxDistance, int ticks) { + this.shakes.add(new EntityScreenShake(intensity, duration, durationFalloffStart, maxDistance, ticks)); + } + + public void tick() { + this.shakes.removeIf(EntityScreenShake::shouldRemove); + for (EntityScreenShake entityScreenShake : this.shakes) { + entityScreenShake.ticks += 1; + } + } + + public void syncWithPlayer(ServerPlayer serverPlayer) { + for (EntityScreenShake nbt : this.getShakes()) { + ScreenShakeManager.sendEntityScreenShakeTo(serverPlayer, this.entity, nbt.intensity, nbt.duration, nbt.durationFalloffStart, nbt.maxDistance, nbt.ticks); + } + } + + public ArrayList getShakes() { + return this.shakes; + } + + public static class EntityScreenShake { + public final float intensity; + public final int duration; + public final int durationFalloffStart; + public final float maxDistance; + public int ticks; + + public static final Codec CODEC = RecordCodecBuilder.create((instance) -> instance.group( + Codec.FLOAT.fieldOf("Intensity").forGetter(EntityScreenShake::getIntensity), + Codec.INT.fieldOf("Duration").forGetter(EntityScreenShake::getDuration), + Codec.INT.fieldOf("FalloffStart").forGetter(EntityScreenShake::getDurationFalloffStart), + Codec.FLOAT.fieldOf("MaxDistance").forGetter(EntityScreenShake::getMaxDistance), + Codec.INT.fieldOf("Ticks").forGetter(EntityScreenShake::getTicks) + ).apply(instance, EntityScreenShake::new)); + + public EntityScreenShake(float intensity, int duration, int durationFalloffStart, float maxDistance, int ticks) { + this.intensity = intensity; + this.duration = duration; + this.durationFalloffStart = durationFalloffStart; + this.maxDistance = maxDistance; + this.ticks = ticks; + } + + public boolean shouldRemove() { + return this.ticks > this.duration; + } + + public float getIntensity() { + return this.intensity; + } + + public int getDuration() { + return this.duration; + } + + public int getDurationFalloffStart() { + return this.durationFalloffStart; + } + + public float getMaxDistance() { + return this.maxDistance; + } + + public int getTicks() { + return this.ticks; + } + + } +} diff --git a/src/main/java/net/frozenblock/lib/screenshake/impl/ScreenShakeManagerInterface.java b/src/main/java/net/frozenblock/lib/screenshake/impl/ScreenShakeManagerInterface.java new file mode 100644 index 0000000..a07997a --- /dev/null +++ b/src/main/java/net/frozenblock/lib/screenshake/impl/ScreenShakeManagerInterface.java @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.screenshake.impl; + +import net.frozenblock.lib.screenshake.api.ScreenShakeManager; + +public interface ScreenShakeManagerInterface { + + public ScreenShakeManager frozenLib$getScreenShakeManager(); + +} diff --git a/src/main/java/net/frozenblock/lib/screenshake/impl/ScreenShakeStorage.java b/src/main/java/net/frozenblock/lib/screenshake/impl/ScreenShakeStorage.java new file mode 100644 index 0000000..3223119 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/screenshake/impl/ScreenShakeStorage.java @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.screenshake.impl; + +import net.frozenblock.lib.screenshake.api.ScreenShakeManager; +import net.minecraft.core.HolderLookup; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.world.level.saveddata.SavedData; +import org.jetbrains.annotations.NotNull; + +public class ScreenShakeStorage extends SavedData { + public static final String SCREEN_SHAKE_FILE_ID = "frozenlib_screen_shakes"; + private final ScreenShakeManager screenShakeManager; + + public ScreenShakeStorage(ScreenShakeManager screenShakeManager) { + this.screenShakeManager = screenShakeManager; + this.setDirty(); + } + + @NotNull + @Override + public CompoundTag save(CompoundTag compoundTag, HolderLookup.Provider provider) { + this.screenShakeManager.save(compoundTag); + return compoundTag; + } + + @NotNull + public static ScreenShakeStorage load(CompoundTag compoundTag, ScreenShakeManager manager) { + ScreenShakeStorage storage = new ScreenShakeStorage(manager); + + storage.screenShakeManager.load(compoundTag); + return storage; + } +} diff --git a/src/main/java/net/frozenblock/lib/screenshake/impl/network/EntityScreenShakePacket.java b/src/main/java/net/frozenblock/lib/screenshake/impl/network/EntityScreenShakePacket.java new file mode 100644 index 0000000..0107403 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/screenshake/impl/network/EntityScreenShakePacket.java @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.screenshake.impl.network; + +import net.frozenblock.lib.FrozenSharedConstants; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import org.jetbrains.annotations.NotNull; + +public record EntityScreenShakePacket( + int entityId, + float intensity, + int duration, + int falloffStart, + float maxDistance, + int ticks +) implements CustomPacketPayload { + + public static final Type PACKET_TYPE = new Type<>( + FrozenSharedConstants.id("screen_shake_entity_packet") + ); + public static final StreamCodec CODEC = StreamCodec.ofMember(EntityScreenShakePacket::write, EntityScreenShakePacket::new); + + public EntityScreenShakePacket(FriendlyByteBuf buf) { + this(buf.readVarInt(), buf.readFloat(), buf.readInt(), buf.readInt(), buf.readFloat(), buf.readInt()); + } + + public void write(FriendlyByteBuf buf) { + buf.writeVarInt(this.entityId()); + buf.writeFloat(this.intensity()); + buf.writeInt(this.duration()); + buf.writeInt(this.falloffStart()); + buf.writeFloat(this.maxDistance()); + buf.writeInt(this.ticks()); + } + + @Override + @NotNull + public Type type() { + return PACKET_TYPE; + } +} diff --git a/src/main/java/net/frozenblock/lib/screenshake/impl/network/RemoveEntityScreenShakePacket.java b/src/main/java/net/frozenblock/lib/screenshake/impl/network/RemoveEntityScreenShakePacket.java new file mode 100644 index 0000000..c159cce --- /dev/null +++ b/src/main/java/net/frozenblock/lib/screenshake/impl/network/RemoveEntityScreenShakePacket.java @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.screenshake.impl.network; + +import net.frozenblock.lib.FrozenSharedConstants; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.codec.ByteBufCodecs; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import org.jetbrains.annotations.NotNull; + +public record RemoveEntityScreenShakePacket( + int entityId +) implements CustomPacketPayload { + + public static final Type PACKET_TYPE = new Type<>( + FrozenSharedConstants.id("remove_entity_screen_shakes_packet") + ); + public static final StreamCodec CODEC = ByteBufCodecs.VAR_INT.map(RemoveEntityScreenShakePacket::new, RemoveEntityScreenShakePacket::entityId).cast(); + + @Override + @NotNull + public Type type() { + return PACKET_TYPE; + } +} diff --git a/src/main/java/net/frozenblock/lib/screenshake/impl/network/RemoveScreenShakePacket.java b/src/main/java/net/frozenblock/lib/screenshake/impl/network/RemoveScreenShakePacket.java new file mode 100644 index 0000000..52bc6a3 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/screenshake/impl/network/RemoveScreenShakePacket.java @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.screenshake.impl.network; + +import net.frozenblock.lib.FrozenSharedConstants; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import org.jetbrains.annotations.NotNull; + +public record RemoveScreenShakePacket() implements CustomPacketPayload { + + public static final Type PACKET_TYPE = new Type<>( + FrozenSharedConstants.id("remove_screen_shakes_packet") + ); + public static final StreamCodec CODEC = StreamCodec.ofMember(RemoveScreenShakePacket::write, RemoveScreenShakePacket::new); + + public RemoveScreenShakePacket(FriendlyByteBuf buf) { + this(); + } + + public void write(FriendlyByteBuf buf) { + } + + @Override + @NotNull + public Type type() { + return PACKET_TYPE; + } +} diff --git a/src/main/java/net/frozenblock/lib/screenshake/impl/network/ScreenShakePacket.java b/src/main/java/net/frozenblock/lib/screenshake/impl/network/ScreenShakePacket.java new file mode 100644 index 0000000..f6e0c67 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/screenshake/impl/network/ScreenShakePacket.java @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.screenshake.impl.network; + +import net.frozenblock.lib.FrozenSharedConstants; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.world.phys.Vec3; +import org.jetbrains.annotations.NotNull; + +public record ScreenShakePacket( + float intensity, + int duration, + int falloffStart, + Vec3 pos, + float maxDistance, + int ticks +) implements CustomPacketPayload { + + public static final Type PACKET_TYPE = new Type<>( + FrozenSharedConstants.id("screen_shake_packet") + ); + public static final StreamCodec CODEC = StreamCodec.ofMember(ScreenShakePacket::write, ScreenShakePacket::new); + + public ScreenShakePacket(FriendlyByteBuf buf) { + this( + buf.readFloat(), + buf.readInt(), + buf.readInt(), + buf.readVec3(), + buf.readFloat(), + buf.readInt() + ); + } + + public void write(FriendlyByteBuf buf) { + buf.writeFloat(this.intensity()); + buf.writeInt(this.duration()); + buf.writeInt(this.falloffStart()); + buf.writeVec3(this.pos()); + buf.writeFloat(this.maxDistance()); + buf.writeInt(this.ticks()); + } + + @Override + @NotNull + public Type type() { + return PACKET_TYPE; + } +} diff --git a/src/main/java/net/frozenblock/lib/screenshake/mixin/EntityMixin.java b/src/main/java/net/frozenblock/lib/screenshake/mixin/EntityMixin.java new file mode 100644 index 0000000..9cdd605 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/screenshake/mixin/EntityMixin.java @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.screenshake.mixin; + +import net.frozenblock.lib.screenshake.impl.EntityScreenShakeInterface; +import net.frozenblock.lib.screenshake.impl.EntityScreenShakeManager; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.level.Level; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +@Mixin(Entity.class) +public class EntityMixin implements EntityScreenShakeInterface { + + @Unique + public EntityScreenShakeManager frozenLib$entityScreenShakeManager; + + @Inject(method = "", at = @At("TAIL")) + private void frozenLib$setScreenShakeManager(EntityType entityType, Level level, CallbackInfo info) { + Entity entity = Entity.class.cast(this); + this.frozenLib$entityScreenShakeManager = new EntityScreenShakeManager(entity); + } + + @Inject(method = "saveWithoutId", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/entity/Entity;addAdditionalSaveData(Lnet/minecraft/nbt/CompoundTag;)V", shift = At.Shift.AFTER)) + public void frozenLib$saveScreenShakeData(CompoundTag compoundTag, CallbackInfoReturnable info) { + if (this.frozenLib$entityScreenShakeManager != null) { + this.frozenLib$entityScreenShakeManager.save(compoundTag); + } + } + + @Inject(method = "load", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/entity/Entity;readAdditionalSaveData(Lnet/minecraft/nbt/CompoundTag;)V", shift = At.Shift.AFTER)) + public void frozenLib$loadScreenShakeData(CompoundTag compoundTag, CallbackInfo info) { + this.frozenLib$entityScreenShakeManager.load(compoundTag); + } + + @Inject(method = "tick", at = @At("TAIL")) + public void frozenLib$tickScreenShake(CallbackInfo info) { + Entity entity = Entity.class.cast(this); + if (!entity.level().isClientSide) { + this.frozenLib$entityScreenShakeManager.tick(); + } + } + + @Unique + @Override + public EntityScreenShakeManager getScreenShakeManager() { + return this.frozenLib$entityScreenShakeManager; + } + + @Unique + @Override + public void addScreenShake(float intensity, int duration, int durationFalloffStart, float maxDistance, int ticks) { + this.getScreenShakeManager().addShake(intensity, duration, durationFalloffStart, maxDistance, ticks); + } +} diff --git a/src/main/java/net/frozenblock/lib/screenshake/mixin/ServerLevelMixin.java b/src/main/java/net/frozenblock/lib/screenshake/mixin/ServerLevelMixin.java new file mode 100644 index 0000000..46bcc1d --- /dev/null +++ b/src/main/java/net/frozenblock/lib/screenshake/mixin/ServerLevelMixin.java @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.screenshake.mixin; + +import net.frozenblock.lib.screenshake.api.ScreenShakeManager; +import net.frozenblock.lib.screenshake.impl.ScreenShakeManagerInterface; +import net.minecraft.server.level.ServerLevel; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Unique; + +@Mixin(ServerLevel.class) +public class ServerLevelMixin implements ScreenShakeManagerInterface { + + @Unique + private final ScreenShakeManager frozenLib$screenShakeManager = new ScreenShakeManager(ServerLevel.class.cast(this)); + + @Unique + @Override + public ScreenShakeManager frozenLib$getScreenShakeManager() { + return this.frozenLib$screenShakeManager; + } + +} diff --git a/src/main/java/net/frozenblock/lib/screenshake/mixin/ServerPlayerMixin.java b/src/main/java/net/frozenblock/lib/screenshake/mixin/ServerPlayerMixin.java new file mode 100644 index 0000000..3b9b38e --- /dev/null +++ b/src/main/java/net/frozenblock/lib/screenshake/mixin/ServerPlayerMixin.java @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.screenshake.mixin; + +import net.frozenblock.lib.screenshake.impl.EntityScreenShakeInterface; +import net.frozenblock.lib.screenshake.impl.EntityScreenShakeManager; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.server.network.ServerGamePacketListenerImpl; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.level.portal.DimensionTransition; +import org.jetbrains.annotations.Nullable; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +@Mixin(ServerPlayer.class) +public class ServerPlayerMixin { + + @Shadow + public ServerGamePacketListenerImpl connection; + @Shadow + private boolean isChangingDimension; + + @Unique @Nullable + private CompoundTag frozenLib$savedScreenShakesTag; + @Unique + private boolean frozenLib$hasSyncedScreenShakes = false; + + @Inject(method = "tick", at = @At(value = "TAIL")) + public void frozenLib$syncScreenShakes(CallbackInfo info) { + EntityScreenShakeManager entityScreenShakeManager = ((EntityScreenShakeInterface)ServerPlayer.class.cast(this)).getScreenShakeManager(); + if (!this.frozenLib$hasSyncedScreenShakes && this.connection != null && this.connection.isAcceptingMessages() && !this.isChangingDimension) { + entityScreenShakeManager.syncWithPlayer(ServerPlayer.class.cast(this)); + this.frozenLib$hasSyncedScreenShakes = true; + } + } + + @Inject(method = "changeDimension", at = @At(value = "HEAD")) + public void frozenLib$changeDimensionSaveScreenShakes(DimensionTransition dimensionTransition, CallbackInfoReturnable cir) { + CompoundTag tempTag = new CompoundTag(); + EntityScreenShakeManager entityScreenShakeManager = ((EntityScreenShakeInterface)ServerPlayer.class.cast(this)).getScreenShakeManager(); + entityScreenShakeManager.save(tempTag); + this.frozenLib$savedScreenShakesTag = tempTag; + } + + @Inject(method = "changeDimension", at = @At(value = "RETURN")) + public void frozenLib$changeDimensionLoadScreenShakes(DimensionTransition dimensionTransition, CallbackInfoReturnable cir) { + if (this.frozenLib$savedScreenShakesTag != null) { + EntityScreenShakeManager entityScreenShakeManager = ((EntityScreenShakeInterface)ServerPlayer.class.cast(this)).getScreenShakeManager(); + entityScreenShakeManager.load(this.frozenLib$savedScreenShakesTag); + this.frozenLib$hasSyncedScreenShakes = false; + } + } + +} diff --git a/src/main/java/net/frozenblock/lib/screenshake/mixin/client/GameRendererMixin.java b/src/main/java/net/frozenblock/lib/screenshake/mixin/client/GameRendererMixin.java new file mode 100644 index 0000000..feb00f1 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/screenshake/mixin/client/GameRendererMixin.java @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.screenshake.mixin.client; + +import com.llamalad7.mixinextras.injector.ModifyExpressionValue; +import com.llamalad7.mixinextras.sugar.Local; +import com.mojang.blaze3d.vertex.PoseStack; +import net.frozenblock.lib.screenshake.api.client.ScreenShaker; +import net.minecraft.client.DeltaTracker; +import net.minecraft.client.renderer.GameRenderer; +import net.neoforged.api.distmarker.Dist; +import net.neoforged.api.distmarker.OnlyIn; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; + +@OnlyIn(Dist.CLIENT) +@Mixin(GameRenderer.class) +public class GameRendererMixin { + + @ModifyExpressionValue(method = "renderLevel", at = @At(value = "NEW", target = "()Lcom/mojang/blaze3d/vertex/PoseStack")) + public PoseStack frozenLib$shakeLevel(PoseStack matrixStack, DeltaTracker tracker, @Local(ordinal = 0) float deltaTime) { + ScreenShaker.shake(matrixStack, deltaTime); + return matrixStack; + } + +} diff --git a/src/main/java/net/frozenblock/lib/sculk/api/BooleanPropertySculkBehavior.java b/src/main/java/net/frozenblock/lib/sculk/api/BooleanPropertySculkBehavior.java new file mode 100644 index 0000000..94cdf9f --- /dev/null +++ b/src/main/java/net/frozenblock/lib/sculk/api/BooleanPropertySculkBehavior.java @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.sculk.api; + +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.util.Mth; +import net.minecraft.util.RandomSource; +import net.minecraft.world.level.LevelAccessor; +import net.minecraft.world.level.block.SculkBehaviour; +import net.minecraft.world.level.block.SculkSpreader; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.block.state.properties.BooleanProperty; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Collection; + +public record BooleanPropertySculkBehavior(BooleanProperty changingProperty, boolean propertySetValue) implements SculkBehaviour { + + @Override + public int attemptUseCharge(SculkSpreader.ChargeCursor cursor, LevelAccessor level, @NotNull BlockPos catalystPos, @NotNull RandomSource random, @NotNull SculkSpreader spreadManager, boolean shouldConvertToBlock) { + BlockState placementState = null; + BlockPos cursorPos = cursor.getPos(); + BlockState currentState = level.getBlockState(cursorPos); + if (currentState.hasProperty(this.changingProperty)) { + if (currentState.getValue(this.changingProperty) != this.propertySetValue) { + placementState = currentState.setValue(this.changingProperty, this.propertySetValue); + } + } + + if (placementState != null) { + level.setBlock(cursorPos, placementState, 3); + return cursor.getCharge() - 1; + } + return random.nextInt(spreadManager.chargeDecayRate()) == 0 ? Mth.floor((float) cursor.getCharge() * 0.5F) : cursor.getCharge(); + } + + @Override + public boolean attemptSpreadVein(LevelAccessor level, @NotNull BlockPos pos, @NotNull BlockState state, @Nullable Collection directions, boolean markForPostProcessing) { + BlockState placementState = null; + BlockState currentState = level.getBlockState(pos); + if (currentState.hasProperty(this.changingProperty)) { + if (currentState.getValue(this.changingProperty) != this.propertySetValue) { + placementState = currentState.setValue(this.changingProperty, this.propertySetValue); + } + } + + if (placementState != null) { + level.setBlock(pos, placementState, 3); + return true; + } + return false; + } +} diff --git a/src/main/java/net/frozenblock/lib/sound/api/FlyBySoundHub.java b/src/main/java/net/frozenblock/lib/sound/api/FlyBySoundHub.java new file mode 100644 index 0000000..0e040be --- /dev/null +++ b/src/main/java/net/frozenblock/lib/sound/api/FlyBySoundHub.java @@ -0,0 +1,154 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.sound.api; + +import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; +import net.minecraft.client.Minecraft; +import net.minecraft.client.resources.sounds.EntityBoundSoundInstance; +import net.minecraft.sounds.SoundEvent; +import net.minecraft.sounds.SoundSource; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.phys.AABB; +import net.minecraft.world.phys.Vec3; +import net.neoforged.api.distmarker.Dist; +import net.neoforged.api.distmarker.OnlyIn; +import org.jetbrains.annotations.NotNull; + +import java.util.Map; + +@OnlyIn(Dist.CLIENT) +public final class FlyBySoundHub { + + private static final int PLAY_COOLDOWN = 40; + private static final int MIN_DISTANCE_FOR_REMOVAL = 16; + private static final double AUTO_ENTITY_DISTANCE = 3; + private static final int AUTO_ENTITY_COOLDOWN = 1; + private static final int PREDICTION_TICKS = 3; + private static final double OVERALL_SENSITIVITY = 1.75; + private static final double HORIZONTAL_SENSITIVITY = 1; + private static final double VERTICAL_SENSITIVITY = 0.3; + private static final double BASE_ENTITY_BOUNDING_BOX_EXPANSION = 0.7; + private static final double BOUNDING_BOX_EXPANSION_PER_VELOCITY = 5; + + /** + * Plays sounds automatically when a certain entity is near. + */ + public static final Map, FlyBySound> AUTO_ENTITIES_AND_SOUNDS = new Object2ObjectOpenHashMap<>(); + + /** + * The currently playing flyby sounds. + */ + public static final Map FLYBY_ENTITIES_AND_SOUNDS = new Object2ObjectOpenHashMap<>(); + + /** + * Cooldowns for playing a sound from the same entity. + */ + public static final Map ENTITY_COOLDOWNS = new Object2ObjectOpenHashMap<>(); + //private static int checkAroundCooldown; + + public static void update(@NotNull Minecraft client, Entity cameraEntity, boolean autoSounds) { + if (client.level != null && cameraEntity != null && client.level.tickRateManager().runsNormally()) { + Vec3 cameraPos = cameraEntity.getEyePosition(); + double cameraEntityWidth = cameraEntity.getBbWidth(); + double detectionWidth = cameraEntityWidth * 2; + AABB playerHeadBox = new AABB(cameraEntity.getEyePosition().add(-detectionWidth, -detectionWidth, -detectionWidth), cameraEntity.getEyePosition().add(detectionWidth, detectionWidth, detectionWidth)); + + for (Entity entity : FLYBY_ENTITIES_AND_SOUNDS.keySet()) { + if (entity != null) { + Vec3 entityVelocity = (entity.getPosition(1F).subtract(entity.getPosition(0F))).scale(OVERALL_SENSITIVITY); + entityVelocity = entityVelocity.multiply(HORIZONTAL_SENSITIVITY, VERTICAL_SENSITIVITY, HORIZONTAL_SENSITIVITY); + double entityVelocityLength = entityVelocity.length(); + AABB entityBox = entity.getBoundingBox().inflate(BASE_ENTITY_BOUNDING_BOX_EXPANSION + (entityVelocityLength * BOUNDING_BOX_EXPANSION_PER_VELOCITY)); + + if (playerHeadBox.intersects(entityBox)) { + Vec3 entityPos = entity.getPosition(1F); + int cooldown = ENTITY_COOLDOWNS.getOrDefault(entity, 0) - 1; + ENTITY_COOLDOWNS.put(entity, cooldown); + Vec3 movedPos = entityPos.add(entityVelocity.scale(PREDICTION_TICKS)); + + if (hasPassed(cameraPos, cameraEntityWidth, entityPos, movedPos) && cooldown <= 0) { + double deltaDistance = Math.abs(entityPos.distanceTo(cameraPos) - movedPos.distanceTo(cameraPos)); + FlyBySound flyBy = FLYBY_ENTITIES_AND_SOUNDS.get(entity); + float volume = (float) (flyBy.volume + (deltaDistance)); + client.getSoundManager().play(new EntityBoundSoundInstance(flyBy.sound, flyBy.category, volume, flyBy.pitch, entity, client.level.random.nextLong())); + ENTITY_COOLDOWNS.put(entity, PLAY_COOLDOWN); + } + } + } + } + + //Remove entities that aren't active + for (Entity entity : FLYBY_ENTITIES_AND_SOUNDS.keySet().stream().toList()) { + if (entity == null || entity.isRemoved() || entity.isSilent() || (cameraPos.distanceTo(entity.position()) > MIN_DISTANCE_FOR_REMOVAL)) { + FLYBY_ENTITIES_AND_SOUNDS.remove(entity); + } + } + + //Check for entities in the auto flyby list + if (!AUTO_ENTITIES_AND_SOUNDS.isEmpty()) { + //if (checkAroundCooldown > 0) { + // --checkAroundCooldown; + //} else + if (autoSounds) { + //checkAroundCooldown = AUTO_ENTITY_COOLDOWN; + AABB box = new AABB(cameraPos.add(-AUTO_ENTITY_DISTANCE, -AUTO_ENTITY_DISTANCE, -AUTO_ENTITY_DISTANCE), cameraPos.add(AUTO_ENTITY_DISTANCE, AUTO_ENTITY_DISTANCE, AUTO_ENTITY_DISTANCE)); + for (Entity entity : client.level.getEntities(cameraEntity, box)) { + EntityType type = entity.getType(); + if (AUTO_ENTITIES_AND_SOUNDS.containsKey(type)) { + addEntity(entity, AUTO_ENTITIES_AND_SOUNDS.get(type)); + } + } + } + } + } else { + FLYBY_ENTITIES_AND_SOUNDS.clear(); + } + } + + public static boolean hasPassed(@NotNull Vec3 cameraPos, double cameraWidth, @NotNull Vec3 oldCoord, @NotNull Vec3 newCoord) { + return hasPassedCoordinate(cameraPos.x(), cameraWidth, 0.35, oldCoord.x(), newCoord.x()) || + hasPassedCoordinate(cameraPos.z(), cameraWidth, 0.35, oldCoord.z(), newCoord.z()) || + hasPassedCoordinate(cameraPos.y(), cameraWidth, 0.25, oldCoord.y(), newCoord.y()); + } + + public static boolean hasPassedCoordinate(double cameraCoord, double cameraWidth, double triggerWidth, double oldCoord, double newCoord) { + double cameraTriggerWidth = cameraWidth * triggerWidth; + double minCamera = cameraCoord - cameraWidth; + double minCameraTrigger = cameraCoord - cameraTriggerWidth; + double maxCamera = cameraCoord + cameraWidth; + double maxCameraTrigger = cameraCoord + cameraTriggerWidth; + if (oldCoord < minCamera) { + return newCoord > minCameraTrigger; + } else if (oldCoord > maxCamera) { + return newCoord < maxCameraTrigger; + } + return false; + } + + public static void addEntity(Entity entity, FlyBySound flyBySound) { + FLYBY_ENTITIES_AND_SOUNDS.put(entity, flyBySound); + } + + public static void addEntityType(EntityType type, FlyBySound flyBySound) { + AUTO_ENTITIES_AND_SOUNDS.put(type, flyBySound); + } + + public record FlyBySound(float pitch, float volume, SoundSource category, SoundEvent sound) { + } +} diff --git a/src/main/java/net/frozenblock/lib/sound/api/FrozenSoundPackets.java b/src/main/java/net/frozenblock/lib/sound/api/FrozenSoundPackets.java new file mode 100644 index 0000000..d53f547 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/sound/api/FrozenSoundPackets.java @@ -0,0 +1,185 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.sound.api; + +import lombok.experimental.UtilityClass; +import net.frozenblock.lib.sound.api.networking.*; +import net.frozenblock.lib.sound.impl.EntityLoopingFadingDistanceSoundInterface; +import net.frozenblock.lib.sound.impl.EntityLoopingSoundInterface; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Holder; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.sounds.SoundEvent; +import net.minecraft.sounds.SoundSource; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.Level; +import net.minecraft.world.phys.Vec3; +import net.neoforged.neoforge.network.PacketDistributor; +import org.jetbrains.annotations.NotNull; + +@UtilityClass +public class FrozenSoundPackets { + + public static void createLocalSound(@NotNull Level level, BlockPos pos, Holder sound, SoundSource source, float volume, float pitch, boolean distanceDelay) { + if (!level.isClientSide) { + var packet = new LocalSoundPacket(Vec3.atCenterOf(pos), sound, source, volume, pitch, distanceDelay); + PacketDistributor.sendToPlayersTrackingChunk((ServerLevel) level, new ChunkPos(pos), packet); + } + } + + public static void createLocalSound(@NotNull Level level, double x, double y, double z, Holder sound, SoundSource source, float volume, float pitch, boolean distanceDelay) { + if (!level.isClientSide) { + var packet = new LocalSoundPacket(new Vec3(x, y, z), sound, source, volume, pitch, distanceDelay); + PacketDistributor.sendToPlayersTrackingChunk((ServerLevel) level, new ChunkPos(BlockPos.containing(x, y, z)), packet); + } + } + + public static void createFlybySound(@NotNull Level world, Entity entity, Holder sound, SoundSource category, float volume, float pitch) { + if (!world.isClientSide) { + var packet = new FlyBySoundPacket(entity.getId(), sound, category, volume, pitch); + PacketDistributor.sendToPlayersTrackingEntity(entity, packet); + if (entity instanceof ServerPlayer serverPlayer) { + PacketDistributor.sendToPlayer(serverPlayer, packet); + } + } + } + + public static void createMovingRestrictionSound(@NotNull Level world, Entity entity, Holder sound, SoundSource category, float volume, float pitch, ResourceLocation predicate, boolean stopOnDeath) { + if (!world.isClientSide) { + var packet = new MovingRestrictionSoundPacket(entity.getId(), sound, category, volume, pitch, predicate, stopOnDeath, false); + PacketDistributor.sendToPlayersTrackingEntity(entity, packet); + if (entity instanceof ServerPlayer serverPlayer) { + PacketDistributor.sendToPlayer(serverPlayer, packet); + } + } + } + + public static void createMovingRestrictionLoopingSound(@NotNull Level world, Entity entity, Holder sound, SoundSource category, float volume, float pitch, ResourceLocation predicate, boolean stopOnDeath) { + if (!world.isClientSide) { + var packet = new MovingRestrictionSoundPacket(entity.getId(), sound, category, volume, pitch, predicate, stopOnDeath, true); + PacketDistributor.sendToPlayersTrackingEntity(entity, packet); + if (entity instanceof ServerPlayer serverPlayer) { + PacketDistributor.sendToPlayer(serverPlayer, packet); + } + ((EntityLoopingSoundInterface)entity).frozenLib$addSound(sound.unwrapKey().orElseThrow().location(), category, volume, pitch, predicate, stopOnDeath); + } + } + + public static void createMovingRestrictionLoopingSound(ServerPlayer player, @NotNull Entity entity, Holder sound, SoundSource category, float volume, float pitch, ResourceLocation id, boolean stopOnDeath) { + var packet = new MovingRestrictionSoundPacket(entity.getId(), sound, category, volume, pitch, id, stopOnDeath, true); + PacketDistributor.sendToPlayer(player, packet); + } + + public static void createMovingRestrictionLoopingFadingDistanceSound(@NotNull Level level, Entity entity, Holder sound, Holder sound2, SoundSource category, float volume, float pitch, ResourceLocation predicate, boolean stopOnDeath, float fadeDist, float maxDist) { + if (!level.isClientSide) { + CustomPacketPayload packet = new MovingFadingDistanceSwitchingRestrictionSoundPacket( + entity.getId(), + sound, + sound2, + category, + volume, + pitch, + fadeDist, + maxDist, + predicate, + stopOnDeath, + true + ); + PacketDistributor.sendToPlayersTrackingEntity(entity, packet); + if (entity instanceof ServerPlayer serverPlayer) { + PacketDistributor.sendToPlayer(serverPlayer, packet); + } + ((EntityLoopingFadingDistanceSoundInterface)entity).frozenLib$addFadingDistanceSound(sound.unwrapKey().orElseThrow().location(), sound2.unwrapKey().orElseThrow().location(), category, volume, pitch, predicate, stopOnDeath, fadeDist, maxDist); + } + } + + public static void createMovingRestrictionLoopingFadingDistanceSound(ServerPlayer player, @NotNull Entity entity, Holder sound, Holder sound2, SoundSource category, float volume, float pitch, ResourceLocation predicate, boolean stopOnDeath, float fadeDist, float maxDist) { + CustomPacketPayload packet = new MovingFadingDistanceSwitchingRestrictionSoundPacket( + entity.getId(), + sound, + sound2, + category, + volume, + pitch, + fadeDist, + maxDist, + predicate, + stopOnDeath, + true + ); + PacketDistributor.sendToPlayer(player, packet); + } + + public static void createMovingRestrictionFadingDistanceSound(ServerPlayer player, @NotNull Entity entity, Holder sound, Holder sound2, SoundSource category, float volume, float pitch, ResourceLocation predicate, boolean stopOnDeath, float fadeDist, float maxDist) { + CustomPacketPayload packet = new MovingFadingDistanceSwitchingRestrictionSoundPacket( + entity.getId(), + sound, + sound2, + category, + volume, + pitch, + fadeDist, + maxDist, + predicate, + stopOnDeath, + false + ); + PacketDistributor.sendToPlayer(player, packet); + } + + public static void createFadingDistanceSound(@NotNull Level level, Vec3 pos, Holder sound, Holder sound2, SoundSource category, float volume, float pitch, float fadeDist, float maxDist) { + if (!level.isClientSide) { + CustomPacketPayload packet = new FadingDistanceSwitchingSoundPacket( + pos, + sound, + sound2, + category, + volume, + pitch, + fadeDist, + maxDist + ); + PacketDistributor.sendToPlayersTrackingChunk((ServerLevel) level, new ChunkPos(BlockPos.containing(pos)), packet); + } + } + + public static void createStartingMovingRestrictionLoopingSound(@NotNull Level world, Entity entity, Holder startingSound, Holder sound, SoundSource category, float volume, float pitch, ResourceLocation predicate, boolean stopOnDeath) { + if (!world.isClientSide) { + var packet = new StartingMovingRestrictionSoundLoopPacket(entity.getId(), startingSound, sound, category, volume, pitch, predicate, stopOnDeath); + PacketDistributor.sendToPlayersTrackingEntity(entity, packet); + if (entity instanceof ServerPlayer serverPlayer) { + PacketDistributor.sendToPlayer(serverPlayer, packet); + } + ((EntityLoopingSoundInterface)entity).frozenLib$addSound(sound.unwrapKey().orElseThrow().location(), category, volume, pitch, predicate, stopOnDeath); + } + } + + public static void createStartingMovingRestrictionLoopingSound(ServerPlayer player, @NotNull Entity entity, Holder startingSound, Holder sound, SoundSource category, float volume, float pitch, ResourceLocation predicate, boolean stopOnDeath) { + PacketDistributor.sendToPlayer(player, new StartingMovingRestrictionSoundLoopPacket(entity.getId(), startingSound, sound, category, volume, pitch, predicate, stopOnDeath)); + } + + public static void createLocalPlayerSound(ServerPlayer player, Holder sound, float volume, float pitch) { + var packet = new LocalPlayerSoundPacket(sound, volume, pitch); + PacketDistributor.sendToPlayer(player, packet); + } + +} diff --git a/src/main/java/net/frozenblock/lib/sound/api/MovingLoopingFadingDistanceSoundEntityManager.java b/src/main/java/net/frozenblock/lib/sound/api/MovingLoopingFadingDistanceSoundEntityManager.java new file mode 100644 index 0000000..ab898db --- /dev/null +++ b/src/main/java/net/frozenblock/lib/sound/api/MovingLoopingFadingDistanceSoundEntityManager.java @@ -0,0 +1,197 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.sound.api; + +import com.mojang.serialization.Codec; +import com.mojang.serialization.DataResult; +import com.mojang.serialization.Dynamic; +import com.mojang.serialization.codecs.RecordCodecBuilder; +import net.frozenblock.lib.FrozenSharedConstants; +import net.frozenblock.lib.sound.api.predicate.SoundPredicate; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.NbtOps; +import net.minecraft.nbt.Tag; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.sounds.SoundSource; +import net.minecraft.world.entity.Entity; +import org.slf4j.Logger; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.Optional; + +public class MovingLoopingFadingDistanceSoundEntityManager { + private final ArrayList sounds = new ArrayList<>(); + public final Entity entity; + + public MovingLoopingFadingDistanceSoundEntityManager(Entity entity) { + this.entity = entity; + } + + public void load(CompoundTag nbt) { + if (nbt.contains("frozenDistanceSounds", 9)) { + this.sounds.clear(); + DataResult> var10000 = FadingDistanceSoundLoopNBT.CODEC.listOf().parse(new Dynamic<>(NbtOps.INSTANCE, nbt.getList("frozenDistanceSounds", 10))); + Logger var10001 = FrozenSharedConstants.LOGGER4; + Objects.requireNonNull(var10001); + Optional> list = var10000.resultOrPartial(var10001::error); + if (list.isPresent()) { + List allSounds = list.get(); + this.sounds.addAll(allSounds); + } + } + } + + public void save(CompoundTag nbt) { + if (!this.sounds.isEmpty()) { + DataResult var10000 = FadingDistanceSoundLoopNBT.CODEC.listOf().encodeStart(NbtOps.INSTANCE, this.sounds); + Logger var10001 = FrozenSharedConstants.LOGGER4; + Objects.requireNonNull(var10001); + var10000.resultOrPartial(var10001::error).ifPresent((cursorsNbt) -> nbt.put("frozenDistanceSounds", cursorsNbt)); + } + } + + public void addSound(ResourceLocation soundID, ResourceLocation soundID2, SoundSource category, float volume, float pitch, ResourceLocation restrictionId, boolean stopOnDeath, float fadeDist, float maxDist) { + this.sounds.add(new FadingDistanceSoundLoopNBT(soundID, soundID2, category, volume, pitch, restrictionId, stopOnDeath, fadeDist, maxDist)); + SoundPredicate.getPredicate(restrictionId).onStart(this.entity); + } + + public ArrayList getSounds() { + return this.sounds; + } + + public void tick() { + if (!this.sounds.isEmpty()) { + ArrayList soundsToRemove = new ArrayList<>(); + for (FadingDistanceSoundLoopNBT nbt : this.sounds) { + SoundPredicate.LoopPredicate predicate = SoundPredicate.getPredicate(nbt.restrictionID); + if (!predicate.test(this.entity)) { + soundsToRemove.add(nbt); + predicate.onStop(this.entity); + } + } + this.sounds.removeAll(soundsToRemove); + } + } + + public void syncWithPlayer(ServerPlayer serverPlayer) { + for (FadingDistanceSoundLoopNBT nbt : this.getSounds()) { + FrozenSoundPackets.createMovingRestrictionLoopingFadingDistanceSound( + serverPlayer, + this.entity, + BuiltInRegistries.SOUND_EVENT.getHolder(nbt.getSoundEventID()).orElseThrow(), + BuiltInRegistries.SOUND_EVENT.getHolder(nbt.getSound2EventID()).orElseThrow(), + SoundSource.valueOf(SoundSource.class, nbt.getOrdinal()), + nbt.volume, + nbt.pitch, + nbt.restrictionID, + nbt.stopOnDeath, + nbt.fadeDist, + nbt.maxDist + ); + } + } + + public static class FadingDistanceSoundLoopNBT { + public final ResourceLocation soundEventID; + public final ResourceLocation sound2EventID; + public final String categoryOrdinal; + public final float volume; + public final float pitch; + public final float fadeDist; + public final float maxDist; + public final ResourceLocation restrictionID; + public final boolean stopOnDeath; + + public static final Codec CODEC = RecordCodecBuilder.create((instance) -> instance.group( + ResourceLocation.CODEC.fieldOf("soundEventID").forGetter(FadingDistanceSoundLoopNBT::getSoundEventID), + ResourceLocation.CODEC.fieldOf("sound2EventID").forGetter(FadingDistanceSoundLoopNBT::getSound2EventID), + Codec.STRING.fieldOf("categoryOrdinal").forGetter(FadingDistanceSoundLoopNBT::getOrdinal), + Codec.FLOAT.fieldOf("volume").forGetter(FadingDistanceSoundLoopNBT::getVolume), + Codec.FLOAT.fieldOf("pitch").forGetter(FadingDistanceSoundLoopNBT::getPitch), + ResourceLocation.CODEC.fieldOf("restrictionID").forGetter(FadingDistanceSoundLoopNBT::getRestrictionID), + Codec.BOOL.fieldOf("stopOnDeath").forGetter(FadingDistanceSoundLoopNBT::getStopOnDeath), + Codec.FLOAT.fieldOf("fadeDist").forGetter(FadingDistanceSoundLoopNBT::getFadeDist), + Codec.FLOAT.fieldOf("maxDist").forGetter(FadingDistanceSoundLoopNBT::getMaxDist) + ).apply(instance, FadingDistanceSoundLoopNBT::new)); + + public FadingDistanceSoundLoopNBT(ResourceLocation soundEventID, ResourceLocation sound2EventID, String ordinal, float vol, float pitch, ResourceLocation restrictionID, boolean stopOnDeath, float fadeDist, float maxDist) { + this.soundEventID = soundEventID; + this.sound2EventID = sound2EventID; + this.categoryOrdinal = ordinal; + this.volume = vol; + this.pitch = pitch; + this.restrictionID = restrictionID; + this.stopOnDeath = stopOnDeath; + this.fadeDist = fadeDist; + this.maxDist = maxDist; + } + + public FadingDistanceSoundLoopNBT(ResourceLocation soundEventID, ResourceLocation sound2EventID, SoundSource category, float vol, float pitch, ResourceLocation restrictionID, boolean stopOnDeath, float fadeDist, float maxDist) { + this.soundEventID = soundEventID; + this.sound2EventID = sound2EventID; + this.categoryOrdinal = category.toString(); + this.volume = vol; + this.pitch = pitch; + this.restrictionID = restrictionID; + this.stopOnDeath = stopOnDeath; + this.fadeDist = fadeDist; + this.maxDist = maxDist; + } + + public ResourceLocation getSoundEventID() { + return this.soundEventID; + } + + public ResourceLocation getSound2EventID() { + return this.sound2EventID; + } + + public String getOrdinal() { + return this.categoryOrdinal; + } + + public float getVolume() { + return this.volume; + } + + public float getPitch() { + return this.pitch; + } + + public float getFadeDist() { + return this.fadeDist; + } + + public float getMaxDist() { + return this.maxDist; + } + + public ResourceLocation getRestrictionID() { + return this.restrictionID; + } + + public boolean getStopOnDeath() { + return this.stopOnDeath; + } + + } +} diff --git a/src/main/java/net/frozenblock/lib/sound/api/MovingLoopingSoundEntityManager.java b/src/main/java/net/frozenblock/lib/sound/api/MovingLoopingSoundEntityManager.java new file mode 100644 index 0000000..8aacb4c --- /dev/null +++ b/src/main/java/net/frozenblock/lib/sound/api/MovingLoopingSoundEntityManager.java @@ -0,0 +1,170 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.sound.api; + +import com.mojang.serialization.Codec; +import com.mojang.serialization.DataResult; +import com.mojang.serialization.Dynamic; +import com.mojang.serialization.codecs.RecordCodecBuilder; +import net.frozenblock.lib.FrozenSharedConstants; +import net.frozenblock.lib.sound.api.predicate.SoundPredicate; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.NbtOps; +import net.minecraft.nbt.Tag; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.sounds.SoundSource; +import net.minecraft.world.entity.Entity; +import org.slf4j.Logger; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.Optional; + +public class MovingLoopingSoundEntityManager { + private final ArrayList sounds = new ArrayList<>(); + public Entity entity; + + public MovingLoopingSoundEntityManager(Entity entity) { + this.entity = entity; + } + + public void load(CompoundTag nbt) { + if (nbt.contains("frozenSounds", 9)) { + this.sounds.clear(); + DataResult> var10000 = SoundLoopData.CODEC.listOf().parse(new Dynamic<>(NbtOps.INSTANCE, nbt.getList("frozenSounds", 10))); + Logger var10001 = FrozenSharedConstants.LOGGER4; + Objects.requireNonNull(var10001); + Optional> list = var10000.resultOrPartial(var10001::error); + if (list.isPresent()) { + List allSounds = list.get(); + this.sounds.addAll(allSounds); + } + } + } + + public void save(CompoundTag nbt) { + if (!this.sounds.isEmpty()) { + DataResult var10000 = SoundLoopData.CODEC.listOf().encodeStart(NbtOps.INSTANCE, this.sounds); + Logger var10001 = FrozenSharedConstants.LOGGER4; + Objects.requireNonNull(var10001); + var10000.resultOrPartial(var10001::error).ifPresent((cursorsNbt) -> nbt.put("frozenSounds", cursorsNbt)); + } + } + + public void addSound(ResourceLocation soundID, SoundSource category, float volume, float pitch, ResourceLocation restrictionId, boolean stopOnDeath) { + this.sounds.add(new SoundLoopData(soundID, category, volume, pitch, restrictionId, stopOnDeath)); + SoundPredicate.getPredicate(restrictionId).onStart(this.entity); + } + + public List getSounds() { + return this.sounds; + } + + public void tick() { + if (!this.sounds.isEmpty()) { + ArrayList soundsToRemove = new ArrayList<>(); + for (SoundLoopData nbt : this.sounds) { + SoundPredicate.LoopPredicate predicate = SoundPredicate.getPredicate(nbt.restrictionID); + if (!predicate.test(this.entity)) { + soundsToRemove.add(nbt); + predicate.onStop(this.entity); + } + } + this.sounds.removeAll(soundsToRemove); + } + } + + public void syncWithPlayer(ServerPlayer serverPlayer) { + for (SoundLoopData nbt : this.getSounds()) { + FrozenSoundPackets.createMovingRestrictionLoopingSound( + serverPlayer, + this.entity, + BuiltInRegistries.SOUND_EVENT.getHolder(nbt.getSoundEventID()).orElseThrow(), + SoundSource.valueOf(SoundSource.class, nbt.getOrdinal()), + nbt.volume, + nbt.pitch, + nbt.restrictionID, + nbt.stopOnDeath + ); + } + } + + public static class SoundLoopData { + public final ResourceLocation soundEventID; + public final String categoryOrdinal; + public final float volume; + public final float pitch; + public final ResourceLocation restrictionID; + public final boolean stopOnDeath; + + public static final Codec CODEC = RecordCodecBuilder.create(instance -> instance.group( + ResourceLocation.CODEC.fieldOf("soundEventID").forGetter(SoundLoopData::getSoundEventID), + Codec.STRING.fieldOf("categoryOrdinal").forGetter(SoundLoopData::getOrdinal), + Codec.FLOAT.fieldOf("volume").forGetter(SoundLoopData::getVolume), + Codec.FLOAT.fieldOf("pitch").forGetter(SoundLoopData::getPitch), + ResourceLocation.CODEC.fieldOf("restrictionID").forGetter(SoundLoopData::getRestrictionID), + Codec.BOOL.fieldOf("stopOnDeath").forGetter(SoundLoopData::getStopOnDeath) + ).apply(instance, SoundLoopData::new)); + + public SoundLoopData(ResourceLocation soundEventID, String ordinal, float vol, float pitch, ResourceLocation restrictionID, boolean stopOnDeath) { + this.soundEventID = soundEventID; + this.categoryOrdinal = ordinal; + this.volume = vol; + this.pitch = pitch; + this.restrictionID = restrictionID; + this.stopOnDeath = stopOnDeath; + } + + public SoundLoopData(ResourceLocation soundEventID, SoundSource category, float vol, float pitch, ResourceLocation restrictionID, boolean stopOnDeath) { + this.soundEventID = soundEventID; + this.categoryOrdinal = category.toString(); + this.volume = vol; + this.pitch = pitch; + this.restrictionID = restrictionID; + this.stopOnDeath = stopOnDeath; + } + + public ResourceLocation getSoundEventID() { + return this.soundEventID; + } + + public String getOrdinal() { + return this.categoryOrdinal; + } + + public float getVolume() { + return this.volume; + } + + public float getPitch() { + return this.pitch; + } + + public ResourceLocation getRestrictionID() { + return this.restrictionID; + } + + public boolean getStopOnDeath() { + return this.stopOnDeath; + } + + } +} diff --git a/src/main/java/net/frozenblock/lib/sound/api/RestrictedSoundInstance.java b/src/main/java/net/frozenblock/lib/sound/api/RestrictedSoundInstance.java new file mode 100644 index 0000000..32bfeea --- /dev/null +++ b/src/main/java/net/frozenblock/lib/sound/api/RestrictedSoundInstance.java @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.sound.api; + +import net.frozenblock.lib.sound.api.predicate.SoundPredicate; +import net.minecraft.client.resources.sounds.AbstractTickableSoundInstance; +import net.minecraft.sounds.SoundEvent; +import net.minecraft.sounds.SoundSource; +import net.minecraft.util.RandomSource; +import net.minecraft.world.entity.Entity; + +public abstract class RestrictedSoundInstance extends AbstractTickableSoundInstance { + + protected final T entity; + protected final SoundPredicate.LoopPredicate predicate; + protected boolean firstTick = true; + + protected RestrictedSoundInstance(SoundEvent soundEvent, SoundSource soundSource, RandomSource randomSource, T entity, SoundPredicate.LoopPredicate predicate) { + super(soundEvent, soundSource, randomSource); + this.entity = entity; + this.predicate = predicate; + this.predicate.onStart(entity); + } + + @Override + public boolean canPlaySound() { + return !this.entity.isSilent(); + } + + @Override + public boolean canStartSilent() { + return true; + } + + @Override + public void stop() { + this.predicate.onStop(this.entity); + super.stop(); + } + + public boolean test() { + if (this.firstTick) { + this.firstTick = false; + Boolean firstTickBool = this.predicate.firstTickTest(this.entity); + if (firstTickBool != null) return firstTickBool; + } + return this.predicate.test(this.entity); + } +} diff --git a/src/main/java/net/frozenblock/lib/sound/api/block_sound_group/BlockSoundGroupOverwrite.java b/src/main/java/net/frozenblock/lib/sound/api/block_sound_group/BlockSoundGroupOverwrite.java new file mode 100644 index 0000000..892f8e3 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/sound/api/block_sound_group/BlockSoundGroupOverwrite.java @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.sound.api.block_sound_group; + +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.level.block.SoundType; +import org.jetbrains.annotations.NotNull; + +import java.util.function.BooleanSupplier; + +public record BlockSoundGroupOverwrite(@NotNull ResourceLocation blockId, @NotNull SoundType soundOverwrite, BooleanSupplier condition) { +} diff --git a/src/main/java/net/frozenblock/lib/sound/api/block_sound_group/BlockSoundGroupOverwrites.java b/src/main/java/net/frozenblock/lib/sound/api/block_sound_group/BlockSoundGroupOverwrites.java new file mode 100644 index 0000000..0bc36ca --- /dev/null +++ b/src/main/java/net/frozenblock/lib/sound/api/block_sound_group/BlockSoundGroupOverwrites.java @@ -0,0 +1,84 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.sound.api.block_sound_group; + +import lombok.experimental.UtilityClass; +import net.frozenblock.lib.sound.impl.block_sound_group.BlockSoundGroupManager; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.tags.TagKey; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.SoundType; +import org.jetbrains.annotations.Nullable; + +import java.util.List; +import java.util.function.BooleanSupplier; + +/** + * Allows you to add any block by either adding its registry (Blocks.STONE) or its ID ("stone"). + * If you want to add a modded block, make sure to put the nameSpaceID in the first field, then the ID and soundGroup. + * Or you could just be normal and add the block itself instead of the ID. + * You can also add a LIST of blocks (IDs not allowed) by using new Block[]{block1, block2}. + */ +@UtilityClass +public class BlockSoundGroupOverwrites { + + private static final BlockSoundGroupManager MANAGER = BlockSoundGroupManager.INSTANCE; + + @Nullable + public static List getOverwrites() { + return MANAGER.getOverwrites(); + } + + @Nullable + public static BlockSoundGroupOverwrite getOverwrite(ResourceLocation id) { + return MANAGER.getOverwrite(id); + } + + /** + * This will only work with vanilla blocks. + */ + public static void addBlock(String id, SoundType sounds, BooleanSupplier condition) { + MANAGER.addBlock(id, sounds, condition); + } + + /** + * Adds a block with the specified namespace and id. + */ + public static void addBlock(String namespace, String id, SoundType sounds, BooleanSupplier condition) { + MANAGER.addBlock(namespace, id, sounds, condition); + } + + /** + * Adds a block with the specified {@link ResourceLocation}. + */ + public static void addBlock(ResourceLocation key, SoundType sounds, BooleanSupplier condition) { + MANAGER.addBlock(key, sounds, condition); + } + + public static void addBlock(Block block, SoundType sounds, BooleanSupplier condition) { + MANAGER.addBlock(block, sounds, condition); + } + + public static void addBlocks(Block[] blocks, SoundType sounds, BooleanSupplier condition) { + MANAGER.addBlocks(blocks, sounds, condition); + } + + public static void addBlockTag(TagKey tag, SoundType sounds, BooleanSupplier condition) { + MANAGER.addBlockTag(tag, sounds, condition); + } +} diff --git a/src/main/java/net/frozenblock/lib/sound/api/block_sound_group/SoundCodecs.java b/src/main/java/net/frozenblock/lib/sound/api/block_sound_group/SoundCodecs.java new file mode 100644 index 0000000..0568010 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/sound/api/block_sound_group/SoundCodecs.java @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.sound.api.block_sound_group; + +import com.mojang.serialization.Codec; +import com.mojang.serialization.codecs.RecordCodecBuilder; +import lombok.experimental.UtilityClass; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.level.block.SoundType; + +@UtilityClass +public class SoundCodecs { + + public static final Codec SOUND_TYPE = RecordCodecBuilder.create(instance -> + instance.group( + Codec.FLOAT.fieldOf("volume").forGetter(SoundType::getVolume), + Codec.FLOAT.fieldOf("pitch").forGetter(SoundType::getPitch), + BuiltInRegistries.SOUND_EVENT.byNameCodec().fieldOf("break_sound").forGetter(SoundType::getBreakSound), + BuiltInRegistries.SOUND_EVENT.byNameCodec().fieldOf("step_sound").forGetter(SoundType::getStepSound), + BuiltInRegistries.SOUND_EVENT.byNameCodec().fieldOf("place_sound").forGetter(SoundType::getPlaceSound), + BuiltInRegistries.SOUND_EVENT.byNameCodec().fieldOf("hit_sound").forGetter(SoundType::getHitSound), + BuiltInRegistries.SOUND_EVENT.byNameCodec().fieldOf("fall_sound").forGetter(SoundType::getFallSound) + ).apply(instance, SoundType::new) + ); + + public static final Codec SOUND_GROUP_OVERWRITE = RecordCodecBuilder.create(instance -> + instance.group( + ResourceLocation.CODEC.fieldOf("id").forGetter(BlockSoundGroupOverwrite::blockId), + SOUND_TYPE.fieldOf("sound_type").forGetter(BlockSoundGroupOverwrite::soundOverwrite) + ).apply(instance, (id, soundType) -> new BlockSoundGroupOverwrite(id, soundType, () -> true)) + ); +} diff --git a/src/main/java/net/frozenblock/lib/sound/api/damagesource/PlayerDamageSourceSounds.java b/src/main/java/net/frozenblock/lib/sound/api/damagesource/PlayerDamageSourceSounds.java new file mode 100644 index 0000000..6944133 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/sound/api/damagesource/PlayerDamageSourceSounds.java @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.sound.api.damagesource; + +import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; +import lombok.experimental.UtilityClass; +import net.frozenblock.lib.FrozenSharedConstants; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.sounds.SoundEvent; +import net.minecraft.sounds.SoundEvents; +import net.minecraft.world.damagesource.DamageSource; + +import java.util.Map; + +@UtilityClass +public class PlayerDamageSourceSounds { + + private static final Map DAMAGE_SOURCE_RESOURCE_LOCATION_MAP = new Object2ObjectOpenHashMap<>(); + private static final Map RESOURCE_LOCATION_SOUND_EVENT_MAP = new Object2ObjectOpenHashMap<>(); + private static final ResourceLocation DEFAULT_ID = FrozenSharedConstants.id("default_damage_source"); + + public static void addDamageSound(DamageSource source, SoundEvent sound, ResourceLocation registry) { + DAMAGE_SOURCE_RESOURCE_LOCATION_MAP.put(source, registry); + RESOURCE_LOCATION_SOUND_EVENT_MAP.put(registry, sound); + } + + public static SoundEvent getDamageSound(DamageSource source) { + + return DAMAGE_SOURCE_RESOURCE_LOCATION_MAP.containsKey(source) ? getDamageSound(DAMAGE_SOURCE_RESOURCE_LOCATION_MAP.get(source)) : SoundEvents.PLAYER_HURT; + } + + public static SoundEvent getDamageSound(ResourceLocation location) { + return RESOURCE_LOCATION_SOUND_EVENT_MAP.getOrDefault(location, SoundEvents.PLAYER_HURT); + } + + public static ResourceLocation getDamageID(DamageSource source) { + return DAMAGE_SOURCE_RESOURCE_LOCATION_MAP.getOrDefault(source, DEFAULT_ID); + } + + public static boolean containsSource(DamageSource source) { + return DAMAGE_SOURCE_RESOURCE_LOCATION_MAP.containsKey(source); + } + + public static boolean containsSource(ResourceLocation location) { + return RESOURCE_LOCATION_SOUND_EVENT_MAP.containsKey(location); + } +} diff --git a/src/main/java/net/frozenblock/lib/sound/api/instances/MovingParticleSoundLoop.java b/src/main/java/net/frozenblock/lib/sound/api/instances/MovingParticleSoundLoop.java new file mode 100644 index 0000000..f7e97d4 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/sound/api/instances/MovingParticleSoundLoop.java @@ -0,0 +1,80 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.sound.api.instances; + +import net.minecraft.client.particle.Particle; +import net.minecraft.client.resources.sounds.AbstractTickableSoundInstance; +import net.minecraft.client.resources.sounds.SoundInstance; +import net.minecraft.sounds.SoundEvent; +import net.minecraft.sounds.SoundSource; +import net.neoforged.api.distmarker.Dist; +import net.neoforged.api.distmarker.OnlyIn; + +@OnlyIn(Dist.CLIENT) +public class MovingParticleSoundLoop extends AbstractTickableSoundInstance { + + private final T particle; + private final int fadeInTicks; + private final float increaseVolumeBy; + private int ticks; + private final boolean stopOnDeath; + + public MovingParticleSoundLoop(T particle, SoundEvent sound, SoundSource category, float volume, float pitch, int fadeInTicks, boolean stopOnDeath) { + super(sound, category, SoundInstance.createUnseededRandom()); + this.particle = particle; + this.looping = true; + this.delay = 0; + this.pitch = pitch; + + this.x = particle.x; + this.y = particle.y; + this.z = particle.z; + + this.fadeInTicks = fadeInTicks; + this.increaseVolumeBy = fadeInTicks != 0 ? (volume / fadeInTicks) : volume; + this.volume = fadeInTicks != 0 ? 0 : volume; + + this.stopOnDeath = stopOnDeath; + } + + @Override + public boolean canPlaySound() { + return this.particle.isAlive(); + } + + @Override + public boolean canStartSilent() { + return true; + } + + @Override + public void tick() { + if (this.ticks < this.fadeInTicks) { + this.volume += this.increaseVolumeBy; + this.ticks += 1; + } + if (this.particle == null || (this.stopOnDeath && !this.particle.isAlive())) { + this.stop(); + } else { + this.x = this.particle.x; + this.y = this.particle.y; + this.z = this.particle.z; + } + } + +} diff --git a/src/main/java/net/frozenblock/lib/sound/api/instances/RestrictedMovingSound.java b/src/main/java/net/frozenblock/lib/sound/api/instances/RestrictedMovingSound.java new file mode 100644 index 0000000..310fed4 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/sound/api/instances/RestrictedMovingSound.java @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.sound.api.instances; + +import net.frozenblock.lib.sound.api.RestrictedSoundInstance; +import net.frozenblock.lib.sound.api.predicate.SoundPredicate; +import net.minecraft.client.resources.sounds.SoundInstance; +import net.minecraft.sounds.SoundEvent; +import net.minecraft.sounds.SoundSource; +import net.minecraft.world.entity.Entity; +import net.neoforged.api.distmarker.Dist; +import net.neoforged.api.distmarker.OnlyIn; + +@OnlyIn(Dist.CLIENT) +public class RestrictedMovingSound extends RestrictedSoundInstance { + + private final boolean stopOnDeath; + + public RestrictedMovingSound(T entity, SoundEvent sound, SoundSource category, float volume, float pitch, SoundPredicate.LoopPredicate predicate, boolean stopOnDeath) { + super(sound, category, SoundInstance.createUnseededRandom(), entity, predicate); + this.looping = false; + this.volume = volume; + this.pitch = pitch; + this.x = entity.getX(); + this.y = entity.getY(); + this.z = entity.getZ(); + this.stopOnDeath = stopOnDeath; + + this.predicate.onStart(this.entity); + } + + public void tick() { + if (this.stopOnDeath && this.entity.isRemoved()) { + this.stop(); + } else { + if (!this.test()) { + this.stop(); + } else { + this.x = this.entity.getX(); + this.y = this.entity.getY(); + this.z = this.entity.getZ(); + } + } + } +} diff --git a/src/main/java/net/frozenblock/lib/sound/api/instances/RestrictedMovingSoundLoop.java b/src/main/java/net/frozenblock/lib/sound/api/instances/RestrictedMovingSoundLoop.java new file mode 100644 index 0000000..f99310e --- /dev/null +++ b/src/main/java/net/frozenblock/lib/sound/api/instances/RestrictedMovingSoundLoop.java @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.sound.api.instances; + +import net.frozenblock.lib.sound.api.RestrictedSoundInstance; +import net.frozenblock.lib.sound.api.predicate.SoundPredicate; +import net.minecraft.client.resources.sounds.SoundInstance; +import net.minecraft.sounds.SoundEvent; +import net.minecraft.sounds.SoundSource; +import net.minecraft.world.entity.Entity; +import net.neoforged.api.distmarker.Dist; +import net.neoforged.api.distmarker.OnlyIn; + +@OnlyIn(Dist.CLIENT) +public class RestrictedMovingSoundLoop extends RestrictedSoundInstance { + + private final boolean stopOnDeath; + + public RestrictedMovingSoundLoop(T entity, SoundEvent sound, SoundSource category, float volume, float pitch, SoundPredicate.LoopPredicate predicate, boolean stopOnDeath) { + super(sound, category, SoundInstance.createUnseededRandom(), entity, predicate); + this.looping = true; + this.delay = 0; + this.volume = volume; + this.pitch = pitch; + + this.x = entity.getX(); + this.y = entity.getY(); + this.z = entity.getZ(); + this.stopOnDeath = stopOnDeath; + } + + @Override + public void tick() { + if (this.stopOnDeath && this.entity.isRemoved()) { + this.stop(); + } else { + if (!this.test()) { + this.stop(); + } else { + this.x = this.entity.getX(); + this.y = this.entity.getY(); + this.z = this.entity.getZ(); + } + } + } + +} diff --git a/src/main/java/net/frozenblock/lib/sound/api/instances/RestrictedStartingSound.java b/src/main/java/net/frozenblock/lib/sound/api/instances/RestrictedStartingSound.java new file mode 100644 index 0000000..8059a2e --- /dev/null +++ b/src/main/java/net/frozenblock/lib/sound/api/instances/RestrictedStartingSound.java @@ -0,0 +1,93 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.sound.api.instances; + +import net.frozenblock.lib.sound.api.RestrictedSoundInstance; +import net.frozenblock.lib.sound.api.predicate.SoundPredicate; +import net.minecraft.client.Minecraft; +import net.minecraft.client.resources.sounds.AbstractSoundInstance; +import net.minecraft.client.resources.sounds.SoundInstance; +import net.minecraft.sounds.SoundEvent; +import net.minecraft.sounds.SoundSource; +import net.minecraft.world.entity.Entity; +import net.neoforged.api.distmarker.Dist; +import net.neoforged.api.distmarker.OnlyIn; + +@OnlyIn(Dist.CLIENT) +public class RestrictedStartingSound extends RestrictedSoundInstance { + + public final boolean stopOnDeath; + public boolean hasSwitched = false; + public final AbstractSoundInstance nextSound; + + public RestrictedStartingSound(T entity, SoundEvent startingSound, SoundSource category, float volume, float pitch, SoundPredicate.LoopPredicate predicate, boolean stopOnDeath, AbstractSoundInstance nextSound) { + super(startingSound, category, SoundInstance.createUnseededRandom(), entity, predicate); + this.nextSound = nextSound; + this.looping = false; + this.delay = 0; + this.volume = volume; + this.pitch = pitch; + + this.x = entity.getX(); + this.y = entity.getY(); + this.z = entity.getZ(); + this.stopOnDeath = stopOnDeath; + } + + public void startNextSound() { + this.stop(); + this.hasSwitched = true; + Minecraft.getInstance().getSoundManager().play(this.nextSound); + } + + @Override + public boolean isStopped() { + if (this.hasSwitched) { + return true; + } + return super.isStopped(); + } + + @Override + public void tick() { + if (!this.isStopped()) { + if (this.stopOnDeath && this.entity.isRemoved()) { + this.stop(); + } else { + if (!this.test()) { + this.stop(); + } else { + var soundManager = Minecraft.getInstance().getSoundManager(); + var soundEngine = soundManager.soundEngine; + var channelHandle = soundEngine.instanceToChannel.get(this); + if (channelHandle != null) { + channelHandle.execute(source -> { + if (!source.playing()) { + this.startNextSound(); + } + }); + } + + this.x = this.entity.getX(); + this.y = this.entity.getY(); + this.z = this.entity.getZ(); + } + } + } + } +} diff --git a/src/main/java/net/frozenblock/lib/sound/api/instances/distance_based/FadingDistanceSwitchingSound.java b/src/main/java/net/frozenblock/lib/sound/api/instances/distance_based/FadingDistanceSwitchingSound.java new file mode 100644 index 0000000..40ac827 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/sound/api/instances/distance_based/FadingDistanceSwitchingSound.java @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.sound.api.instances.distance_based; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.resources.sounds.AbstractTickableSoundInstance; +import net.minecraft.client.resources.sounds.SoundInstance; +import net.minecraft.sounds.SoundEvent; +import net.minecraft.sounds.SoundSource; +import net.minecraft.world.phys.Vec3; +import net.neoforged.api.distmarker.Dist; +import net.neoforged.api.distmarker.OnlyIn; + +@OnlyIn(Dist.CLIENT) +public class FadingDistanceSwitchingSound extends AbstractTickableSoundInstance { + + private final boolean isFarSound; + private final double maxDist; + private final double fadeDist; + private final float maxVol; + + public FadingDistanceSwitchingSound(SoundEvent sound, SoundSource category, float volume, float pitch, double fadeDist, double maxDist, float maxVol, boolean isFarSound, Vec3 pos) { + super(sound, category, SoundInstance.createUnseededRandom()); + this.delay = 0; + this.volume = volume; + this.pitch = pitch; + + this.isFarSound = isFarSound; + this.maxDist = maxDist; + this.fadeDist = fadeDist; + this.maxVol = maxVol; + this.x = pos.x; + this.y = pos.y; + this.z = pos.z; + } + + @Override + public void tick() { + Minecraft client = Minecraft.getInstance(); + if (client.player != null) { + double distance = Math.sqrt(client.player.distanceToSqr(this.x, this.y, this.z)); + if (distance < this.fadeDist) { + this.volume = !this.isFarSound ? this.maxVol : 0.001F; + } else if (distance > this.maxDist) { + this.volume = this.isFarSound ? this.maxVol : 0.001F; + } else { + //Gets lower as you move farther + float fadeProgress = (float) ((this.maxDist - distance) / (this.maxDist - this.fadeDist)); + this.volume = this.isFarSound ? (1F - fadeProgress) * this.maxVol : fadeProgress * this.maxVol; + } + } + } + +} diff --git a/src/main/java/net/frozenblock/lib/sound/api/instances/distance_based/RestrictedMovingFadingDistanceSwitchingSound.java b/src/main/java/net/frozenblock/lib/sound/api/instances/distance_based/RestrictedMovingFadingDistanceSwitchingSound.java new file mode 100644 index 0000000..fb4fa4d --- /dev/null +++ b/src/main/java/net/frozenblock/lib/sound/api/instances/distance_based/RestrictedMovingFadingDistanceSwitchingSound.java @@ -0,0 +1,84 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.sound.api.instances.distance_based; + +import net.frozenblock.lib.sound.api.RestrictedSoundInstance; +import net.frozenblock.lib.sound.api.predicate.SoundPredicate; +import net.minecraft.client.Minecraft; +import net.minecraft.client.resources.sounds.SoundInstance; +import net.minecraft.sounds.SoundEvent; +import net.minecraft.sounds.SoundSource; +import net.minecraft.world.entity.Entity; +import net.neoforged.api.distmarker.Dist; +import net.neoforged.api.distmarker.OnlyIn; + +@OnlyIn(Dist.CLIENT) +public class RestrictedMovingFadingDistanceSwitchingSound extends RestrictedSoundInstance { + + private final boolean stopOnDeath; + private final boolean isFarSound; + private final double maxDist; + private final double fadeDist; + private final float maxVol; + + public RestrictedMovingFadingDistanceSwitchingSound(T entity, SoundEvent sound, SoundSource category, float volume, float pitch, SoundPredicate.LoopPredicate predicate, boolean stopOnDeath, double fadeDist, double maxDist, float maxVol, boolean isFarSound) { + super(sound, category, SoundInstance.createUnseededRandom(), entity, predicate); + this.looping = true; + this.delay = 0; + this.volume = volume; + this.pitch = pitch; + + this.x = entity.getX(); + this.y = entity.getY(); + this.z = entity.getZ(); + this.stopOnDeath = stopOnDeath; + this.isFarSound = isFarSound; + this.maxDist = maxDist; + this.fadeDist = fadeDist; + this.maxVol = maxVol; + } + + @Override + public void tick() { + Minecraft client = Minecraft.getInstance(); + if (this.entity.isRemoved()) { + this.stop(); + } else { + if (!this.test()) { + this.stop(); + } else { + this.x = (float) this.entity.getX(); + this.y = (float) this.entity.getY(); + this.z = (float) this.entity.getZ(); + if (client.player != null) { + float distance = client.player.distanceTo(this.entity); + if (distance < this.fadeDist) { + this.volume = !this.isFarSound ? this.maxVol : 0.001F; + } else if (distance > this.maxDist) { + this.volume = this.isFarSound ? this.maxVol : 0.001F; + } else { + //Gets lower as you move farther + float fadeProgress = (float) ((this.maxDist - distance) / (this.maxDist - this.fadeDist)); + this.volume = this.isFarSound ? (1F - fadeProgress) * this.maxVol : fadeProgress * this.maxVol; + } + } + } + } + } + +} diff --git a/src/main/java/net/frozenblock/lib/sound/api/instances/distance_based/RestrictedMovingFadingDistanceSwitchingSoundLoop.java b/src/main/java/net/frozenblock/lib/sound/api/instances/distance_based/RestrictedMovingFadingDistanceSwitchingSoundLoop.java new file mode 100644 index 0000000..2ac5ed9 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/sound/api/instances/distance_based/RestrictedMovingFadingDistanceSwitchingSoundLoop.java @@ -0,0 +1,82 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.sound.api.instances.distance_based; + +import net.frozenblock.lib.sound.api.RestrictedSoundInstance; +import net.frozenblock.lib.sound.api.predicate.SoundPredicate; +import net.minecraft.client.Minecraft; +import net.minecraft.client.resources.sounds.SoundInstance; +import net.minecraft.sounds.SoundEvent; +import net.minecraft.sounds.SoundSource; +import net.minecraft.world.entity.Entity; +import net.neoforged.api.distmarker.Dist; +import net.neoforged.api.distmarker.OnlyIn; + +@OnlyIn(Dist.CLIENT) +public class RestrictedMovingFadingDistanceSwitchingSoundLoop extends RestrictedSoundInstance { + + private final boolean isFarSound; + private final double maxDist; + private final double fadeDist; + private final float maxVol; + + public RestrictedMovingFadingDistanceSwitchingSoundLoop(T entity, SoundEvent sound, SoundSource category, float volume, float pitch, SoundPredicate.LoopPredicate predicate, boolean stopOnDeath, double fadeDist, double maxDist, float maxVol, boolean isFarSound) { + super(sound, category, SoundInstance.createUnseededRandom(), entity, predicate); + this.looping = true; + this.delay = 0; + this.volume = volume; + this.pitch = pitch; + + this.x = entity.getX(); + this.y = entity.getY(); + this.z = entity.getZ(); + this.isFarSound = isFarSound; + this.maxDist = maxDist; + this.fadeDist = fadeDist; + this.maxVol = maxVol; + } + + @Override + public void tick() { + Minecraft client = Minecraft.getInstance(); + if (this.entity.isRemoved()) { + this.stop(); + } else { + if (!this.test()) { + this.stop(); + } else { + this.x = (float) this.entity.getX(); + this.y = (float) this.entity.getY(); + this.z = (float) this.entity.getZ(); + if (client.player != null) { + float distance = client.player.distanceTo(this.entity); + if (distance < this.fadeDist) { + this.volume = !this.isFarSound ? this.maxVol : 0.001F; + } else if (distance > this.maxDist) { + this.volume = this.isFarSound ? this.maxVol : 0.001F; + } else { + //Gets lower as you move farther + float fadeProgress = (float) ((this.maxDist - distance) / (this.maxDist - this.fadeDist)); + this.volume = this.isFarSound ? (1F - fadeProgress) * this.maxVol : fadeProgress * this.maxVol; + } + } + } + } + } + +} diff --git a/src/main/java/net/frozenblock/lib/sound/api/networking/FadingDistanceSwitchingSoundPacket.java b/src/main/java/net/frozenblock/lib/sound/api/networking/FadingDistanceSwitchingSoundPacket.java new file mode 100644 index 0000000..fc3bf94 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/sound/api/networking/FadingDistanceSwitchingSoundPacket.java @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.sound.api.networking; + +import net.frozenblock.lib.FrozenSharedConstants; +import net.minecraft.core.Holder; +import net.minecraft.core.registries.Registries; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.ByteBufCodecs; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.sounds.SoundEvent; +import net.minecraft.sounds.SoundSource; +import net.minecraft.world.phys.Vec3; +import org.jetbrains.annotations.NotNull; + +public record FadingDistanceSwitchingSoundPacket( + Vec3 pos, + Holder closeSound, + Holder farSound, + SoundSource category, + float volume, + float pitch, + float fadeDist, + float maxDist +) implements CustomPacketPayload { + + public static final Type PACKET_TYPE = new Type<>( + FrozenSharedConstants.id("fading_distance_sound_packet") + ); + public static final StreamCodec CODEC = StreamCodec.ofMember(FadingDistanceSwitchingSoundPacket::write, FadingDistanceSwitchingSoundPacket::new); + + public FadingDistanceSwitchingSoundPacket(@NotNull RegistryFriendlyByteBuf buf) { + this( + buf.readVec3(), + ByteBufCodecs.holderRegistry(Registries.SOUND_EVENT).decode(buf), + ByteBufCodecs.holderRegistry(Registries.SOUND_EVENT).decode(buf), + buf.readEnum(SoundSource.class), + buf.readFloat(), + buf.readFloat(), + buf.readFloat(), + buf.readFloat() + ); + } + + public void write(@NotNull RegistryFriendlyByteBuf buf) { + buf.writeVec3(this.pos()); + ByteBufCodecs.holderRegistry(Registries.SOUND_EVENT).encode(buf, this.closeSound()); + ByteBufCodecs.holderRegistry(Registries.SOUND_EVENT).encode(buf, this.farSound()); + buf.writeEnum(this.category()); + buf.writeFloat(this.volume()); + buf.writeFloat(this.pitch()); + buf.writeFloat(this.fadeDist()); + buf.writeFloat(this.maxDist()); + } + + @Override + @NotNull + public Type type() { + return PACKET_TYPE; + } +} diff --git a/src/main/java/net/frozenblock/lib/sound/api/networking/FlyBySoundPacket.java b/src/main/java/net/frozenblock/lib/sound/api/networking/FlyBySoundPacket.java new file mode 100644 index 0000000..5c48b01 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/sound/api/networking/FlyBySoundPacket.java @@ -0,0 +1,82 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.sound.api.networking; + +import net.frozenblock.lib.FrozenSharedConstants; +import net.frozenblock.lib.sound.api.FlyBySoundHub; +import net.minecraft.client.multiplayer.ClientLevel; +import net.minecraft.core.Holder; +import net.minecraft.core.registries.Registries; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.ByteBufCodecs; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.sounds.SoundEvent; +import net.minecraft.sounds.SoundSource; +import net.minecraft.world.entity.Entity; +import net.neoforged.api.distmarker.Dist; +import net.neoforged.api.distmarker.OnlyIn; +import net.neoforged.neoforge.network.handling.IPayloadContext; +import org.jetbrains.annotations.NotNull; + +public record FlyBySoundPacket( + int id, + Holder sound, + SoundSource category, + float volume, + float pitch +) implements CustomPacketPayload { + public static final Type PACKET_TYPE = new Type<>( + FrozenSharedConstants.id("flyby_sound_packet") + ); + public static final StreamCodec CODEC = StreamCodec.ofMember(FlyBySoundPacket::write, FlyBySoundPacket::new); + + public FlyBySoundPacket(@NotNull RegistryFriendlyByteBuf buf) { + this( + buf.readVarInt(), + ByteBufCodecs.holderRegistry(Registries.SOUND_EVENT).decode(buf), + buf.readEnum(SoundSource.class), + buf.readFloat(), + buf.readFloat() + ); + } + + public void write(@NotNull RegistryFriendlyByteBuf buf) { + buf.writeVarInt(this.id); + ByteBufCodecs.holderRegistry(Registries.SOUND_EVENT).encode(buf, this.sound); + buf.writeEnum(this.category); + buf.writeFloat(this.volume); + buf.writeFloat(this.pitch); + } + + @OnlyIn(Dist.CLIENT) + public static void receive(@NotNull FlyBySoundPacket packet, final IPayloadContext ctx) { + ClientLevel level = (ClientLevel) ctx.player().level(); + Entity entity = level.getEntity(packet.id()); + if (entity != null) { + FlyBySoundHub.FlyBySound flyBySound = new FlyBySoundHub.FlyBySound(packet.pitch(), packet.volume(), packet.category(), packet.sound().value()); + FlyBySoundHub.addEntity(entity, flyBySound); + } + } + + @Override + @NotNull + public Type type() { + return PACKET_TYPE; + } +} diff --git a/src/main/java/net/frozenblock/lib/sound/api/networking/LocalPlayerSoundPacket.java b/src/main/java/net/frozenblock/lib/sound/api/networking/LocalPlayerSoundPacket.java new file mode 100644 index 0000000..8004fd1 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/sound/api/networking/LocalPlayerSoundPacket.java @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.sound.api.networking; + +import net.frozenblock.lib.FrozenSharedConstants; +import net.minecraft.core.Holder; +import net.minecraft.core.registries.Registries; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.ByteBufCodecs; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.sounds.SoundEvent; +import org.jetbrains.annotations.NotNull; + +public record LocalPlayerSoundPacket(Holder sound, float volume, float pitch) implements CustomPacketPayload { + public static final Type PACKET_TYPE = new Type<>( + FrozenSharedConstants.id("local_player_sound_packet") + ); + public static final StreamCodec CODEC = StreamCodec.ofMember(LocalPlayerSoundPacket::write, LocalPlayerSoundPacket::new); + + public LocalPlayerSoundPacket(@NotNull RegistryFriendlyByteBuf buf) { + this( + ByteBufCodecs.holderRegistry(Registries.SOUND_EVENT).decode(buf), + buf.readFloat(), + buf.readFloat() + ); + } + + public void write(@NotNull RegistryFriendlyByteBuf buf) { + ByteBufCodecs.holderRegistry(Registries.SOUND_EVENT).encode(buf, this.sound); + buf.writeFloat(this.volume); + buf.writeFloat(this.pitch); + } + + @Override + @NotNull + public Type type() { + return PACKET_TYPE; + } +} diff --git a/src/main/java/net/frozenblock/lib/sound/api/networking/LocalSoundPacket.java b/src/main/java/net/frozenblock/lib/sound/api/networking/LocalSoundPacket.java new file mode 100644 index 0000000..791a0ac --- /dev/null +++ b/src/main/java/net/frozenblock/lib/sound/api/networking/LocalSoundPacket.java @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.sound.api.networking; + +import net.frozenblock.lib.FrozenSharedConstants; +import net.minecraft.core.Holder; +import net.minecraft.core.registries.Registries; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.ByteBufCodecs; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.sounds.SoundEvent; +import net.minecraft.sounds.SoundSource; +import net.minecraft.world.phys.Vec3; +import org.jetbrains.annotations.NotNull; + +public record LocalSoundPacket( + Vec3 pos, + Holder sound, + SoundSource category, + float volume, + float pitch, + boolean distanceDelay +) implements CustomPacketPayload { + public static final Type PACKET_TYPE = new Type<>( + FrozenSharedConstants.id("local_sound_packet") + ); + public static final StreamCodec CODEC = StreamCodec.ofMember(LocalSoundPacket::write, LocalSoundPacket::new); + + public LocalSoundPacket(@NotNull RegistryFriendlyByteBuf buf) { + this( + buf.readVec3(), + ByteBufCodecs.holderRegistry(Registries.SOUND_EVENT).decode(buf), + buf.readEnum(SoundSource.class), + buf.readFloat(), + buf.readFloat(), + buf.readBoolean() + ); + } + + public void write(@NotNull RegistryFriendlyByteBuf buf) { + buf.writeVec3(this.pos); + ByteBufCodecs.holderRegistry(Registries.SOUND_EVENT).encode(buf, this.sound); + buf.writeEnum(this.category); + buf.writeFloat(this.volume); + buf.writeFloat(this.pitch); + buf.writeBoolean(this.distanceDelay); + } + + @Override + @NotNull + public Type type() { + return PACKET_TYPE; + } +} diff --git a/src/main/java/net/frozenblock/lib/sound/api/networking/MovingFadingDistanceSwitchingRestrictionSoundPacket.java b/src/main/java/net/frozenblock/lib/sound/api/networking/MovingFadingDistanceSwitchingRestrictionSoundPacket.java new file mode 100644 index 0000000..16b3959 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/sound/api/networking/MovingFadingDistanceSwitchingRestrictionSoundPacket.java @@ -0,0 +1,86 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.sound.api.networking; + +import net.frozenblock.lib.FrozenSharedConstants; +import net.minecraft.core.Holder; +import net.minecraft.core.registries.Registries; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.ByteBufCodecs; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.sounds.SoundEvent; +import net.minecraft.sounds.SoundSource; +import org.jetbrains.annotations.NotNull; + +public record MovingFadingDistanceSwitchingRestrictionSoundPacket( + int id, + Holder closeSound, + Holder farSound, + SoundSource category, + float volume, + float pitch, + float fadeDist, + float maxDist, + ResourceLocation predicateId, + boolean stopOnDeath, + boolean looping +) implements CustomPacketPayload { + + public static final Type PACKET_TYPE = new Type<>( + FrozenSharedConstants.id("moving_fading_restriction_sound_packet") + ); + public static final StreamCodec CODEC = StreamCodec.ofMember(MovingFadingDistanceSwitchingRestrictionSoundPacket::write, MovingFadingDistanceSwitchingRestrictionSoundPacket::new); + + public MovingFadingDistanceSwitchingRestrictionSoundPacket(@NotNull RegistryFriendlyByteBuf buf) { + this( + buf.readVarInt(), + ByteBufCodecs.holderRegistry(Registries.SOUND_EVENT).decode(buf), + ByteBufCodecs.holderRegistry(Registries.SOUND_EVENT).decode(buf), + buf.readEnum(SoundSource.class), + buf.readFloat(), + buf.readFloat(), + buf.readFloat(), + buf.readFloat(), + buf.readResourceLocation(), + buf.readBoolean(), + buf.readBoolean() + ); + } + + public void write(@NotNull RegistryFriendlyByteBuf buf) { + buf.writeVarInt(this.id()); + ByteBufCodecs.holderRegistry(Registries.SOUND_EVENT).encode(buf, this.closeSound()); + ByteBufCodecs.holderRegistry(Registries.SOUND_EVENT).encode(buf, this.farSound()); + buf.writeEnum(this.category()); + buf.writeFloat(this.volume()); + buf.writeFloat(this.pitch()); + buf.writeFloat(this.fadeDist()); + buf.writeFloat(this.maxDist()); + buf.writeResourceLocation(predicateId()); + buf.writeBoolean(this.stopOnDeath()); + buf.writeBoolean(this.looping()); + } + + @Override + @NotNull + public Type type() { + return PACKET_TYPE; + } +} diff --git a/src/main/java/net/frozenblock/lib/sound/api/networking/MovingRestrictionSoundPacket.java b/src/main/java/net/frozenblock/lib/sound/api/networking/MovingRestrictionSoundPacket.java new file mode 100644 index 0000000..e30111e --- /dev/null +++ b/src/main/java/net/frozenblock/lib/sound/api/networking/MovingRestrictionSoundPacket.java @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.sound.api.networking; + +import net.frozenblock.lib.FrozenSharedConstants; +import net.minecraft.core.Holder; +import net.minecraft.core.registries.Registries; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.ByteBufCodecs; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.sounds.SoundEvent; +import net.minecraft.sounds.SoundSource; +import org.jetbrains.annotations.NotNull; + +public record MovingRestrictionSoundPacket( + int id, + Holder sound, + SoundSource category, + float volume, + float pitch, + ResourceLocation predicateId, + boolean stopOnDeath, + boolean looping +) implements CustomPacketPayload { + + public static final Type PACKET_TYPE = new Type<>( + FrozenSharedConstants.id("moving_restriction_sound_packet") + ); + public static final StreamCodec CODEC = StreamCodec.ofMember(MovingRestrictionSoundPacket::write, MovingRestrictionSoundPacket::new); + + public MovingRestrictionSoundPacket(@NotNull RegistryFriendlyByteBuf buf) { + this( + buf.readVarInt(), + ByteBufCodecs.holderRegistry(Registries.SOUND_EVENT).decode(buf), + buf.readEnum(SoundSource.class), + buf.readFloat(), + buf.readFloat(), + buf.readResourceLocation(), + buf.readBoolean(), + buf.readBoolean() + ); + } + + public void write(@NotNull RegistryFriendlyByteBuf buf) { + buf.writeVarInt(this.id); + ByteBufCodecs.holderRegistry(Registries.SOUND_EVENT).encode(buf, this.sound); + buf.writeEnum(this.category); + buf.writeFloat(this.volume); + buf.writeFloat(this.pitch); + buf.writeResourceLocation(predicateId); + buf.writeBoolean(this.stopOnDeath); + buf.writeBoolean(this.looping); + } + + @Override + @NotNull + public Type type() { + return PACKET_TYPE; + } +} diff --git a/src/main/java/net/frozenblock/lib/sound/api/networking/StartingMovingRestrictionSoundLoopPacket.java b/src/main/java/net/frozenblock/lib/sound/api/networking/StartingMovingRestrictionSoundLoopPacket.java new file mode 100644 index 0000000..ea1e38d --- /dev/null +++ b/src/main/java/net/frozenblock/lib/sound/api/networking/StartingMovingRestrictionSoundLoopPacket.java @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.sound.api.networking; + +import net.frozenblock.lib.FrozenSharedConstants; +import net.minecraft.core.Holder; +import net.minecraft.core.registries.Registries; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.ByteBufCodecs; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.sounds.SoundEvent; +import net.minecraft.sounds.SoundSource; +import org.jetbrains.annotations.NotNull; + +public record StartingMovingRestrictionSoundLoopPacket( + int id, + Holder startingSound, + Holder sound, + SoundSource category, + float volume, + float pitch, + ResourceLocation predicateId, + boolean stopOnDeath +) implements CustomPacketPayload { + public static final Type PACKET_TYPE = new Type<>(FrozenSharedConstants.id("starting_moving_restriction_looping_sound_packet")); + public static final StreamCodec CODEC = StreamCodec.ofMember(StartingMovingRestrictionSoundLoopPacket::write, StartingMovingRestrictionSoundLoopPacket::new); + + public StartingMovingRestrictionSoundLoopPacket(@NotNull RegistryFriendlyByteBuf buf) { + this( + buf.readVarInt(), + ByteBufCodecs.holderRegistry(Registries.SOUND_EVENT).decode(buf), + ByteBufCodecs.holderRegistry(Registries.SOUND_EVENT).decode(buf), + buf.readEnum(SoundSource.class), + buf.readFloat(), + buf.readFloat(), + buf.readResourceLocation(), + buf.readBoolean() + ); + } + + public void write(@NotNull RegistryFriendlyByteBuf buf) { + buf.writeVarInt(this.id); + ByteBufCodecs.holderRegistry(Registries.SOUND_EVENT).encode(buf, this.startingSound); + ByteBufCodecs.holderRegistry(Registries.SOUND_EVENT).encode(buf, this.sound); + buf.writeEnum(this.category); + buf.writeFloat(this.volume); + buf.writeFloat(this.pitch); + buf.writeResourceLocation(predicateId); + buf.writeBoolean(this.stopOnDeath); + } + + @Override + @NotNull + public Type type() { + return PACKET_TYPE; + } +} diff --git a/src/main/java/net/frozenblock/lib/sound/api/predicate/SoundPredicate.java b/src/main/java/net/frozenblock/lib/sound/api/predicate/SoundPredicate.java new file mode 100644 index 0000000..1044381 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/sound/api/predicate/SoundPredicate.java @@ -0,0 +1,90 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.sound.api.predicate; + +import net.frozenblock.lib.FrozenSharedConstants; +import net.frozenblock.lib.registry.api.FrozenRegistry; +import net.minecraft.core.Registry; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.entity.Entity; +import net.neoforged.neoforge.registries.RegisterEvent; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.function.Supplier; + +public final class SoundPredicate { + public static final ResourceLocation DEFAULT_ID = FrozenSharedConstants.id("default"); + public static final ResourceLocation NOT_SILENT_AND_ALIVE_ID = FrozenSharedConstants.id("not_silent_and_alive"); + + private final Supplier> predicateSupplier; + + public SoundPredicate(Supplier> predicateSupplier) { + this.predicateSupplier = predicateSupplier; + } + + @SuppressWarnings("unchecked") + public static LoopPredicate getPredicate(@Nullable ResourceLocation id) { + if (id != null) { + if (FrozenRegistry.SOUND_PREDICATE.containsKey(id)) { + SoundPredicate predicate = (SoundPredicate) FrozenRegistry.SOUND_PREDICATE.get(id); + if (predicate != null) { + return predicate.predicateSupplier.get(); + } + } else if (FrozenRegistry.SOUND_PREDICATE_UNSYNCED.containsKey(id)) { + SoundPredicate predicate = (SoundPredicate) FrozenRegistry.SOUND_PREDICATE_UNSYNCED.get(id); + if (predicate != null) { + return predicate.predicateSupplier.get(); + } + } + FrozenSharedConstants.LOGGER.error("Unable to find sound predicate " + id + "! Using default sound predicate instead!"); + } + return defaultPredicate(); + } + + @Contract(pure = true) + public static @NotNull LoopPredicate defaultPredicate() { + return entity -> !entity.isSilent(); + } + @Contract(pure = true) + public static @NotNull LoopPredicate notSilentAndAlive() { + return entity -> !entity.isSilent(); + } + + public static void init(RegisterEvent.RegisterHelper> registry) { + registry.register(DEFAULT_ID, new SoundPredicate<>(SoundPredicate::defaultPredicate)); + registry.register(NOT_SILENT_AND_ALIVE_ID, new SoundPredicate<>(SoundPredicate::notSilentAndAlive)); + } + + @FunctionalInterface + public interface LoopPredicate { + boolean test(T entity); + + @Nullable + default Boolean firstTickTest(T entity) { + return null; + } + + default void onStart(@Nullable T entity) { + } + + default void onStop(@Nullable T entity) { + } + } +} diff --git a/src/main/java/net/frozenblock/lib/sound/impl/EntityLoopingFadingDistanceSoundInterface.java b/src/main/java/net/frozenblock/lib/sound/impl/EntityLoopingFadingDistanceSoundInterface.java new file mode 100644 index 0000000..768c715 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/sound/impl/EntityLoopingFadingDistanceSoundInterface.java @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.sound.impl; + +import net.frozenblock.lib.sound.api.MovingLoopingFadingDistanceSoundEntityManager; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.sounds.SoundSource; +import org.jetbrains.annotations.ApiStatus; + +@ApiStatus.Internal +public interface EntityLoopingFadingDistanceSoundInterface { + + MovingLoopingFadingDistanceSoundEntityManager frozenLib$getFadingSoundManager(); + + void frozenLib$addFadingDistanceSound(ResourceLocation soundID, ResourceLocation sound2ID, SoundSource category, float volume, float pitch, ResourceLocation restrictionId, boolean stopOnDeath, float fadeDist, float maxDist); + +} diff --git a/src/main/java/net/frozenblock/lib/sound/impl/EntityLoopingSoundInterface.java b/src/main/java/net/frozenblock/lib/sound/impl/EntityLoopingSoundInterface.java new file mode 100644 index 0000000..8754a5d --- /dev/null +++ b/src/main/java/net/frozenblock/lib/sound/impl/EntityLoopingSoundInterface.java @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.sound.impl; + +import net.frozenblock.lib.sound.api.MovingLoopingSoundEntityManager; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.sounds.SoundSource; +import org.jetbrains.annotations.ApiStatus; + +@ApiStatus.Internal +public interface EntityLoopingSoundInterface { + + MovingLoopingSoundEntityManager frozenLib$getSoundManager(); + + void frozenLib$addSound(ResourceLocation soundID, SoundSource category, float volume, float pitch, ResourceLocation restrictionId, boolean stopOnDeath); + +} diff --git a/src/main/java/net/frozenblock/lib/sound/impl/block_sound_group/BlockSoundGroupManager.java b/src/main/java/net/frozenblock/lib/sound/impl/block_sound_group/BlockSoundGroupManager.java new file mode 100644 index 0000000..31a68ec --- /dev/null +++ b/src/main/java/net/frozenblock/lib/sound/impl/block_sound_group/BlockSoundGroupManager.java @@ -0,0 +1,194 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.sound.impl.block_sound_group; + +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.mojang.datafixers.util.Pair; +import com.mojang.serialization.DataResult; +import com.mojang.serialization.JsonOps; +import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; +import net.frozenblock.lib.FrozenLogUtils; +import net.frozenblock.lib.FrozenSharedConstants; +import net.frozenblock.lib.sound.api.block_sound_group.BlockSoundGroupOverwrite; +import net.frozenblock.lib.sound.api.block_sound_group.SoundCodecs; +import net.minecraft.core.Holder; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.packs.resources.Resource; +import net.minecraft.server.packs.resources.ResourceManager; +import net.minecraft.server.packs.resources.SimplePreparableReloadListener; +import net.minecraft.tags.TagKey; +import net.minecraft.util.GsonHelper; +import net.minecraft.util.profiling.ProfilerFiller; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.SoundType; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.BufferedReader; +import java.io.IOException; +import java.util.List; +import java.util.Map; +import java.util.function.BooleanSupplier; + +@ApiStatus.Internal +public class BlockSoundGroupManager extends SimplePreparableReloadListener { + private static final Logger LOGGER = LoggerFactory.getLogger("FrozenLib Block Sound Group Manager"); + private static final String DIRECTORY = "blocksoundoverwrites"; + + public static final BlockSoundGroupManager INSTANCE = new BlockSoundGroupManager(); + + private Map overwrites; + private final Map queuedOverwrites = new Object2ObjectOpenHashMap<>(); + + @Nullable + public List getOverwrites() { + if (this.overwrites != null) { + return this.overwrites.values().stream().toList(); + } + return null; + } + + @Nullable + public BlockSoundGroupOverwrite getOverwrite(ResourceLocation id) { + return this.overwrites.get(id); + } + + /** + * Adds a block with the specified {@link ResourceLocation}. + */ + public void addBlock(ResourceLocation key, SoundType sounds, BooleanSupplier condition) { + if (!BuiltInRegistries.BLOCK.containsKey(key)) { + FrozenLogUtils.log("Error whilst adding a block to BlockSoundGroupOverwrites: The specified block id has not been added to the Registry", true); + } else { + this.queuedOverwrites.put(getPath(key), new BlockSoundGroupOverwrite(key, sounds, condition)); + } + } + + /** + * This will only work with vanilla blocks. + */ + public void addBlock(String id, SoundType sounds, BooleanSupplier condition) { + var key = ResourceLocation.parse(id); + addBlock(key, sounds, condition); + } + + /** + * Adds a block with the specified namespace and id. + */ + public void addBlock(String namespace, String id, SoundType sounds, BooleanSupplier condition) { + var key = ResourceLocation.fromNamespaceAndPath(namespace, id); + addBlock(key, sounds, condition); + } + + public void addBlock(Block block, SoundType sounds, BooleanSupplier condition) { + var key = BuiltInRegistries.BLOCK.getKey(block); + addBlock(key, sounds, condition); + } + + public void addBlocks(Block[] blocks, SoundType sounds, BooleanSupplier condition) { + for (Block block : blocks) { + var key = BuiltInRegistries.BLOCK.getKey(block); + addBlock(key, sounds, condition); + } + } + + public void addBlockTag(TagKey tag, SoundType sounds, BooleanSupplier condition) { + + var tagIterable = BuiltInRegistries.BLOCK.getTag(tag); + if (tagIterable.isEmpty()) { + FrozenLogUtils.log("Error whilst adding a tag to BlockSoundGroupOverwrites: Tag is invalid", true); + } else { + for (Holder block : tagIterable.get()) { + var key = block.unwrapKey().orElseThrow().location(); + addBlock(key, sounds, condition); + } + } + } + + public static ResourceLocation getPath(ResourceLocation blockId) { + return ResourceLocation.fromNamespaceAndPath(blockId.getNamespace(), DIRECTORY + "/" + blockId.getPath() + ".json"); + } + + @Override + public SoundGroupLoader prepare(ResourceManager manager, ProfilerFiller profiler) { + return new SoundGroupLoader(manager, profiler); + } + + @Override + public void apply(SoundGroupLoader prepared, ResourceManager manager, ProfilerFiller profiler) { + this.overwrites = prepared.getOverwrites(); + this.overwrites.putAll(this.queuedOverwrites); + } + + @NotNull + public ResourceLocation getFabricId() { + return FrozenSharedConstants.id("block_sound_group_reloader"); + } + + public static class SoundGroupLoader { + private final ResourceManager manager; + private final ProfilerFiller profiler; + private final Map overwrites = new Object2ObjectOpenHashMap<>(); + + public SoundGroupLoader(ResourceManager manager, ProfilerFiller profiler) { + this.manager = manager; + this.profiler = profiler; + this.loadSoundOverwrites(); + } + + private void loadSoundOverwrites() { + profiler.push("Load Sound Overwrites"); + Map resources = manager.listResources(DIRECTORY, id -> id.getPath().endsWith(".json")); + var entrySet = resources.entrySet(); + for (Map.Entry entry : entrySet) { + this.addOverwrite(entry.getKey(), entry.getValue()); + } + profiler.pop(); + } + + private void addOverwrite(ResourceLocation id, Resource resource) { + BufferedReader reader; + try { + reader = resource.openAsReader(); + } catch (IOException e) { + LOGGER.error(String.format("Unable to open BufferedReader for id %s", id), e); + return; + } + + JsonObject json = GsonHelper.parse(reader); + DataResult> result = SoundCodecs.SOUND_GROUP_OVERWRITE.decode(JsonOps.INSTANCE, json); + + if (result.error().isPresent()) { + LOGGER.error(String.format("Unable to parse sound overwrite file %s. \nReason: %s", id, result.error().get().message())); + return; + } + + ResourceLocation overwriteId = ResourceLocation.fromNamespaceAndPath(id.getNamespace(), id.getPath().substring((DIRECTORY + "/").length())); + overwrites.put(overwriteId, result.result().orElseThrow().getFirst()); + } + + public Map getOverwrites() { + return this.overwrites; + } + } +} diff --git a/src/main/java/net/frozenblock/lib/sound/impl/block_sound_group/SimpleResourceReloadListener.java b/src/main/java/net/frozenblock/lib/sound/impl/block_sound_group/SimpleResourceReloadListener.java new file mode 100644 index 0000000..2508f97 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/sound/impl/block_sound_group/SimpleResourceReloadListener.java @@ -0,0 +1,22 @@ +package net.frozenblock.lib.sound.impl.block_sound_group; + +import net.minecraft.server.packs.resources.PreparableReloadListener; +import net.minecraft.server.packs.resources.ResourceManager; +import net.minecraft.server.packs.resources.ResourceManagerReloadListener; +import net.minecraft.util.profiling.ProfilerFiller; + +import java.util.Objects; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Executor; + +public interface SimpleResourceReloadListener extends ResourceManagerReloadListener { + default CompletableFuture reload(PreparableReloadListener.PreparationBarrier helper, ResourceManager manager, ProfilerFiller loadProfiler, ProfilerFiller applyProfiler, Executor loadExecutor, Executor applyExecutor) { + CompletableFuture var10000 = this.load(manager, loadProfiler, loadExecutor); + Objects.requireNonNull(helper); + return var10000.thenCompose(helper::wait).thenCompose((o) -> this.apply(o, manager, applyProfiler, applyExecutor)); + } + + CompletableFuture load(ResourceManager var1, ProfilerFiller var2, Executor var3); + + CompletableFuture apply(T var1, ResourceManager var2, ProfilerFiller var3, Executor var4); +} diff --git a/src/main/java/net/frozenblock/lib/sound/mixin/EntityMixin.java b/src/main/java/net/frozenblock/lib/sound/mixin/EntityMixin.java new file mode 100644 index 0000000..a44faa5 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/sound/mixin/EntityMixin.java @@ -0,0 +1,115 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.sound.mixin; + +import net.frozenblock.lib.sound.api.MovingLoopingFadingDistanceSoundEntityManager; +import net.frozenblock.lib.sound.api.MovingLoopingSoundEntityManager; +import net.frozenblock.lib.sound.impl.EntityLoopingFadingDistanceSoundInterface; +import net.frozenblock.lib.sound.impl.EntityLoopingSoundInterface; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.sounds.SoundSource; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.level.Level; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +@Mixin(Entity.class) +public abstract class EntityMixin implements EntityLoopingSoundInterface, EntityLoopingFadingDistanceSoundInterface { + + @Unique + public MovingLoopingSoundEntityManager frozenLib$loopingSoundManager; + @Unique + public MovingLoopingFadingDistanceSoundEntityManager frozenLib$loopingFadingDistanceSoundManager; + + @Inject(method = "", at = @At("TAIL")) + private void frozenLib$setLoopingSoundManagers(EntityType entityType, Level level, CallbackInfo info) { + Entity entity = Entity.class.cast(this); + this.frozenLib$loopingSoundManager = new MovingLoopingSoundEntityManager(entity); + this.frozenLib$loopingFadingDistanceSoundManager = new MovingLoopingFadingDistanceSoundEntityManager(entity); + } + + @Inject( + method = "saveWithoutId", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/world/entity/Entity;addAdditionalSaveData(Lnet/minecraft/nbt/CompoundTag;)V", + shift = At.Shift.AFTER + ) + ) + public void frozenLib$saveLoopingSoundData(CompoundTag compoundTag, CallbackInfoReturnable info) { + if (this.frozenLib$loopingSoundManager != null) { + this.frozenLib$loopingSoundManager.save(compoundTag); + } + if (this.frozenLib$loopingFadingDistanceSoundManager != null) { + this.frozenLib$loopingFadingDistanceSoundManager.save(compoundTag); + } + } + + @Inject( + method = "load", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/world/entity/Entity;readAdditionalSaveData(Lnet/minecraft/nbt/CompoundTag;)V", + shift = At.Shift.AFTER + ) + ) + public void frozenLib$loadLoopingSoundData(CompoundTag compoundTag, CallbackInfo info) { + this.frozenLib$loopingSoundManager.load(compoundTag); + this.frozenLib$loopingFadingDistanceSoundManager.load(compoundTag); + } + + @Inject(method = "tick", at = @At("TAIL")) + public void frozenLib$tickSounds(CallbackInfo info) { + Entity entity = Entity.class.cast(this); + if (!entity.level().isClientSide) { + this.frozenLib$loopingSoundManager.tick(); + this.frozenLib$loopingFadingDistanceSoundManager.tick(); + } + } + + @Unique + @Override + public MovingLoopingSoundEntityManager frozenLib$getSoundManager() { + return this.frozenLib$loopingSoundManager; + } + + @Unique + @Override + public void frozenLib$addSound(ResourceLocation soundID, SoundSource category, float volume, float pitch, ResourceLocation restrictionId, boolean stopOnDeath) { + this.frozenLib$loopingSoundManager.addSound(soundID, category, volume, pitch, restrictionId, stopOnDeath); + } + + @Unique + @Override + public MovingLoopingFadingDistanceSoundEntityManager frozenLib$getFadingSoundManager() { + return this.frozenLib$loopingFadingDistanceSoundManager; + } + + @Unique + @Override + public void frozenLib$addFadingDistanceSound(ResourceLocation soundID, ResourceLocation sound2ID, SoundSource category, float volume, float pitch, ResourceLocation restrictionId, boolean stopOnDeath, float fadeDist, float maxDist) { + this.frozenLib$loopingFadingDistanceSoundManager.addSound(soundID, sound2ID, category, volume, pitch, restrictionId, stopOnDeath, fadeDist, maxDist); + } + +} diff --git a/src/main/java/net/frozenblock/lib/sound/mixin/LivingEntityMixin.java b/src/main/java/net/frozenblock/lib/sound/mixin/LivingEntityMixin.java new file mode 100644 index 0000000..0c9b30f --- /dev/null +++ b/src/main/java/net/frozenblock/lib/sound/mixin/LivingEntityMixin.java @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.sound.mixin; + +import com.llamalad7.mixinextras.injector.ModifyReturnValue; +import net.frozenblock.lib.sound.api.damagesource.PlayerDamageSourceSounds; +import net.minecraft.sounds.SoundEvent; +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.world.entity.LivingEntity; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; + +@Mixin(LivingEntity.class) +public abstract class LivingEntityMixin { + + @ModifyReturnValue(method = "getHurtSound", at = @At("RETURN")) + private SoundEvent playHurtSound(SoundEvent original, DamageSource source) { + if (PlayerDamageSourceSounds.containsSource(source)) { + return PlayerDamageSourceSounds.getDamageSound(source); + } + return original; + } + +} diff --git a/src/main/java/net/frozenblock/lib/sound/mixin/client/BlockBehaviourMixin.java b/src/main/java/net/frozenblock/lib/sound/mixin/client/BlockBehaviourMixin.java new file mode 100644 index 0000000..5d3b8ac --- /dev/null +++ b/src/main/java/net/frozenblock/lib/sound/mixin/client/BlockBehaviourMixin.java @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.sound.mixin.client; + +import net.frozenblock.lib.sound.api.block_sound_group.BlockSoundGroupOverwrite; +import net.frozenblock.lib.sound.api.block_sound_group.BlockSoundGroupOverwrites; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.SoundType; +import net.minecraft.world.level.block.state.BlockBehaviour; +import net.minecraft.world.level.block.state.BlockState; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +@Mixin(BlockBehaviour.class) +public final class BlockBehaviourMixin { + + @Inject(method = "getSoundType", at = @At("RETURN"), cancellable = true) + private void getSoundGroupOverride(BlockState state, CallbackInfoReturnable info) { + Block block = state.getBlock(); + ResourceLocation id = BuiltInRegistries.BLOCK.getKey(block); + var overwrites = BlockSoundGroupOverwrites.getOverwrites(); + if (overwrites != null) { + for (BlockSoundGroupOverwrite overwrite : overwrites) { + if (overwrite.blockId().equals(id) && overwrite.condition().getAsBoolean()) { + info.setReturnValue(overwrite.soundOverwrite()); + } + } + } + } + +} diff --git a/src/main/java/net/frozenblock/lib/spotting_icons/api/ClientSpottingIconMethods.java b/src/main/java/net/frozenblock/lib/spotting_icons/api/ClientSpottingIconMethods.java new file mode 100644 index 0000000..021f751 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/spotting_icons/api/ClientSpottingIconMethods.java @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.spotting_icons.api; + +import net.minecraft.client.Minecraft; +import net.minecraft.resources.ResourceLocation; +import net.neoforged.api.distmarker.Dist; +import net.neoforged.api.distmarker.OnlyIn; + +@OnlyIn(Dist.CLIENT) +public class ClientSpottingIconMethods { + + public static boolean hasTexture(ResourceLocation resourceLocation) { + return Minecraft.getInstance().getResourceManager().getResource(ResourceLocation.fromNamespaceAndPath(resourceLocation.getNamespace(), resourceLocation.getPath())).isPresent(); + } + +} diff --git a/src/main/java/net/frozenblock/lib/spotting_icons/api/SpottingIconManager.java b/src/main/java/net/frozenblock/lib/spotting_icons/api/SpottingIconManager.java new file mode 100644 index 0000000..e67cc1d --- /dev/null +++ b/src/main/java/net/frozenblock/lib/spotting_icons/api/SpottingIconManager.java @@ -0,0 +1,141 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.spotting_icons.api; + +import com.mojang.serialization.Codec; +import com.mojang.serialization.DataResult; +import com.mojang.serialization.Dynamic; +import com.mojang.serialization.codecs.RecordCodecBuilder; +import io.netty.buffer.Unpooled; +import net.frozenblock.lib.FrozenSharedConstants; +import net.frozenblock.lib.spotting_icons.impl.SpottingIconPacket; +import net.frozenblock.lib.spotting_icons.impl.SpottingIconRemovePacket; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.NbtOps; +import net.minecraft.nbt.Tag; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.entity.Entity; +import net.neoforged.neoforge.network.PacketDistributor; +import org.slf4j.Logger; + +import java.util.Objects; +import java.util.Optional; + +public class SpottingIconManager { + public Entity entity; + public int ticksToCheck; + public SpottingIcon icon; + public boolean clientHasIconResource; + + public SpottingIconManager(Entity entity) { + this.entity = entity; + } + + public void tick() { + if (this.ticksToCheck > 0) { + --this.ticksToCheck; + } else { + this.ticksToCheck = 20; + if (this.icon != null) { + if (this.entity.level().isClientSide) { + this.clientHasIconResource = ClientSpottingIconMethods.hasTexture(this.icon.texture()); + } + if (!SpottingIconPredicate.getPredicate(this.icon.restrictionID).test(this.entity)) { + this.removeIcon(); + } + } + } + } + + public void setIcon(ResourceLocation texture, float startFade, float endFade, ResourceLocation restrictionID) { + this.icon = new SpottingIcon(texture, startFade, endFade, restrictionID); + if (!this.entity.level().isClientSide) { + CustomPacketPayload packet = new SpottingIconPacket(this.entity.getId(), texture, startFade, endFade, restrictionID); + PacketDistributor.sendToPlayersTrackingEntity(this.entity, packet); + } else { + this.clientHasIconResource = ClientSpottingIconMethods.hasTexture(this.icon.texture()); + } + SpottingIconPredicate.getPredicate(this.icon.restrictionID).onAdded(this.entity); + } + + public void removeIcon() { + SpottingIconPredicate.getPredicate(this.icon.restrictionID).onRemoved(this.entity); + this.icon = null; + if (!this.entity.level().isClientSide) { + CustomPacketPayload packet = new SpottingIconRemovePacket(this.entity.getId()); + PacketDistributor.sendToPlayersTrackingEntity(this.entity, packet); + } + } + + public void sendIconPacket(ServerPlayer player) { + if (this.icon != null) { + FriendlyByteBuf byteBuf = new FriendlyByteBuf(Unpooled.buffer()); + byteBuf.writeVarInt(this.entity.getId()); + byteBuf.writeResourceLocation(this.icon.texture); + byteBuf.writeFloat(this.icon.startFadeDist); + byteBuf.writeFloat(this.icon.endFadeDist); + byteBuf.writeResourceLocation(this.icon.restrictionID); + PacketDistributor.sendToPlayer( + player, + new SpottingIconPacket( + this.entity.getId(), + this.icon.texture, + this.icon.startFadeDist(), + this.icon.endFadeDist(), + this.icon.restrictionID() + ) + ); + } + } + + public void load(CompoundTag nbt) { + this.ticksToCheck = nbt.getInt("frozenSpottingIconTicksToCheck"); + if (nbt.contains("frozenSpottingIcons")) { + this.icon = null; + DataResult var10000 = SpottingIcon.CODEC.parse(new Dynamic<>(NbtOps.INSTANCE, nbt.getCompound("frozenSpottingIcons"))); + Logger var10001 = FrozenSharedConstants.LOGGER4; + Objects.requireNonNull(var10001); + Optional icon = var10000.resultOrPartial(var10001::error); + icon.ifPresent(spottingIcon -> this.icon = spottingIcon); + } + } + + public void save(CompoundTag nbt) { + nbt.putInt("frozenSpottingIconTicksToCheck", this.ticksToCheck); + if (this.icon != null) { + DataResult var10000 = SpottingIcon.CODEC.encodeStart(NbtOps.INSTANCE, this.icon); + Logger var10001 = FrozenSharedConstants.LOGGER4; + Objects.requireNonNull(var10001); + var10000.resultOrPartial(var10001::error).ifPresent((iconNBT) -> nbt.put("frozenSpottingIcons", iconNBT)); + } else if (nbt.contains("frozenSpottingIcons")) { + nbt.remove("frozenSpottingIcons"); + } + } + + public record SpottingIcon(ResourceLocation texture, float startFadeDist, float endFadeDist, ResourceLocation restrictionID) { + public static final Codec CODEC = RecordCodecBuilder.create((instance) -> instance.group( + ResourceLocation.CODEC.fieldOf("texture").forGetter(SpottingIcon::texture), + Codec.FLOAT.fieldOf("startFadeDist").forGetter(SpottingIcon::startFadeDist), + Codec.FLOAT.fieldOf("endFadeDist").forGetter(SpottingIcon::endFadeDist), + ResourceLocation.CODEC.fieldOf("restrictionID").forGetter(SpottingIcon::restrictionID) + ).apply(instance, SpottingIcon::new)); + } +} diff --git a/src/main/java/net/frozenblock/lib/spotting_icons/api/SpottingIconPredicate.java b/src/main/java/net/frozenblock/lib/spotting_icons/api/SpottingIconPredicate.java new file mode 100644 index 0000000..69f471e --- /dev/null +++ b/src/main/java/net/frozenblock/lib/spotting_icons/api/SpottingIconPredicate.java @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.spotting_icons.api; + +import net.frozenblock.lib.FrozenSharedConstants; +import net.frozenblock.lib.registry.api.FrozenRegistry; +import net.minecraft.core.Registry; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.entity.Entity; +import net.neoforged.neoforge.registries.RegisterEvent; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public final class SpottingIconPredicate { + + private final IconPredicate predicate; + + public SpottingIconPredicate(IconPredicate predicate) { + this.predicate = predicate; + } + + @SuppressWarnings("unchecked") + public static IconPredicate getPredicate(@Nullable ResourceLocation id) { + if (id != null) { + if (FrozenRegistry.SPOTTING_ICON_PREDICATE.containsKey(id)) { + SpottingIconPredicate predicate = (SpottingIconPredicate) FrozenRegistry.SPOTTING_ICON_PREDICATE.get(id); + if (predicate != null) { + return predicate.predicate; + } + } + FrozenSharedConstants.LOGGER.error("Unable to find spotting icon predicate " + id + "! Using default spotting icon predicate instead!"); + } + return defaultPredicate(); + } + + @FunctionalInterface + public interface IconPredicate { + boolean test(T entity); + + default void onAdded(T entity) { + + } + + default void onRemoved(T entity) { + } + } + + @NotNull + @Contract(pure = true) + public static IconPredicate defaultPredicate() { + return Entity::isAlive; + } + + public static ResourceLocation DEFAULT_ID = FrozenSharedConstants.id("default"); + + public static void init(RegisterEvent.RegisterHelper> registry) { + registry.register(DEFAULT_ID, new SpottingIconPredicate<>(defaultPredicate())); + } +} diff --git a/src/main/java/net/frozenblock/lib/spotting_icons/impl/EntityRenderDispatcherWithIcon.java b/src/main/java/net/frozenblock/lib/spotting_icons/impl/EntityRenderDispatcherWithIcon.java new file mode 100644 index 0000000..7731787 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/spotting_icons/impl/EntityRenderDispatcherWithIcon.java @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.spotting_icons.impl; + +import com.mojang.blaze3d.vertex.PoseStack; +import net.minecraft.client.renderer.MultiBufferSource; +import net.minecraft.world.entity.Entity; + +public interface EntityRenderDispatcherWithIcon { + + void frozenLib$renderIcon(E entity, double x, double y, double z, float rotationYaw, float partialTicks, PoseStack matrixStack, MultiBufferSource buffer, int packedLight); + +} diff --git a/src/main/java/net/frozenblock/lib/spotting_icons/impl/EntityRendererWithIcon.java b/src/main/java/net/frozenblock/lib/spotting_icons/impl/EntityRendererWithIcon.java new file mode 100644 index 0000000..8aa22a2 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/spotting_icons/impl/EntityRendererWithIcon.java @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.spotting_icons.impl; + +import com.mojang.blaze3d.vertex.PoseStack; +import net.minecraft.client.renderer.MultiBufferSource; +import net.minecraft.world.entity.Entity; + +public interface EntityRendererWithIcon { + + void frozenLib$renderIcon(T entity, float entityYaw, float partialTick, PoseStack poseStack, MultiBufferSource buffer, int packedLight); + +} diff --git a/src/main/java/net/frozenblock/lib/spotting_icons/impl/EntitySpottingIconInterface.java b/src/main/java/net/frozenblock/lib/spotting_icons/impl/EntitySpottingIconInterface.java new file mode 100644 index 0000000..1d713ba --- /dev/null +++ b/src/main/java/net/frozenblock/lib/spotting_icons/impl/EntitySpottingIconInterface.java @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.spotting_icons.impl; + +import net.frozenblock.lib.spotting_icons.api.SpottingIconManager; + +public interface EntitySpottingIconInterface { + + SpottingIconManager getSpottingIconManager(); + +} diff --git a/src/main/java/net/frozenblock/lib/spotting_icons/impl/SpottingIconPacket.java b/src/main/java/net/frozenblock/lib/spotting_icons/impl/SpottingIconPacket.java new file mode 100644 index 0000000..18fe264 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/spotting_icons/impl/SpottingIconPacket.java @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.spotting_icons.impl; + +import net.frozenblock.lib.FrozenSharedConstants; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.resources.ResourceLocation; +import org.jetbrains.annotations.NotNull; + +public record SpottingIconPacket( + int entityId, + ResourceLocation texture, + float startFade, + float endFade, + ResourceLocation restrictionID +) implements CustomPacketPayload { + + public static final Type PACKET_TYPE = new Type<>( + FrozenSharedConstants.id("spotting_icon_packet") + ); + public static final StreamCodec CODEC = StreamCodec.ofMember(SpottingIconPacket::write, SpottingIconPacket::new); + + public SpottingIconPacket(FriendlyByteBuf buf) { + this(buf.readVarInt(), buf.readResourceLocation(), buf.readFloat(), buf.readFloat(), buf.readResourceLocation()); + } + + public void write(FriendlyByteBuf buf) { + buf.writeVarInt(this.entityId()); + buf.writeResourceLocation(this.texture()); + buf.writeFloat(this.startFade()); + buf.writeFloat(this.endFade()); + buf.writeResourceLocation(this.restrictionID()); + } + + @Override + @NotNull + public Type type() { + return PACKET_TYPE; + } +} diff --git a/src/main/java/net/frozenblock/lib/spotting_icons/impl/SpottingIconRemovePacket.java b/src/main/java/net/frozenblock/lib/spotting_icons/impl/SpottingIconRemovePacket.java new file mode 100644 index 0000000..5b625b1 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/spotting_icons/impl/SpottingIconRemovePacket.java @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.spotting_icons.impl; + +import net.frozenblock.lib.FrozenSharedConstants; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.codec.ByteBufCodecs; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import org.jetbrains.annotations.NotNull; + +public record SpottingIconRemovePacket( + int entityId +) implements CustomPacketPayload { + + public static final Type PACKET_TYPE = new Type<>( + FrozenSharedConstants.id("spotting_icon_remove_packet") + ); + public static final StreamCodec CODEC = ByteBufCodecs.VAR_INT + .map(SpottingIconRemovePacket::new, SpottingIconRemovePacket::entityId) + .cast(); + + public void write(FriendlyByteBuf buf) { + buf.writeVarInt(this.entityId()); + } + + @Override + @NotNull + public Type type() { + return PACKET_TYPE; + } +} diff --git a/src/main/java/net/frozenblock/lib/spotting_icons/mixin/EntityMixin.java b/src/main/java/net/frozenblock/lib/spotting_icons/mixin/EntityMixin.java new file mode 100644 index 0000000..6fd7af6 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/spotting_icons/mixin/EntityMixin.java @@ -0,0 +1,72 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.spotting_icons.mixin; + +import net.frozenblock.lib.spotting_icons.api.SpottingIconManager; +import net.frozenblock.lib.spotting_icons.impl.EntitySpottingIconInterface; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.level.Level; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +@Mixin(Entity.class) +public class EntityMixin implements EntitySpottingIconInterface { + + @Unique + public SpottingIconManager frozenLib$SpottingIconManager; + + @Inject(method = "", at = @At("TAIL")) + private void frozenLib$setIconManager(EntityType entityType, Level level, CallbackInfo info) { + Entity entity = Entity.class.cast(this); + this.frozenLib$SpottingIconManager = new SpottingIconManager(entity); + } + + + @Inject(method = "saveWithoutId", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/entity/Entity;addAdditionalSaveData(Lnet/minecraft/nbt/CompoundTag;)V", shift = At.Shift.AFTER)) + public void frozenLib$saveIconManager(CompoundTag compoundTag, CallbackInfoReturnable info) { + if (this.frozenLib$SpottingIconManager != null) { + this.frozenLib$SpottingIconManager.save(compoundTag); + } + } + + @Inject(method = "load", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/entity/Entity;readAdditionalSaveData(Lnet/minecraft/nbt/CompoundTag;)V", shift = At.Shift.AFTER)) + public void frozenLib$loadIconManager(CompoundTag compoundTag, CallbackInfo info) { + this.frozenLib$SpottingIconManager.load(compoundTag); + } + + @Inject(method = "tick", at = @At("TAIL")) + public void frozenLib$tickIcon(CallbackInfo info) { + Entity entity = Entity.class.cast(this); + if (!entity.level().isClientSide) { + this.frozenLib$SpottingIconManager.tick(); + } + } + + @Unique + @Override + public SpottingIconManager getSpottingIconManager() { + return this.frozenLib$SpottingIconManager; + } + +} diff --git a/src/main/java/net/frozenblock/lib/spotting_icons/mixin/client/EntityRenderDispatcherMixin.java b/src/main/java/net/frozenblock/lib/spotting_icons/mixin/client/EntityRenderDispatcherMixin.java new file mode 100644 index 0000000..66871d6 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/spotting_icons/mixin/client/EntityRenderDispatcherMixin.java @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.spotting_icons.mixin.client; + +import com.mojang.blaze3d.vertex.PoseStack; +import net.frozenblock.lib.spotting_icons.impl.EntityRenderDispatcherWithIcon; +import net.frozenblock.lib.spotting_icons.impl.EntityRendererWithIcon; +import net.minecraft.CrashReport; +import net.minecraft.CrashReportCategory; +import net.minecraft.ReportedException; +import net.minecraft.client.renderer.MultiBufferSource; +import net.minecraft.client.renderer.entity.EntityRenderDispatcher; +import net.minecraft.client.renderer.entity.EntityRenderer; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.level.Level; +import net.minecraft.world.phys.Vec3; +import net.neoforged.api.distmarker.Dist; +import net.neoforged.api.distmarker.OnlyIn; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.Unique; + +@OnlyIn(Dist.CLIENT) +@Mixin(EntityRenderDispatcher.class) +public class EntityRenderDispatcherMixin implements EntityRenderDispatcherWithIcon { + + @Shadow + private Level level; + + @Unique + public void frozenLib$renderIcon( + E entity, double x, double y, double z, float rotationYaw, float partialTicks, PoseStack matrixStack, MultiBufferSource buffer, int packedLight + ) { + EntityRenderer entityRenderer = this.getRenderer(entity); + try { + Vec3 vec3 = entityRenderer.getRenderOffset(entity, partialTicks); + double d = x + vec3.x(); + double e = y + vec3.y(); + double f = z + vec3.z(); + matrixStack.pushPose(); + matrixStack.translate(d, e, f); + ((EntityRendererWithIcon) entityRenderer).frozenLib$renderIcon(entity, rotationYaw, partialTicks, matrixStack, buffer, packedLight); + matrixStack.popPose(); + } catch (Throwable throwable) { + CrashReport crashReport = CrashReport.forThrowable(throwable, "Rendering entity icon in world"); + CrashReportCategory crashReportCategory = crashReport.addCategory("Entity icon being rendered"); + entity.fillCrashReportCategory(crashReportCategory); + CrashReportCategory crashReportCategory2 = crashReport.addCategory("Renderer details"); + crashReportCategory2.setDetail("Assigned renderer", entityRenderer); + crashReportCategory2.setDetail("Location", CrashReportCategory.formatLocation(this.level, x, y, z)); + crashReportCategory2.setDetail("Rotation", rotationYaw); + crashReportCategory2.setDetail("Delta", partialTicks); + throw new ReportedException(crashReport); + } + } + + @Shadow + public EntityRenderer getRenderer(T entity) { + throw new AssertionError("Mixin injection failed - FrozenLib EntityRenderDispatcherMixin"); + } + +} diff --git a/src/main/java/net/frozenblock/lib/spotting_icons/mixin/client/EntityRendererMixin.java b/src/main/java/net/frozenblock/lib/spotting_icons/mixin/client/EntityRendererMixin.java new file mode 100644 index 0000000..0f99571 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/spotting_icons/mixin/client/EntityRendererMixin.java @@ -0,0 +1,84 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.spotting_icons.mixin.client; + +import com.mojang.blaze3d.vertex.PoseStack; +import com.mojang.blaze3d.vertex.VertexConsumer; +import net.frozenblock.lib.entity.api.rendering.FrozenRenderType; +import net.frozenblock.lib.spotting_icons.api.SpottingIconManager; +import net.frozenblock.lib.spotting_icons.impl.EntityRendererWithIcon; +import net.frozenblock.lib.spotting_icons.impl.EntitySpottingIconInterface; +import net.minecraft.client.renderer.MultiBufferSource; +import net.minecraft.client.renderer.entity.EntityRenderDispatcher; +import net.minecraft.client.renderer.entity.EntityRenderer; +import net.minecraft.client.renderer.texture.OverlayTexture; +import net.minecraft.util.Mth; +import net.minecraft.world.entity.Entity; +import net.neoforged.api.distmarker.Dist; +import net.neoforged.api.distmarker.OnlyIn; +import org.jetbrains.annotations.NotNull; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.Unique; + +@OnlyIn(Dist.CLIENT) +@Mixin(EntityRenderer.class) +public abstract class EntityRendererMixin implements EntityRendererWithIcon { + + @Shadow + @Final + protected EntityRenderDispatcher entityRenderDispatcher; + + @Unique + @Override + public void frozenLib$renderIcon(T entity, float entityYaw, float partialTick, PoseStack poseStack, MultiBufferSource buffer, int packedLight) { + SpottingIconManager iconManager = ((EntitySpottingIconInterface) entity).getSpottingIconManager(); + SpottingIconManager.SpottingIcon icon = iconManager.icon; + if (icon != null) { + double dist = Mth.sqrt((float) this.entityRenderDispatcher.distanceToSqr(entity)); + if (dist > icon.startFadeDist() && iconManager.clientHasIconResource) { + float endDist = icon.endFadeDist() - icon.startFadeDist(); + dist -= icon.startFadeDist(); + int alpha = (int) ((dist > endDist ? 1F : (float) Math.min(1F, dist / endDist)) * 255F); + float f = entity.getBbHeight() + 1F; + poseStack.pushPose(); + poseStack.translate(0.0D, f, 0.0D); + poseStack.mulPose(this.entityRenderDispatcher.cameraOrientation()); + poseStack.scale(-1, 1, 1); + PoseStack.Pose pose = poseStack.last(); + VertexConsumer vertexConsumer = buffer.getBuffer(FrozenRenderType.entityTranslucentEmissiveAlwaysRender(((EntitySpottingIconInterface) entity).getSpottingIconManager().icon.texture())); + frozenLib$vertex(vertexConsumer, pose, packedLight, 0.0F, 0, 0, 1, alpha); + frozenLib$vertex(vertexConsumer, pose, packedLight, 1.0F, 0, 1, 1, alpha); + frozenLib$vertex(vertexConsumer, pose, packedLight, 1.0F, 1, 1, 0, alpha); + frozenLib$vertex(vertexConsumer, pose, packedLight, 0.0F, 1, 0, 0, alpha); + poseStack.popPose(); + } + } + } + + @Unique + private static void frozenLib$vertex(@NotNull VertexConsumer vertexConsumer, PoseStack.Pose pose, int i, float f, int j, int u, int v, int alpha) { + vertexConsumer.addVertex(pose, f - 0.5F, (float)j - 0.5F, 0.0F) + .setColor(255, 255, 255, alpha) + .setUv((float)u, (float)v) + .setOverlay(OverlayTexture.NO_OVERLAY) + .setLight(i) + .setNormal(pose, 0.0F, 1.0F, 0.0F); + } +} diff --git a/src/main/java/net/frozenblock/lib/spotting_icons/mixin/client/LevelRendererMixin.java b/src/main/java/net/frozenblock/lib/spotting_icons/mixin/client/LevelRendererMixin.java new file mode 100644 index 0000000..dffc4ec --- /dev/null +++ b/src/main/java/net/frozenblock/lib/spotting_icons/mixin/client/LevelRendererMixin.java @@ -0,0 +1,100 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.spotting_icons.mixin.client; + +import com.llamalad7.mixinextras.sugar.Local; +import com.mojang.blaze3d.vertex.PoseStack; +import net.frozenblock.lib.spotting_icons.impl.EntityRenderDispatcherWithIcon; +import net.minecraft.client.Camera; +import net.minecraft.client.DeltaTracker; +import net.minecraft.client.multiplayer.ClientLevel; +import net.minecraft.client.renderer.*; +import net.minecraft.client.renderer.entity.EntityRenderDispatcher; +import net.minecraft.util.Mth; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.phys.Vec3; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.joml.Matrix4f; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(LevelRenderer.class) +public class LevelRendererMixin { + @Shadow + @Final + private RenderBuffers renderBuffers; + @Shadow + @Final + private EntityRenderDispatcher entityRenderDispatcher; + @Shadow + @Nullable + private ClientLevel level; + + @Inject( + method = "renderLevel", + at = @At( + value = "INVOKE_ASSIGN", + target = "Lnet/minecraft/client/renderer/RenderBuffers;bufferSource()Lnet/minecraft/client/renderer/MultiBufferSource$BufferSource;", + shift = At.Shift.AFTER + ) + ) + public void frozenLib$renderLevel( + DeltaTracker deltaTracker, + boolean renderBlockOutline, + Camera camera, + GameRenderer gameRenderer, + LightTexture lightmapTextureManager, + Matrix4f projectionMatrix, + Matrix4f matrix4f, + CallbackInfo info, + @Local(ordinal = 0) float deltaTime, + @Local PoseStack poseStack + ) { + if (this.level != null) { + MultiBufferSource.BufferSource bufferSource = this.renderBuffers.bufferSource(); + for (Entity entity : this.level.entitiesForRendering()) { + Vec3 vec3 = camera.getPosition(); + double d = vec3.x(); + double e = vec3.y(); + double f = vec3.z(); + if (entity.tickCount == 0) { + entity.xOld = entity.getX(); + entity.yOld = entity.getY(); + entity.zOld = entity.getZ(); + } + this.frozenLib$renderEntityIcon(entity, d, e, f, deltaTime, poseStack, bufferSource); + } + } + } + + @Unique + private void frozenLib$renderEntityIcon(@NotNull Entity entity, double camX, double camY, double camZ, float partialTick, PoseStack poseStack, MultiBufferSource bufferSource) { + double d = Mth.lerp(partialTick, entity.xOld, entity.getX()); + double e = Mth.lerp(partialTick, entity.yOld, entity.getY()); + double f = Mth.lerp(partialTick, entity.zOld, entity.getZ()); + float g = Mth.lerp(partialTick, entity.yRotO, entity.getYRot()); + ((EntityRenderDispatcherWithIcon)this.entityRenderDispatcher).frozenLib$renderIcon(entity, d - camX, e - camY, f - camZ, g, partialTick, poseStack, bufferSource, this.entityRenderDispatcher.getPackedLightCoords(entity, partialTick)); + } + +} diff --git a/src/main/java/net/frozenblock/lib/stencil/api/StencilRenderer.java b/src/main/java/net/frozenblock/lib/stencil/api/StencilRenderer.java new file mode 100644 index 0000000..00eb73b --- /dev/null +++ b/src/main/java/net/frozenblock/lib/stencil/api/StencilRenderer.java @@ -0,0 +1,110 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.stencil.api; + +import com.google.common.collect.Lists; +import com.mojang.blaze3d.vertex.VertexConsumer; +import net.frozenblock.lib.entity.api.rendering.FrozenRenderType; +import net.minecraft.client.renderer.MultiBufferSource; +import net.minecraft.client.renderer.RenderType; +import net.minecraft.util.FastColor; +import net.minecraft.util.Mth; +import net.neoforged.api.distmarker.Dist; +import net.neoforged.api.distmarker.OnlyIn; +import org.joml.Matrix4f; +import org.joml.Vector3f; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +@OnlyIn(Dist.CLIENT) +public class StencilRenderer { + private static final RenderType[] DYNAMIC_LIGHT = new RenderType[]{FrozenRenderType.dynamicLightStencil(), FrozenRenderType.dynamicLightColor()}; + + public static final Triangle[] FACES_SPHERE = StencilRenderer.createNSphere(2); + + public static final Triangle[] FACES_CONE = StencilRenderer.createNCone(12); + + public static void render(Triangle[] triangles, Matrix4f matrix4f, MultiBufferSource multiBufferSource, int i) { + int j = FastColor.ARGB32.red(i); + int k = FastColor.ARGB32.green(i); + int l = FastColor.ARGB32.blue(i); + int m = FastColor.ARGB32.alpha(i); + for (RenderType renderType : DYNAMIC_LIGHT) { + VertexConsumer vertexConsumer = multiBufferSource.getBuffer(renderType); + for (Triangle triangle : triangles) { + vertexConsumer.addVertex(matrix4f, triangle.p0.x, triangle.p0.y, triangle.p0.z).setColor(j, k, l, m); + vertexConsumer.addVertex(matrix4f, triangle.p2.x, triangle.p2.y, triangle.p2.z).setColor(j, k, l, m); + vertexConsumer.addVertex(matrix4f, triangle.p1.x, triangle.p1.y, triangle.p1.z).setColor(j, k, l, m); + } + } + } + + private static List makeOctahedron() { + float f = (float)(1.0 / Math.sqrt(2.0)); + Vector3f[] vector3fs = new Vector3f[]{new Vector3f(0.0f, 1.0f, 0.0f), new Vector3f(0.0f, -1.0f, 0.0f), new Vector3f(-1.0f, 0.0f, -1.0f).mul(f), new Vector3f(1.0f, 0.0f, -1.0f).mul(f), new Vector3f(1.0f, 0.0f, 1.0f).mul(f), new Vector3f(-1.0f, 0.0f, 1.0f).mul(f)}; + ArrayList list = Lists.newArrayList(); + list.add(new Triangle(vector3fs[0], vector3fs[3], vector3fs[4])); + list.add(new Triangle(vector3fs[0], vector3fs[4], vector3fs[5])); + list.add(new Triangle(vector3fs[0], vector3fs[5], vector3fs[2])); + list.add(new Triangle(vector3fs[0], vector3fs[2], vector3fs[3])); + list.add(new Triangle(vector3fs[1], vector3fs[4], vector3fs[3])); + list.add(new Triangle(vector3fs[1], vector3fs[5], vector3fs[4])); + list.add(new Triangle(vector3fs[1], vector3fs[2], vector3fs[5])); + list.add(new Triangle(vector3fs[1], vector3fs[3], vector3fs[2])); + return list; + } + + public static Triangle[] createNSphere(int i) { + List list = StencilRenderer.makeOctahedron(); + for (int j = 0; j < i; ++j) { + int k = list.size(); + for (int l = 0; l < k; ++l) { + list.addAll(list.remove(0).subdivideSpherical()); + } + } + return list.toArray(new Triangle[0]); + } + + public static Triangle[] createNCone(int i) { + float f = (float)Math.PI * 2 / (float)i; + ArrayList list = Lists.newArrayList(); + for (int j = 0; j < i; ++j) { + list.add(new Vector3f(Mth.cos((float)j * f), 0.0f, Mth.sin((float)j * f))); + } + Vector3f vector3f = new Vector3f(0.0f, 0.0f, 0.0f); + Vector3f vector3f2 = new Vector3f(0.0f, -1.0f, 0.0f); + ArrayList list2 = Lists.newArrayList(); + for (int k = 0; k < i; ++k) { + list2.add(new Triangle(list.get(k), list.get((k + 1) % i), vector3f)); + list2.add(new Triangle(list.get((k + 1) % i), list.get(k), vector3f2)); + } + return list2.toArray(new Triangle[0]); + } + + @OnlyIn(Dist.CLIENT) + public record Triangle(Vector3f p0, Vector3f p1, Vector3f p2) { + Collection subdivideSpherical() { + Vector3f vector3f = this.p0.add(this.p1, new Vector3f()).div(2.0f).normalize(); + Vector3f vector3f2 = this.p1.add(this.p2, new Vector3f()).div(2.0f).normalize(); + Vector3f vector3f3 = this.p2.add(this.p0, new Vector3f()).div(2.0f).normalize(); + return List.of(new Triangle(this.p0, vector3f, vector3f3), new Triangle(vector3f, this.p1, vector3f2), new Triangle(vector3f2, this.p2, vector3f3), new Triangle(vector3f, vector3f2, vector3f3)); + } + } +} diff --git a/src/main/java/net/frozenblock/lib/storage/api/HopperUntouchableList.java b/src/main/java/net/frozenblock/lib/storage/api/HopperUntouchableList.java new file mode 100644 index 0000000..1a382a8 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/storage/api/HopperUntouchableList.java @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.storage.api; + +import net.minecraft.world.CompoundContainer; +import net.minecraft.world.Container; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.entity.BlockEntityType; + +import java.util.ArrayList; + +public class HopperUntouchableList { + + public static final ArrayList> BLACKLISTED_TYPES = new ArrayList<>(); + + public static boolean inventoryContainsBlacklisted(Container inventory) { + if (inventory instanceof BlockEntity block) { + return BLACKLISTED_TYPES.contains(block.getType()); + } else if (inventory instanceof CompoundContainer doubleInventory) { + if (doubleInventory.container1 instanceof BlockEntity block) { + if (BLACKLISTED_TYPES.contains(block.getType())) { + return true; + } + } + if (doubleInventory.container2 instanceof BlockEntity block) { + return BLACKLISTED_TYPES.contains(block.getType()); + } + } + return false; + } + +} diff --git a/src/main/java/net/frozenblock/lib/storage/mixin/HopperBlockEntityMixin.java b/src/main/java/net/frozenblock/lib/storage/mixin/HopperBlockEntityMixin.java new file mode 100644 index 0000000..3509e7c --- /dev/null +++ b/src/main/java/net/frozenblock/lib/storage/mixin/HopperBlockEntityMixin.java @@ -0,0 +1,94 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.storage.mixin; + +import com.llamalad7.mixinextras.injector.ModifyExpressionValue; +import com.llamalad7.mixinextras.sugar.Share; +import com.llamalad7.mixinextras.sugar.ref.LocalRef; +import net.frozenblock.lib.storage.api.HopperUntouchableList; +import net.minecraft.core.BlockPos; +import net.minecraft.world.Container; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.entity.Hopper; +import net.minecraft.world.level.block.entity.HopperBlockEntity; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +@Mixin(HopperBlockEntity.class) +public abstract class HopperBlockEntityMixin { + + @ModifyExpressionValue( + method = "ejectItems", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/world/level/block/entity/HopperBlockEntity;getAttachedContainer(Lnet/minecraft/world/level/Level;Lnet/minecraft/core/BlockPos;Lnet/minecraft/world/level/block/entity/HopperBlockEntity;)Lnet/minecraft/world/Container;", + ordinal = 0 + ) + ) + private static Container frozenLib$preventEjectionA(Container original, @Share("frozenLib$container") LocalRef containerRef) { + containerRef.set(original); + return original; + } + + @Inject( + method = "ejectItems", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/world/level/block/entity/HopperBlockEntity;getAttachedContainer(Lnet/minecraft/world/level/Level;Lnet/minecraft/core/BlockPos;Lnet/minecraft/world/level/block/entity/HopperBlockEntity;)Lnet/minecraft/world/Container;", + ordinal = 0, + shift = At.Shift.AFTER + ), + cancellable = true + ) + private static void frozenLib$preventEjectionB(Level level, BlockPos blockPos, HopperBlockEntity hopperBlockEntity, CallbackInfoReturnable info, @Share("frozenLib$container") LocalRef containerRef) { + if (HopperUntouchableList.inventoryContainsBlacklisted(containerRef.get())) { + info.setReturnValue(false); + } + } + + @ModifyExpressionValue( + method = "suckInItems", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/world/level/block/entity/HopperBlockEntity;getSourceContainer(Lnet/minecraft/world/level/Level;Lnet/minecraft/world/level/block/entity/Hopper;Lnet/minecraft/core/BlockPos;Lnet/minecraft/world/level/block/state/BlockState;)Lnet/minecraft/world/Container;", + ordinal = 0 + ) + ) + private static Container frozenLib$preventInsertionA(Container original, @Share("frozenLib$container") LocalRef containerRef) { + containerRef.set(original); + return original; + } + + @Inject( + method = "suckInItems", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/world/level/block/entity/HopperBlockEntity;getSourceContainer(Lnet/minecraft/world/level/Level;Lnet/minecraft/world/level/block/entity/Hopper;Lnet/minecraft/core/BlockPos;Lnet/minecraft/world/level/block/state/BlockState;)Lnet/minecraft/world/Container;", + ordinal = 0, + shift = At.Shift.AFTER + ), + cancellable = true + ) + private static void frozenLib$preventInsertionB(Level level, Hopper hopper, CallbackInfoReturnable info, @Share("frozenLib$container") LocalRef containerRef) { + if (HopperUntouchableList.inventoryContainsBlacklisted(containerRef.get())) { + info.setReturnValue(false); + } + } +} diff --git a/src/main/java/net/frozenblock/lib/tag/api/FrozenBiomeTags.java b/src/main/java/net/frozenblock/lib/tag/api/FrozenBiomeTags.java new file mode 100644 index 0000000..158bae3 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/tag/api/FrozenBiomeTags.java @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.tag.api; + +import lombok.experimental.UtilityClass; +import net.frozenblock.lib.FrozenSharedConstants; +import net.minecraft.core.registries.Registries; +import net.minecraft.tags.TagKey; +import net.minecraft.world.level.biome.Biome; +import org.jetbrains.annotations.NotNull; + +@UtilityClass +public class FrozenBiomeTags { + + public static final TagKey CAN_LIGHTNING_OVERRIDE = of("can_lightning_override"); + public static final TagKey CANNOT_LIGHTNING_OVERRIDE = of("cannot_lightning_override"); + + @NotNull + private static TagKey of(String path) { + return TagKey.create(Registries.BIOME, FrozenSharedConstants.id(path)); + } +} diff --git a/src/main/java/net/frozenblock/lib/tag/api/FrozenBlockTags.java b/src/main/java/net/frozenblock/lib/tag/api/FrozenBlockTags.java new file mode 100644 index 0000000..77132c9 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/tag/api/FrozenBlockTags.java @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.tag.api; + +import lombok.experimental.UtilityClass; +import net.frozenblock.lib.FrozenSharedConstants; +import net.minecraft.core.registries.Registries; +import net.minecraft.tags.TagKey; +import net.minecraft.world.level.block.Block; +import org.jetbrains.annotations.NotNull; + +@UtilityClass +public class FrozenBlockTags { + + public static final TagKey DRIPSTONE_CAN_DRIP_ON = bind("dripstone_can_drip"); + + @NotNull + private static TagKey bind(String path) { + return TagKey.create(Registries.BLOCK, FrozenSharedConstants.id(path)); + } +} diff --git a/src/main/java/net/frozenblock/lib/tag/api/FrozenEntityTags.java b/src/main/java/net/frozenblock/lib/tag/api/FrozenEntityTags.java new file mode 100644 index 0000000..d0a032f --- /dev/null +++ b/src/main/java/net/frozenblock/lib/tag/api/FrozenEntityTags.java @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.tag.api; + +import lombok.experimental.UtilityClass; +import net.frozenblock.lib.FrozenSharedConstants; +import net.minecraft.core.registries.Registries; +import net.minecraft.tags.TagKey; +import net.minecraft.world.entity.EntityType; +import org.jetbrains.annotations.NotNull; + +@UtilityClass +public class FrozenEntityTags { + + public static final TagKey> CREEPER_IGNORES = bind("creeper_ignores"); + + @NotNull + private static TagKey> bind(String path) { + return TagKey.create(Registries.ENTITY_TYPE, FrozenSharedConstants.id(path)); + } +} diff --git a/src/main/java/net/frozenblock/lib/tag/api/FrozenItemTags.java b/src/main/java/net/frozenblock/lib/tag/api/FrozenItemTags.java new file mode 100644 index 0000000..1bc32be --- /dev/null +++ b/src/main/java/net/frozenblock/lib/tag/api/FrozenItemTags.java @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.tag.api; + +import lombok.experimental.UtilityClass; +import net.frozenblock.lib.FrozenSharedConstants; +import net.minecraft.core.registries.Registries; +import net.minecraft.tags.TagKey; +import net.minecraft.world.item.Item; +import org.jetbrains.annotations.NotNull; + +@UtilityClass +public class FrozenItemTags { + public static final TagKey NO_USE_GAME_EVENTS = bind("dont_emit_use_game_events"); + public static final TagKey ALWAYS_SAVE_COOLDOWNS = bind("always_save_cooldowns"); + + @NotNull + private static TagKey bind(String path) { + return TagKey.create(Registries.ITEM, FrozenSharedConstants.id(path)); + } +} diff --git a/src/main/java/net/frozenblock/lib/tag/api/TagKeyArgument.java b/src/main/java/net/frozenblock/lib/tag/api/TagKeyArgument.java new file mode 100644 index 0000000..d794f74 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/tag/api/TagKeyArgument.java @@ -0,0 +1,134 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.tag.api; + +import com.google.gson.JsonObject; +import com.mojang.brigadier.StringReader; +import com.mojang.brigadier.arguments.ArgumentType; +import com.mojang.brigadier.context.CommandContext; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import com.mojang.brigadier.exceptions.DynamicCommandExceptionType; +import com.mojang.brigadier.suggestion.Suggestions; +import com.mojang.brigadier.suggestion.SuggestionsBuilder; +import net.minecraft.commands.CommandBuildContext; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.commands.SharedSuggestionProvider; +import net.minecraft.commands.synchronization.ArgumentTypeInfo; +import net.minecraft.core.Registry; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.resources.ResourceKey; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.tags.TagKey; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; + +public class TagKeyArgument implements ArgumentType> { + private static final Collection EXAMPLES = Arrays.asList("foo", "foo:bar", "012", "#skeletons", "#minecraft:skeletons"); + final ResourceKey> registryKey; + + public TagKeyArgument(ResourceKey> registryKey) { + this.registryKey = registryKey; + } + + public static TagKeyArgument tagKey(ResourceKey> registryKey) { + return new TagKeyArgument<>(registryKey); + } + + public static Result getTagKey( + CommandContext context, String argument, ResourceKey> registryKey, DynamicCommandExceptionType dynamicCommandExceptionType + ) throws CommandSyntaxException { + Result result = context.getArgument(argument, Result.class); + Optional> optional = result.cast(registryKey); + return optional.orElseThrow(() -> dynamicCommandExceptionType.create(result)); + } + + public Result parse(StringReader reader) throws CommandSyntaxException { + int cursor = reader.getCursor(); + + try { + reader.skip(); + ResourceLocation resourceLocation = ResourceLocation.read(reader); + return new Result<>(TagKey.create(this.registryKey, resourceLocation)); + } catch (CommandSyntaxException var4) { + reader.setCursor(cursor); + throw var4; + } + } + + @Override + public CompletableFuture listSuggestions(CommandContext commandContext, SuggestionsBuilder suggestionsBuilder) { + Object var4 = commandContext.getSource(); + return var4 instanceof SharedSuggestionProvider sharedSuggestionProvider + ? sharedSuggestionProvider.suggestRegistryElements(this.registryKey, SharedSuggestionProvider.ElementSuggestionType.TAGS, suggestionsBuilder, commandContext) + : suggestionsBuilder.buildFuture(); + } + + @Override + public Collection getExamples() { + return EXAMPLES; + } + + public static class Info implements ArgumentTypeInfo, Info.Template> { + public void serializeToNetwork(Template template, FriendlyByteBuf buffer) { + buffer.writeResourceLocation(template.registryKey.location()); + } + + public Template deserializeFromNetwork(FriendlyByteBuf buffer) { + ResourceLocation resourceLocation = buffer.readResourceLocation(); + return new Template(ResourceKey.createRegistryKey(resourceLocation)); + } + + public void serializeToJson(Template template, JsonObject json) { + json.addProperty("registry", template.registryKey.location().toString()); + } + + public Template unpack(TagKeyArgument argument) { + return new Template(argument.registryKey); + } + + public final class Template implements ArgumentTypeInfo.Template> { + final ResourceKey> registryKey; + + Template(ResourceKey> registryKey) { + this.registryKey = registryKey; + } + + public TagKeyArgument instantiate(CommandBuildContext context) { + return new TagKeyArgument<>(this.registryKey); + } + + @Override + public ArgumentTypeInfo, ?> type() { + return Info.this; + } + } + } + + record Result(TagKey key) { + public Optional> cast(ResourceKey> registryKey) { + return this.key.cast(registryKey).map(Result::new); + } + + public String asPrintable() { + return "#" + this.key.location(); + } + } +} diff --git a/src/main/java/net/frozenblock/lib/tag/api/TagListCommand.java b/src/main/java/net/frozenblock/lib/tag/api/TagListCommand.java new file mode 100644 index 0000000..b434e02 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/tag/api/TagListCommand.java @@ -0,0 +1,161 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.tag.api; + +import com.mojang.brigadier.CommandDispatcher; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import com.mojang.brigadier.exceptions.DynamicCommandExceptionType; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.commands.Commands; +import net.minecraft.core.Holder; +import net.minecraft.core.HolderSet; +import net.minecraft.core.Registry; +import net.minecraft.core.registries.Registries; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceKey; + +public class TagListCommand { + private TagListCommand() {} + + private static final DynamicCommandExceptionType ERROR_TAG_INVALID = new DynamicCommandExceptionType( + type -> Component.translatable("commands.frozenlib.taglist.tag.invalid", type) + ); + + public static void register(CommandDispatcher dispatcher) { + dispatcher.register( + Commands.literal("taglist") + .requires(source -> source.hasPermission(2)) + .then( + Commands.literal("biome") + .then( + Commands.argument("biome", TagKeyArgument.tagKey(Registries.BIOME)) + .executes( + context -> list( + context.getSource(), + Registries.BIOME, + TagKeyArgument.getTagKey(context, "biome", Registries.BIOME, ERROR_TAG_INVALID) + ) + ) + ) + ) + .then( + Commands.literal("block") + .then( + Commands.argument("block", TagKeyArgument.tagKey(Registries.BLOCK)) + .executes( + context -> list( + context.getSource(), + Registries.BLOCK, + TagKeyArgument.getTagKey(context, "block", Registries.BLOCK, ERROR_TAG_INVALID) + ) + ) + ) + ) + .then( + Commands.literal("entity_type") + .then( + Commands.argument("entity_type", TagKeyArgument.tagKey(Registries.ENTITY_TYPE)) + .executes( + context -> list( + context.getSource(), + Registries.ENTITY_TYPE, + TagKeyArgument.getTagKey(context, "entity_type", Registries.ENTITY_TYPE, ERROR_TAG_INVALID) + ) + ) + ) + ) + .then( + Commands.literal("fluid") + .then( + Commands.argument("fluid", TagKeyArgument.tagKey(Registries.FLUID)) + .executes( + context -> list( + context.getSource(), + Registries.FLUID, + TagKeyArgument.getTagKey(context, "fluid", Registries.FLUID, ERROR_TAG_INVALID) + ) + ) + ) + ) + .then( + Commands.literal("instrument") + .then( + Commands.argument("instrument", TagKeyArgument.tagKey(Registries.INSTRUMENT)) + .executes( + context -> list( + context.getSource(), + Registries.INSTRUMENT, + TagKeyArgument.getTagKey(context, "instrument", Registries.INSTRUMENT, ERROR_TAG_INVALID) + ) + ) + ) + ) + .then( + Commands.literal("item") + .then( + Commands.argument("item", TagKeyArgument.tagKey(Registries.ITEM)) + .executes( + context -> list( + context.getSource(), + Registries.ITEM, + TagKeyArgument.getTagKey(context, "item", Registries.ITEM, ERROR_TAG_INVALID) + ) + ) + ) + ) + .then( + Commands.literal("structure") + .then( + Commands.argument("structure", TagKeyArgument.tagKey(Registries.STRUCTURE)) + .executes( + context -> list( + context.getSource(), + Registries.STRUCTURE, + TagKeyArgument.getTagKey(context, "structure", Registries.STRUCTURE, ERROR_TAG_INVALID) + ) + ) + ) + ) + ); + } + + private static int list(CommandSourceStack source, ResourceKey> registryKey, TagKeyArgument.Result tag) throws CommandSyntaxException { + Registry registry = source.getLevel().registryAccess().registryOrThrow(registryKey); + String printable = tag.asPrintable(); + HolderSet.Named holderSet = registry.getTag(tag.key()).orElseThrow( + () -> ERROR_TAG_INVALID.create(printable) + ); + int size = holderSet.size(); + + for (Holder value : holderSet) { + if (holderSet.contains(value)) { + source.sendSuccess( + () -> Component.literal( + value.unwrapKey().orElseThrow().location().toString() + ), + true + ); + } + } + source.sendSuccess( + () -> Component.translatable("commands.frozenlib.taglist.footer", size, printable), + true + ); + return size; + } +} diff --git a/src/main/java/net/frozenblock/lib/tag/api/TagUtils.java b/src/main/java/net/frozenblock/lib/tag/api/TagUtils.java new file mode 100644 index 0000000..2d7d8d5 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/tag/api/TagUtils.java @@ -0,0 +1,96 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.tag.api; + +import lombok.experimental.UtilityClass; +import net.frozenblock.lib.math.api.AdvancedMath; +import net.minecraft.core.Holder; +import net.minecraft.core.Registry; +import net.minecraft.core.RegistryAccess; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.resources.ResourceKey; +import net.minecraft.tags.TagKey; +import net.minecraft.util.RandomSource; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.Objects; +import java.util.Optional; + +/** + * Contains methods related to {@link TagKey}s. + */ +@UtilityClass +public class TagUtils { + + @Nullable + public static T getRandomEntry(TagKey tag) { + return getRandomEntry(AdvancedMath.random(), tag); + } + + @SuppressWarnings("unchecked") + @Nullable + public static T getRandomEntry(RandomSource random, TagKey tag) { + Optional> maybeRegistry = BuiltInRegistries.REGISTRY.getOptional(tag.registry().location()); + Objects.requireNonNull(random); + Objects.requireNonNull(tag); + + if (maybeRegistry.isPresent()) { + Registry registry = (Registry) maybeRegistry.get(); + if (tag.isFor(registry.key())) { + ArrayList entries = new ArrayList<>(); + for (Holder entry : registry.getTagOrEmpty(tag)) { + var optionalKey = entry.unwrapKey(); + if (optionalKey.isPresent()) { + var key = optionalKey.get(); + registry.getOptional(key).ifPresent(entries::add); + } + } + if (!entries.isEmpty()) { + return entries.get(random.nextInt(entries.size())); + } + } + } + return null; + } + + public static boolean isIn(TagKey tagKey, T entry) { + return isIn(null, tagKey, entry); + } + + public static boolean isIn(@Nullable RegistryAccess registryManager, TagKey tagKey, T entry) { + Objects.requireNonNull(tagKey); + Objects.requireNonNull(entry); + Optional maybeRegistry; + if (registryManager != null) { + maybeRegistry = registryManager.registry(tagKey.registry()); + } else { + maybeRegistry = BuiltInRegistries.REGISTRY.getOptional(tagKey.registry().location()); + } + + if (maybeRegistry.isPresent() && tagKey.isFor(((Registry)maybeRegistry.get()).key())) { + Registry registry = (Registry)maybeRegistry.get(); + Optional> maybeKey = registry.getResourceKey(entry); + if (maybeKey.isPresent()) { + return registry.getHolderOrThrow((ResourceKey)maybeKey.get()).is(tagKey); + } + } + + return false; + } +} diff --git a/src/main/java/net/frozenblock/lib/tag/mixin/CreeperMixin.java b/src/main/java/net/frozenblock/lib/tag/mixin/CreeperMixin.java new file mode 100644 index 0000000..8c12f17 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/tag/mixin/CreeperMixin.java @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.tag.mixin; + +import net.frozenblock.lib.tag.api.FrozenEntityTags; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.entity.monster.Creeper; +import org.jetbrains.annotations.Nullable; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(Creeper.class) +public class CreeperMixin { + + @Inject(method = "setTarget", at = @At("HEAD"), cancellable = true) + public void frozenLib$ignoreTag(@Nullable LivingEntity livingEntity, CallbackInfo info) { + if (livingEntity != null) { + if (livingEntity.getType().is(FrozenEntityTags.CREEPER_IGNORES)) { + info.cancel(); + } + } + } + +} diff --git a/src/main/java/net/frozenblock/lib/weather/mixin/LightningOverrideMixin.java b/src/main/java/net/frozenblock/lib/weather/mixin/LightningOverrideMixin.java new file mode 100644 index 0000000..a913dca --- /dev/null +++ b/src/main/java/net/frozenblock/lib/weather/mixin/LightningOverrideMixin.java @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.weather.mixin; + +import com.llamalad7.mixinextras.injector.wrapoperation.Operation; +import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; +import net.frozenblock.lib.tag.api.FrozenBiomeTags; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Holder; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.level.biome.Biome; +import net.minecraft.world.level.levelgen.Heightmap; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; + +@Mixin(ServerLevel.class) +public final class LightningOverrideMixin { + + @WrapOperation( + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/server/level/ServerLevel;isRainingAt(Lnet/minecraft/core/BlockPos;)Z"), + method = "tickChunk" + ) + public boolean frozenLib$getLightningTarget(ServerLevel serverLevel, BlockPos pos, Operation operation) { + return this.frozenLib$newLightningCheck(pos); + } + + @Unique + public boolean frozenLib$newLightningCheck(BlockPos position) { + ServerLevel level = ServerLevel.class.cast(this); + if (!level.isRaining() || !level.canSeeSky(position)) { + return false; + } + if (level.getHeightmapPos(Heightmap.Types.MOTION_BLOCKING, position).getY() > position.getY()) { + return false; + } + Holder biome = level.getBiome(position); + return ( + (biome.value().hasPrecipitation() && biome.value().warmEnoughToRain(position)) + || biome.is(FrozenBiomeTags.CAN_LIGHTNING_OVERRIDE) + ) && !biome.is(FrozenBiomeTags.CANNOT_LIGHTNING_OVERRIDE); + } + +} diff --git a/src/main/java/net/frozenblock/lib/wind/api/ClientWindManager.java b/src/main/java/net/frozenblock/lib/wind/api/ClientWindManager.java new file mode 100644 index 0000000..da3fbca --- /dev/null +++ b/src/main/java/net/frozenblock/lib/wind/api/ClientWindManager.java @@ -0,0 +1,269 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.wind.api; + +import com.mojang.datafixers.util.Pair; +import it.unimi.dsi.fastutil.objects.ObjectArrayList; +import net.frozenblock.lib.config.frozenlib_config.FrozenLibConfig; +import net.frozenblock.lib.math.api.AdvancedMath; +import net.frozenblock.lib.math.api.EasyNoiseSampler; +import net.minecraft.client.multiplayer.ClientLevel; +import net.minecraft.core.BlockPos; +import net.minecraft.util.Mth; +import net.minecraft.util.RandomSource; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.LightLayer; +import net.minecraft.world.level.levelgen.synth.ImprovedNoise; +import net.minecraft.world.phys.Vec3; +import net.neoforged.api.distmarker.Dist; +import net.neoforged.api.distmarker.OnlyIn; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Supplier; + +@OnlyIn(Dist.CLIENT) +public final class ClientWindManager { + public static final List EXTENSIONS = new ObjectArrayList<>(); + private static final List> WIND_DISTURBANCES_A = new ArrayList<>(); + private static final List> WIND_DISTURBANCES_B = new ArrayList<>(); + private static boolean isSwitched; + + public static List> getWindDisturbances() { + return !isSwitched ? WIND_DISTURBANCES_A : WIND_DISTURBANCES_B; + } + + public static List> getWindDisturbanceStash() { + return isSwitched ? WIND_DISTURBANCES_A : WIND_DISTURBANCES_B; + } + + public static void clearWindDisturbances() { + getWindDisturbances().clear(); + } + + public static void clearAllWindDisturbances() { + getWindDisturbances().clear(); + getWindDisturbanceStash().clear(); + } + + public static void clearAndSwitchWindDisturbances() { + clearWindDisturbances(); + isSwitched = !isSwitched; + } + + public synchronized static void addWindDisturbance(@NotNull WindDisturbance windDisturbance) { + getWindDisturbanceStash().add(windDisturbance); + } + + public static long time; + public static boolean overrideWind; + public static Vec3 commandWind = Vec3.ZERO; + + public static double prevWindX; + public static double prevWindY; + public static double prevWindZ; + public static double windX; + public static double windY; + public static double windZ; + + public static double prevLaggedWindX; + public static double prevLaggedWindY; + public static double prevLaggedWindZ; + public static double laggedWindX; + public static double laggedWindY; + public static double laggedWindZ; + public static boolean hasInitialized; + + public static void addExtension(@Nullable Supplier extension) { + if (extension != null) addExtension(extension.get()); + } + + public static void addExtension(@Nullable ClientWindManagerExtension extension) { + if (extension != null) EXTENSIONS.add(extension); + } + + public static ImprovedNoise noise = EasyNoiseSampler.createXoroNoise(0L); + + public static void setSeed(long seed) { + noise = EasyNoiseSampler.createXoroNoise(seed); + } + + public static double getWindX(float partialTick) { + return Mth.lerp(partialTick, prevWindX, windX); + } + + public static double getWindY(float partialTick) { + return Mth.lerp(partialTick, prevWindY, windY); + } + + public static double getWindZ(float partialTick) { + return Mth.lerp(partialTick, prevWindZ, windZ); + } + + public static boolean shouldUseWind() { + return hasInitialized || FrozenLibConfig.USE_WIND_ON_NON_FROZEN_SERVERS; + } + + public static void tick(@NotNull ClientLevel level) { + if (level.tickRateManager().runsNormally()) { + float thunderLevel = level.getThunderLevel(1F) * 0.03F; + //WIND + prevWindX = windX; + prevWindY = windY; + prevWindZ = windZ; + time += 1; + double calcTime = time * 0.0005D; + double calcTimeY = time * 0.00035D; + Vec3 vec3 = sampleVec3(noise, calcTime, calcTimeY, calcTime); + windX = vec3.x + (vec3.x * thunderLevel); + windY = vec3.y + (vec3.y * thunderLevel); + windZ = vec3.z + (vec3.z * thunderLevel); + + //LAGGED WIND + prevLaggedWindX = laggedWindX; + prevLaggedWindY = laggedWindY; + prevLaggedWindZ = laggedWindZ; + double calcLaggedTime = (time - 40D) * 0.0005D; + double calcLaggedTimeY = (time - 60D) * 0.00035D; + Vec3 laggedVec = sampleVec3(noise, calcLaggedTime, calcLaggedTimeY, calcLaggedTime); + laggedWindX = laggedVec.x + (laggedVec.x * thunderLevel); + laggedWindY = laggedVec.y + (laggedVec.y * thunderLevel); + laggedWindZ = laggedVec.z + (laggedVec.z * thunderLevel); + + // EXTENSIONS + for (ClientWindManagerExtension extension : EXTENSIONS) { + extension.baseTick(); + extension.clientTick(); + } + + if (!hasInitialized && time > 80D && FrozenLibConfig.USE_WIND_ON_NON_FROZEN_SERVERS) { + RandomSource randomSource = AdvancedMath.random(); + noise = EasyNoiseSampler.createXoroNoise(randomSource.nextLong()); + time = randomSource.nextLong(); + hasInitialized = true; + } + } + } + + @NotNull + public static Vec3 getWindMovement(@NotNull Level level, @NotNull BlockPos pos) { + return getWindMovement(level, Vec3.atBottomCenterOf(pos)); + } + + @NotNull + public static Vec3 getWindMovement(@NotNull Level level, @NotNull BlockPos pos, double scale) { + return getWindMovement(level, Vec3.atBottomCenterOf(pos), scale); + } + + @NotNull + public static Vec3 getWindMovement(@NotNull Level level, @NotNull BlockPos pos, double scale, double clamp) { + return getWindMovement(level, Vec3.atBottomCenterOf(pos), scale, clamp); + } + + @NotNull + public static Vec3 getWindMovement(@NotNull Level level, @NotNull Vec3 pos) { + return getWindMovement(level, pos, 1D); + } + + @NotNull + public static Vec3 getWindMovement(@NotNull Level level, @NotNull Vec3 pos, double scale) { + return getWindMovement(level, pos, scale, Double.MAX_VALUE); + } + + @NotNull + public static Vec3 getWindMovement(@NotNull Level level, @NotNull Vec3 pos, double scale, double clamp) { + return getWindMovement(level, pos, scale, clamp, 1D); + } + + @NotNull + public static Vec3 getWindMovement(@NotNull Level level, @NotNull Vec3 pos, double scale, double clamp, double windDisturbanceScale) { + double brightness = level.getBrightness(LightLayer.SKY, BlockPos.containing(pos)); + double windScale = (Math.max((brightness - (Math.max(15 - brightness, 0))), 0) * 0.0667D); + Pair disturbance = WindManager.calculateWindDisturbance(getWindDisturbances(), level, pos); + double disturbanceAmount = disturbance.getFirst(); + Vec3 windDisturbance = disturbance.getSecond(); + double newWindX = Mth.lerp(disturbanceAmount, windX * windScale, windDisturbance.x * windDisturbanceScale) * scale; + double newWindY = Mth.lerp(disturbanceAmount, windY * windScale, windDisturbance.y * windDisturbanceScale) * scale; + double newWindZ = Mth.lerp(disturbanceAmount, windZ * windScale, windDisturbance.z * windDisturbanceScale) * scale; + return new Vec3( + Mth.clamp(newWindX, -clamp, clamp), + Mth.clamp(newWindY, -clamp, clamp), + Mth.clamp(newWindZ, -clamp, clamp) + ); + } + + @NotNull + public static Vec3 getWindMovement3D(@NotNull BlockPos pos, double stretch) { + return getWindMovement3D(Vec3.atBottomCenterOf(pos), stretch); + } + + @NotNull + public static Vec3 getWindMovement3D(@NotNull BlockPos pos, double scale, double stretch) { + return getWindMovement3D(Vec3.atBottomCenterOf(pos), scale, stretch); + } + + @NotNull + public static Vec3 getWindMovement3D(@NotNull BlockPos pos, double scale, double clamp, double stretch) { + return getWindMovement3D(Vec3.atBottomCenterOf(pos), scale, clamp, stretch); + } + + @NotNull + public static Vec3 getWindMovement3D(@NotNull Vec3 pos, double stretch) { + return getWindMovement3D(pos, 1D, stretch); + } + + @NotNull + public static Vec3 getWindMovement3D(@NotNull Vec3 pos, double scale, double stretch) { + return getWindMovement3D(pos, scale, Double.MAX_VALUE, stretch); + } + + @NotNull + public static Vec3 getWindMovement3D(@NotNull Vec3 pos, double scale, double clamp, double stretch) { + Vec3 wind = sample3D(pos, stretch); + return new Vec3(Mth.clamp((wind.x()) * scale, -clamp, clamp), + Mth.clamp((wind.y()) * scale, -clamp, clamp), + Mth.clamp((wind.z()) * scale, -clamp, clamp)); + } + + @NotNull + public static Vec3 sampleVec3(@NotNull ImprovedNoise sampler, double x, double y, double z) { + if (shouldUseWind()) { + if (!overrideWind) { + double windX = sampler.noise(x, 0D, 0D); + double windY = sampler.noise(0D, y, 0D); + double windZ = sampler.noise(0D, 0D, z); + return new Vec3(windX, windY, windZ); + } + return commandWind; + } else { + return Vec3.ZERO; + } + } + + @NotNull + public static Vec3 sample3D(@NotNull Vec3 pos, double stretch) { + double sampledTime = time * 0.1D; + double xyz = pos.x() + pos.y() + pos.z(); + double windX = noise.noise((xyz + sampledTime) * stretch, 0D, 0D); + double windY = noise.noise(0D, (xyz + sampledTime) * stretch, 0D); + double windZ = noise.noise(0D, 0D, (xyz + sampledTime) * stretch); + return new Vec3(windX, windY, windZ); + } +} diff --git a/src/main/java/net/frozenblock/lib/wind/api/ClientWindManagerExtension.java b/src/main/java/net/frozenblock/lib/wind/api/ClientWindManagerExtension.java new file mode 100644 index 0000000..fd0d85d --- /dev/null +++ b/src/main/java/net/frozenblock/lib/wind/api/ClientWindManagerExtension.java @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.wind.api; + +import net.minecraft.client.Minecraft; +import net.minecraft.network.FriendlyByteBuf; +import net.neoforged.api.distmarker.Dist; +import net.neoforged.api.distmarker.OnlyIn; + +@OnlyIn(Dist.CLIENT) +public interface ClientWindManagerExtension { + + void clientTick(); + + void baseTick(); + + void receiveSyncPacket(FriendlyByteBuf byteBuf, Minecraft minecraft); +} diff --git a/src/main/java/net/frozenblock/lib/wind/api/WindDisturbance.java b/src/main/java/net/frozenblock/lib/wind/api/WindDisturbance.java new file mode 100644 index 0000000..b3c7ff4 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/wind/api/WindDisturbance.java @@ -0,0 +1,122 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.wind.api; + +import com.google.common.collect.ImmutableList; +import net.frozenblock.lib.registry.api.FrozenRegistry; +import net.frozenblock.lib.wind.impl.networking.WindDisturbancePacket; +import net.minecraft.core.BlockPos; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.level.ChunkTrackingView; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.phys.AABB; +import net.minecraft.world.phys.Vec3; +import org.jetbrains.annotations.NotNull; + +import java.util.Optional; + +public class WindDisturbance { + public static final DisturbanceResult DUMMY_RESULT = new DisturbanceResult(0D, 0D, Vec3.ZERO); + + private final Optional source; + private final Vec3 origin; + private final AABB affectedArea; + private final WindDisturbanceLogic disturbanceLogic; + + public WindDisturbance(Optional source, Vec3 origin, AABB affectedArea, WindDisturbanceLogic disturbanceLogic) { + this.source = source; + this.origin = origin; + this.affectedArea = affectedArea; + this.disturbanceLogic = disturbanceLogic; + } + + public DisturbanceResult calculateDisturbanceResult(Level level, Vec3 windTarget) { + if (this.affectedArea.contains(windTarget)) { + DisturbanceResult disturbanceResult = this.disturbanceLogic.getLogic().calculateDisturbanceResult( + this.source, + level, + this.origin, + this.affectedArea, + windTarget + ); + if (disturbanceResult != null) { + return disturbanceResult; + } + } + return DUMMY_RESULT; + } + + public boolean isWithinViewDistance(@NotNull ChunkTrackingView chunkTrackingView) { + for (double xCorner : ImmutableList.of(this.affectedArea.minX, this.affectedArea.maxX)) { + for (double zCorner : ImmutableList.of(this.affectedArea.minZ, this.affectedArea.maxZ)) { + ChunkPos chunkPos = new ChunkPos(BlockPos.containing(xCorner, 0, zCorner)); + if (chunkTrackingView.isInViewDistance(chunkPos.x, chunkPos.z)) { + return true; + } + } + } + return false; + } + + public Optional toPacket() { + ResourceLocation resourceLocation = Optional.ofNullable(FrozenRegistry.WIND_DISTURBANCE_LOGIC.getKey(this.disturbanceLogic)) + .orElseGet(() -> FrozenRegistry.WIND_DISTURBANCE_LOGIC_UNSYNCED.getKey(this.disturbanceLogic)); + + if (resourceLocation != null) { + return Optional.of( + new WindDisturbancePacket( + this.affectedArea, + this.origin, + this.getSourceTypeFromSource(), + resourceLocation, + this.encodePosOrID() + ) + ); + } + + return Optional.empty(); + } + + private WindDisturbanceLogic.SourceType getSourceTypeFromSource() { + if (this.source.isPresent()) { + if (this.source.get() instanceof Entity) { + return WindDisturbanceLogic.SourceType.ENTITY; + } else if (this.source.get() instanceof BlockEntity) { + return WindDisturbanceLogic.SourceType.BLOCK_ENTITY; + } + } + return WindDisturbanceLogic.SourceType.NONE; + } + + private long encodePosOrID() { + if (this.source.isPresent()) { + if (this.source.get() instanceof Entity entity) { + return entity.getId(); + } else if (this.source.get() instanceof BlockEntity blockEntity) { + return blockEntity.getBlockPos().asLong(); + } + } + return 0L; + } + + public record DisturbanceResult(double strength, double weight, Vec3 wind) { + } +} diff --git a/src/main/java/net/frozenblock/lib/wind/api/WindDisturbanceLogic.java b/src/main/java/net/frozenblock/lib/wind/api/WindDisturbanceLogic.java new file mode 100644 index 0000000..4269a47 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/wind/api/WindDisturbanceLogic.java @@ -0,0 +1,146 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.wind.api; + +import net.frozenblock.lib.FrozenSharedConstants; +import net.frozenblock.lib.math.api.AdvancedMath; +import net.frozenblock.lib.registry.api.FrozenRegistry; +import net.minecraft.core.Registry; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.util.Mth; +import net.minecraft.world.entity.monster.breeze.Breeze; +import net.minecraft.world.entity.projectile.windcharge.AbstractWindCharge; +import net.minecraft.world.level.Level; +import net.minecraft.world.phys.AABB; +import net.minecraft.world.phys.Vec3; +import net.neoforged.neoforge.registries.RegisterEvent; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; + +import java.util.Optional; + +public final class WindDisturbanceLogic { + public static final ResourceLocation DEFAULT_ID = FrozenSharedConstants.id("default"); + public static final ResourceLocation BREEZE = FrozenSharedConstants.id("breeze"); + public static final ResourceLocation WIND_CHARGE = FrozenSharedConstants.id("wind_charge"); + public static final WindDisturbanceLogic DUMMY_LOGIC = new WindDisturbanceLogic((source, level, windOrigin, affectedArea, windTarget) -> WindDisturbance.DUMMY_RESULT); + private final DisturbanceLogic disturbanceLogic; + + public WindDisturbanceLogic(DisturbanceLogic disturbanceLogic) { + this.disturbanceLogic = disturbanceLogic; + } + + public DisturbanceLogic getLogic() { + return this.disturbanceLogic; + } + + @FunctionalInterface + public interface DisturbanceLogic { + WindDisturbance.DisturbanceResult calculateDisturbanceResult(Optional source, Level level, Vec3 windOrigin, AABB affectedArea, Vec3 windTarget); + } + + public static Optional getWindDisturbanceLogic(ResourceLocation id) { + if (id != null) { + if (FrozenRegistry.WIND_DISTURBANCE_LOGIC.containsKey(id)) { + WindDisturbanceLogic disturbanceLogic = FrozenRegistry.WIND_DISTURBANCE_LOGIC.get(id); + if (disturbanceLogic != null) { + return Optional.of(disturbanceLogic); + } + } else if (FrozenRegistry.WIND_DISTURBANCE_LOGIC_UNSYNCED.containsKey(id)) { + WindDisturbanceLogic disturbanceLogic = FrozenRegistry.WIND_DISTURBANCE_LOGIC_UNSYNCED.get(id); + if (disturbanceLogic != null) { + return Optional.of(disturbanceLogic); + } + } + FrozenSharedConstants.LOGGER.error("Unable to find wind disturbance logic " + id + "!"); + } + return Optional.empty(); + } + + @NotNull + @Contract(pure = true) + public static DisturbanceLogic defaultPredicate() { + return (source, level, windOrigin, affectedArea, windTarget) -> WindDisturbance.DUMMY_RESULT; + } + + public static void init(RegisterEvent.RegisterHelper> registry) { + registry.register(DEFAULT_ID, new WindDisturbanceLogic<>(defaultPredicate())); + registry.register(BREEZE, new WindDisturbanceLogic<>(breeze())); + registry.register(WIND_CHARGE, new WindDisturbanceLogic<>(windCharge())); + } + + public enum SourceType { + ENTITY, + BLOCK_ENTITY, + NONE + } + + private static final double WIND_RANGE_BREEZE = 6D; + private static final double WIND_RANGE_WIND_CHARGE = 5D; + + @NotNull + @Contract(pure = true) + private static DisturbanceLogic breeze() { + return (source, level, windOrigin, affectedArea, windTarget) -> { + if (source.isPresent()) { + double distance = windOrigin.distanceTo(windTarget); + if (distance <= WIND_RANGE_BREEZE) { + Vec3 breezeLookVec = source.get().getForward(); + Vec3 differenceInPoses = windOrigin.subtract(windTarget); + double scaledDistance = (WIND_RANGE_BREEZE - distance) / WIND_RANGE_BREEZE; + double strengthFromDistance = Mth.clamp((WIND_RANGE_BREEZE - distance) / (WIND_RANGE_BREEZE * 0.75D), 0D, 1D); + double angleBetween = AdvancedMath.getAngleBetweenXZ(breezeLookVec, differenceInPoses); + + double x = Math.cos((angleBetween * Math.PI) / 180D); + double z = -Math.sin((angleBetween * Math.PI) / 180D); + x = -Mth.lerp(scaledDistance, (x - (differenceInPoses.x * 0.45D)) * 0.5D, x); + z = -Mth.lerp(scaledDistance, (z - (differenceInPoses.z * 0.45D)) * 0.5D, z); + + Vec3 windVec = new Vec3(x, strengthFromDistance, z).scale(1D); + return new WindDisturbance.DisturbanceResult( + strengthFromDistance, + WIND_RANGE_BREEZE - distance, + windVec + ); + } + } + return null; + }; + } + + @NotNull + @Contract(pure = true) + private static DisturbanceLogic windCharge() { + return (source, level, windOrigin, affectedArea, windTarget) -> { + if (source.isPresent()) { + double distance = windOrigin.distanceTo(windTarget); + if (distance <= WIND_RANGE_WIND_CHARGE) { + Vec3 chargeMovement = source.get().getDeltaMovement(); + double strengthFromDistance = Mth.clamp((WIND_RANGE_WIND_CHARGE - distance) / (WIND_RANGE_WIND_CHARGE * 0.5D), 0D, 1D); + Vec3 windVec = new Vec3(chargeMovement.x, chargeMovement.y, chargeMovement.z).scale(3D * strengthFromDistance); + return new WindDisturbance.DisturbanceResult( + strengthFromDistance, + (WIND_RANGE_WIND_CHARGE - distance) * 2D, + windVec + ); + } + } + return null; + }; + } +} diff --git a/src/main/java/net/frozenblock/lib/wind/api/WindDisturbingEntity.java b/src/main/java/net/frozenblock/lib/wind/api/WindDisturbingEntity.java new file mode 100644 index 0000000..0559135 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/wind/api/WindDisturbingEntity.java @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.wind.api; + +import net.minecraft.resources.ResourceLocation; + +public interface WindDisturbingEntity { + + ResourceLocation frozenLib$getWindDisturbanceLogicID(); + + double frozenLib$getWindWidth(); + + double frozenLib$getWindHeight(); + + double frozenLib$getWindAreaYOffset(); + + boolean frozenLib$useSyncPacket(); + +} diff --git a/src/main/java/net/frozenblock/lib/wind/api/WindManager.java b/src/main/java/net/frozenblock/lib/wind/api/WindManager.java new file mode 100644 index 0000000..33e1923 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/wind/api/WindManager.java @@ -0,0 +1,392 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.wind.api; + +import com.mojang.datafixers.util.Pair; +import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; +import it.unimi.dsi.fastutil.objects.ObjectArrayList; +import net.frozenblock.lib.math.api.EasyNoiseSampler; +import net.frozenblock.lib.wind.impl.WindManagerInterface; +import net.frozenblock.lib.wind.impl.WindStorage; +import net.frozenblock.lib.wind.impl.networking.WindDisturbancePacket; +import net.frozenblock.lib.wind.impl.networking.WindSyncPacket; +import net.minecraft.core.BlockPos; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.util.Mth; +import net.minecraft.util.datafix.DataFixTypes; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.LightLayer; +import net.minecraft.world.level.levelgen.synth.ImprovedNoise; +import net.minecraft.world.level.saveddata.SavedData; +import net.minecraft.world.phys.Vec3; +import net.neoforged.neoforge.network.PacketDistributor; +import org.jetbrains.annotations.NotNull; + +import java.util.*; +import java.util.function.Function; + +public class WindManager { + private static final long MIN_TIME_VALUE = Long.MIN_VALUE + 1; + public static final Map, Integer> EXTENSION_PROVIDERS = new Object2ObjectOpenHashMap<>(); + private final List> windDisturbancesA = new ArrayList<>(); + private final List> windDisturbancesB = new ArrayList<>(); + private boolean isSwitchedServer; + public final List attachedExtensions; + + public boolean overrideWind; + public long time; + public Vec3 commandWind = Vec3.ZERO; + public double windX; + public double windY; + public double windZ; + public double laggedWindX; + public double laggedWindY; + public double laggedWindZ; + public long seed; + private boolean seedSet = false; + + private final ServerLevel level; + public ImprovedNoise noise; + + @SuppressWarnings("unchecked") + public WindManager(@NotNull ServerLevel level) { + this.level = level; + this.noise = EasyNoiseSampler.createXoroNoise(this.seed); + List extensions = new ObjectArrayList<>(); + Map.Entry, Integer>[] extensionProviders = EXTENSION_PROVIDERS.entrySet().toArray(new Map.Entry[0]); + Arrays.sort(extensionProviders, Map.Entry.comparingByValue()); + + for (Map.Entry, Integer> extensionFunc : extensionProviders) { + var extension = extensionFunc.getKey().apply(this); + extensions.add(extension); + } + this.attachedExtensions = extensions; + } + + public static void addExtension(Function extension, int priority) { + if (extension != null) EXTENSION_PROVIDERS.put(extension, priority); + } + + public static void addExtension(Function extension) { + addExtension(extension, 1000); + } + + public void addWindDisturbanceAndSync(@NotNull WindDisturbance windDisturbance) { + Optional optionalPacket = windDisturbance.toPacket(); + if (optionalPacket.isPresent()) { + for (ServerPlayer player : level.players()) { + if (windDisturbance.isWithinViewDistance(player.getChunkTrackingView())) { + PacketDistributor.sendToPlayer(player, optionalPacket.get()); + } + } + + } + this.addWindDisturbance(windDisturbance); + } + + public void addWindDisturbance(@NotNull WindDisturbance windDisturbance) { + this.getWindDisturbanceStash().add(windDisturbance); + } + + private List> getWindDisturbances() { + return !this.isSwitchedServer ? this.windDisturbancesA : this.windDisturbancesB; + } + + private List> getWindDisturbanceStash() { + return this.isSwitchedServer ? this.windDisturbancesA : this.windDisturbancesB; + } + + public void clearWindDisturbances() { + this.getWindDisturbances().clear(); + } + + public void clearAllWindDisturbances() { + this.getWindDisturbances().clear(); + this.getWindDisturbanceStash().clear(); + } + + public void clearAndSwitchWindDisturbances() { + this.clearWindDisturbances(); + this.isSwitchedServer = !this.isSwitchedServer; + } + + @NotNull + public static WindManager getWindManager(@NotNull ServerLevel level) { + return ((WindManagerInterface)level).frozenLib$getWindManager(); + } + + @NotNull + public SavedData.Factory createData() { + return new SavedData.Factory<>( + () -> new WindStorage(this), + (tag, provider) -> WindStorage.load(tag, this), + DataFixTypes.SAVED_DATA_RANDOM_SEQUENCES + ); + } + + public void tick(@NotNull ServerLevel level) { + if (!this.seedSet) { + this.seedSet = true; + this.seed = level.getSeed(); + this.noise = EasyNoiseSampler.createXoroNoise(this.seed); + } + if (level.tickRateManager().runsNormally()) { + this.runResetsIfNeeded(); + + this.time += 1; + //WIND + float thunderLevel = this.level.getThunderLevel(1F) * 0.03F; + double calcTime = this.time * 0.0005; + double calcTimeY = this.time * 0.00035; + Vec3 vec3 = sampleVec3(calcTime, calcTimeY, calcTime); + this.windX = vec3.x + (vec3.x * thunderLevel); + this.windY = vec3.y + (vec3.y * thunderLevel); + this.windZ = vec3.z + (vec3.z * thunderLevel); + //LAGGED WIND + double calcLaggedTime = (this.time - 40) * 0.0005; + double calcLaggedTimeY = (this.time - 60) * 0.00035; + Vec3 laggedVec = sampleVec3(calcLaggedTime, calcLaggedTimeY, calcLaggedTime); + this.laggedWindX = laggedVec.x + (laggedVec.x * thunderLevel); + this.laggedWindY = laggedVec.y + (laggedVec.y * thunderLevel); + this.laggedWindZ = laggedVec.z + (laggedVec.z * thunderLevel); + + //EXTENSIONS + for (WindManagerExtension extension : this.attachedExtensions) { + extension.baseTick(level); + extension.tick(level); + } + + //SYNC WITH CLIENTS IN CASE OF DESYNC + if (this.time % 20 == 0) { + this.sendSync(this.level); + } + } + } + + //Reset values in case of potential overflow + private boolean runResetsIfNeeded() { + boolean needsReset = false; + if (Math.abs(this.time) == Long.MAX_VALUE) { + needsReset = true; + this.time = MIN_TIME_VALUE; + } + if (Math.abs(this.windX) == Double.MAX_VALUE) { + needsReset = true; + this.windX = 0; + } + if (Math.abs(this.windY) == Double.MAX_VALUE) { + needsReset = true; + this.windY = 0; + } + if (Math.abs(this.windZ) == Double.MAX_VALUE) { + needsReset = true; + this.windZ = 0; + } + if (Math.abs(this.laggedWindX) == Double.MAX_VALUE) { + needsReset = true; + this.laggedWindX = 0; + } + if (Math.abs(this.laggedWindY) == Double.MAX_VALUE) { + needsReset = true; + this.laggedWindY = 0; + } + if (Math.abs(this.laggedWindZ) == Double.MAX_VALUE) { + needsReset = true; + this.laggedWindZ = 0; + } + + //EXTENSIONS + for (WindManagerExtension extension : this.attachedExtensions) { + if (extension.runResetsIfNeeded()) { + needsReset = true; + } + } + + if (needsReset) { + this.sendSync(this.level); + } + return needsReset; + } + + @NotNull + public WindSyncPacket createSyncPacket() { + return new WindSyncPacket( + this.time, + this.seed, + this.overrideWind, + this.commandWind + ); + } + + public void sendSync(@NotNull ServerLevel level) { + WindSyncPacket packet = this.createSyncPacket(); + for (ServerPlayer player : level.players()) { + this.sendSyncToPlayer(packet, player); + } + } + + public void sendSyncToPlayer(@NotNull WindSyncPacket packet, @NotNull ServerPlayer player) { + PacketDistributor.sendToPlayer(player, packet); + for (WindManagerExtension extension : this.attachedExtensions) { + PacketDistributor.sendToPlayer(player, extension.syncPacket(packet)); + } + } + + @NotNull + public Vec3 getWindMovement(@NotNull BlockPos pos) { + return this.getWindMovement(Vec3.atBottomCenterOf(pos)); + } + + @NotNull + public Vec3 getWindMovement(@NotNull BlockPos pos, double scale) { + return this.getWindMovement(Vec3.atBottomCenterOf(pos), scale); + } + + @NotNull + public Vec3 getWindMovement(@NotNull BlockPos pos, double scale, double clamp) { + return this.getWindMovement(Vec3.atBottomCenterOf(pos), scale, clamp); + } + + @NotNull + public Vec3 getWindMovement(@NotNull Vec3 pos) { + return this.getWindMovement(pos, 1D); + } + + @NotNull + public Vec3 getWindMovement(@NotNull Vec3 pos, double scale) { + return this.getWindMovement(pos, scale, Double.MAX_VALUE); + } + + @NotNull + public Vec3 getWindMovement(@NotNull Vec3 pos, double scale, double clamp) { + return this.getWindMovement(pos, scale, clamp, 1D); + } + + @NotNull + public Vec3 getWindMovement(@NotNull Vec3 pos, double scale, double clamp, double windDisturbanceScale) { + double brightness = this.level.getBrightness(LightLayer.SKY, BlockPos.containing(pos)); + double windScale = (Math.max((brightness - (Math.max(15 - brightness, 0))), 0) * 0.0667D); + Pair disturbance = this.calculateWindDisturbance(level, pos); + double disturbanceAmount = disturbance.getFirst(); + Vec3 windDisturbance = disturbance.getSecond(); + double windX = Mth.lerp(disturbanceAmount, this.windX * windScale, windDisturbance.x * windDisturbanceScale) * scale; + double windY = Mth.lerp(disturbanceAmount, this.windY * windScale, windDisturbance.y * windDisturbanceScale) * scale; + double windZ = Mth.lerp(disturbanceAmount, this.windZ * windScale, windDisturbance.z * windDisturbanceScale) * scale; + return new Vec3( + Mth.clamp(windX, -clamp, clamp), + Mth.clamp(windY, -clamp, clamp), + Mth.clamp(windZ, -clamp, clamp) + ); + } + + @NotNull + public Vec3 getWindMovement3D(@NotNull BlockPos pos, double stretch) { + return this.getWindMovement3D(Vec3.atBottomCenterOf(pos), stretch); + } + + @NotNull + public Vec3 getWindMovement3D(@NotNull BlockPos pos, double scale, double stretch) { + return this.getWindMovement3D(Vec3.atBottomCenterOf(pos), scale, stretch); + } + + @NotNull + public Vec3 getWindMovement3D(@NotNull BlockPos pos, double scale, double clamp, double stretch) { + return this.getWindMovement3D(Vec3.atBottomCenterOf(pos), scale, clamp, stretch); + } + + @NotNull + public Vec3 getWindMovement3D(@NotNull Vec3 pos, double stretch) { + return this.getWindMovement3D(pos, 1D, stretch); + } + + @NotNull + public Vec3 getWindMovement3D(@NotNull Vec3 pos, double scale, double stretch) { + return this.getWindMovement3D(pos, scale, Double.MAX_VALUE, stretch); + } + + @NotNull + public Vec3 getWindMovement3D(@NotNull Vec3 pos, double scale, double clamp, double stretch) { + Vec3 wind = this.sample3D(pos, stretch); + return new Vec3(Mth.clamp((wind.x()) * scale, -clamp, clamp), + Mth.clamp((wind.y()) * scale, -clamp, clamp), + Mth.clamp((wind.z()) * scale, -clamp, clamp)); + } + + @NotNull + private Vec3 sampleVec3(double x, double y, double z) { + if (!this.overrideWind) { + double windX = this.noise.noise(x, 0D, 0D); + double windY = this.noise.noise(0D, y, 0D); + double windZ = this.noise.noise(0D, 0D, z); + return new Vec3(windX, windY, windZ); + } + return this.commandWind; + } + + @NotNull + private Vec3 sample3D(@NotNull Vec3 pos, double stretch) { + double sampledTime = this.time * 0.1D; + double xyz = pos.x() + pos.y() + pos.z(); + double windX = this.noise.noise((xyz + sampledTime) * stretch, 0D, 0D); + double windY = this.noise.noise(0D, (xyz + sampledTime) * stretch, 0D); + double windZ = this.noise.noise(0D, 0D, (xyz + sampledTime) * stretch); + return new Vec3(windX, windY, windZ); + } + + @NotNull + private Pair calculateWindDisturbance(@NotNull Level level, @NotNull Vec3 pos) { + return calculateWindDisturbance(this.getWindDisturbances(), level, pos); + } + + @NotNull + public static Pair calculateWindDisturbance(@NotNull List> windDisturbances, @NotNull Level level, @NotNull Vec3 pos) { + ArrayList> winds = new ArrayList<>(); + double strength = 0D; + for (WindDisturbance windDisturbance : windDisturbances) { + WindDisturbance.DisturbanceResult disturbanceResult = windDisturbance.calculateDisturbanceResult(level, pos); + if (disturbanceResult.strength() != 0D && disturbanceResult.weight() != 0D) { + strength = Math.max(strength, disturbanceResult.strength()); + winds.add(Pair.of(disturbanceResult.weight(), disturbanceResult.wind())); + } + } + + double finalX = 0D; + double finalY = 0D; + double finalZ = 0D; + if (!winds.isEmpty()) { + double x = 0D; + double y = 0D; + double z = 0D; + double sumOfWeights = 0D; + for (Pair pair : winds) { + double weight = pair.getFirst(); + sumOfWeights += weight; + Vec3 windVec = pair.getSecond(); + x += weight * windVec.x; + y += weight * windVec.y; + z += weight * windVec.z; + } + finalX = x / sumOfWeights; + finalY = y / sumOfWeights; + finalZ = z / sumOfWeights; + } + + return Pair.of(strength, new Vec3(finalX, finalY, finalZ)); + } +} diff --git a/src/main/java/net/frozenblock/lib/wind/api/WindManagerExtension.java b/src/main/java/net/frozenblock/lib/wind/api/WindManagerExtension.java new file mode 100644 index 0000000..fc9bce9 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/wind/api/WindManagerExtension.java @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.wind.api; + +import net.frozenblock.lib.wind.impl.networking.WindSyncPacket; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.level.ServerLevel; + +public interface WindManagerExtension { + + ResourceLocation extensionID(); + + void tick(ServerLevel level); + + void baseTick(ServerLevel level); + + boolean runResetsIfNeeded(); + + CustomPacketPayload syncPacket(WindSyncPacket packet); + + void load(CompoundTag compoundTag); + + void save(CompoundTag compoundTag); +} diff --git a/src/main/java/net/frozenblock/lib/wind/api/command/WindOverrideCommand.java b/src/main/java/net/frozenblock/lib/wind/api/command/WindOverrideCommand.java new file mode 100644 index 0000000..b458f99 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/wind/api/command/WindOverrideCommand.java @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.wind.api.command; + +import com.mojang.brigadier.CommandDispatcher; +import com.mojang.brigadier.arguments.BoolArgumentType; +import com.mojang.brigadier.arguments.DoubleArgumentType; +import net.frozenblock.lib.wind.api.WindManager; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.commands.Commands; +import net.minecraft.network.chat.Component; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.phys.Vec3; + +public class WindOverrideCommand { + + public static void register(CommandDispatcher dispatcher) { + dispatcher.register(Commands.literal("windoverride").requires(source -> source.hasPermission(2)) + .then(Commands.argument("x", DoubleArgumentType.doubleArg()).then(Commands.argument("y", DoubleArgumentType.doubleArg()).then(Commands.argument("z", DoubleArgumentType.doubleArg()).executes(context -> setWind(context.getSource(), DoubleArgumentType.getDouble(context, "x"), DoubleArgumentType.getDouble(context, "y"), DoubleArgumentType.getDouble(context, "z")))))) + .then(Commands.argument("overrideEnabled", BoolArgumentType.bool()).executes(context -> setWind(context.getSource(), BoolArgumentType.getBool(context, "overrideEnabled")))) + ); + } + + private static int setWind(CommandSourceStack source, boolean bl) { + ServerLevel level = source.getLevel(); + WindManager windManager = WindManager.getWindManager(level); + windManager.overrideWind = bl; + windManager.sendSync(level); + source.sendSuccess(() -> Component.translatable("commands.wind.toggle.success", bl), true); + return 1; + } + + private static int setWind(CommandSourceStack source, double x, double y, double z) { + ServerLevel level = source.getLevel(); + WindManager windManager = WindManager.getWindManager(level); + windManager.overrideWind = true; + windManager.windX = x; + windManager.windY = y; + windManager.windZ = z; + windManager.commandWind = new Vec3(windManager.windX, windManager.windY, windManager.windZ); + windManager.sendSync(level); + source.sendSuccess(() -> Component.translatable("commands.wind.success", x, y, z), true); + return 1; + } + +} diff --git a/src/main/java/net/frozenblock/lib/wind/impl/WindDisturbingEntityImpl.java b/src/main/java/net/frozenblock/lib/wind/impl/WindDisturbingEntityImpl.java new file mode 100644 index 0000000..3afd26d --- /dev/null +++ b/src/main/java/net/frozenblock/lib/wind/impl/WindDisturbingEntityImpl.java @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.wind.impl; + +import net.frozenblock.lib.wind.api.WindDisturbance; + +public interface WindDisturbingEntityImpl { + + WindDisturbance frozenLib$makeWindDisturbance(); + +} diff --git a/src/main/java/net/frozenblock/lib/wind/impl/WindManagerInterface.java b/src/main/java/net/frozenblock/lib/wind/impl/WindManagerInterface.java new file mode 100644 index 0000000..db7370c --- /dev/null +++ b/src/main/java/net/frozenblock/lib/wind/impl/WindManagerInterface.java @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.wind.impl; + +import net.frozenblock.lib.wind.api.WindManager; + +public interface WindManagerInterface { + + public WindManager frozenLib$getWindManager(); + +} diff --git a/src/main/java/net/frozenblock/lib/wind/impl/WindStorage.java b/src/main/java/net/frozenblock/lib/wind/impl/WindStorage.java new file mode 100644 index 0000000..4fed759 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/wind/impl/WindStorage.java @@ -0,0 +1,90 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.wind.impl; + +import net.frozenblock.lib.FrozenLogUtils; +import net.frozenblock.lib.FrozenSharedConstants; +import net.frozenblock.lib.wind.api.WindManager; +import net.frozenblock.lib.wind.api.WindManagerExtension; +import net.minecraft.core.HolderLookup; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.world.level.saveddata.SavedData; +import net.minecraft.world.phys.Vec3; +import org.jetbrains.annotations.NotNull; + +public class WindStorage extends SavedData { + public static final String WIND_FILE_ID = "frozenlib_wind"; + private final WindManager windManager; + + public WindStorage(WindManager windManager) { + this.windManager = windManager; + this.setDirty(); + } + + @Override + public @NotNull CompoundTag save(@NotNull CompoundTag compoundTag, HolderLookup.Provider provider) { + compoundTag.putLong("time", this.windManager.time); + compoundTag.putBoolean("overrideWind", this.windManager.overrideWind); + compoundTag.putDouble("commandWindX", this.windManager.commandWind.x()); + compoundTag.putDouble("commandWindY", this.windManager.commandWind.y()); + compoundTag.putDouble("commandWindZ", this.windManager.commandWind.z()); + compoundTag.putDouble("windX", this.windManager.windX); + compoundTag.putDouble("windY", this.windManager.windY); + compoundTag.putDouble("windZ", this.windManager.windZ); + compoundTag.putDouble("laggedWindX", this.windManager.laggedWindX); + compoundTag.putDouble("laggedWindY", this.windManager.laggedWindY); + compoundTag.putDouble("laggedWindZ", this.windManager.laggedWindZ); + compoundTag.putLong("seed", this.windManager.seed); + + // EXTENSIONS + for (WindManagerExtension extension : this.windManager.attachedExtensions) { + CompoundTag extensionTag = new CompoundTag(); + extension.save(extensionTag); + compoundTag.put(extension.extensionID().toString(), extensionTag); + } + + FrozenLogUtils.log("Saving WindManager data.", FrozenSharedConstants.UNSTABLE_LOGGING); + + return compoundTag; + } + + public static @NotNull WindStorage load(@NotNull CompoundTag compoundTag, WindManager manager) { + WindStorage windStorage = new WindStorage(manager); + + windStorage.windManager.time = compoundTag.getLong("time"); + windStorage.windManager.overrideWind = compoundTag.getBoolean("overrideWind"); + windStorage.windManager.commandWind = new Vec3(compoundTag.getDouble("commandWindX"), compoundTag.getDouble("commandWindY"), compoundTag.getDouble("commandWindZ")); + windStorage.windManager.windX = compoundTag.getDouble("windX"); + windStorage.windManager.windY = compoundTag.getDouble("windY"); + windStorage.windManager.windZ = compoundTag.getDouble("windZ"); + windStorage.windManager.laggedWindX = compoundTag.getDouble("laggedWindX"); + windStorage.windManager.laggedWindY = compoundTag.getDouble("laggedWindY"); + windStorage.windManager.laggedWindZ = compoundTag.getDouble("laggedWindZ"); + windStorage.windManager.seed = compoundTag.getLong("seed"); + + // EXTENSIONS + for (WindManagerExtension extension : windStorage.windManager.attachedExtensions) { + CompoundTag extensionTag = compoundTag.getCompound(extension.extensionID().toString()); + extension.load(extensionTag); + } + + FrozenLogUtils.log("Loading WindManager data.", FrozenSharedConstants.UNSTABLE_LOGGING); + + return windStorage; + } +} diff --git a/src/main/java/net/frozenblock/lib/wind/impl/networking/WindDisturbancePacket.java b/src/main/java/net/frozenblock/lib/wind/impl/networking/WindDisturbancePacket.java new file mode 100644 index 0000000..1c5310e --- /dev/null +++ b/src/main/java/net/frozenblock/lib/wind/impl/networking/WindDisturbancePacket.java @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.wind.impl.networking; + +import net.frozenblock.lib.FrozenSharedConstants; +import net.frozenblock.lib.wind.api.WindDisturbanceLogic; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.phys.AABB; +import net.minecraft.world.phys.Vec3; +import org.jetbrains.annotations.NotNull; + +public record WindDisturbancePacket( + AABB affectedArea, + Vec3 origin, + WindDisturbanceLogic.SourceType disturbanceSourceType, + ResourceLocation id, + long posOrID + +) implements CustomPacketPayload { + public static final Type PACKET_TYPE = new Type<>( + FrozenSharedConstants.id("wind_disturbance_packet") + ); + public static final StreamCodec CODEC = StreamCodec.ofMember(WindDisturbancePacket::write, WindDisturbancePacket::new); + + public WindDisturbancePacket(@NotNull RegistryFriendlyByteBuf buf) { + this( + new AABB(buf.readVec3(), buf.readVec3()), + buf.readVec3(), + buf.readEnum(WindDisturbanceLogic.SourceType.class), + buf.readResourceLocation(), + buf.readLong() + ); + } + + public void write(@NotNull RegistryFriendlyByteBuf buf) { + AABB affectedArea = this.affectedArea(); + buf.writeVec3(new Vec3(affectedArea.minX, affectedArea.minY, affectedArea.minZ)); + buf.writeVec3(new Vec3(affectedArea.maxX, affectedArea.maxY, affectedArea.maxZ)); + buf.writeVec3(this.origin()); + buf.writeEnum(this.disturbanceSourceType()); + buf.writeResourceLocation(this.id()); + buf.writeLong(this.posOrID()); + } + + @Override + @NotNull + public Type type() { + return PACKET_TYPE; + } +} diff --git a/src/main/java/net/frozenblock/lib/wind/impl/networking/WindSyncPacket.java b/src/main/java/net/frozenblock/lib/wind/impl/networking/WindSyncPacket.java new file mode 100644 index 0000000..8aac2c7 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/wind/impl/networking/WindSyncPacket.java @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.wind.impl.networking; + +import net.frozenblock.lib.FrozenSharedConstants; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.world.phys.Vec3; +import org.jetbrains.annotations.NotNull; + +public record WindSyncPacket( + long windTime, + long seed, + boolean override, + Vec3 commandWind +) implements CustomPacketPayload { + + + public static final Type PACKET_TYPE = new Type<>( + FrozenSharedConstants.id("wind_sync_packet") + ); + public static final StreamCodec CODEC = StreamCodec.ofMember(WindSyncPacket::write, WindSyncPacket::create); + + public static WindSyncPacket create(@NotNull FriendlyByteBuf buf) { + return new WindSyncPacket( + buf.readLong(), + buf.readLong(), + buf.readBoolean(), + buf.readVec3() + ); + } + + public void write(@NotNull FriendlyByteBuf buf) { + buf.writeLong(this.windTime()); + buf.writeLong(this.seed()); + buf.writeBoolean(this.override()); + buf.writeVec3(this.commandWind()); + } + + @Override + @NotNull + public Type type() { + return PACKET_TYPE; + } +} diff --git a/src/main/java/net/frozenblock/lib/wind/mixin/AbstractWindChargeMixin.java b/src/main/java/net/frozenblock/lib/wind/mixin/AbstractWindChargeMixin.java new file mode 100644 index 0000000..521d536 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/wind/mixin/AbstractWindChargeMixin.java @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.wind.mixin; + +import net.frozenblock.lib.wind.api.WindDisturbanceLogic; +import net.frozenblock.lib.wind.api.WindDisturbingEntity; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.entity.projectile.windcharge.AbstractWindCharge; +import org.jetbrains.annotations.Nullable; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Unique; + +@Mixin(AbstractWindCharge.class) +public abstract class AbstractWindChargeMixin implements WindDisturbingEntity { + + @Unique + @Nullable + @Override + public ResourceLocation frozenLib$getWindDisturbanceLogicID() { + return WindDisturbanceLogic.WIND_CHARGE; + } + + @Unique + @Override + public double frozenLib$getWindWidth() { + return 10D; + } + + @Unique + @Override + public double frozenLib$getWindHeight() { + return 10D; + } + +} diff --git a/src/main/java/net/frozenblock/lib/wind/mixin/BreezeMixin.java b/src/main/java/net/frozenblock/lib/wind/mixin/BreezeMixin.java new file mode 100644 index 0000000..20a03a7 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/wind/mixin/BreezeMixin.java @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.wind.mixin; + +import net.frozenblock.lib.wind.api.WindDisturbanceLogic; +import net.frozenblock.lib.wind.api.WindDisturbingEntity; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.entity.monster.breeze.Breeze; +import org.jetbrains.annotations.Nullable; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Unique; + +@Mixin(Breeze.class) +public abstract class BreezeMixin implements WindDisturbingEntity { + + @Unique + @Nullable + @Override + public ResourceLocation frozenLib$getWindDisturbanceLogicID() { + return WindDisturbanceLogic.BREEZE; + } + + @Unique + @Override + public double frozenLib$getWindWidth() { + return 12D; + } + + @Unique + @Override + public double frozenLib$getWindHeight() { + return 10D; + } + + @Override + public double frozenLib$getWindAreaYOffset() { + return 1D; + } +} diff --git a/src/main/java/net/frozenblock/lib/wind/mixin/EntityMixin.java b/src/main/java/net/frozenblock/lib/wind/mixin/EntityMixin.java new file mode 100644 index 0000000..62f20d4 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/wind/mixin/EntityMixin.java @@ -0,0 +1,132 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.wind.mixin; + +import net.frozenblock.lib.wind.api.WindDisturbance; +import net.frozenblock.lib.wind.api.WindDisturbanceLogic; +import net.frozenblock.lib.wind.api.WindDisturbingEntity; +import net.frozenblock.lib.wind.api.WindManager; +import net.frozenblock.lib.wind.impl.WindDisturbingEntityImpl; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.level.Level; +import net.minecraft.world.phys.AABB; +import net.minecraft.world.phys.Vec3; +import org.jetbrains.annotations.Nullable; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +import java.util.Optional; + +@Mixin(Entity.class) +public abstract class EntityMixin implements WindDisturbingEntity, WindDisturbingEntityImpl { + + @Inject( + method = "baseTick", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/world/entity/Entity;checkBelowWorld()V" + ) + ) + public void frozenLib$addWindDisturbanceServer(CallbackInfo info) { + if (this.level() instanceof ServerLevel serverLevel) { + WindDisturbance windDisturbance = this.frozenLib$makeWindDisturbance(); + if (windDisturbance != null) { + WindManager windManager = WindManager.getWindManager(serverLevel); + if (this.frozenLib$useSyncPacket()) { + windManager.addWindDisturbanceAndSync(windDisturbance); + } else { + windManager.addWindDisturbance(windDisturbance); + } + } + } + } + + @Unique + @Nullable + @Override + public ResourceLocation frozenLib$getWindDisturbanceLogicID() { + return null; + } + + @Unique + @Override + public double frozenLib$getWindWidth() { + return 2D; + } + + @Unique + @Override + public double frozenLib$getWindHeight() { + return 2D; + } + + @Override + public double frozenLib$getWindAreaYOffset() { + return 0D; + } + + @Unique + @Nullable + @Override + public WindDisturbance frozenLib$makeWindDisturbance() { + ResourceLocation disturbanceLogicID = this.frozenLib$getWindDisturbanceLogicID(); + if (disturbanceLogicID != null) { + Optional disturbanceLogic = WindDisturbanceLogic.getWindDisturbanceLogic(disturbanceLogicID); + if (disturbanceLogic.isPresent()) { + Entity entity = Entity.class.cast(this); + double scale = entity instanceof LivingEntity livingEntity ? livingEntity.getScale() : 1D; + Vec3 position = entity.getBoundingBox().getCenter(); + return new WindDisturbance( + Optional.of(entity), + position, + AABB.ofSize( + position, + this.frozenLib$getWindWidth() * scale, + this.frozenLib$getWindHeight() * scale, + this.frozenLib$getWindWidth() * scale + ).move( + 0D, + this.frozenLib$getWindAreaYOffset() * scale, + 0D + ), + disturbanceLogic.get() + ); + } + } + return null; + } + + @Unique + @Override + public boolean frozenLib$useSyncPacket() { + return false; + } + + @Shadow + public Level level() { + throw new AssertionError("Mixin injection failed - FrozenLib EntityMixin."); + } + +} diff --git a/src/main/java/net/frozenblock/lib/wind/mixin/ServerLevelMixin.java b/src/main/java/net/frozenblock/lib/wind/mixin/ServerLevelMixin.java new file mode 100644 index 0000000..9dee6bc --- /dev/null +++ b/src/main/java/net/frozenblock/lib/wind/mixin/ServerLevelMixin.java @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.wind.mixin; + +import net.frozenblock.lib.wind.api.WindManager; +import net.frozenblock.lib.wind.impl.WindManagerInterface; +import net.minecraft.server.level.ServerLevel; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Unique; + +@Mixin(ServerLevel.class) +public class ServerLevelMixin implements WindManagerInterface { + + @Unique + private final WindManager frozenLib$windManager = new WindManager(ServerLevel.class.cast(this)); + + @Unique + @Override + public WindManager frozenLib$getWindManager() { + return this.frozenLib$windManager; + } + +} diff --git a/src/main/java/net/frozenblock/lib/wind/mixin/client/EntityMixin.java b/src/main/java/net/frozenblock/lib/wind/mixin/client/EntityMixin.java new file mode 100644 index 0000000..db5a283 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/wind/mixin/client/EntityMixin.java @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.wind.mixin.client; + +import net.frozenblock.lib.wind.api.ClientWindManager; +import net.frozenblock.lib.wind.api.WindDisturbance; +import net.frozenblock.lib.wind.api.WindDisturbingEntity; +import net.frozenblock.lib.wind.impl.WindDisturbingEntityImpl; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.level.Level; +import net.neoforged.api.distmarker.Dist; +import net.neoforged.api.distmarker.OnlyIn; +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; + +@OnlyIn(Dist.CLIENT) +@Mixin(Entity.class) +public abstract class EntityMixin implements WindDisturbingEntity, WindDisturbingEntityImpl { + + @Inject( + method = "baseTick", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/world/entity/Entity;checkBelowWorld()V" + ) + ) + public void frozenLib$addWindDisturbanceClient(CallbackInfo info) { + if (this.level().isClientSide && !this.frozenLib$useSyncPacket()) { + WindDisturbance windDisturbance = this.frozenLib$makeWindDisturbance(); + if (windDisturbance != null) { + ClientWindManager.addWindDisturbance(windDisturbance); + } + } + } + + @Shadow + public Level level() { + throw new AssertionError("Mixin injection failed - FrozenLib EntityMixin."); + } + +} diff --git a/src/main/java/net/frozenblock/lib/worldgen/biome/api/FrozenBiome.java b/src/main/java/net/frozenblock/lib/worldgen/biome/api/FrozenBiome.java new file mode 100644 index 0000000..c08ae7f --- /dev/null +++ b/src/main/java/net/frozenblock/lib/worldgen/biome/api/FrozenBiome.java @@ -0,0 +1,161 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.worldgen.biome.api; + +import com.google.common.collect.ImmutableList; +import com.mojang.datafixers.util.Pair; +import net.minecraft.core.Holder; +import net.minecraft.core.registries.Registries; +import net.minecraft.data.worldgen.BootstrapContext; +import net.minecraft.resources.ResourceKey; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.sounds.Music; +import net.minecraft.sounds.SoundEvent; +import net.minecraft.world.level.biome.*; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; + +public abstract class FrozenBiome { + private static final List BIOMES = new ArrayList<>(); + private final ResourceKey key = ResourceKey.create(Registries.BIOME, ResourceLocation.fromNamespaceAndPath(this.modID(), this.biomeID())); + + protected FrozenBiome() { + BIOMES.add(this); + } + + public abstract String modID(); + + public abstract String biomeID(); + + public abstract float temperature(); + + public abstract float downfall(); + + public abstract boolean hasPrecipitation(); + + public Biome.TemperatureModifier temperatureModifier() { + return Biome.TemperatureModifier.NONE; + } + + public abstract int skyColor(); + + public abstract int fogColor(); + + public abstract int waterColor(); + + public abstract int waterFogColor(); + + @Nullable + public abstract Integer foliageColorOverride(); + + @Nullable + public abstract Integer grassColorOverride(); + + public BiomeSpecialEffects.GrassColorModifier grassColorModifier() { + return BiomeSpecialEffects.GrassColorModifier.NONE; + } + + @Nullable + public abstract AmbientParticleSettings ambientParticleSettings(); + + @Nullable + public abstract Holder ambientLoopSound(); + + @Nullable + public abstract AmbientMoodSettings ambientMoodSettings(); + + @Nullable + public abstract AmbientAdditionsSettings ambientAdditionsSound(); + + @Nullable + public abstract Music backgroundMusic(); + + public final @NotNull Biome create(@NotNull BootstrapContext entries) { + Biome.BiomeBuilder biomeBuilder = new Biome.BiomeBuilder(); + biomeBuilder.temperature(this.temperature()) + .temperatureAdjustment(this.temperatureModifier()) + .downfall(this.downfall()) + .hasPrecipitation(this.hasPrecipitation()); + + var placedFeatures = entries.lookup(Registries.PLACED_FEATURE); + var worldCarvers = entries.lookup(Registries.CONFIGURED_CARVER); + BiomeGenerationSettings.Builder featureBuilder = new BiomeGenerationSettings.Builder(placedFeatures, worldCarvers); + this.addFeatures(featureBuilder); + biomeBuilder.generationSettings(featureBuilder.build()); + + MobSpawnSettings.Builder spawnBuilder = new MobSpawnSettings.Builder(); + this.addSpawns(spawnBuilder); + biomeBuilder.mobSpawnSettings(spawnBuilder.build()); + + BiomeSpecialEffects.Builder specialEffectsBuilder = new BiomeSpecialEffects.Builder(); + specialEffectsBuilder.skyColor(this.skyColor()) + .fogColor(this.fogColor()) + .waterColor(this.waterColor()) + .waterFogColor(this.waterFogColor()) + .grassColorModifier(this.grassColorModifier()); + + if (this.foliageColorOverride() != null) specialEffectsBuilder.foliageColorOverride(this.foliageColorOverride()); + if (this.grassColorOverride() != null) specialEffectsBuilder.grassColorOverride(this.grassColorOverride()); + if (this.ambientParticleSettings() != null) specialEffectsBuilder.ambientParticle(this.ambientParticleSettings()); + if (this.ambientLoopSound() != null) specialEffectsBuilder.ambientLoopSound(this.ambientLoopSound()); + if (this.ambientMoodSettings() != null) specialEffectsBuilder.ambientMoodSound(this.ambientMoodSettings()); + if (this.ambientAdditionsSound() != null) specialEffectsBuilder.ambientAdditionsSound(this.ambientAdditionsSound()); + if (this.backgroundMusic() != null) specialEffectsBuilder.backgroundMusic(this.backgroundMusic()); + + biomeBuilder.specialEffects(specialEffectsBuilder.build()); + + return biomeBuilder.build(); + } + + public abstract void addFeatures(BiomeGenerationSettings.Builder features); + + public abstract void addSpawns(MobSpawnSettings.Builder spawns); + + public abstract void injectToOverworld(Consumer>> consumer); + + public ResourceKey getKey() { + return this.key; + } + + protected final void addSurfaceBiome(@NotNull Consumer>> consumer, Climate.Parameter temperature, Climate.Parameter humidity, Climate.Parameter continentalness, Climate.Parameter erosion, Climate.Parameter weirdness, float offset) { + consumer.accept(Pair.of(Climate.parameters(temperature, humidity, continentalness, erosion, Climate.Parameter.point(0.0F), weirdness, offset), this.getKey())); + consumer.accept(Pair.of(Climate.parameters(temperature, humidity, continentalness, erosion, Climate.Parameter.point(1.0F), weirdness, offset), this.getKey())); + } + + protected final void addSemiDeepBiome(@NotNull Consumer>> parameters, Climate.Parameter temperature, Climate.Parameter humidity, Climate.Parameter continentalness, Climate.Parameter erosion, Climate.Parameter weirdness, float offset) { + parameters.accept(Pair.of(Climate.parameters(temperature, humidity, continentalness, erosion, Climate.Parameter.span(0.4F, 1.0F), weirdness, offset), this.getKey())); + } + + protected final void addUndergroundBiome(@NotNull Consumer>> consumer, Climate.Parameter temperature, Climate.Parameter humidity, Climate.Parameter continentalness, Climate.Parameter erosion, Climate.Parameter weirdness, float offset) { + consumer.accept(Pair.of(Climate.parameters(temperature, humidity, continentalness, erosion, Climate.Parameter.span(0.2F, 0.9F), weirdness, offset), this.getKey())); + } + + protected final void addBottomBiome(@NotNull Consumer>> consumer, Climate.Parameter temerature, Climate.Parameter humidity, Climate.Parameter continentalness, Climate.Parameter erosion, Climate.Parameter weirdness, float offset) { + consumer.accept(Pair.of(Climate.parameters(temerature, humidity, continentalness, erosion, Climate.Parameter.point(1.1F), weirdness, offset), this.getKey())); + } + + @NotNull + public static ImmutableList getFrozenBiomes() { + return ImmutableList.copyOf(BIOMES); + } + +} diff --git a/src/main/java/net/frozenblock/lib/worldgen/biome/api/FrozenBiomeSourceAccess.java b/src/main/java/net/frozenblock/lib/worldgen/biome/api/FrozenBiomeSourceAccess.java new file mode 100644 index 0000000..7656e28 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/worldgen/biome/api/FrozenBiomeSourceAccess.java @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.worldgen.biome.api; + +public interface FrozenBiomeSourceAccess { + + boolean frozenLib_shouldModifyBiomeEntries(); + + void frozenLib_setModifyBiomeEntries(boolean modifyBiomeEntries); +} diff --git a/src/main/java/net/frozenblock/lib/worldgen/biome/api/FrozenOverworldBiomes.java b/src/main/java/net/frozenblock/lib/worldgen/biome/api/FrozenOverworldBiomes.java new file mode 100644 index 0000000..52a0716 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/worldgen/biome/api/FrozenOverworldBiomes.java @@ -0,0 +1,158 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.worldgen.biome.api; + +import lombok.experimental.UtilityClass; +import net.frozenblock.lib.worldgen.biome.api.parameters.FrozenBiomeParameters; +import net.frozenblock.lib.worldgen.biome.impl.OverworldBiomeData; +import net.minecraft.resources.ResourceKey; +import net.minecraft.world.level.biome.Biome; +import net.minecraft.world.level.biome.Climate; +import org.jetbrains.annotations.NotNull; + +import java.util.List; + +/** + * API that exposes the internals of Minecraft's overworld biome code. + */ +@UtilityClass +public class FrozenOverworldBiomes { + + /** + * Adds a biome to the Overworld generator. + * + * @param biome The biome to add. Must not be null. + * @param targetPoint data about the given {@link Biome}'s spawning information in the Overworld. + * @see Climate.TargetPoint + */ + public static void addOverworldBiome(ResourceKey biome, Climate.@NotNull TargetPoint targetPoint) { + OverworldBiomeData.addOverworldBiome(biome, Climate.parameters( + targetPoint.temperature(), + targetPoint.humidity(), + targetPoint.continentalness(), + targetPoint.erosion(), + targetPoint.depth(), + targetPoint.weirdness(), + 0 + )); + } + + /** + * Adds a biome to the Overworld generator. + * + * @param biome The {@link Biome} to add. Must not be null. + * @param parameterPoint data about the given {@link Biome}'s spawning information in the Overworld. + * @see Climate.ParameterPoint + */ + public static void addOverworldBiome(ResourceKey biome, Climate.ParameterPoint parameterPoint) { + OverworldBiomeData.addOverworldBiome(biome, parameterPoint); + } + + /** + * Adds a biome to the Overworld generator. + * + * @param biome The {@link Biome} to add. Must not be null. + * @param weirdnesses The specific weirdnesses the biome should be added to. + */ + public static void addOverworldBiome( + ResourceKey biome, + Climate.Parameter temperature, + Climate.Parameter humidity, + Climate.Parameter continentalness, + Climate.Parameter erosion, + float offset, + Climate.Parameter... weirdnesses + ) { + addOverworldBiome(biome, temperature, humidity, continentalness, erosion, offset, List.of(weirdnesses)); + } + + /** + * Adds a biome to the Overworld generator. + * + * @param biome The {@link Biome} to add. Must not be null. + * @param weirdnesses The specific weirdnesses the biome should be added to. + */ + public static void addOverworldBiome( + ResourceKey biome, + Climate.Parameter temperature, + Climate.Parameter humidity, + Climate.Parameter continentalness, + Climate.Parameter erosion, + float offset, + List weirdnesses + ) { + addOverworldBiome(biome, temperature, humidity, continentalness, Climate.Parameter.point(0.0F), erosion, offset, weirdnesses); + addOverworldBiome(biome, temperature, humidity, continentalness, Climate.Parameter.point(1.0F), erosion, offset, weirdnesses); + } + + /** + * Adds a biome to the Overworld generator. + * + * @param biome The {@link Biome} to add. Must not be null. + * @param weirdnesses The specific weirdnesses the biome should be added to. + */ + public static void addOverworldBiome( + ResourceKey biome, + Climate.Parameter temperature, + Climate.Parameter humidity, + Climate.Parameter continentalness, + Climate.Parameter erosion, + Climate.Parameter depth, + float offset, + Climate.Parameter... weirdnesses + ) { + addOverworldBiome(biome, temperature, humidity, continentalness, erosion, depth, offset, List.of(weirdnesses)); + } + + /** + * Adds a biome to the Overworld generator. + * + * @param biome The {@link Biome} to add. Must not be null. + * @param weirdnesses The specific weirdnesses the biome should be added to. + */ + public static void addOverworldBiome( + ResourceKey biome, + Climate.Parameter temperature, + Climate.Parameter humidity, + Climate.Parameter continentalness, + Climate.Parameter erosion, + Climate.Parameter depth, + float offset, + List weirdnesses + ) { + FrozenBiomeParameters.addWeirdness(weirdness -> OverworldBiomeData.addOverworldBiome( + biome, Climate.parameters( + temperature, + humidity, + continentalness, + erosion, + depth, + weirdness, + offset + ) + ), weirdnesses); + } + + /** + * Returns true if the given biome can generate in the Overworld, considering the Vanilla Overworld biomes, + * and any biomes added to the Overworld by mods. + */ + public static boolean canGenerateInOverworld(ResourceKey biome) { + return OverworldBiomeData.canGenerateInOverworld(biome); + } +} diff --git a/src/main/java/net/frozenblock/lib/worldgen/biome/api/parameters/BiomeParameters.java b/src/main/java/net/frozenblock/lib/worldgen/biome/api/parameters/BiomeParameters.java new file mode 100644 index 0000000..0fde49d --- /dev/null +++ b/src/main/java/net/frozenblock/lib/worldgen/biome/api/parameters/BiomeParameters.java @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.worldgen.biome.api.parameters; + +import net.minecraft.world.level.biome.Climate; + +import java.util.ArrayList; +import java.util.List; + +public class BiomeParameters { + public final List points = new ArrayList<>(); + + public void add(Climate.ParameterPoint... points) { + this.points.addAll(List.of(points)); + } +} diff --git a/src/main/java/net/frozenblock/lib/worldgen/biome/api/parameters/Continentalness.java b/src/main/java/net/frozenblock/lib/worldgen/biome/api/parameters/Continentalness.java new file mode 100644 index 0000000..41fa521 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/worldgen/biome/api/parameters/Continentalness.java @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.worldgen.biome.api.parameters; + +import net.minecraft.world.level.biome.Climate; + +public final class Continentalness { + public static final Climate.Parameter MUSHROOM_FIELDS = OverworldBiomeBuilderParameters.MUSHROOM_FIELDS_CONTINENTALNESS; + public static final Climate.Parameter DEEP_OCEAN = OverworldBiomeBuilderParameters.DEEP_OCEAN_CONTINENTALNESS; + public static final Climate.Parameter OCEAN = OverworldBiomeBuilderParameters.OCEAN_CONTINENTALNESS; + public static final Climate.Parameter COAST = OverworldBiomeBuilderParameters.COAST_CONTINENTALNESS; + public static final Climate.Parameter INLAND = OverworldBiomeBuilderParameters.INLAND_CONTINENTALNESS; + public static final Climate.Parameter NEAR_INLAND = OverworldBiomeBuilderParameters.NEAR_INLAND_CONTINENTALNESS; + public static final Climate.Parameter MID_INLAND = OverworldBiomeBuilderParameters.MID_INLAND_CONTINENTALNESS; + public static final Climate.Parameter FAR_INLAND = OverworldBiomeBuilderParameters.FAR_INLAND_CONTINENTALNESS; + public static final Climate.Parameter FULL_RANGE = OverworldBiomeBuilderParameters.FULL_RANGE; +} diff --git a/src/main/java/net/frozenblock/lib/worldgen/biome/api/parameters/Depth.java b/src/main/java/net/frozenblock/lib/worldgen/biome/api/parameters/Depth.java new file mode 100644 index 0000000..9c35e13 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/worldgen/biome/api/parameters/Depth.java @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.worldgen.biome.api.parameters; + +import net.minecraft.world.level.biome.Climate; + +public final class Depth { + + public static final Climate.Parameter[] depths = new Climate.Parameter[]{ + Climate.Parameter.point(0.0F), + Climate.Parameter.span(0.2F, 0.9F), + Climate.Parameter.point(1.0F) + }; + + public static final Climate.Parameter SURFACE = depths[0]; + public static final Climate.Parameter UNDERGROUND = depths[1]; + public static final Climate.Parameter FLOOR = depths[2]; + public static final Climate.Parameter FULL_RANGE = Climate.Parameter.span(-1.0F, 1.0F); +} diff --git a/src/main/java/net/frozenblock/lib/worldgen/biome/api/parameters/Erosion.java b/src/main/java/net/frozenblock/lib/worldgen/biome/api/parameters/Erosion.java new file mode 100644 index 0000000..7940046 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/worldgen/biome/api/parameters/Erosion.java @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.worldgen.biome.api.parameters; + +import net.minecraft.world.level.biome.Climate; + +public final class Erosion { + public static final Climate.Parameter EROSION_0 = OverworldBiomeBuilderParameters.EROSIONS[0]; + public static final Climate.Parameter EROSION_1 = OverworldBiomeBuilderParameters.EROSIONS[1]; + public static final Climate.Parameter EROSION_2 = OverworldBiomeBuilderParameters.EROSIONS[2]; + public static final Climate.Parameter EROSION_3 = OverworldBiomeBuilderParameters.EROSIONS[3]; + public static final Climate.Parameter EROSION_4 = OverworldBiomeBuilderParameters.EROSIONS[4]; + public static final Climate.Parameter EROSION_5 = OverworldBiomeBuilderParameters.EROSIONS[5]; + public static final Climate.Parameter EROSION_6 = OverworldBiomeBuilderParameters.EROSIONS[6]; + public static final Climate.Parameter FULL_RANGE = OverworldBiomeBuilderParameters.FULL_RANGE; +} diff --git a/src/main/java/net/frozenblock/lib/worldgen/biome/api/parameters/FrozenBiomeParameters.java b/src/main/java/net/frozenblock/lib/worldgen/biome/api/parameters/FrozenBiomeParameters.java new file mode 100644 index 0000000..3fc0587 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/worldgen/biome/api/parameters/FrozenBiomeParameters.java @@ -0,0 +1,107 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.worldgen.biome.api.parameters; + +import lombok.experimental.UtilityClass; +import net.minecraft.world.level.biome.Climate; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; + +import java.util.List; + +@UtilityClass +public class FrozenBiomeParameters { + + public static void addWeirdness(BiomeRunnable runnable, @NotNull List weirdnesses) { + for (Climate.Parameter weirdness : weirdnesses) { + runnable.run(weirdness); + } + } + + /** + * Returns climate parameters in between both specified parameters. + *

+ * Is NOT identical to Climate.Parameter;span. + * Instead, this will meet in the middle of both parameters. + */ + @NotNull + public static Climate.Parameter inBetween(Climate.Parameter par1, Climate.Parameter par2, float width) { + if (width >= 1F || width <= 0F) { + throw new IllegalArgumentException("FrozenLib: Cannot run inBetween if width >= 1 or width <= 0!"); + } + width *= 0.5F; + float lowest = par1.min(); + float highest = par2.max(); + if (lowest > highest) { + throw new IllegalArgumentException("FrozenLib: Cannot run inBetween when lower parameter is higher than the first!"); + } + float difference = (highest - lowest); + float middle = lowest + (difference * 0.5F); + float offset = difference * width; + return Climate.Parameter.span(middle - offset, middle + offset); + } + + @NotNull + public static Climate.Parameter inBetweenLowCutoff(Climate.Parameter par1, Climate.Parameter par2, float width) { + if (width >= 1F || width <= 0F) { + throw new IllegalArgumentException("FrozenLib: Cannot run inBetweenLowCutoff if width >= 1 or width <= 0!"); + } + width *= 0.5F; + float lowest = par1.min(); + float highest = par2.max(); + if (lowest > highest) { + throw new IllegalArgumentException("FrozenLib: Cannot run inBetweenLowCutoff when lower parameter is higher than the first!"); + } + float difference = (highest - lowest); + float middle = lowest + (difference * 0.5F); + float offset = difference * width; + return Climate.Parameter.span(lowest, middle - offset); + } + + @NotNull + public static Climate.Parameter inBetweenHighCutoff(Climate.Parameter par1, Climate.Parameter par2, float width) { + if (width >= 1F || width <= 0F) { + throw new IllegalArgumentException("FrozenLib: Cannot run inBetweenHighCutoff if width >= 1 or width <= 0!"); + } + width *= 0.5F; + float lowest = par1.min(); + float highest = par2.max(); + if (lowest > highest) { + throw new IllegalArgumentException("FrozenLib: Cannot run inBetweenHighCutoff when lower parameter is higher than the first!"); + } + float difference = (highest - lowest); + float middle = lowest + (difference * 0.5F); + float offset = difference * width; + return Climate.Parameter.span(middle + offset, highest); + } + + @NotNull + @Contract("_, _ -> new") + public static Climate.Parameter squish(@NotNull Climate.Parameter parameter, float squish) { + return Climate.Parameter.span(parameter.min() + squish, parameter.max() - squish); + } + + public static boolean isWeird(@NotNull Climate.ParameterPoint point) { + return point.weirdness().max() < 0L; + } + + @FunctionalInterface + public interface BiomeRunnable { + void run(Climate.Parameter weirdness); + } +} diff --git a/src/main/java/net/frozenblock/lib/worldgen/biome/api/parameters/Humidity.java b/src/main/java/net/frozenblock/lib/worldgen/biome/api/parameters/Humidity.java new file mode 100644 index 0000000..94675b5 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/worldgen/biome/api/parameters/Humidity.java @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.worldgen.biome.api.parameters; + +import net.minecraft.world.level.biome.Climate; + +public final class Humidity { + public static final Climate.Parameter ARID = OverworldBiomeBuilderParameters.HUMIDITIES[0]; + public static final Climate.Parameter DRY = OverworldBiomeBuilderParameters.HUMIDITIES[1]; + public static final Climate.Parameter NEUTRAL = OverworldBiomeBuilderParameters.HUMIDITIES[2]; + public static final Climate.Parameter WET = OverworldBiomeBuilderParameters.HUMIDITIES[3]; + public static final Climate.Parameter HUMID = OverworldBiomeBuilderParameters.HUMIDITIES[4]; + + public static final Climate.Parameter ONE = OverworldBiomeBuilderParameters.HUMIDITIES[0]; + public static final Climate.Parameter TWO = OverworldBiomeBuilderParameters.HUMIDITIES[1]; + public static final Climate.Parameter THREE = OverworldBiomeBuilderParameters.HUMIDITIES[2]; + public static final Climate.Parameter FOUR = OverworldBiomeBuilderParameters.HUMIDITIES[3]; + public static final Climate.Parameter FIVE = OverworldBiomeBuilderParameters.HUMIDITIES[4]; + + public static final Climate.Parameter FULL_RANGE = OverworldBiomeBuilderParameters.FULL_RANGE; +} diff --git a/src/main/java/net/frozenblock/lib/worldgen/biome/api/parameters/OverworldBiomeBuilderParameters.java b/src/main/java/net/frozenblock/lib/worldgen/biome/api/parameters/OverworldBiomeBuilderParameters.java new file mode 100644 index 0000000..0b89377 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/worldgen/biome/api/parameters/OverworldBiomeBuilderParameters.java @@ -0,0 +1,145 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.worldgen.biome.api.parameters; + +import com.google.common.collect.ImmutableList; +import com.mojang.datafixers.util.Pair; +import lombok.experimental.UtilityClass; +import net.minecraft.resources.ResourceKey; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.level.biome.Biome; +import net.minecraft.world.level.biome.Climate; +import net.minecraft.world.level.biome.OverworldBiomeBuilder; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Consumer; + +@UtilityClass +public class OverworldBiomeBuilderParameters { + + public static final float VALLEY_SIZE = 0.05F; + public static final float LOW_START = 0.26666668F; + public static final float HIGH_START = 0.4F; + public static final float HIGH_END = 0.93333334F; + public static final float PEAK_SIZE = 0.1F; + public static final float PEAK_START = 0.56666666F; + public static final float PEAK_END = 0.7666667F; + public static final float NEAR_INLAND_START = -0.11F; + public static final float MID_INLAND_START = 0.03F; + public static final float FAR_INLAND_START = 0.3F; + public static final float EROSION_INDEX_1_START = -0.78F; + public static final float EROSION_INDEX_2_START = -0.375F; + private static final float EROSION_DEEP_DARK_DRYNESS_THRESHOLD = -0.225F; + private static final float DEPTH_DEEP_DARK_DRYNESS_THRESHOLD = 0.9F; + public static final Climate.Parameter FULL_RANGE = Climate.Parameter.span(-1.0F, 1.0F); + public static final Climate.Parameter[] TEMPERATURES = new Climate.Parameter[]{ + Climate.Parameter.span(-1.0F, -0.45F), + Climate.Parameter.span(-0.45F, -0.15F), + Climate.Parameter.span(-0.15F, 0.2F), + Climate.Parameter.span(0.2F, 0.55F), + Climate.Parameter.span(0.55F, 1.0F) + }; + public static final Climate.Parameter[] HUMIDITIES = new Climate.Parameter[]{ + Climate.Parameter.span(-1.0F, -0.35F), + Climate.Parameter.span(-0.35F, -0.1F), + Climate.Parameter.span(-0.1F, 0.1F), + Climate.Parameter.span(0.1F, 0.3F), + Climate.Parameter.span(0.3F, 1.0F) + }; + public static final Climate.Parameter[] EROSIONS = new Climate.Parameter[]{ + Climate.Parameter.span(-1.0F, -0.78F), + Climate.Parameter.span(-0.78F, -0.375F), + Climate.Parameter.span(-0.375F, -0.2225F), + Climate.Parameter.span(-0.2225F, 0.05F), + Climate.Parameter.span(0.05F, 0.45F), + Climate.Parameter.span(0.45F, 0.55F), + Climate.Parameter.span(0.55F, 1.0F) + }; + public static final Climate.Parameter FROZEN_RANGE = TEMPERATURES[0]; + public static final Climate.Parameter UNFROZEN_RANGE = Climate.Parameter.span(TEMPERATURES[1], TEMPERATURES[4]); + public static final Climate.Parameter MUSHROOM_FIELDS_CONTINENTALNESS = Climate.Parameter.span(-1.2F, -1.05F); + public static final Climate.Parameter DEEP_OCEAN_CONTINENTALNESS = Climate.Parameter.span(-1.05F, -0.455F); + public static final Climate.Parameter OCEAN_CONTINENTALNESS = Climate.Parameter.span(-0.455F, -0.19F); + public static final Climate.Parameter COAST_CONTINENTALNESS = Climate.Parameter.span(-0.19F, -0.11F); + public static final Climate.Parameter INLAND_CONTINENTALNESS = Climate.Parameter.span(-0.11F, 0.55F); + public static final Climate.Parameter NEAR_INLAND_CONTINENTALNESS = Climate.Parameter.span(-0.11F, 0.03F); + public static final Climate.Parameter MID_INLAND_CONTINENTALNESS = Climate.Parameter.span(0.03F, 0.3F); + public static final Climate.Parameter FAR_INLAND_CONTINENTALNESS = Climate.Parameter.span(0.3F, 1.0F); + public static final Map BIOMES = new LinkedHashMap<>(); + private static boolean hasRun = false; + + public static BiomeParameters getParameters(ResourceLocation location) { + runBiomes(); + return getOrCreateParameters(location); + } + + private static void runBiomes() { + if (!hasRun) { + hasRun = true; + addBiomes(pair -> addParameters(pair.getFirst(), pair.getSecond())); + } + } + + public static BiomeParameters getParameters(ResourceKey key) { + return getParameters(key.location()); + } + + private static void addParameters(ResourceLocation location, Climate.ParameterPoint parameters) { + BiomeParameters biomeParameters = getOrCreateParameters(location); + biomeParameters.add(parameters); + } + + private static BiomeParameters getOrCreateParameters(ResourceLocation location) { + if (BIOMES.containsKey(location)) { + return BIOMES.get(location); + } else { + BiomeParameters parameters = new BiomeParameters(); + BIOMES.put(location, parameters); + return parameters; + } + } + + public static List points(BiomeParameters parameters) { + return parameters.points; + } + + public static List points(ResourceKey key) { + return points(key.location()); + } + + public static List points(ResourceLocation location) { + return points(OverworldBiomeBuilderParameters.getParameters(location)); + } + + private static void addBiomes(Consumer> key) { + ImmutableList.Builder>> builder = new ImmutableList.Builder<>(); + new OverworldBiomeBuilder().addBiomes(parameterPointResourceKeyPair -> builder.add(parameterPointResourceKeyPair)); + builder.build().forEach(parameterPointResourceKeyPair -> key.accept(new Pair<>(parameterPointResourceKeyPair.getSecond().location(), parameterPointResourceKeyPair.getFirst()))); + } + + private static final List OFF_COAST_POINTS = new ArrayList<>(); + + public static List getOffCoastPoints() { + runBiomes(); + return OFF_COAST_POINTS; + } + +} diff --git a/src/main/java/net/frozenblock/lib/worldgen/biome/api/parameters/Temperature.java b/src/main/java/net/frozenblock/lib/worldgen/biome/api/parameters/Temperature.java new file mode 100644 index 0000000..18104ac --- /dev/null +++ b/src/main/java/net/frozenblock/lib/worldgen/biome/api/parameters/Temperature.java @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.worldgen.biome.api.parameters; + +import net.minecraft.world.level.biome.Climate; + +public final class Temperature { + public static final Climate.Parameter ICY = OverworldBiomeBuilderParameters.TEMPERATURES[0]; + public static final Climate.Parameter COOL = OverworldBiomeBuilderParameters.TEMPERATURES[1]; + public static final Climate.Parameter NEUTRAL = OverworldBiomeBuilderParameters.TEMPERATURES[2]; + public static final Climate.Parameter WARM = OverworldBiomeBuilderParameters.TEMPERATURES[3]; + public static final Climate.Parameter HOT = OverworldBiomeBuilderParameters.TEMPERATURES[4]; + + public static final Climate.Parameter ONE = OverworldBiomeBuilderParameters.TEMPERATURES[0]; + public static final Climate.Parameter TWO = OverworldBiomeBuilderParameters.TEMPERATURES[1]; + public static final Climate.Parameter THREE = OverworldBiomeBuilderParameters.TEMPERATURES[2]; + public static final Climate.Parameter FOUR = OverworldBiomeBuilderParameters.TEMPERATURES[3]; + public static final Climate.Parameter FIVE = OverworldBiomeBuilderParameters.TEMPERATURES[4]; + + public static final Climate.Parameter FULL_RANGE = OverworldBiomeBuilderParameters.FULL_RANGE; +} diff --git a/src/main/java/net/frozenblock/lib/worldgen/biome/api/parameters/Weirdness.java b/src/main/java/net/frozenblock/lib/worldgen/biome/api/parameters/Weirdness.java new file mode 100644 index 0000000..46e560d --- /dev/null +++ b/src/main/java/net/frozenblock/lib/worldgen/biome/api/parameters/Weirdness.java @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.worldgen.biome.api.parameters; + +import net.minecraft.world.level.biome.Climate; + +public final class Weirdness { + + public static final Climate.Parameter[] weirdnesses = new Climate.Parameter[]{ + Climate.Parameter.span(-1.0F, -0.93333334F), // 0 + Climate.Parameter.span(-0.93333334F, -0.7666667F), // 1 + Climate.Parameter.span(-0.7666667F, -0.56666666F), // 2 + Climate.Parameter.span(-0.56666666F, -0.4F), // 3 + Climate.Parameter.span(-0.4F, -0.26666668F), // 4 + Climate.Parameter.span(-0.26666668F, -0.05F), // 5 + Climate.Parameter.span(-0.05F, 0.05F), // 6 + Climate.Parameter.span(0.05F, 0.26666668F), // 7 + Climate.Parameter.span(0.26666668F, 0.4F), // 8 + Climate.Parameter.span(0.4F, 0.56666666F), // 9 + Climate.Parameter.span(0.56666666F, 0.7666667F), // 10 + Climate.Parameter.span(0.7666667F, 0.93333334F), // 11 + Climate.Parameter.span(0.93333334F, 1.0F) // 12 + }; + + public static final Climate.Parameter MID_SLICE_NORMAL_ASCENDING = weirdnesses[0]; + public static final Climate.Parameter HIGH_SLICE_NORMAL_ASCENDING = weirdnesses[1]; + public static final Climate.Parameter PEAK_NORMAL = weirdnesses[2]; + public static final Climate.Parameter HIGH_SLICE_NORMAL_DESCENDING = weirdnesses[3]; + public static final Climate.Parameter MID_SLICE_NORMAL_DESCENDING = weirdnesses[4]; + public static final Climate.Parameter LOW_SLICE_NORMAL_DESCENDING = weirdnesses[5]; + public static final Climate.Parameter VALLEY = weirdnesses[6]; + public static final Climate.Parameter LOW_SLICE_VARIANT_ASCENDING = weirdnesses[7]; + public static final Climate.Parameter MID_SLICE_VARIANT_ASCENDING = weirdnesses[8]; + public static final Climate.Parameter HIGH_SLICE_VARIANT_ASCENDING = weirdnesses[9]; + public static final Climate.Parameter PEAK_VARIANT = weirdnesses[10]; + public static final Climate.Parameter HIGH_SLICE_VARIANT_DESCENDING = weirdnesses[11]; + public static final Climate.Parameter MID_SLICE_VARIANT_DESCENDING = weirdnesses[12]; + public static final Climate.Parameter FULL_RANGE = OverworldBiomeBuilderParameters.FULL_RANGE; +} diff --git a/src/main/java/net/frozenblock/lib/worldgen/biome/impl/OverworldBiomeData.java b/src/main/java/net/frozenblock/lib/worldgen/biome/impl/OverworldBiomeData.java new file mode 100644 index 0000000..3f6f64d --- /dev/null +++ b/src/main/java/net/frozenblock/lib/worldgen/biome/impl/OverworldBiomeData.java @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.worldgen.biome.impl; + +import com.google.common.base.Preconditions; +import com.mojang.datafixers.util.Pair; +import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; +import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet; +import net.frozenblock.lib.FrozenSharedConstants; +import net.minecraft.resources.ResourceKey; +import net.minecraft.world.level.biome.Biome; +import net.minecraft.world.level.biome.Climate; +import net.minecraft.world.level.biome.MultiNoiseBiomeSourceParameterList; +import org.jetbrains.annotations.ApiStatus; +import org.slf4j.Logger; + +import java.util.ArrayList; +import java.util.Map; +import java.util.Set; +import java.util.function.Function; + +@ApiStatus.Internal +public final class OverworldBiomeData { + + private static final Set> OVERWORLD_BIOMES = new ObjectOpenHashSet<>(); + + private static final Map, Climate.ParameterPoint> OVERWORLD_BIOME_NOISE_POINTS = new Object2ObjectOpenHashMap<>(); + + private static final Logger LOGGER = FrozenSharedConstants.LOGGER; + + private OverworldBiomeData() { + } + + public static void addOverworldBiome(ResourceKey biome, Climate.ParameterPoint spawnNoisePoint) { + Preconditions.checkArgument(biome != null, "Biome is null"); + Preconditions.checkArgument(spawnNoisePoint != null, "Climate.ParameterPoint is null"); + OVERWORLD_BIOME_NOISE_POINTS.put(biome, spawnNoisePoint); + clearBiomeSourceCache(); + } + + public static Map, Climate.ParameterPoint> getOverworldBiomeNoisePoints() { + return OVERWORLD_BIOME_NOISE_POINTS; + } + + public static boolean canGenerateInOverworld(ResourceKey biome) { + return MultiNoiseBiomeSourceParameterList.Preset.OVERWORLD.usedBiomes().anyMatch(input -> input.equals(biome)); + } + + private static void clearBiomeSourceCache() { + OVERWORLD_BIOMES.clear(); // Clear cached biome source data + } + + public static Climate.ParameterList withModdedBiomeEntries(Climate.ParameterList entries, Function, T> biomes) { + if (OVERWORLD_BIOME_NOISE_POINTS.isEmpty()) { + return entries; + } + + ArrayList> entryList = new ArrayList<>(entries.values()); + + for (Map.Entry, Climate.ParameterPoint> entry : OVERWORLD_BIOME_NOISE_POINTS.entrySet()) { + entryList.add(Pair.of(entry.getValue(), biomes.apply(entry.getKey()))); + } + + return new Climate.ParameterList<>(entryList); + } +} diff --git a/src/main/java/net/frozenblock/lib/worldgen/biome/mixin/DebugLevelSourceAccessor.java b/src/main/java/net/frozenblock/lib/worldgen/biome/mixin/DebugLevelSourceAccessor.java new file mode 100644 index 0000000..75385b0 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/worldgen/biome/mixin/DebugLevelSourceAccessor.java @@ -0,0 +1,30 @@ +package net.frozenblock.lib.worldgen.biome.mixin; + +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.levelgen.DebugLevelSource; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Mutable; +import org.spongepowered.asm.mixin.gen.Accessor; + +import java.util.List; + +@Mixin(DebugLevelSource.class) +public interface DebugLevelSourceAccessor { + @Accessor + @Mutable + static void setALL_BLOCKS(List blockStates) { + throw new UnsupportedOperationException(); + } + + @Accessor + @Mutable + static void setGRID_WIDTH(int length) { + throw new UnsupportedOperationException(); + } + + @Accessor + @Mutable + static void setGRID_HEIGHT(int length) { + throw new UnsupportedOperationException(); + } +} diff --git a/src/main/java/net/frozenblock/lib/worldgen/biome/mixin/MultiNoiseBiomeSourceMixin.java b/src/main/java/net/frozenblock/lib/worldgen/biome/mixin/MultiNoiseBiomeSourceMixin.java new file mode 100644 index 0000000..ac2f920 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/worldgen/biome/mixin/MultiNoiseBiomeSourceMixin.java @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.worldgen.biome.mixin; + +import net.frozenblock.lib.worldgen.biome.api.FrozenBiomeSourceAccess; +import net.minecraft.world.level.biome.MultiNoiseBiomeSource; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Unique; + +@Mixin(MultiNoiseBiomeSource.class) +public class MultiNoiseBiomeSourceMixin implements FrozenBiomeSourceAccess { + + @Unique + private boolean frozenLib$modifyBiomeEntries = true; + + @Unique + @Override + public void frozenLib_setModifyBiomeEntries(boolean modifyBiomeEntries) { + this.frozenLib$modifyBiomeEntries = modifyBiomeEntries; + } + + @Unique + @Override + public boolean frozenLib_shouldModifyBiomeEntries() { + return this.frozenLib$modifyBiomeEntries; + } +} diff --git a/src/main/java/net/frozenblock/lib/worldgen/biome/mixin/OverworldBiomeBuilderMixin.java b/src/main/java/net/frozenblock/lib/worldgen/biome/mixin/OverworldBiomeBuilderMixin.java new file mode 100644 index 0000000..2555147 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/worldgen/biome/mixin/OverworldBiomeBuilderMixin.java @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.worldgen.biome.mixin; + +import com.mojang.datafixers.util.Pair; +import net.frozenblock.lib.worldgen.biome.api.FrozenBiome; +import net.minecraft.resources.ResourceKey; +import net.minecraft.world.level.biome.Biome; +import net.minecraft.world.level.biome.Climate; +import net.minecraft.world.level.biome.OverworldBiomeBuilder; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +import java.util.function.Consumer; + + +@Mixin(value = OverworldBiomeBuilder.class, priority = 69420) +public class OverworldBiomeBuilderMixin { + + @Inject( + method = "addBiomes", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/world/level/biome/OverworldBiomeBuilder;addUndergroundBiomes(Ljava/util/function/Consumer;)V", + shift = At.Shift.AFTER + ) + ) + public void frozenLib$injectFrozenBiomesToOverworld(Consumer>> consumer, CallbackInfo info) { + for (FrozenBiome frozenBiome : FrozenBiome.getFrozenBiomes()) { + frozenBiome.injectToOverworld(consumer); + } + } +} diff --git a/src/main/java/net/frozenblock/lib/worldgen/biome/mixin/OverworldBiomePresetMixin.java b/src/main/java/net/frozenblock/lib/worldgen/biome/mixin/OverworldBiomePresetMixin.java new file mode 100644 index 0000000..d98f62e --- /dev/null +++ b/src/main/java/net/frozenblock/lib/worldgen/biome/mixin/OverworldBiomePresetMixin.java @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.worldgen.biome.mixin; + +import com.llamalad7.mixinextras.injector.ModifyReturnValue; +import net.frozenblock.lib.worldgen.biome.impl.OverworldBiomeData; +import net.minecraft.resources.ResourceKey; +import net.minecraft.world.level.biome.Biome; +import net.minecraft.world.level.biome.Climate; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; + +import java.util.function.Function; + + +@Mixin(targets = "net/minecraft/world/level/biome/MultiNoiseBiomeSourceParameterList$Preset$2", priority = 991) +public class OverworldBiomePresetMixin { + + @ModifyReturnValue(method = "apply", at = @At("RETURN")) + private Climate.ParameterList apply(Climate.ParameterList original, Function, T> function) { + return OverworldBiomeData.withModdedBiomeEntries(original, function); + } +} diff --git a/src/main/java/net/frozenblock/lib/worldgen/biome/package-info.java b/src/main/java/net/frozenblock/lib/worldgen/biome/package-info.java new file mode 100644 index 0000000..182566c --- /dev/null +++ b/src/main/java/net/frozenblock/lib/worldgen/biome/package-info.java @@ -0,0 +1,22 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +/** + * This package is based off of Fabric API's Biome API but with additional functionality. + */ + +package net.frozenblock.lib.worldgen.biome; \ No newline at end of file diff --git a/src/main/java/net/frozenblock/lib/worldgen/feature/api/FrozenConfiguredFeature.java b/src/main/java/net/frozenblock/lib/worldgen/feature/api/FrozenConfiguredFeature.java new file mode 100644 index 0000000..db103b4 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/worldgen/feature/api/FrozenConfiguredFeature.java @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.worldgen.feature.api; + +import net.frozenblock.lib.FrozenLogUtils; +import net.minecraft.core.Holder; +import net.minecraft.core.registries.Registries; +import net.minecraft.resources.ResourceKey; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.level.LevelReader; +import net.minecraft.world.level.levelgen.feature.ConfiguredFeature; +import net.minecraft.world.level.levelgen.feature.Feature; +import net.minecraft.world.level.levelgen.feature.configurations.FeatureConfiguration; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; + +public class FrozenConfiguredFeature> { + + /** + * Can be used for setting all bootstrap contexts on 1.19.3 + */ + public static final List> FEATURES = new ArrayList<>(); + + private final ResourceKey> key; + + public FrozenConfiguredFeature(ResourceLocation key) { + this.key = ResourceKey.create(Registries.CONFIGURED_FEATURE, key); + FEATURES.add(this); + } + + public ResourceKey> getKey() { + return key; + } + + public Holder> getHolder(@Nullable LevelReader level) { + if (level == null) + return FrozenFeatureUtils.BOOTSTRAP_CONTEXT.lookup(Registries.CONFIGURED_FEATURE).getOrThrow(this.getKey()); + return level.registryAccess().lookupOrThrow(Registries.CONFIGURED_FEATURE).getOrThrow(this.getKey()); + } + + public Holder> getHolder() { + return getHolder(null); + } + + public ConfiguredFeature getConfiguredFeature(LevelReader level) { + return getHolder(level).value(); + } + + @SuppressWarnings("unchecked") + public > FrozenConfiguredFeature makeAndSetHolder(F feature, FC config) { + FrozenLogUtils.log("Registering configured feature: " + this.getKey().location(), true); + + assert FrozenFeatureUtils.BOOTSTRAP_CONTEXT != null : "Bootstrap context is null while registering " + this.getKey().location(); + + assert feature != null : "Feature is null whilst registering " + this.getKey().location(); + assert config != null : "Feature configuration is null whilst registering " + this.getKey().location(); + + FrozenFeatureUtils.BOOTSTRAP_CONTEXT.register((ResourceKey) this.getKey(), new ConfiguredFeature<>(feature, config)); + return this; + } +} diff --git a/src/main/java/net/frozenblock/lib/worldgen/feature/api/FrozenConfiguredFeatureUtils.java b/src/main/java/net/frozenblock/lib/worldgen/feature/api/FrozenConfiguredFeatureUtils.java new file mode 100644 index 0000000..a023ea5 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/worldgen/feature/api/FrozenConfiguredFeatureUtils.java @@ -0,0 +1,91 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.worldgen.feature.api; + +import lombok.experimental.UtilityClass; +import net.minecraft.core.Holder; +import net.minecraft.core.Registry; +import net.minecraft.core.registries.Registries; +import net.minecraft.data.registries.VanillaRegistries; +import net.minecraft.data.worldgen.BootstrapContext; +import net.minecraft.data.worldgen.features.FeatureUtils; +import net.minecraft.resources.ResourceKey; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.level.levelgen.feature.ConfiguredFeature; +import net.minecraft.world.level.levelgen.feature.Feature; +import net.minecraft.world.level.levelgen.feature.configurations.FeatureConfiguration; +import net.minecraft.world.level.levelgen.feature.configurations.NoneFeatureConfiguration; +import org.jetbrains.annotations.NotNull; +import org.quiltmc.qsl.frozenblock.core.registry.api.event.DynamicRegistryManagerSetupContext; + +import java.util.Set; + +@UtilityClass +public class FrozenConfiguredFeatureUtils { + + public static ResourceKey> createKey(String namespace, String path) { + return ResourceKey.create(Registries.CONFIGURED_FEATURE, ResourceLocation.fromNamespaceAndPath(namespace, path)); + } + + public static Holder> register(DynamicRegistryManagerSetupContext context, DynamicRegistryManagerSetupContext.RegistryMap registries, String namespace, String id, Feature feature) { + return register(context, registries, namespace, id, feature, FeatureConfiguration.NONE); + } + + public static , C extends ConfiguredFeature> Holder.Reference register(DynamicRegistryManagerSetupContext context, DynamicRegistryManagerSetupContext.RegistryMap registries, @NotNull String namespace, @NotNull String id, F feature, @NotNull FC config) { + var configuredRegistry = registries.get(Registries.CONFIGURED_FEATURE); + final ConfiguredFeature configuredFeature = new ConfiguredFeature<>(feature, config); + Registry.register(configuredRegistry, ResourceLocation.fromNamespaceAndPath(namespace, id), configuredFeature); + var featureEntry = getExact(registries, configuredFeature); + return (Holder.Reference) featureEntry; + } + + public static void register( + BootstrapContext> BootstrapContext, ResourceKey> registryKey, Feature feature + ) { + FeatureUtils.register(BootstrapContext, registryKey, feature, FeatureConfiguration.NONE); + } + + public static > Holder> register( + BootstrapContext> entries, ResourceKey> registryKey, F feature, FC featureConfiguration + ) { + return entries.register(registryKey, new ConfiguredFeature<>(feature, featureConfiguration)); + } + + public static > Holder> register( + DynamicRegistryManagerSetupContext entries, ResourceKey> registryKey, F feature, FC featureConfiguration + ) { + var registry = entries.getRegistries(Set.of(Registries.CONFIGURED_FEATURE)); + var value = registry.register(Registries.CONFIGURED_FEATURE, registryKey.location(), new ConfiguredFeature<>(feature, featureConfiguration)); + return Holder.direct(value); + } + + public static > Holder.Reference> getExact(DynamicRegistryManagerSetupContext.RegistryMap registries, V value) { + var configuredRegistry = registries.get(Registries.CONFIGURED_FEATURE); + var holder = configuredRegistry.getHolderOrThrow(configuredRegistry.getResourceKey(value).orElseThrow()); + var exactHolder = getExactReference(holder); + return exactHolder; + } + + public static , V extends ConfiguredFeature> Holder.Reference getExactReference(Holder.Reference reference) { + return (Holder.Reference) reference; + } + + public static Holder> getHolder(ResourceKey> resourceKey) { + return VanillaRegistries.createLookup().lookupOrThrow(Registries.CONFIGURED_FEATURE).getOrThrow(resourceKey); + } +} diff --git a/src/main/java/net/frozenblock/lib/worldgen/feature/api/FrozenFeatureUtils.java b/src/main/java/net/frozenblock/lib/worldgen/feature/api/FrozenFeatureUtils.java new file mode 100644 index 0000000..bac1d45 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/worldgen/feature/api/FrozenFeatureUtils.java @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.worldgen.feature.api; + +import lombok.experimental.UtilityClass; +import net.minecraft.data.worldgen.BootstrapContext; + +@UtilityClass +public class FrozenFeatureUtils { + + public static BootstrapContext BOOTSTRAP_CONTEXT = null; +} diff --git a/src/main/java/net/frozenblock/lib/worldgen/feature/api/FrozenFeatures.java b/src/main/java/net/frozenblock/lib/worldgen/feature/api/FrozenFeatures.java new file mode 100644 index 0000000..4d01089 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/worldgen/feature/api/FrozenFeatures.java @@ -0,0 +1,84 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.worldgen.feature.api; + +import net.frozenblock.lib.FrozenSharedConstants; +import net.frozenblock.lib.worldgen.feature.api.features.*; +import net.frozenblock.lib.worldgen.feature.api.features.config.*; +import net.minecraft.core.Registry; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.world.level.levelgen.feature.Feature; +import net.minecraft.world.level.levelgen.feature.configurations.SimpleBlockConfiguration; +import net.minecraft.world.level.levelgen.feature.configurations.VegetationPatchConfiguration; +import net.neoforged.neoforge.registries.RegisterEvent; + +public class FrozenFeatures { + + public static final NoisePathFeature NOISE_PATH_FEATURE = new NoisePathFeature(PathFeatureConfig.CODEC); + public static final NoisePathTagFeature NOISE_PATH_TAG_FEATURE = new NoisePathTagFeature(PathTagFeatureConfig.CODEC); + public static final NoisePlantFeature NOISE_PLANT_FEATURE = new NoisePlantFeature(PathFeatureConfig.CODEC); + public static final NoisePathSwapUnderWaterFeature NOISE_PATH_SWAP_UNDER_WATER_FEATURE = new NoisePathSwapUnderWaterFeature(PathSwapUnderWaterFeatureConfig.CODEC); + public static final NoisePathSwapUnderWaterTagFeature NOISE_PATH_SWAP_UNDER_WATER_TAG_FEATURE = new NoisePathSwapUnderWaterTagFeature(PathSwapUnderWaterTagFeatureConfig.CODEC); + public static final NoisePathUnderWaterFeature NOISE_PATH_UNDER_WATER_FEATURE = new NoisePathUnderWaterFeature(PathFeatureConfig.CODEC); + public static final NoisePathTagUnderWaterFeature NOISE_PATH_TAG_UNDER_WATER_FEATURE = new NoisePathTagUnderWaterFeature(PathTagFeatureConfig.CODEC); + public static final ColumnWithDiskFeature COLUMN_WITH_DISK_FEATURE = new ColumnWithDiskFeature(ColumnWithDiskFeatureConfig.CODEC); + public static final UpwardsColumnFeature UPWARDS_COLUMN_FEATURE = new UpwardsColumnFeature(ColumnFeatureConfig.CODEC); + public static final DownwardsColumnFeature DOWNWARDS_COLUMN_FEATURE = new DownwardsColumnFeature(ColumnFeatureConfig.CODEC); + public static final CircularWaterloggedVegetationPatchFeature CIRCULAR_WATERLOGGED_VEGETATION_PATCH = new CircularWaterloggedVegetationPatchFeature(VegetationPatchConfiguration.CODEC); + public static final CircularWaterloggedVegetationPatchLessBordersFeature CIRCULAR_WATERLOGGED_VEGETATION_PATCH_LESS_BORDERS = new CircularWaterloggedVegetationPatchLessBordersFeature(VegetationPatchConfiguration.CODEC); + public static final FadingDiskTagFeature FADING_DISK_TAG_FEATURE = new FadingDiskTagFeature(FadingDiskTagFeatureConfig.CODEC); + public static final FadingDiskTagExceptInBiomeFeature FADING_DISK_TAG_EXCEPT_IN_BIOME_FEATURE = new FadingDiskTagExceptInBiomeFeature(FadingDiskTagBiomeFeatureConfig.CODEC); + public static final FadingDiskFeature FADING_DISK_FEATURE = new FadingDiskFeature(FadingDiskFeatureConfig.CODEC); + public static final FadingDiskCarpetFeature FADING_DISK_CARPET_FEATURE = new FadingDiskCarpetFeature(FadingDiskCarpetFeatureConfig.CODEC); + public static final FadingDiskWithPileTagFeature FADING_DISK_WITH_PILE_TAG_FEATURE = new FadingDiskWithPileTagFeature(FadingDiskTagFeatureConfig.CODEC); + public static final CurvingTunnelFeature CURVING_TUNNEL_FEATURE = new CurvingTunnelFeature(CurvingTunnelFeatureConfig.CODEC); + public static final CircularLavaVegetationPatchFeature CIRCULAR_LAVA_VEGETATION_PATCH = new CircularLavaVegetationPatchFeature(VegetationPatchConfiguration.CODEC); + public static final CircularLavaVegetationPatchLessBordersFeature CIRCULAR_LAVA_VEGETATION_PATCH_LESS_BORDERS = new CircularLavaVegetationPatchLessBordersFeature(VegetationPatchConfiguration.CODEC); + public static final SimpleBlockScheduleTickFeature SIMPLE_BLOCK_SCHEDULE_TICK_FEATURE = new SimpleBlockScheduleTickFeature(SimpleBlockConfiguration.CODEC); + public static final FadingDiskTagScheduleTickFeature FADING_DISK_TAG_SCHEDULE_TICK_FEATURE = new FadingDiskTagScheduleTickFeature(FadingDiskTagFeatureConfig.CODEC); + public static final NoisePathScheduleTickFeature NOISE_PATH_SCHEDULE_TICK_FEATURE = new NoisePathScheduleTickFeature(PathFeatureConfig.CODEC); + public static final ComboFeature COMBO_FEATURE = new ComboFeature(ComboFeatureConfig.CODEC); + + public static void init(RegisterEvent.RegisterHelper> registry) { + registry.register(FrozenSharedConstants.id("noise_path_feature"), NOISE_PATH_FEATURE); + registry.register(FrozenSharedConstants.id("noise_path_tag_feature"), NOISE_PATH_TAG_FEATURE); + registry.register(FrozenSharedConstants.id("noise_plant_feature"), NOISE_PLANT_FEATURE); + registry.register(FrozenSharedConstants.id("noise_path_swap_under_water_feature"), NOISE_PATH_SWAP_UNDER_WATER_FEATURE); + registry.register(FrozenSharedConstants.id("noise_path_swap_under_water_tag_feature"), NOISE_PATH_SWAP_UNDER_WATER_TAG_FEATURE); + registry.register(FrozenSharedConstants.id("noise_path_under_water_feature"), NOISE_PATH_UNDER_WATER_FEATURE); + registry.register(FrozenSharedConstants.id("noise_path_tag_under_water_feature"), NOISE_PATH_TAG_UNDER_WATER_FEATURE); + registry.register(FrozenSharedConstants.id("column_with_disk_feature"), COLUMN_WITH_DISK_FEATURE); + registry.register(FrozenSharedConstants.id("upwards_column"), UPWARDS_COLUMN_FEATURE); + registry.register(FrozenSharedConstants.id("downwards_column"), DOWNWARDS_COLUMN_FEATURE); + registry.register(FrozenSharedConstants.id("circular_waterlogged_vegetation_patch"), CIRCULAR_WATERLOGGED_VEGETATION_PATCH); + registry.register(FrozenSharedConstants.id("circular_waterlogged_vegetation_patch_less_borders"), CIRCULAR_WATERLOGGED_VEGETATION_PATCH_LESS_BORDERS); + registry.register(FrozenSharedConstants.id("fading_disk_tag_feature"), FADING_DISK_TAG_FEATURE); + registry.register(FrozenSharedConstants.id("fading_disk_tag_except_in_biome_feature"), FADING_DISK_TAG_EXCEPT_IN_BIOME_FEATURE); + registry.register(FrozenSharedConstants.id("fading_disk_feature"), FADING_DISK_FEATURE); + registry.register(FrozenSharedConstants.id("fading_disk_carpet_feature"), FADING_DISK_CARPET_FEATURE); + registry.register(FrozenSharedConstants.id("fading_disk_with_pile_tag_feature"), FADING_DISK_WITH_PILE_TAG_FEATURE); + registry.register(FrozenSharedConstants.id("curving_tunnel_feature"), CURVING_TUNNEL_FEATURE); + registry.register(FrozenSharedConstants.id("circular_lava_vegetation_patch"), CIRCULAR_LAVA_VEGETATION_PATCH); + registry.register(FrozenSharedConstants.id("circular_lava_vegetation_patch_less_borders"), CIRCULAR_LAVA_VEGETATION_PATCH_LESS_BORDERS); + registry.register(FrozenSharedConstants.id("simple_block_schedule_tick"), SIMPLE_BLOCK_SCHEDULE_TICK_FEATURE); + registry.register(FrozenSharedConstants.id("noise_path_schedule_tick_feature"), NOISE_PATH_SCHEDULE_TICK_FEATURE); + registry.register(FrozenSharedConstants.id("fading_disk_tag_schedule_tick_feature"), FADING_DISK_TAG_SCHEDULE_TICK_FEATURE); + registry.register(FrozenSharedConstants.id("combo_feature"), COMBO_FEATURE); + } + +} diff --git a/src/main/java/net/frozenblock/lib/worldgen/feature/api/FrozenPlacedFeature.java b/src/main/java/net/frozenblock/lib/worldgen/feature/api/FrozenPlacedFeature.java new file mode 100644 index 0000000..3aa5b8e --- /dev/null +++ b/src/main/java/net/frozenblock/lib/worldgen/feature/api/FrozenPlacedFeature.java @@ -0,0 +1,85 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.worldgen.feature.api; + +import net.frozenblock.lib.FrozenLogUtils; +import net.minecraft.core.Holder; +import net.minecraft.core.registries.Registries; +import net.minecraft.resources.ResourceKey; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.level.levelgen.feature.ConfiguredFeature; +import net.minecraft.world.level.levelgen.feature.configurations.FeatureConfiguration; +import net.minecraft.world.level.levelgen.placement.PlacedFeature; +import net.minecraft.world.level.levelgen.placement.PlacementModifier; + +import java.util.ArrayList; +import java.util.List; + +public class FrozenPlacedFeature { + + /** + * Can be used for setting all bootstrap contexts on 1.19.3 + */ + public static final List FEATURES = new ArrayList<>(); + + private final ResourceKey key; + + private Holder> configuredHolder; + + public FrozenPlacedFeature(ResourceLocation key) { + this.key = ResourceKey.create(Registries.PLACED_FEATURE, key); + FEATURES.add(this); + } + + public ResourceKey getKey() { + return key; + } + + public Holder> getConfiguredHolder() { + assert this.configuredHolder.value() != null : "Trying get null holder from placed feature " + this.getKey().location(); + return this.configuredHolder; + } + + public FrozenPlacedFeature setConfiguredHolder(Holder> configuredHolder) { + this.configuredHolder = configuredHolder; + return this; + } + + public Holder getHolder() { + return FrozenFeatureUtils.BOOTSTRAP_CONTEXT.lookup(Registries.PLACED_FEATURE).getOrThrow(this.getKey()); + } + + @SuppressWarnings({"unchecked", "rawtypes"}) + public FrozenPlacedFeature makeAndSetHolder(Holder> configuredHolder, List modifiers) { + setConfiguredHolder(configuredHolder); + + FrozenLogUtils.log("Registering placed feature " + this.getKey().location(), true); + + assert FrozenFeatureUtils.BOOTSTRAP_CONTEXT != null : "Boostrap context is null when writing FrozenPlacedFeature " + this.getKey().location(); + assert configuredHolder != null : "Configured feature holder for FrozenPlacedFeature " + this.getKey().location() + " null"; + assert modifiers != null : "Placement modifiers for FrozenPlacedFeature " + this.getKey().location() + " null"; + + FrozenFeatureUtils.BOOTSTRAP_CONTEXT.register((ResourceKey) this.getKey(), new PlacedFeature(configuredHolder, modifiers)); + + return this; + } + + public FrozenPlacedFeature makeAndSetHolder(Holder> configuredHolder, PlacementModifier... modifiers) { + return this.makeAndSetHolder(configuredHolder, List.of(modifiers)); + } +} diff --git a/src/main/java/net/frozenblock/lib/worldgen/feature/api/FrozenPlacementUtils.java b/src/main/java/net/frozenblock/lib/worldgen/feature/api/FrozenPlacementUtils.java new file mode 100644 index 0000000..949203f --- /dev/null +++ b/src/main/java/net/frozenblock/lib/worldgen/feature/api/FrozenPlacementUtils.java @@ -0,0 +1,84 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.worldgen.feature.api; + +import lombok.experimental.UtilityClass; +import net.minecraft.core.Holder; +import net.minecraft.core.registries.Registries; +import net.minecraft.data.registries.VanillaRegistries; +import net.minecraft.data.worldgen.BootstrapContext; +import net.minecraft.resources.ResourceKey; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.level.levelgen.feature.ConfiguredFeature; +import net.minecraft.world.level.levelgen.placement.PlacedFeature; +import net.minecraft.world.level.levelgen.placement.PlacementModifier; +import org.quiltmc.qsl.frozenblock.core.registry.api.event.DynamicRegistryManagerSetupContext; + +import java.util.List; +import java.util.Set; + +@UtilityClass +public class FrozenPlacementUtils { + + public static ResourceKey createKey(String namespace, String path) { + return ResourceKey.create(Registries.PLACED_FEATURE, ResourceLocation.fromNamespaceAndPath(namespace, path)); + } + + public static Holder register( + BootstrapContext entries, + ResourceKey registryKey, + Holder> holder, + List list + ) { + return entries.register(registryKey, new PlacedFeature(holder, List.copyOf(list))); + } + + public static Holder register( + BootstrapContext entries, + ResourceKey registryKey, + Holder> holder, + PlacementModifier... placementModifiers + ) { + return register(entries, registryKey, holder, List.of(placementModifiers)); + } + + public static Holder register( + DynamicRegistryManagerSetupContext entries, + ResourceKey registryKey, + ResourceKey> configuredKey, + List list + ) { + var registry = entries.getRegistries(Set.of(Registries.CONFIGURED_FEATURE, Registries.PLACED_FEATURE)); + var configured = entries.registryManager().lookupOrThrow(Registries.CONFIGURED_FEATURE).getOrThrow(configuredKey); + var value = registry.register(Registries.PLACED_FEATURE, registryKey.location(), new PlacedFeature(configured, List.copyOf(list))); + return Holder.direct(value); + } + + public static Holder register( + DynamicRegistryManagerSetupContext entries, + ResourceKey registryKey, + ResourceKey> resourceKey, + PlacementModifier... placementModifiers + ) { + return register(entries, registryKey, resourceKey, List.of(placementModifiers)); + } + + public static Holder getHolder(ResourceKey resourceKey) { + return VanillaRegistries.createLookup().lookupOrThrow(Registries.PLACED_FEATURE).getOrThrow(resourceKey); + } +} diff --git a/src/main/java/net/frozenblock/lib/worldgen/feature/api/features/CircularLavaVegetationPatchFeature.java b/src/main/java/net/frozenblock/lib/worldgen/feature/api/features/CircularLavaVegetationPatchFeature.java new file mode 100644 index 0000000..264e0b4 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/worldgen/feature/api/features/CircularLavaVegetationPatchFeature.java @@ -0,0 +1,143 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.worldgen.feature.api.features; + +import com.mojang.serialization.Codec; +import net.minecraft.core.BlockPos; +import net.minecraft.core.BlockPos.MutableBlockPos; +import net.minecraft.core.Direction; +import net.minecraft.util.RandomSource; +import net.minecraft.world.level.WorldGenLevel; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.state.BlockBehaviour; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.chunk.ChunkGenerator; +import net.minecraft.world.level.levelgen.feature.FeaturePlaceContext; +import net.minecraft.world.level.levelgen.feature.VegetationPatchFeature; +import net.minecraft.world.level.levelgen.feature.configurations.VegetationPatchConfiguration; +import org.jetbrains.annotations.NotNull; + +import java.util.HashSet; +import java.util.Iterator; +import java.util.Set; +import java.util.function.Predicate; + +public class CircularLavaVegetationPatchFeature extends VegetationPatchFeature { + + public CircularLavaVegetationPatchFeature(Codec codec) { + super(codec); + } + + @Override + public boolean place(@NotNull FeaturePlaceContext context) { + WorldGenLevel worldGenLevel = context.level(); + VegetationPatchConfiguration vegetationPatchConfiguration = context.config(); + RandomSource randomSource = context.random(); + BlockPos blockPos = context.origin(); + Predicate predicate = (state) -> state.is(vegetationPatchConfiguration.replaceable); + int radius = vegetationPatchConfiguration.xzRadius.sample(randomSource) + 1; + Set set = this.placeGroundPatch(worldGenLevel, vegetationPatchConfiguration, randomSource, blockPos, predicate, radius, radius); + this.distributeVegetation(context, worldGenLevel, vegetationPatchConfiguration, randomSource, set, radius, radius); + return !set.isEmpty(); + } + + public Set placeCircularGroundPatch(WorldGenLevel level, @NotNull VegetationPatchConfiguration config, RandomSource random, @NotNull BlockPos pos, Predicate state, int xRadius, int zRadius) { + MutableBlockPos mutableBlockPos = pos.mutable(); + MutableBlockPos mutableBlockPos2 = mutableBlockPos.mutable(); + Direction direction = config.surface.getDirection(); + Direction direction2 = direction.getOpposite(); + Set set = new HashSet<>(); + + for (int i = -xRadius; i <= xRadius; ++i) { + boolean bl = i == -xRadius || i == xRadius; + + for (int j = -zRadius; j <= zRadius; ++j) { + boolean bl2 = j == -zRadius || j == zRadius; + boolean bl3 = bl || bl2; + boolean bl4 = bl && bl2; + boolean bl5 = bl3 && !bl4; + if (!bl4 && (!bl5 || config.extraEdgeColumnChance != 0.0F && !(random.nextFloat() > config.extraEdgeColumnChance))) { + mutableBlockPos.setWithOffset(pos, i, 0, j); + + if (Math.sqrt(mutableBlockPos.distSqr(pos)) <= xRadius) { + int k; + for (k = 0; level.isStateAtPosition(mutableBlockPos, BlockBehaviour.BlockStateBase::isAir) && k < config.verticalRange; ++k) { + mutableBlockPos.move(direction); + } + + for (k = 0; level.isStateAtPosition(mutableBlockPos, (statex) -> !statex.isAir()) && k < config.verticalRange; ++k) { + mutableBlockPos.move(direction2); + } + + mutableBlockPos2.setWithOffset(mutableBlockPos, config.surface.getDirection()); + BlockState blockState = level.getBlockState(mutableBlockPos2); + if (level.isEmptyBlock(mutableBlockPos) && blockState.isFaceSturdy(level, mutableBlockPos2, config.surface.getDirection().getOpposite())) { + int l = config.depth.sample(random) + (config.extraBottomBlockChance > 0.0F && random.nextFloat() < config.extraBottomBlockChance ? 1 : 0); + BlockPos blockPos = mutableBlockPos2.immutable(); + boolean bl6 = this.placeGround(level, config, state, random, mutableBlockPos2, l); + if (bl6) { + set.add(blockPos); + } + } + } + } + } + } + + return set; + } + + @Override + protected Set placeGroundPatch(WorldGenLevel level, VegetationPatchConfiguration config, RandomSource random, BlockPos pos, Predicate state, int xRadius, int zRadius) { + Set set = this.placeCircularGroundPatch(level, config, random, pos, state, xRadius, zRadius); + Set set2 = new HashSet<>(); + MutableBlockPos mutableBlockPos = new MutableBlockPos(); + Iterator var11 = set.iterator(); + + BlockPos blockPos; + while (var11.hasNext()) { + blockPos = var11.next(); + if (!isExposed(level, blockPos, mutableBlockPos)) { + set2.add(blockPos); + } + } + + var11 = set2.iterator(); + + while (var11.hasNext()) { + blockPos = var11.next(); + level.setBlock(blockPos, Blocks.LAVA.defaultBlockState(), 2); + } + + return set2; + } + + private static boolean isExposed(WorldGenLevel level, BlockPos pos, MutableBlockPos mutablePos) { + return isExposedDirection(level, pos, mutablePos, Direction.NORTH) || isExposedDirection(level, pos, mutablePos, Direction.EAST) || isExposedDirection(level, pos, mutablePos, Direction.SOUTH) || isExposedDirection(level, pos, mutablePos, Direction.WEST) || isExposedDirection(level, pos, mutablePos, Direction.DOWN); + } + + private static boolean isExposedDirection(@NotNull WorldGenLevel level, BlockPos pos, @NotNull MutableBlockPos mutablePos, Direction direction) { + mutablePos.setWithOffset(pos, direction); + return !level.getBlockState(mutablePos).isFaceSturdy(level, mutablePos, direction.getOpposite()); + } + + @Override + protected boolean placeVegetation(WorldGenLevel level, VegetationPatchConfiguration config, ChunkGenerator chunkGenerator, RandomSource random, @NotNull BlockPos pos) { + return super.placeVegetation(level, config, chunkGenerator, random, pos.below()); + } +} diff --git a/src/main/java/net/frozenblock/lib/worldgen/feature/api/features/CircularLavaVegetationPatchLessBordersFeature.java b/src/main/java/net/frozenblock/lib/worldgen/feature/api/features/CircularLavaVegetationPatchLessBordersFeature.java new file mode 100644 index 0000000..0e2b3b4 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/worldgen/feature/api/features/CircularLavaVegetationPatchLessBordersFeature.java @@ -0,0 +1,152 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.worldgen.feature.api.features; + +import com.mojang.serialization.Codec; +import net.minecraft.core.BlockPos; +import net.minecraft.core.BlockPos.MutableBlockPos; +import net.minecraft.core.Direction; +import net.minecraft.util.RandomSource; +import net.minecraft.world.level.WorldGenLevel; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.state.BlockBehaviour; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.chunk.ChunkGenerator; +import net.minecraft.world.level.levelgen.feature.FeaturePlaceContext; +import net.minecraft.world.level.levelgen.feature.VegetationPatchFeature; +import net.minecraft.world.level.levelgen.feature.configurations.VegetationPatchConfiguration; +import org.jetbrains.annotations.NotNull; + +import java.util.HashSet; +import java.util.Iterator; +import java.util.Set; +import java.util.function.Predicate; + +public class CircularLavaVegetationPatchLessBordersFeature extends VegetationPatchFeature { + + public CircularLavaVegetationPatchLessBordersFeature(Codec codec) { + super(codec); + } + + @Override + public boolean place(@NotNull FeaturePlaceContext context) { + WorldGenLevel worldGenLevel = context.level(); + VegetationPatchConfiguration vegetationPatchConfiguration = context.config(); + RandomSource randomSource = context.random(); + BlockPos blockPos = context.origin(); + Predicate predicate = (state) -> state.is(vegetationPatchConfiguration.replaceable); + int radius = vegetationPatchConfiguration.xzRadius.sample(randomSource) + 1; + Set set = this.placeGroundPatch(worldGenLevel, vegetationPatchConfiguration, randomSource, blockPos, predicate, radius, radius); + this.distributeVegetation(context, worldGenLevel, vegetationPatchConfiguration, randomSource, set, radius, radius); + return !set.isEmpty(); + } + + public Set placeCircularGroundPatch(WorldGenLevel level, @NotNull VegetationPatchConfiguration config, RandomSource random, @NotNull BlockPos pos, Predicate state, int xRadius, int zRadius) { + MutableBlockPos mutableBlockPos = pos.mutable(); + MutableBlockPos mutableBlockPos2 = mutableBlockPos.mutable(); + Direction direction = config.surface.getDirection(); + Direction direction2 = direction.getOpposite(); + Set set = new HashSet<>(); + + for (int i = -xRadius; i <= xRadius; ++i) { + for (int j = -zRadius; j <= zRadius; ++j) { + mutableBlockPos.setWithOffset(pos, i, 0, j); + + if (Math.sqrt(mutableBlockPos.distSqr(pos)) <= xRadius) { + int k; + for (k = 0; level.isStateAtPosition(mutableBlockPos, BlockBehaviour.BlockStateBase::isAir) && k < config.verticalRange; ++k) { + mutableBlockPos.move(direction); + } + + for (k = 0; level.isStateAtPosition(mutableBlockPos, (statex) -> !statex.isAir()) && k < config.verticalRange; ++k) { + mutableBlockPos.move(direction2); + } + + mutableBlockPos2.setWithOffset(mutableBlockPos, config.surface.getDirection()); + BlockState blockState = level.getBlockState(mutableBlockPos2); + if (level.isEmptyBlock(mutableBlockPos) && blockState.isFaceSturdy(level, mutableBlockPos2, config.surface.getDirection().getOpposite())) { + int l = config.depth.sample(random) + (config.extraBottomBlockChance > 0.0F && random.nextFloat() < config.extraBottomBlockChance ? 1 : 0); + BlockPos blockPos = mutableBlockPos2.immutable(); + boolean bl6 = this.placeGround(level, config, state, random, mutableBlockPos2, l); + if (bl6) { + set.add(blockPos); + } + } + } + } + } + + return set; + } + + @Override + protected Set placeGroundPatch(WorldGenLevel level, VegetationPatchConfiguration config, RandomSource random, BlockPos pos, Predicate state, int xRadius, int zRadius) { + Set set = this.placeCircularGroundPatch(level, config, random, pos, state, xRadius, zRadius); + Set set2 = new HashSet<>(); + MutableBlockPos mutableBlockPos = new MutableBlockPos(); + Iterator var11 = set.iterator(); + + BlockPos blockPos; + while (var11.hasNext()) { + blockPos = var11.next(); + if (!isExposed(level, blockPos, mutableBlockPos)) { + set2.add(blockPos); + } + } + + var11 = set2.iterator(); + + while (var11.hasNext()) { + blockPos = var11.next(); + level.setBlock(blockPos, Blocks.LAVA.defaultBlockState(), 2); + } + + return set2; + } + + private static boolean isExposed(WorldGenLevel level, BlockPos pos, MutableBlockPos mutablePos) { + return isExposedDirection(level, pos, mutablePos, Direction.NORTH) + || isExposedDirection(level, pos, mutablePos, Direction.EAST) + || isExposedDirection(level, pos, mutablePos, Direction.SOUTH) + || isExposedDirection(level, pos, mutablePos, Direction.WEST) + || isExposedDirection(level, pos, mutablePos, Direction.DOWN); + } + + private static boolean isExposedDirection(@NotNull WorldGenLevel level, BlockPos pos, @NotNull MutableBlockPos mutablePos, Direction direction) { + mutablePos.setWithOffset(pos, direction); + BlockState state = level.getBlockState(mutablePos); + return !state.isFaceSturdy(level, mutablePos, direction.getOpposite()) && !state.is(Blocks.LAVA); + } + + @Override + protected boolean placeVegetation(WorldGenLevel level, VegetationPatchConfiguration config, ChunkGenerator chunkGenerator, RandomSource random, @NotNull BlockPos pos) { + return super.placeVegetation(level, config, chunkGenerator, random, pos.below()); + } + + @Override + protected boolean placeGround(WorldGenLevel level, VegetationPatchConfiguration config, Predicate replaceableblocks, RandomSource random, MutableBlockPos mutablePos, int maxDistance) { + for (int i = 0; i < maxDistance; ++i) { + BlockState blockState2 = level.getBlockState(mutablePos); + if (!replaceableblocks.test(blockState2)) { + return i != 0; + } + mutablePos.move(config.surface.getDirection()); + } + return true; + } +} diff --git a/src/main/java/net/frozenblock/lib/worldgen/feature/api/features/CircularWaterloggedVegetationPatchFeature.java b/src/main/java/net/frozenblock/lib/worldgen/feature/api/features/CircularWaterloggedVegetationPatchFeature.java new file mode 100644 index 0000000..e03982d --- /dev/null +++ b/src/main/java/net/frozenblock/lib/worldgen/feature/api/features/CircularWaterloggedVegetationPatchFeature.java @@ -0,0 +1,153 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.worldgen.feature.api.features; + +import com.mojang.serialization.Codec; +import net.minecraft.core.BlockPos; +import net.minecraft.core.BlockPos.MutableBlockPos; +import net.minecraft.core.Direction; +import net.minecraft.util.RandomSource; +import net.minecraft.world.level.WorldGenLevel; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.state.BlockBehaviour; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.block.state.properties.BlockStateProperties; +import net.minecraft.world.level.chunk.ChunkGenerator; +import net.minecraft.world.level.levelgen.feature.FeaturePlaceContext; +import net.minecraft.world.level.levelgen.feature.VegetationPatchFeature; +import net.minecraft.world.level.levelgen.feature.configurations.VegetationPatchConfiguration; +import org.jetbrains.annotations.NotNull; + +import java.util.HashSet; +import java.util.Iterator; +import java.util.Set; +import java.util.function.Predicate; + +public class CircularWaterloggedVegetationPatchFeature extends VegetationPatchFeature { + + public CircularWaterloggedVegetationPatchFeature(Codec codec) { + super(codec); + } + + @Override + public boolean place(@NotNull FeaturePlaceContext context) { + WorldGenLevel worldGenLevel = context.level(); + VegetationPatchConfiguration vegetationPatchConfiguration = context.config(); + RandomSource randomSource = context.random(); + BlockPos blockPos = context.origin(); + Predicate predicate = (state) -> state.is(vegetationPatchConfiguration.replaceable); + int radius = vegetationPatchConfiguration.xzRadius.sample(randomSource) + 1; + Set set = this.placeGroundPatch(worldGenLevel, vegetationPatchConfiguration, randomSource, blockPos, predicate, radius, radius); + this.distributeVegetation(context, worldGenLevel, vegetationPatchConfiguration, randomSource, set, radius, radius); + return !set.isEmpty(); + } + + public Set placeCircularGroundPatch(WorldGenLevel level, @NotNull VegetationPatchConfiguration config, RandomSource random, @NotNull BlockPos pos, Predicate state, int xRadius, int zRadius) { + MutableBlockPos mutableBlockPos = pos.mutable(); + MutableBlockPos mutableBlockPos2 = mutableBlockPos.mutable(); + Direction direction = config.surface.getDirection(); + Direction direction2 = direction.getOpposite(); + Set set = new HashSet<>(); + + for (int i = -xRadius; i <= xRadius; ++i) { + boolean bl = i == -xRadius || i == xRadius; + + for (int j = -zRadius; j <= zRadius; ++j) { + boolean bl2 = j == -zRadius || j == zRadius; + boolean bl3 = bl || bl2; + boolean bl4 = bl && bl2; + boolean bl5 = bl3 && !bl4; + if (!bl4 && (!bl5 || config.extraEdgeColumnChance != 0.0F && !(random.nextFloat() > config.extraEdgeColumnChance))) { + mutableBlockPos.setWithOffset(pos, i, 0, j); + + if (Math.sqrt(mutableBlockPos.distSqr(pos)) <= xRadius) { + int k; + for (k = 0; level.isStateAtPosition(mutableBlockPos, BlockBehaviour.BlockStateBase::isAir) && k < config.verticalRange; ++k) { + mutableBlockPos.move(direction); + } + + for (k = 0; level.isStateAtPosition(mutableBlockPos, (statex) -> !statex.isAir()) && k < config.verticalRange; ++k) { + mutableBlockPos.move(direction2); + } + + mutableBlockPos2.setWithOffset(mutableBlockPos, config.surface.getDirection()); + BlockState blockState = level.getBlockState(mutableBlockPos2); + if (level.isEmptyBlock(mutableBlockPos) && blockState.isFaceSturdy(level, mutableBlockPos2, config.surface.getDirection().getOpposite())) { + int l = config.depth.sample(random) + (config.extraBottomBlockChance > 0.0F && random.nextFloat() < config.extraBottomBlockChance ? 1 : 0); + BlockPos blockPos = mutableBlockPos2.immutable(); + boolean bl6 = this.placeGround(level, config, state, random, mutableBlockPos2, l); + if (bl6) { + set.add(blockPos); + } + } + } + } + } + } + + return set; + } + + @Override + protected Set placeGroundPatch(WorldGenLevel level, VegetationPatchConfiguration config, RandomSource random, BlockPos pos, Predicate state, int xRadius, int zRadius) { + Set set = this.placeCircularGroundPatch(level, config, random, pos, state, xRadius, zRadius); + Set set2 = new HashSet<>(); + MutableBlockPos mutableBlockPos = new MutableBlockPos(); + Iterator var11 = set.iterator(); + + BlockPos blockPos; + while (var11.hasNext()) { + blockPos = var11.next(); + if (!isExposed(level, blockPos, mutableBlockPos)) { + set2.add(blockPos); + } + } + + var11 = set2.iterator(); + + while (var11.hasNext()) { + blockPos = var11.next(); + level.setBlock(blockPos, Blocks.WATER.defaultBlockState(), 2); + } + + return set2; + } + + private static boolean isExposed(WorldGenLevel level, BlockPos pos, MutableBlockPos mutablePos) { + return isExposedDirection(level, pos, mutablePos, Direction.NORTH) || isExposedDirection(level, pos, mutablePos, Direction.EAST) || isExposedDirection(level, pos, mutablePos, Direction.SOUTH) || isExposedDirection(level, pos, mutablePos, Direction.WEST) || isExposedDirection(level, pos, mutablePos, Direction.DOWN); + } + + private static boolean isExposedDirection(@NotNull WorldGenLevel level, BlockPos pos, @NotNull MutableBlockPos mutablePos, Direction direction) { + mutablePos.setWithOffset(pos, direction); + return !level.getBlockState(mutablePos).isFaceSturdy(level, mutablePos, direction.getOpposite()); + } + + @Override + protected boolean placeVegetation(WorldGenLevel level, VegetationPatchConfiguration config, ChunkGenerator chunkGenerator, RandomSource random, @NotNull BlockPos pos) { + if (super.placeVegetation(level, config, chunkGenerator, random, pos.below())) { + BlockState blockState = level.getBlockState(pos); + if (blockState.hasProperty(BlockStateProperties.WATERLOGGED) && !(Boolean) blockState.getValue(BlockStateProperties.WATERLOGGED)) { + level.setBlock(pos, blockState.setValue(BlockStateProperties.WATERLOGGED, true), 2); + } + + return true; + } else { + return false; + } + } +} diff --git a/src/main/java/net/frozenblock/lib/worldgen/feature/api/features/CircularWaterloggedVegetationPatchLessBordersFeature.java b/src/main/java/net/frozenblock/lib/worldgen/feature/api/features/CircularWaterloggedVegetationPatchLessBordersFeature.java new file mode 100644 index 0000000..1924c8e --- /dev/null +++ b/src/main/java/net/frozenblock/lib/worldgen/feature/api/features/CircularWaterloggedVegetationPatchLessBordersFeature.java @@ -0,0 +1,162 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.worldgen.feature.api.features; + +import com.mojang.serialization.Codec; +import net.minecraft.core.BlockPos; +import net.minecraft.core.BlockPos.MutableBlockPos; +import net.minecraft.core.Direction; +import net.minecraft.util.RandomSource; +import net.minecraft.world.level.WorldGenLevel; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.state.BlockBehaviour; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.block.state.properties.BlockStateProperties; +import net.minecraft.world.level.chunk.ChunkGenerator; +import net.minecraft.world.level.levelgen.feature.FeaturePlaceContext; +import net.minecraft.world.level.levelgen.feature.VegetationPatchFeature; +import net.minecraft.world.level.levelgen.feature.configurations.VegetationPatchConfiguration; +import org.jetbrains.annotations.NotNull; + +import java.util.HashSet; +import java.util.Iterator; +import java.util.Set; +import java.util.function.Predicate; + +public class CircularWaterloggedVegetationPatchLessBordersFeature extends VegetationPatchFeature { + + public CircularWaterloggedVegetationPatchLessBordersFeature(Codec codec) { + super(codec); + } + + @Override + public boolean place(@NotNull FeaturePlaceContext context) { + WorldGenLevel worldGenLevel = context.level(); + VegetationPatchConfiguration vegetationPatchConfiguration = context.config(); + RandomSource randomSource = context.random(); + BlockPos blockPos = context.origin(); + Predicate predicate = (state) -> state.is(vegetationPatchConfiguration.replaceable); + int radius = vegetationPatchConfiguration.xzRadius.sample(randomSource) + 1; + Set set = this.placeGroundPatch(worldGenLevel, vegetationPatchConfiguration, randomSource, blockPos, predicate, radius, radius); + this.distributeVegetation(context, worldGenLevel, vegetationPatchConfiguration, randomSource, set, radius, radius); + return !set.isEmpty(); + } + + public Set placeCircularGroundPatch(WorldGenLevel level, @NotNull VegetationPatchConfiguration config, RandomSource random, @NotNull BlockPos pos, Predicate state, int xRadius, int zRadius) { + MutableBlockPos mutableBlockPos = pos.mutable(); + MutableBlockPos mutableBlockPos2 = mutableBlockPos.mutable(); + Direction direction = config.surface.getDirection(); + Direction direction2 = direction.getOpposite(); + Set set = new HashSet<>(); + + for (int i = -xRadius; i <= xRadius; ++i) { + for (int j = -zRadius; j <= zRadius; ++j) { + mutableBlockPos.setWithOffset(pos, i, 0, j); + + if (Math.sqrt(mutableBlockPos.distSqr(pos)) <= xRadius) { + int k; + for (k = 0; level.isStateAtPosition(mutableBlockPos, BlockBehaviour.BlockStateBase::isAir) && k < config.verticalRange; ++k) { + mutableBlockPos.move(direction); + } + + for (k = 0; level.isStateAtPosition(mutableBlockPos, (statex) -> !statex.isAir()) && k < config.verticalRange; ++k) { + mutableBlockPos.move(direction2); + } + + mutableBlockPos2.setWithOffset(mutableBlockPos, config.surface.getDirection()); + BlockState blockState = level.getBlockState(mutableBlockPos2); + if (level.isEmptyBlock(mutableBlockPos) && blockState.isFaceSturdy(level, mutableBlockPos2, config.surface.getDirection().getOpposite())) { + int l = config.depth.sample(random) + (config.extraBottomBlockChance > 0.0F && random.nextFloat() < config.extraBottomBlockChance ? 1 : 0); + BlockPos blockPos = mutableBlockPos2.immutable(); + boolean bl6 = this.placeGround(level, config, state, random, mutableBlockPos2, l); + if (bl6) { + set.add(blockPos); + } + } + } + } + } + + return set; + } + + @Override + protected Set placeGroundPatch(WorldGenLevel level, VegetationPatchConfiguration config, RandomSource random, BlockPos pos, Predicate state, int xRadius, int zRadius) { + Set set = this.placeCircularGroundPatch(level, config, random, pos, state, xRadius, zRadius); + Set set2 = new HashSet<>(); + MutableBlockPos mutableBlockPos = new MutableBlockPos(); + Iterator var11 = set.iterator(); + + BlockPos blockPos; + while (var11.hasNext()) { + blockPos = var11.next(); + if (!isExposed(level, blockPos, mutableBlockPos)) { + set2.add(blockPos); + } + } + + var11 = set2.iterator(); + + while (var11.hasNext()) { + blockPos = var11.next(); + level.setBlock(blockPos, Blocks.WATER.defaultBlockState(), 2); + } + + return set2; + } + + private static boolean isExposed(WorldGenLevel level, BlockPos pos, MutableBlockPos mutablePos) { + return isExposedDirection(level, pos, mutablePos, Direction.NORTH) + || isExposedDirection(level, pos, mutablePos, Direction.EAST) + || isExposedDirection(level, pos, mutablePos, Direction.SOUTH) + || isExposedDirection(level, pos, mutablePos, Direction.WEST) + || isExposedDirection(level, pos, mutablePos, Direction.DOWN); + } + + private static boolean isExposedDirection(@NotNull WorldGenLevel level, BlockPos pos, @NotNull MutableBlockPos mutablePos, Direction direction) { + mutablePos.setWithOffset(pos, direction); + BlockState state = level.getBlockState(mutablePos); + return !state.isFaceSturdy(level, mutablePos, direction.getOpposite()) && !state.is(Blocks.WATER); + } + + @Override + protected boolean placeVegetation(WorldGenLevel level, VegetationPatchConfiguration config, ChunkGenerator chunkGenerator, RandomSource random, @NotNull BlockPos pos) { + if (super.placeVegetation(level, config, chunkGenerator, random, pos.below())) { + BlockState blockState = level.getBlockState(pos); + if (blockState.hasProperty(BlockStateProperties.WATERLOGGED) && !(Boolean) blockState.getValue(BlockStateProperties.WATERLOGGED)) { + level.setBlock(pos, blockState.setValue(BlockStateProperties.WATERLOGGED, true), 2); + } + + return true; + } else { + return false; + } + } + + @Override + protected boolean placeGround(WorldGenLevel level, VegetationPatchConfiguration config, Predicate replaceableblocks, RandomSource random, MutableBlockPos mutablePos, int maxDistance) { + for (int i = 0; i < maxDistance; ++i) { + BlockState blockState2 = level.getBlockState(mutablePos); + if (!replaceableblocks.test(blockState2)) { + return i != 0; + } + mutablePos.move(config.surface.getDirection()); + } + return true; + } +} diff --git a/src/main/java/net/frozenblock/lib/worldgen/feature/api/features/ColumnWithDiskFeature.java b/src/main/java/net/frozenblock/lib/worldgen/feature/api/features/ColumnWithDiskFeature.java new file mode 100644 index 0000000..01c799f --- /dev/null +++ b/src/main/java/net/frozenblock/lib/worldgen/feature/api/features/ColumnWithDiskFeature.java @@ -0,0 +1,119 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.worldgen.feature.api.features; + +import com.mojang.serialization.Codec; +import net.frozenblock.lib.worldgen.feature.api.features.config.ColumnWithDiskFeatureConfig; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Holder; +import net.minecraft.util.RandomSource; +import net.minecraft.world.level.WorldGenLevel; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.BushBlock; +import net.minecraft.world.level.block.GrowingPlantBodyBlock; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.levelgen.Heightmap.Types; +import net.minecraft.world.level.levelgen.feature.Feature; +import net.minecraft.world.level.levelgen.feature.FeaturePlaceContext; +import org.jetbrains.annotations.NotNull; + +import java.util.Optional; + +public class ColumnWithDiskFeature extends Feature { + + public ColumnWithDiskFeature(Codec codec) { + super(codec); + } + + @Override + public boolean place(@NotNull FeaturePlaceContext context) { + boolean generated = false; + ColumnWithDiskFeatureConfig config = context.config(); + BlockPos blockPos = context.origin(); + WorldGenLevel level = context.level(); + BlockPos s = blockPos.atY(level.getHeight(Types.MOTION_BLOCKING_NO_LEAVES, blockPos.getX(), blockPos.getZ()) - 1); + RandomSource random = level.getRandom(); + int radius = config.radius().sample(random); + Optional> diskOptional = config.diskBlocks().getRandomElement(random); + // DISK + if (diskOptional.isPresent()) { + BlockPos.MutableBlockPos mutableDisk = s.mutable(); + BlockState disk = diskOptional.get().value().defaultBlockState(); + int bx = s.getX(); + int bz = s.getZ(); + for (int x = bx - radius; x <= bx + radius; x++) { + for (int z = bz - radius; z <= bz + radius; z++) { + double distance = ((bx - x) * (bx - x) + ((bz - z) * (bz - z))); + if (distance < radius * radius) { + mutableDisk.set(x, level.getHeight(Types.MOTION_BLOCKING_NO_LEAVES, x, z) - 1, z); + boolean fade = !mutableDisk.closerThan(s, radius * 0.8); + if (level.getBlockState(mutableDisk).is(config.replaceableBlocks())) { + generated = true; + if (fade) { + if (random.nextFloat() > 0.65F) { + level.setBlock(mutableDisk, disk, 3); + } + } else { + level.setBlock(mutableDisk, disk, 3); + } + } + } + } + } + } + // COLUMN + BlockPos startPos = blockPos.atY(level.getHeight(Types.MOTION_BLOCKING_NO_LEAVES, blockPos.getX(), blockPos.getZ()) - 1); + BlockState column = config.state(); + BlockPos.MutableBlockPos pos = startPos.mutable(); + for (int i = 0; i < config.height().sample(random); i++) { + pos.set(pos.above()); + BlockState state = level.getBlockState(pos); + if (level.getBlockState(pos.below()).is(Blocks.WATER)) { + break; + } + if (state.getBlock() instanceof GrowingPlantBodyBlock || state.getBlock() instanceof BushBlock || state.isAir()) { + level.setBlock(pos, column, 3); + generated = true; + } + } + startPos = startPos.offset(-1, 0, 0); + generated = generated || place(config, level, random, startPos, column, pos); + startPos = startPos.offset(1, 0, 1); + generated = generated || place(config, level, random, startPos, column, pos); + return generated; + } + + private boolean place(@NotNull ColumnWithDiskFeatureConfig config, @NotNull WorldGenLevel level, RandomSource random, @NotNull BlockPos startPos, BlockState column, BlockPos.@NotNull MutableBlockPos pos) { + boolean generated = false; + pos.set(startPos.atY(level.getHeight(Types.MOTION_BLOCKING_NO_LEAVES, startPos.getX(), startPos.getZ()) - 1).mutable()); + for (int i = 0; i < config.additionalHeight().sample(random); i++) { + pos.set(pos.above()); + BlockState state = level.getBlockState(pos); + if (level.getBlockState(pos.below()).is(Blocks.WATER)) { + break; + } + if (state.getBlock() instanceof GrowingPlantBodyBlock || state.getBlock() instanceof BushBlock || state.isAir()) { + level.setBlock(pos, column, 3); + generated = true; + } + } + return generated; + } + +} diff --git a/src/main/java/net/frozenblock/lib/worldgen/feature/api/features/ComboFeature.java b/src/main/java/net/frozenblock/lib/worldgen/feature/api/features/ComboFeature.java new file mode 100644 index 0000000..99f04ac --- /dev/null +++ b/src/main/java/net/frozenblock/lib/worldgen/feature/api/features/ComboFeature.java @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.worldgen.feature.api.features; + +import com.mojang.serialization.Codec; +import net.frozenblock.lib.worldgen.feature.api.features.config.ComboFeatureConfig; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Holder; +import net.minecraft.util.RandomSource; +import net.minecraft.world.level.WorldGenLevel; +import net.minecraft.world.level.chunk.ChunkGenerator; +import net.minecraft.world.level.levelgen.feature.Feature; +import net.minecraft.world.level.levelgen.feature.FeaturePlaceContext; +import net.minecraft.world.level.levelgen.placement.PlacedFeature; +import org.jetbrains.annotations.NotNull; + +public class ComboFeature extends Feature { + + public ComboFeature(Codec codec) { + super(codec); + } + + @Override + public boolean place(@NotNull FeaturePlaceContext context) { + WorldGenLevel worldGenLevel = context.level(); + ComboFeatureConfig config = context.config(); + RandomSource randomSource = context.random(); + BlockPos blockPos = context.origin(); + ChunkGenerator chunkGenerator = context.chunkGenerator(); + boolean placedAny = false; + for (Holder feature : config.features()) { + if (this.place(worldGenLevel, feature, chunkGenerator, randomSource, blockPos)) + placedAny = true; + } + return placedAny; + } + + public boolean place(WorldGenLevel worldGenLevel, @NotNull Holder holder, ChunkGenerator chunkGenerator, RandomSource randomSource, BlockPos blockPos) { + return holder.value().place(worldGenLevel, chunkGenerator, randomSource, blockPos); + } + +} diff --git a/src/main/java/net/frozenblock/lib/worldgen/feature/api/features/CurvingTunnelFeature.java b/src/main/java/net/frozenblock/lib/worldgen/feature/api/features/CurvingTunnelFeature.java new file mode 100644 index 0000000..21f330f --- /dev/null +++ b/src/main/java/net/frozenblock/lib/worldgen/feature/api/features/CurvingTunnelFeature.java @@ -0,0 +1,84 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.worldgen.feature.api.features; + +import com.mojang.serialization.Codec; +import net.frozenblock.lib.worldgen.feature.api.features.config.CurvingTunnelFeatureConfig; +import net.minecraft.core.BlockPos; +import net.minecraft.util.RandomSource; +import net.minecraft.world.level.WorldGenLevel; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.levelgen.Heightmap.Types; +import net.minecraft.world.level.levelgen.feature.Feature; +import net.minecraft.world.level.levelgen.feature.FeaturePlaceContext; +import org.jetbrains.annotations.NotNull; + +import java.util.concurrent.atomic.AtomicBoolean; + +public class CurvingTunnelFeature extends Feature { + + public CurvingTunnelFeature(Codec codec) { + super(codec); + } + + @Override + public boolean place(@NotNull FeaturePlaceContext context) { + AtomicBoolean generated = new AtomicBoolean(false); + CurvingTunnelFeatureConfig config = context.config(); + BlockPos blockPos = context.origin(); + WorldGenLevel level = context.level(); + int radius = config.radius(); + int radiusSquared = radius * radius; + RandomSource random = level.getRandom(); + double curvatureDifference = config.maxCurvature() - config.minCurvature(); + if (curvatureDifference < 0) { + throw new UnsupportedOperationException("minCurvature can not be higher than maxCurvature!"); + } + int bx = blockPos.getX(); + int by = blockPos.getY(); + int bz = blockPos.getZ(); + BlockPos.MutableBlockPos mutable = blockPos.mutable(); + BlockPos endPos = level.getHeightmapPos(Types.OCEAN_FLOOR_WG, blockPos); + int endY = endPos.getY(); + int yDifference = endY - by; + double xCurvature = ((random.nextDouble() * curvatureDifference) + config.minCurvature()) * (random.nextBoolean() ? 1 : -1); + double zCurvature = ((random.nextDouble() * curvatureDifference) + config.minCurvature()) * (random.nextBoolean() ? 1 : -1); + + for (int yOffset = 0; yOffset < yDifference; yOffset++) { + int y = by + yOffset; + double curvatureProgress = Math.sin(((double) yOffset / yDifference) * Math.PI); + int xOffset = (int) (curvatureProgress * xCurvature); + int zOffset = (int) (curvatureProgress * zCurvature); + for (int x = -radius; x <= radius; x++) { + for (int z = -radius; z <= radius; z++) { + double distance = ((-x) * (-x) + (-z) * (-z)); + if (distance <= radiusSquared) { + mutable.set(bx + x, y, bz + z); + mutable.move(xOffset, 0, zOffset); + if (level.getBlockState(mutable).is(config.replaceableBlocks())) { + level.setBlock(mutable, config.state().getState(random, mutable), Block.UPDATE_ALL); + generated.set(true); + } + } + } + } + } + return generated.get(); + } + +} diff --git a/src/main/java/net/frozenblock/lib/worldgen/feature/api/features/DownwardsColumnFeature.java b/src/main/java/net/frozenblock/lib/worldgen/feature/api/features/DownwardsColumnFeature.java new file mode 100644 index 0000000..19ccaf2 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/worldgen/feature/api/features/DownwardsColumnFeature.java @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.worldgen.feature.api.features; + +import com.mojang.serialization.Codec; +import net.frozenblock.lib.worldgen.feature.api.features.config.ColumnFeatureConfig; +import net.minecraft.core.BlockPos; +import net.minecraft.util.RandomSource; +import net.minecraft.world.level.WorldGenLevel; +import net.minecraft.world.level.levelgen.feature.Feature; +import net.minecraft.world.level.levelgen.feature.FeaturePlaceContext; +import net.minecraft.world.level.material.Fluids; +import org.jetbrains.annotations.NotNull; + +public class DownwardsColumnFeature extends Feature { + + public DownwardsColumnFeature(Codec codec) { + super(codec); + } + + @Override + public boolean place(@NotNull FeaturePlaceContext context) { + boolean bl = false; + BlockPos blockPos = context.origin(); + WorldGenLevel level = context.level(); + RandomSource random = level.getRandom(); + BlockPos.MutableBlockPos mutable = blockPos.mutable(); + int bx = blockPos.getX(); + int bz = blockPos.getZ(); + int by = blockPos.getY(); + int height = -context.config().height().sample(random); + for (int y = 0; y > height; y--) { + if (context.config().replaceableBlocks().contains(level.getBlockState(mutable).getBlockHolder()) || level.getBlockState(mutable).isAir() || level.getBlockState(mutable).getFluidState() != Fluids.EMPTY.defaultFluidState()) { + bl = true; + level.setBlock(mutable, context.config().state(), 3); + mutable.set(bx, by + y, bz); + } else { + mutable.set(bx, by + y, bz); + } + } + return bl; + } + +} diff --git a/src/main/java/net/frozenblock/lib/worldgen/feature/api/features/FadingDiskCarpetFeature.java b/src/main/java/net/frozenblock/lib/worldgen/feature/api/features/FadingDiskCarpetFeature.java new file mode 100644 index 0000000..eabb516 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/worldgen/feature/api/features/FadingDiskCarpetFeature.java @@ -0,0 +1,146 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.worldgen.feature.api.features; + +import com.mojang.serialization.Codec; +import net.frozenblock.lib.worldgen.feature.api.features.config.FadingDiskCarpetFeatureConfig; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.util.RandomSource; +import net.minecraft.world.level.WorldGenLevel; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.levelgen.Heightmap; +import net.minecraft.world.level.levelgen.feature.Feature; +import net.minecraft.world.level.levelgen.feature.FeaturePlaceContext; +import org.jetbrains.annotations.NotNull; + +import java.util.concurrent.atomic.AtomicBoolean; + +public class FadingDiskCarpetFeature extends Feature { + + public FadingDiskCarpetFeature(Codec codec) { + super(codec); + } + + @Override + public boolean place(@NotNull FeaturePlaceContext context) { + final AtomicBoolean bl = new AtomicBoolean(false); + BlockPos blockPos = context.origin(); + WorldGenLevel level = context.level(); + FadingDiskCarpetFeatureConfig config = context.config(); + boolean useHeightMapAndNotCircular = config.useHeightmapInsteadOfCircularPlacement(); + Heightmap.Types heightmap = config.heightmap(); + BlockPos s = useHeightMapAndNotCircular ? blockPos.atY(level.getHeight(heightmap, blockPos.getX(), blockPos.getZ())) : blockPos; + RandomSource random = level.getRandom(); + int radius = config.radius().sample(random); + //DISK + BlockPos.MutableBlockPos mutableDisk = s.mutable(); + int bx = s.getX(); + int by = s.getY(); + int bz = s.getZ(); + + for (int x = bx - radius; x <= bx + radius; x++) { + for (int z = bz - radius; z <= bz + radius; z++) { + if (useHeightMapAndNotCircular) { + double distance = ((bx - x) * (bx - x) + (bz - z) * (bz - z)); + if (distance < radius * radius) { + mutableDisk.set(x, level.getHeight(heightmap, x, z), z); + BlockState state = level.getBlockState(mutableDisk); + boolean inner = mutableDisk.closerThan(s, radius * config.innerChance()); + boolean fade = !inner && !mutableDisk.closerThan(s, radius * config.fadeStartDistancePercent()); + boolean choseInner; + if (random.nextFloat() < config.placementChance() && state.isAir()) { + if (fade) { + if (random.nextFloat() > 0.5F) { + BlockState placedState = config.outerState().getState(random, mutableDisk); + if (placedState.canSurvive(level, mutableDisk.move(Direction.DOWN))) { + mutableDisk.move(Direction.UP); + level.setBlock(mutableDisk, config.outerState().getState(random, mutableDisk), 3); + bl.set(true); + } else { + mutableDisk.move(Direction.UP); + } + } + } else { + choseInner = (inner && random.nextFloat() < config.innerChance()); + BlockState placedState = choseInner ? config.innerState().getState(random, mutableDisk) : config.outerState().getState(random, mutableDisk); + if (placedState.canSurvive(level, mutableDisk.move(Direction.DOWN))) { + mutableDisk.move(Direction.UP); + level.setBlock(mutableDisk, placedState, 3); + bl.set(true); + } else { + mutableDisk.move(Direction.UP); + } + } + } + } + } else { + for (int y = by - radius; y <= by + radius; y++) { + double distance = ((bx - x) * (bx - x) + (by - y) * (by - y) + (bz - z) * (bz - z)); + if (distance < radius * radius) { + mutableDisk.set(x, y, z); + BlockState state = level.getBlockState(mutableDisk); + boolean inner = mutableDisk.closerThan(s, radius * config.innerChance()); + boolean fade = !inner && !mutableDisk.closerThan(s, radius * config.fadeStartDistancePercent()); + boolean choseInner; + if (random.nextFloat() < config.placementChance() && state.isAir()) { + if (fade) { + if (random.nextFloat() > 0.5F) { + BlockState placedState = config.outerState().getState(random, mutableDisk); + if (placedState.canSurvive(level, mutableDisk.move(Direction.DOWN))) { + mutableDisk.move(Direction.UP); + level.setBlock(mutableDisk, config.outerState().getState(random, mutableDisk), 3); + bl.set(true); + } else { + mutableDisk.move(Direction.UP); + } + } + } else { + choseInner = (inner && random.nextFloat() < config.innerChance()); + BlockState placedState = choseInner ? config.innerState().getState(random, mutableDisk) : config.outerState().getState(random, mutableDisk); + if (placedState.canSurvive(level, mutableDisk.move(Direction.DOWN))) { + mutableDisk.move(Direction.UP); + level.setBlock(mutableDisk, placedState, 3); + bl.set(true); + } else { + mutableDisk.move(Direction.UP); + } + } + } + } + } + } + } + } + + return bl.get(); + } + + public static boolean isBlockExposedToAir(WorldGenLevel level, @NotNull BlockPos blockPos) { + BlockPos.MutableBlockPos mutableBlockPos = blockPos.mutable(); + for (Direction direction : Direction.values()) { + mutableBlockPos.move(direction); + if (level.getBlockState(mutableBlockPos).isAir()) { + return true; + } + mutableBlockPos.move(direction, -1); + } + return false; + } + +} diff --git a/src/main/java/net/frozenblock/lib/worldgen/feature/api/features/FadingDiskFeature.java b/src/main/java/net/frozenblock/lib/worldgen/feature/api/features/FadingDiskFeature.java new file mode 100644 index 0000000..9583cf9 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/worldgen/feature/api/features/FadingDiskFeature.java @@ -0,0 +1,116 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.worldgen.feature.api.features; + +import com.mojang.serialization.Codec; +import net.frozenblock.lib.worldgen.feature.api.features.config.FadingDiskFeatureConfig; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.tags.BlockTags; +import net.minecraft.util.RandomSource; +import net.minecraft.world.level.WorldGenLevel; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.levelgen.Heightmap; +import net.minecraft.world.level.levelgen.feature.Feature; +import net.minecraft.world.level.levelgen.feature.FeaturePlaceContext; +import net.minecraft.world.level.levelgen.feature.stateproviders.BlockStateProvider; +import org.jetbrains.annotations.NotNull; + +import java.util.concurrent.atomic.AtomicBoolean; + +public class FadingDiskFeature extends Feature { + + public FadingDiskFeature(Codec codec) { + super(codec); + } + + @Override + public boolean place(@NotNull FeaturePlaceContext context) { + AtomicBoolean success = new AtomicBoolean(); + BlockPos blockPos = context.origin(); + WorldGenLevel level = context.level(); + FadingDiskFeatureConfig config = context.config(); + boolean useHeightMapAndNotCircular = config.useHeightmapInsteadOfCircularPlacement(); + Heightmap.Types heightmap = config.heightmap(); + BlockPos s = useHeightMapAndNotCircular ? blockPos.atY(level.getHeight(heightmap, blockPos.getX(), blockPos.getZ())) : blockPos; + RandomSource random = level.getRandom(); + int radius = config.radius().sample(random); + //DISK + BlockPos.MutableBlockPos mutableDisk = s.mutable(); + int bx = s.getX(); + int by = s.getY(); + int bz = s.getZ(); + for (int x = bx - radius; x <= bx + radius; x++) { + for (int z = bz - radius; z <= bz + radius; z++) { + if (useHeightMapAndNotCircular) { + double distance = Math.pow((double) bx - x, 2) + Math.pow((double) bz - z, 2); + success.set(placeAtPos(level, config, s, random, radius, mutableDisk, x, level.getHeight(heightmap, x, z) - 1, z, distance, true)); + } else { + int maxY = by + radius; + for (int y = by - radius; y <= maxY; y++) { + double distance = Math.pow((double) bx - x, 2) + Math.pow((double) by - y, 2) + Math.pow((double) bz - z, 2); + success.set(placeAtPos(level, config, s, random, radius, mutableDisk, x, y, z, distance, false)); + } + } + } + } + + return success.get(); + } + + private static boolean placeAtPos(WorldGenLevel level, FadingDiskFeatureConfig config, BlockPos s, RandomSource random, int radius, BlockPos.MutableBlockPos mutableDisk, int x, int y, int z, double distance, boolean useHeightMapAndNotCircular) { + if (distance < Math.pow(radius, 2)) { + mutableDisk.set(x, y, z); + BlockState state = level.getBlockState(mutableDisk); + if (!useHeightMapAndNotCircular && isBlockExposed(level, mutableDisk)) { + boolean inner = mutableDisk.closerThan(s, radius * config.innerChance()); + boolean fade = !inner && !mutableDisk.closerThan(s, radius * config.fadeStartDistancePercent()); + if (random.nextFloat() < config.placementChance()) { + if (fade) { + if (random.nextFloat() > 0.5F && state.is(config.outerReplaceableBlocks())) { + level.setBlock(mutableDisk, config.outerState().getState(random, mutableDisk), 3); + return true; + } + } else { + boolean choseInner = inner && random.nextFloat() < config.innerChance(); + if (state.is(choseInner ? config.innerReplaceableBlocks() : config.outerReplaceableBlocks())) { + BlockStateProvider newState = choseInner ? config.innerState() : config.outerState(); + level.setBlock(mutableDisk, newState.getState(random, mutableDisk), 3); + return true; + } + } + } + } + } + return false; + } + + public static boolean isBlockExposed(WorldGenLevel level, @NotNull BlockPos blockPos) { + BlockPos.MutableBlockPos mutableBlockPos = blockPos.mutable(); + for (Direction direction : Direction.values()) { + mutableBlockPos.move(direction); + BlockState blockState = level.getBlockState(mutableBlockPos); + if (blockState.isAir() || blockState.is(BlockTags.FIRE)) { + return true; + } + mutableBlockPos.move(direction, -1); + } + return false; + } + +} diff --git a/src/main/java/net/frozenblock/lib/worldgen/feature/api/features/FadingDiskTagExceptInBiomeFeature.java b/src/main/java/net/frozenblock/lib/worldgen/feature/api/features/FadingDiskTagExceptInBiomeFeature.java new file mode 100644 index 0000000..72b07be --- /dev/null +++ b/src/main/java/net/frozenblock/lib/worldgen/feature/api/features/FadingDiskTagExceptInBiomeFeature.java @@ -0,0 +1,125 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.worldgen.feature.api.features; + +import com.mojang.serialization.Codec; +import net.frozenblock.lib.worldgen.feature.api.features.config.FadingDiskTagBiomeFeatureConfig; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.tags.TagKey; +import net.minecraft.util.RandomSource; +import net.minecraft.world.level.WorldGenLevel; +import net.minecraft.world.level.biome.Biome; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.levelgen.Heightmap.Types; +import net.minecraft.world.level.levelgen.feature.Feature; +import net.minecraft.world.level.levelgen.feature.FeaturePlaceContext; +import org.jetbrains.annotations.NotNull; + +import java.util.concurrent.atomic.AtomicBoolean; + +public class FadingDiskTagExceptInBiomeFeature extends Feature { + + public FadingDiskTagExceptInBiomeFeature(Codec codec) { + super(codec); + } + + @Override + public boolean place(@NotNull FeaturePlaceContext context) { + final AtomicBoolean bl = new AtomicBoolean(false); + BlockPos blockPos = context.origin(); + WorldGenLevel level = context.level(); + FadingDiskTagBiomeFeatureConfig config = context.config(); + boolean useHeightMapAndNotCircular = config.useHeightmapInsteadOfCircularPlacement(); + Types heightmap = config.heightmap(); + BlockPos s = useHeightMapAndNotCircular ? blockPos.atY(level.getHeight(heightmap, blockPos.getX(), blockPos.getZ())) : blockPos; + RandomSource random = level.getRandom(); + int radius = config.radius().sample(random); + TagKey ignoredBiomes = config.excludedBiomes(); + //DISK + BlockPos.MutableBlockPos mutableDisk = s.mutable(); + int bx = s.getX(); + int by = s.getY(); + int bz = s.getZ(); + + for (int x = bx - radius; x <= bx + radius; x++) { + for (int z = bz - radius; z <= bz + radius; z++) { + if (useHeightMapAndNotCircular) { + double distance = ((bx - x) * (bx - x) + (bz - z) * (bz - z)); + if (distance < radius * radius) { + mutableDisk.set(x, level.getHeight(heightmap, x, z) - 1, z); + BlockState state = level.getBlockState(mutableDisk); + boolean inner = mutableDisk.closerThan(s, radius * config.innerChance()); + boolean fade = !inner && !mutableDisk.closerThan(s, radius * config.fadeStartDistancePercent()); + boolean choseInner; + if (random.nextFloat() < config.placementChance()) { + if (fade) { + if (random.nextFloat() > 0.5F && state.is(config.outerReplaceableBlocks()) && !level.getBiome(mutableDisk).is(ignoredBiomes)) { + level.setBlock(mutableDisk, config.outerState().getState(random, mutableDisk), 3); + bl.set(true); + } + } else if (state.is((choseInner = (inner && random.nextFloat() < config.innerChance())) ? config.innerReplaceableBlocks() : config.outerReplaceableBlocks()) && !level.getBiome(mutableDisk).is(ignoredBiomes)) { + level.setBlock(mutableDisk, choseInner ? config.innerState().getState(random, mutableDisk) : config.outerState().getState(random, mutableDisk), 3); + bl.set(true); + } + } + } + } else { + for (int y = by - radius; y <= by + radius; y++) { + double distance = ((bx - x) * (bx - x) + (by - y) * (by - y) + (bz - z) * (bz - z)); + if (distance < radius * radius) { + mutableDisk.set(x, y, z); + BlockState state = level.getBlockState(mutableDisk); + if (isBlockExposedToAir(level, mutableDisk)) { + boolean inner = mutableDisk.closerThan(s, radius * config.innerPercent()); + boolean fade = !inner && !mutableDisk.closerThan(s, radius * config.fadeStartDistancePercent()); + boolean choseInner; + if (random.nextFloat() < config.placementChance()) { + if (fade) { + if (random.nextFloat() > 0.5F && state.is(config.outerReplaceableBlocks()) && !level.getBiome(mutableDisk).is(ignoredBiomes)) { + level.setBlock(mutableDisk, config.outerState().getState(random, mutableDisk), 3); + bl.set(true); + } + } else if (state.is((choseInner = (inner && random.nextFloat() < config.innerChance())) ? config.innerReplaceableBlocks() : config.outerReplaceableBlocks()) && !level.getBiome(mutableDisk).is(ignoredBiomes)) { + level.setBlock(mutableDisk, choseInner ? config.innerState().getState(random, mutableDisk) : config.outerState().getState(random, mutableDisk), 3); + bl.set(true); + } + } + } + } + } + } + } + } + + return bl.get(); + } + + public static boolean isBlockExposedToAir(WorldGenLevel level, @NotNull BlockPos blockPos) { + BlockPos.MutableBlockPos mutableBlockPos = blockPos.mutable(); + for (Direction direction : Direction.values()) { + mutableBlockPos.move(direction); + if (level.getBlockState(mutableBlockPos).isAir()) { + return true; + } + mutableBlockPos.move(direction, -1); + } + return false; + } + +} diff --git a/src/main/java/net/frozenblock/lib/worldgen/feature/api/features/FadingDiskTagFeature.java b/src/main/java/net/frozenblock/lib/worldgen/feature/api/features/FadingDiskTagFeature.java new file mode 100644 index 0000000..90360aa --- /dev/null +++ b/src/main/java/net/frozenblock/lib/worldgen/feature/api/features/FadingDiskTagFeature.java @@ -0,0 +1,122 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.worldgen.feature.api.features; + +import com.mojang.serialization.Codec; +import net.frozenblock.lib.worldgen.feature.api.features.config.FadingDiskTagFeatureConfig; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.util.RandomSource; +import net.minecraft.world.level.WorldGenLevel; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.levelgen.Heightmap; +import net.minecraft.world.level.levelgen.feature.Feature; +import net.minecraft.world.level.levelgen.feature.FeaturePlaceContext; +import org.jetbrains.annotations.NotNull; + +import java.util.concurrent.atomic.AtomicBoolean; + +public class FadingDiskTagFeature extends Feature { + + public FadingDiskTagFeature(Codec codec) { + super(codec); + } + + @Override + public boolean place(@NotNull FeaturePlaceContext context) { + final AtomicBoolean bl = new AtomicBoolean(false); + BlockPos blockPos = context.origin(); + WorldGenLevel level = context.level(); + FadingDiskTagFeatureConfig config = context.config(); + boolean useHeightMapAndNotCircular = config.useHeightmapInsteadOfCircularPlacement(); + Heightmap.Types heightmap = config.heightmap(); + BlockPos s = useHeightMapAndNotCircular ? blockPos.atY(level.getHeight(heightmap, blockPos.getX(), blockPos.getZ())) : blockPos; + RandomSource random = level.getRandom(); + int radius = config.radius().sample(random); + //DISK + BlockPos.MutableBlockPos mutableDisk = s.mutable(); + int bx = s.getX(); + int by = s.getY(); + int bz = s.getZ(); + + for (int x = bx - radius; x <= bx + radius; x++) { + for (int z = bz - radius; z <= bz + radius; z++) { + if (useHeightMapAndNotCircular) { + double distance = ((bx - x) * (bx - x) + (bz - z) * (bz - z)); + if (distance < radius * radius) { + mutableDisk.set(x, level.getHeight(heightmap, x, z) - 1, z); + BlockState state = level.getBlockState(mutableDisk); + boolean inner = mutableDisk.closerThan(s, radius * config.innerPercent()); + boolean fade = !inner && !mutableDisk.closerThan(s, radius * config.fadeStartDistancePercent()); + boolean choseInner; + if (random.nextFloat() < config.placementChance()) { + if (fade) { + if (random.nextFloat() > 0.5F && state.is(config.outerReplaceableBlocks())) { + level.setBlock(mutableDisk, config.outerState().getState(random, mutableDisk), 3); + bl.set(true); + } + } else if (state.is((choseInner = (inner && random.nextFloat() < config.innerChance())) ? config.innerReplaceableBlocks() : config.outerReplaceableBlocks())) { + level.setBlock(mutableDisk, choseInner ? config.innerState().getState(random, mutableDisk) : config.outerState().getState(random, mutableDisk), 3); + bl.set(true); + } + } + } + } else { + for (int y = by - radius; y <= by + radius; y++) { + double distance = ((bx - x) * (bx - x) + (by - y) * (by - y) + (bz - z) * (bz - z)); + if (distance < radius * radius) { + mutableDisk.set(x, y, z); + BlockState state = level.getBlockState(mutableDisk); + if (isBlockExposedToAir(level, mutableDisk)) { + boolean inner = mutableDisk.closerThan(s, radius * config.innerPercent()); + boolean fade = !inner && !mutableDisk.closerThan(s, radius * config.fadeStartDistancePercent()); + boolean choseInner; + if (random.nextFloat() < config.placementChance()) { + if (fade) { + if (random.nextFloat() > 0.5F && state.is(config.outerReplaceableBlocks())) { + level.setBlock(mutableDisk, config.outerState().getState(random, mutableDisk), 3); + bl.set(true); + } + } else if (state.is((choseInner = (inner && random.nextFloat() < config.innerChance())) ? config.innerReplaceableBlocks() : config.outerReplaceableBlocks())) { + level.setBlock(mutableDisk, choseInner ? config.innerState().getState(random, mutableDisk) : config.outerState().getState(random, mutableDisk), 3); + bl.set(true); + } + } + } + } + } + } + } + } + + return bl.get(); + } + + public static boolean isBlockExposedToAir(WorldGenLevel level, @NotNull BlockPos blockPos) { + BlockPos.MutableBlockPos mutableBlockPos = blockPos.mutable(); + for (Direction direction : Direction.values()) { + mutableBlockPos.move(direction); + if (level.getBlockState(mutableBlockPos).isAir()) { + return true; + } + mutableBlockPos.move(direction, -1); + } + return false; + } + +} diff --git a/src/main/java/net/frozenblock/lib/worldgen/feature/api/features/FadingDiskTagScheduleTickFeature.java b/src/main/java/net/frozenblock/lib/worldgen/feature/api/features/FadingDiskTagScheduleTickFeature.java new file mode 100644 index 0000000..049ab38 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/worldgen/feature/api/features/FadingDiskTagScheduleTickFeature.java @@ -0,0 +1,130 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.worldgen.feature.api.features; + +import com.mojang.serialization.Codec; +import net.frozenblock.lib.worldgen.feature.api.features.config.FadingDiskTagFeatureConfig; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.util.RandomSource; +import net.minecraft.world.level.WorldGenLevel; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.levelgen.Heightmap; +import net.minecraft.world.level.levelgen.feature.Feature; +import net.minecraft.world.level.levelgen.feature.FeaturePlaceContext; +import org.jetbrains.annotations.NotNull; + +import java.util.concurrent.atomic.AtomicBoolean; + +public class FadingDiskTagScheduleTickFeature extends Feature { + + public FadingDiskTagScheduleTickFeature(Codec codec) { + super(codec); + } + + @Override + public boolean place(@NotNull FeaturePlaceContext context) { + final AtomicBoolean bl = new AtomicBoolean(false); + BlockPos blockPos = context.origin(); + WorldGenLevel level = context.level(); + FadingDiskTagFeatureConfig config = context.config(); + boolean useHeightMapAndNotCircular = config.useHeightmapInsteadOfCircularPlacement(); + Heightmap.Types heightmap = config.heightmap(); + BlockPos s = useHeightMapAndNotCircular ? blockPos.atY(level.getHeight(heightmap, blockPos.getX(), blockPos.getZ())) : blockPos; + RandomSource random = level.getRandom(); + int radius = config.radius().sample(random); + //DISK + BlockPos.MutableBlockPos mutableDisk = s.mutable(); + int bx = s.getX(); + int by = s.getY(); + int bz = s.getZ(); + + for (int x = bx - radius; x <= bx + radius; x++) { + for (int z = bz - radius; z <= bz + radius; z++) { + if (useHeightMapAndNotCircular) { + double distance = ((bx - x) * (bx - x) + (bz - z) * (bz - z)); + if (distance < radius * radius) { + mutableDisk.set(x, level.getHeight(heightmap, x, z) - 1, z); + BlockState state = level.getBlockState(mutableDisk); + boolean inner = mutableDisk.closerThan(s, radius * config.innerPercent()); + boolean fade = !inner && !mutableDisk.closerThan(s, radius * config.fadeStartDistancePercent()); + boolean choseInner; + if (random.nextFloat() < config.placementChance()) { + if (fade) { + if (random.nextFloat() > 0.5F && state.is(config.outerReplaceableBlocks())) { + BlockState setState = config.outerState().getState(random, mutableDisk); + level.setBlock(mutableDisk, setState, 3); + level.scheduleTick(mutableDisk, setState.getBlock(), 1); + bl.set(true); + } + } else if (state.is((choseInner = (inner && random.nextFloat() < config.innerChance())) ? config.innerReplaceableBlocks() : config.outerReplaceableBlocks())) { + BlockState setState = choseInner ? config.innerState().getState(random, mutableDisk) : config.outerState().getState(random, mutableDisk); + level.setBlock(mutableDisk, setState, 3); + level.scheduleTick(mutableDisk, setState.getBlock(), 1); + bl.set(true); + } + } + } + } else { + for (int y = by - radius; y <= by + radius; y++) { + double distance = ((bx - x) * (bx - x) + (by - y) * (by - y) + (bz - z) * (bz - z)); + if (distance < radius * radius) { + mutableDisk.set(x, y, z); + BlockState state = level.getBlockState(mutableDisk); + if (isBlockExposedToAir(level, mutableDisk)) { + boolean inner = mutableDisk.closerThan(s, radius * config.innerPercent()); + boolean fade = !inner && !mutableDisk.closerThan(s, radius * config.fadeStartDistancePercent()); + boolean choseInner; + if (random.nextFloat() < config.placementChance()) { + if (fade) { + if (random.nextFloat() > 0.5F && state.is(config.outerReplaceableBlocks())) { + BlockState setState = config.outerState().getState(random, mutableDisk); + level.setBlock(mutableDisk, setState, 3); + level.scheduleTick(mutableDisk, setState.getBlock(), 1); + bl.set(true); + } + } else if (state.is((choseInner = (inner && random.nextFloat() < config.innerChance())) ? config.innerReplaceableBlocks() : config.outerReplaceableBlocks())) { + BlockState setState = choseInner ? config.innerState().getState(random, mutableDisk) : config.outerState().getState(random, mutableDisk); + level.setBlock(mutableDisk, setState, 3); + level.scheduleTick(mutableDisk, setState.getBlock(), 1); + bl.set(true); + } + } + } + } + } + } + } + } + + return bl.get(); + } + + public static boolean isBlockExposedToAir(WorldGenLevel level, @NotNull BlockPos blockPos) { + BlockPos.MutableBlockPos mutableBlockPos = blockPos.mutable(); + for (Direction direction : Direction.values()) { + mutableBlockPos.move(direction); + if (level.getBlockState(mutableBlockPos).isAir()) { + return true; + } + mutableBlockPos.move(direction, -1); + } + return false; + } + +} diff --git a/src/main/java/net/frozenblock/lib/worldgen/feature/api/features/FadingDiskWithPileTagFeature.java b/src/main/java/net/frozenblock/lib/worldgen/feature/api/features/FadingDiskWithPileTagFeature.java new file mode 100644 index 0000000..fac53f3 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/worldgen/feature/api/features/FadingDiskWithPileTagFeature.java @@ -0,0 +1,163 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.worldgen.feature.api.features; + +import com.mojang.serialization.Codec; +import net.frozenblock.lib.worldgen.feature.api.features.config.FadingDiskTagFeatureConfig; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.util.RandomSource; +import net.minecraft.world.level.LevelAccessor; +import net.minecraft.world.level.WorldGenLevel; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.levelgen.Heightmap; +import net.minecraft.world.level.levelgen.feature.Feature; +import net.minecraft.world.level.levelgen.feature.FeaturePlaceContext; +import org.jetbrains.annotations.NotNull; + +import java.util.concurrent.atomic.AtomicBoolean; + +public class FadingDiskWithPileTagFeature extends Feature { + + public FadingDiskWithPileTagFeature(Codec codec) { + super(codec); + } + + @Override + public boolean place(@NotNull FeaturePlaceContext context) { + final AtomicBoolean bl = new AtomicBoolean(false); + BlockPos blockPos = context.origin(); + WorldGenLevel level = context.level(); + FadingDiskTagFeatureConfig config = context.config(); + boolean useHeightMapAndNotCircular = config.useHeightmapInsteadOfCircularPlacement(); + Heightmap.Types heightmap = config.heightmap(); + BlockPos s = useHeightMapAndNotCircular ? blockPos.atY(level.getHeight(heightmap, blockPos.getX(), blockPos.getZ())) : blockPos; + RandomSource random = level.getRandom(); + int radius = config.radius().sample(random); + //DISK + BlockPos.MutableBlockPos mutableDisk = s.mutable(); + int bx = s.getX(); + int by = s.getY(); + int bz = s.getZ(); + + for (int x = bx - radius; x <= bx + radius; x++) { + for (int z = bz - radius; z <= bz + radius; z++) { + if (useHeightMapAndNotCircular) { + double distance = ((bx - x) * (bx - x) + (bz - z) * (bz - z)); + if (distance < radius * radius) { + mutableDisk.set(x, level.getHeight(heightmap, x, z) - 1, z); + BlockState state = level.getBlockState(mutableDisk); + boolean inner = mutableDisk.closerThan(s, radius * config.innerPercent()); + boolean fade = !inner && !mutableDisk.closerThan(s, radius * config.fadeStartDistancePercent()); + boolean choseInner; + if (random.nextFloat() < config.placementChance()) { + if (fade) { + if (random.nextFloat() > 0.5F && state.is(config.outerReplaceableBlocks())) { + level.setBlock(mutableDisk, config.outerState().getState(random, mutableDisk), 3); + bl.set(true); + } + } else if (state.is((choseInner = (inner && random.nextFloat() < config.innerChance())) ? config.innerReplaceableBlocks() : config.outerReplaceableBlocks())) { + level.setBlock(mutableDisk, choseInner ? config.innerState().getState(random, mutableDisk) : config.outerState().getState(random, mutableDisk), 3); + bl.set(true); + } + } + } + } else { + for (int y = by - radius; y <= by + radius; y++) { + double distance = ((bx - x) * (bx - x) + (by - y) * (by - y) + (bz - z) * (bz - z)); + if (distance < radius * radius) { + mutableDisk.set(x, y, z); + BlockState state = level.getBlockState(mutableDisk); + if (isBlockExposedToAir(level, mutableDisk)) { + boolean inner = mutableDisk.closerThan(s, radius * config.innerPercent()); + boolean fade = !inner && !mutableDisk.closerThan(s, radius * config.fadeStartDistancePercent()); + boolean choseInner; + if (random.nextFloat() < config.placementChance()) { + if (fade) { + if (random.nextFloat() > 0.5F && state.is(config.outerReplaceableBlocks())) { + level.setBlock(mutableDisk, config.outerState().getState(random, mutableDisk), 3); + bl.set(true); + } + } else if (state.is((choseInner = (inner && random.nextFloat() < config.innerChance())) ? config.innerReplaceableBlocks() : config.outerReplaceableBlocks())) { + level.setBlock(mutableDisk, choseInner ? config.innerState().getState(random, mutableDisk) : config.outerState().getState(random, mutableDisk), 3); + bl.set(true); + } + } + } + } + } + } + } + } + + bl.set(this.placePile(context) || bl.get()); + + return bl.get(); + } + + public static boolean isBlockExposedToAir(WorldGenLevel level, @NotNull BlockPos blockPos) { + BlockPos.MutableBlockPos mutableBlockPos = blockPos.mutable(); + for (Direction direction : Direction.values()) { + mutableBlockPos.move(direction); + if (level.getBlockState(mutableBlockPos).isAir()) { + return true; + } + mutableBlockPos.move(direction, -1); + } + return false; + } + + public boolean placePile(@NotNull FeaturePlaceContext context) { + BlockPos blockPos = context.origin(); + WorldGenLevel worldGenLevel = context.level(); + RandomSource randomSource = context.random(); + FadingDiskTagFeatureConfig config = context.config(); + if (blockPos.getY() < worldGenLevel.getMinBuildHeight() + 5) { + return false; + } else { + int i = 2 + randomSource.nextInt(2); + int j = 2 + randomSource.nextInt(2); + + for (BlockPos blockPos2 : BlockPos.betweenClosed(blockPos.offset(-i, 0, -j), blockPos.offset(i, 1, j))) { + int k = blockPos.getX() - blockPos2.getX(); + int l = blockPos.getZ() - blockPos2.getZ(); + if ((float) (k * k + l * l) <= randomSource.nextFloat() * 10.0F - randomSource.nextFloat() * 6.0F) { + this.tryPlaceBlock(worldGenLevel, blockPos2, randomSource, config); + } else if ((double) randomSource.nextFloat() < 0.031D) { + this.tryPlaceBlock(worldGenLevel, blockPos2, randomSource, config); + } + } + + return true; + } + } + + private boolean mayPlaceOn(@NotNull LevelAccessor level, @NotNull BlockPos pos, RandomSource random) { + BlockPos blockPos = pos.below(); + BlockState blockState = level.getBlockState(blockPos); + return blockState.is(Blocks.DIRT_PATH) ? random.nextBoolean() : blockState.isFaceSturdy(level, blockPos, Direction.UP); + } + + private void tryPlaceBlock(@NotNull LevelAccessor level, BlockPos pos, RandomSource random, FadingDiskTagFeatureConfig config) { + if (level.isEmptyBlock(pos) && this.mayPlaceOn(level, pos, random)) { + level.setBlock(pos, config.innerState().getState(random, pos), 4); + } + } + +} diff --git a/src/main/java/net/frozenblock/lib/worldgen/feature/api/features/NoisePathFeature.java b/src/main/java/net/frozenblock/lib/worldgen/feature/api/features/NoisePathFeature.java new file mode 100644 index 0000000..fbd9801 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/worldgen/feature/api/features/NoisePathFeature.java @@ -0,0 +1,100 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.worldgen.feature.api.features; + +import com.mojang.serialization.Codec; +import net.frozenblock.lib.math.api.EasyNoiseSampler; +import net.frozenblock.lib.worldgen.feature.api.features.config.PathFeatureConfig; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.util.RandomSource; +import net.minecraft.world.level.WorldGenLevel; +import net.minecraft.world.level.levelgen.Heightmap.Types; +import net.minecraft.world.level.levelgen.blockpredicates.BlockPredicate; +import net.minecraft.world.level.levelgen.feature.Feature; +import net.minecraft.world.level.levelgen.feature.FeaturePlaceContext; +import net.minecraft.world.level.levelgen.synth.ImprovedNoise; +import org.jetbrains.annotations.NotNull; + +public class NoisePathFeature extends Feature { + + public NoisePathFeature(Codec codec) { + super(codec); + } + + @Override + public boolean place(@NotNull FeaturePlaceContext context) { + boolean generated = false; + PathFeatureConfig config = context.config(); + BlockPos blockPos = context.origin(); + WorldGenLevel level = context.level(); + int radiusSquared = config.radius() * config.radius(); + RandomSource random = level.getRandom(); + long noiseSeed = level.getSeed(); + ImprovedNoise sampler = + config.noise() == 1 ? EasyNoiseSampler.createLocalNoise(noiseSeed) : + config.noise() == 2 ? EasyNoiseSampler.createCheckedNoise(noiseSeed) : + config.noise() == 3 ? EasyNoiseSampler.createLegacyThreadSafeNoise(noiseSeed) : + EasyNoiseSampler.createXoroNoise(noiseSeed); + float chance = config.placementChance(); + int bx = blockPos.getX(); + int by = blockPos.getY(); + int bz = blockPos.getZ(); + BlockPos.MutableBlockPos mutable = blockPos.mutable(); + BlockPredicate predicate = config.onlyPlaceWhenExposed() ? BlockPredicate.ONLY_IN_AIR_OR_WATER_PREDICATE : BlockPredicate.alwaysTrue(); + + for (int x = bx - config.radius(); x <= bx + config.radius(); x++) { + for (int z = bz - config.radius(); z <= bz + config.radius(); z++) { + if (!config.is3D()) { + double distance = ((bx - x) * (bx - x) + ((bz - z) * (bz - z))); + if (distance < radiusSquared) { + mutable.set(x, level.getHeight(Types.OCEAN_FLOOR, x, z) - 1, z); + double sample = EasyNoiseSampler.sample(sampler, mutable, config.noiseScale(), config.scaleY(), config.useY()); + if (sample > config.minThreshold() && sample < config.maxThreshold() && level.getBlockState(mutable).is(config.replaceableBlocks()) && checkSurroundingBlocks(level, mutable, predicate) && random.nextFloat() <= chance) { + generated = true; + level.setBlock(mutable, config.state().getState(random, mutable), 3); + } + } + } else { + for (int y = by - config.radius(); y <= by + config.radius(); y++) { + double distance = ((bx - x) * (bx - x) + ((bz - z) * (bz - z)) + ((by - y) * (by - y))); + if (distance < radiusSquared) { + mutable.set(x, y, z); + double sample = EasyNoiseSampler.sample(sampler, mutable, config.noiseScale(), config.scaleY(), config.useY()); + if (sample > config.minThreshold() && sample < config.maxThreshold() && level.getBlockState(mutable).is(config.replaceableBlocks()) && checkSurroundingBlocks(level, mutable, predicate) && random.nextFloat() <= chance) { + generated = true; + level.setBlock(mutable, config.state().getState(random, mutable), 3); + } + } + } + } + } + } + return generated; + } + + private static boolean checkSurroundingBlocks(WorldGenLevel level, BlockPos pos, BlockPredicate predicate) { + for (Direction direction : Direction.values()) { + if (predicate.test(level, pos.relative(direction))) { + return true; + } + } + return false; + } + +} diff --git a/src/main/java/net/frozenblock/lib/worldgen/feature/api/features/NoisePathScheduleTickFeature.java b/src/main/java/net/frozenblock/lib/worldgen/feature/api/features/NoisePathScheduleTickFeature.java new file mode 100644 index 0000000..357035f --- /dev/null +++ b/src/main/java/net/frozenblock/lib/worldgen/feature/api/features/NoisePathScheduleTickFeature.java @@ -0,0 +1,105 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.worldgen.feature.api.features; + +import com.mojang.serialization.Codec; +import net.frozenblock.lib.math.api.EasyNoiseSampler; +import net.frozenblock.lib.worldgen.feature.api.features.config.PathFeatureConfig; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.util.RandomSource; +import net.minecraft.world.level.WorldGenLevel; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.levelgen.Heightmap.Types; +import net.minecraft.world.level.levelgen.blockpredicates.BlockPredicate; +import net.minecraft.world.level.levelgen.feature.Feature; +import net.minecraft.world.level.levelgen.feature.FeaturePlaceContext; +import net.minecraft.world.level.levelgen.synth.ImprovedNoise; +import org.jetbrains.annotations.NotNull; + +public class NoisePathScheduleTickFeature extends Feature { + + public NoisePathScheduleTickFeature(Codec codec) { + super(codec); + } + + @Override + public boolean place(@NotNull FeaturePlaceContext context) { + boolean generated = false; + PathFeatureConfig config = context.config(); + BlockPos blockPos = context.origin(); + WorldGenLevel level = context.level(); + int radiusSquared = config.radius() * config.radius(); + RandomSource random = level.getRandom(); + long noiseSeed = level.getSeed(); + ImprovedNoise sampler = + config.noise() == 1 ? EasyNoiseSampler.createLocalNoise(noiseSeed) : + config.noise() == 2 ? EasyNoiseSampler.createCheckedNoise(noiseSeed) : + config.noise() == 3 ? EasyNoiseSampler.createLegacyThreadSafeNoise(noiseSeed) : + EasyNoiseSampler.createXoroNoise(noiseSeed); + float chance = config.placementChance(); + int bx = blockPos.getX(); + int by = blockPos.getY(); + int bz = blockPos.getZ(); + BlockPos.MutableBlockPos mutable = blockPos.mutable(); + BlockPredicate predicate = config.onlyPlaceWhenExposed() ? BlockPredicate.ONLY_IN_AIR_OR_WATER_PREDICATE : BlockPredicate.alwaysTrue(); + + for (int x = bx - config.radius(); x <= bx + config.radius(); x++) { + for (int z = bz - config.radius(); z <= bz + config.radius(); z++) { + if (!config.is3D()) { + double distance = ((bx - x) * (bx - x) + ((bz - z) * (bz - z))); + if (distance < radiusSquared) { + mutable.set(x, level.getHeight(Types.OCEAN_FLOOR, x, z) - 1, z); + double sample = EasyNoiseSampler.sample(sampler, mutable, config.noiseScale(), config.scaleY(), config.useY()); + if (sample > config.minThreshold() && sample < config.maxThreshold() && level.getBlockState(mutable).is(config.replaceableBlocks()) && checkSurroundingBlocks(level, mutable, predicate) && random.nextFloat() <= chance) { + generated = true; + BlockState setState = config.state().getState(random, mutable); + level.setBlock(mutable, setState, 3); + level.scheduleTick(mutable, setState.getBlock(), 1); + } + } + } else { + for (int y = by - config.radius(); y <= by + config.radius(); y++) { + double distance = ((bx - x) * (bx - x) + ((bz - z) * (bz - z)) + ((by - y) * (by - y))); + if (distance < radiusSquared) { + mutable.set(x, y, z); + double sample = EasyNoiseSampler.sample(sampler, mutable, config.noiseScale(), config.scaleY(), config.useY()); + if (sample > config.minThreshold() && sample < config.maxThreshold() && level.getBlockState(mutable).is(config.replaceableBlocks()) && checkSurroundingBlocks(level, mutable, predicate) && random.nextFloat() <= chance) { + generated = true; + BlockState setState = config.state().getState(random, mutable); + level.setBlock(mutable, setState, 3); + level.scheduleTick(mutable, setState.getBlock(), 1); + } + } + } + } + } + } + return generated; + } + + private static boolean checkSurroundingBlocks(WorldGenLevel level, BlockPos pos, BlockPredicate predicate) { + for (Direction direction : Direction.values()) { + if (predicate.test(level, pos.relative(direction))) { + return true; + } + } + return false; + } + +} diff --git a/src/main/java/net/frozenblock/lib/worldgen/feature/api/features/NoisePathSwapUnderWaterFeature.java b/src/main/java/net/frozenblock/lib/worldgen/feature/api/features/NoisePathSwapUnderWaterFeature.java new file mode 100644 index 0000000..1440e28 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/worldgen/feature/api/features/NoisePathSwapUnderWaterFeature.java @@ -0,0 +1,104 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.worldgen.feature.api.features; + +import com.mojang.serialization.Codec; +import net.frozenblock.lib.math.api.EasyNoiseSampler; +import net.frozenblock.lib.worldgen.feature.api.features.config.PathSwapUnderWaterFeatureConfig; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.tags.FluidTags; +import net.minecraft.util.RandomSource; +import net.minecraft.world.level.WorldGenLevel; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.levelgen.Heightmap.Types; +import net.minecraft.world.level.levelgen.blockpredicates.BlockPredicate; +import net.minecraft.world.level.levelgen.feature.Feature; +import net.minecraft.world.level.levelgen.feature.FeaturePlaceContext; +import net.minecraft.world.level.levelgen.synth.ImprovedNoise; +import org.jetbrains.annotations.NotNull; + +public class NoisePathSwapUnderWaterFeature extends Feature { + + public NoisePathSwapUnderWaterFeature(Codec codec) { + super(codec); + } + + @Override + public boolean place(@NotNull FeaturePlaceContext context) { + boolean generated = false; + PathSwapUnderWaterFeatureConfig config = context.config(); + BlockPos blockPos = context.origin(); + WorldGenLevel level = context.level(); + int radiusSquared = config.radius() * config.radius(); + RandomSource random = level.getRandom(); + long noiseSeed = level.getSeed(); + ImprovedNoise sampler = + config.noise() == 1 ? EasyNoiseSampler.createLocalNoise(noiseSeed) : + config.noise() == 2 ? EasyNoiseSampler.createCheckedNoise(noiseSeed) : + config.noise() == 3 ? EasyNoiseSampler.createLegacyThreadSafeNoise(noiseSeed) : + EasyNoiseSampler.createXoroNoise(noiseSeed); + float chance = config.placement_chance(); + int bx = blockPos.getX(); + int by = blockPos.getY(); + int bz = blockPos.getZ(); + BlockPos.MutableBlockPos mutable = blockPos.mutable(); + BlockPredicate predicate = config.onlyPlaceWhenExposed() ? BlockPredicate.ONLY_IN_AIR_OR_WATER_PREDICATE : BlockPredicate.alwaysTrue(); + + for (int x = bx - config.radius(); x <= bx + config.radius(); x++) { + for (int z = bz - config.radius(); z <= bz + config.radius(); z++) { + if (!config.is3D()) { + double distance = ((bx - x) * (bx - x) + ((bz - z) * (bz - z))); + if (distance < radiusSquared) { + mutable.set(x, level.getHeight(Types.OCEAN_FLOOR, x, z) - 1, z); + double sample = EasyNoiseSampler.sample(sampler, mutable, config.noiseScale(), config.scaleY(), config.useY()); + if (sample > config.minThreshold() && sample < config.maxThreshold() && level.getBlockState(mutable).is(config.replaceableBlocks()) && checkSurroundingBlocks(level, mutable, predicate) && random.nextFloat() <= chance) { + generated = true; + BlockState setState = level.getFluidState(mutable.immutable().above()).is(FluidTags.WATER) ? config.underWaterState().getState(random, mutable) : config.state().getState(random, mutable); + level.setBlock(mutable, setState, 3); + } + } + } else { + for (int y = by - config.radius(); y <= by + config.radius(); y++) { + double distance = ((bx - x) * (bx - x) + ((bz - z) * (bz - z)) + ((by - y) * (by - y))); + if (distance < radiusSquared) { + mutable.set(x, y, z); + double sample = EasyNoiseSampler.sample(sampler, mutable, config.noiseScale(), config.scaleY(), config.useY()); + if (sample > config.minThreshold() && sample < config.maxThreshold() && level.getBlockState(mutable).is(config.replaceableBlocks()) && checkSurroundingBlocks(level, mutable, predicate) && random.nextFloat() <= chance) { + generated = true; + BlockState setState = level.getFluidState(mutable.immutable().above()).is(FluidTags.WATER) ? config.underWaterState().getState(random, mutable) : config.state().getState(random, mutable); + level.setBlock(mutable, setState, 3); + } + } + } + } + } + } + return generated; + } + + private static boolean checkSurroundingBlocks(WorldGenLevel level, BlockPos pos, BlockPredicate predicate) { + for (Direction direction : Direction.values()) { + if (predicate.test(level, pos.relative(direction))) { + return true; + } + } + return false; + } + +} diff --git a/src/main/java/net/frozenblock/lib/worldgen/feature/api/features/NoisePathSwapUnderWaterTagFeature.java b/src/main/java/net/frozenblock/lib/worldgen/feature/api/features/NoisePathSwapUnderWaterTagFeature.java new file mode 100644 index 0000000..9943f47 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/worldgen/feature/api/features/NoisePathSwapUnderWaterTagFeature.java @@ -0,0 +1,104 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.worldgen.feature.api.features; + +import com.mojang.serialization.Codec; +import net.frozenblock.lib.math.api.EasyNoiseSampler; +import net.frozenblock.lib.worldgen.feature.api.features.config.PathSwapUnderWaterTagFeatureConfig; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.tags.FluidTags; +import net.minecraft.util.RandomSource; +import net.minecraft.world.level.WorldGenLevel; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.levelgen.Heightmap.Types; +import net.minecraft.world.level.levelgen.blockpredicates.BlockPredicate; +import net.minecraft.world.level.levelgen.feature.Feature; +import net.minecraft.world.level.levelgen.feature.FeaturePlaceContext; +import net.minecraft.world.level.levelgen.synth.ImprovedNoise; +import org.jetbrains.annotations.NotNull; + +public class NoisePathSwapUnderWaterTagFeature extends Feature { + + public NoisePathSwapUnderWaterTagFeature(Codec codec) { + super(codec); + } + + @Override + public boolean place(@NotNull FeaturePlaceContext context) { + boolean generated = false; + PathSwapUnderWaterTagFeatureConfig config = context.config(); + BlockPos blockPos = context.origin(); + WorldGenLevel level = context.level(); + int radiusSquared = config.radius() * config.radius(); + RandomSource random = level.getRandom(); + long noiseSeed = level.getSeed(); + ImprovedNoise sampler = + config.noise() == 1 ? EasyNoiseSampler.createLocalNoise(noiseSeed) : + config.noise() == 2 ? EasyNoiseSampler.createCheckedNoise(noiseSeed) : + config.noise() == 3 ? EasyNoiseSampler.createLegacyThreadSafeNoise(noiseSeed) : + EasyNoiseSampler.createXoroNoise(noiseSeed); + float chance = config.placementChance(); + int bx = blockPos.getX(); + int by = blockPos.getY(); + int bz = blockPos.getZ(); + BlockPos.MutableBlockPos mutable = blockPos.mutable(); + BlockPredicate predicate = config.onlyPlaceWhenExposed() ? BlockPredicate.ONLY_IN_AIR_OR_WATER_PREDICATE : BlockPredicate.alwaysTrue(); + + for (int x = bx - config.radius(); x <= bx + config.radius(); x++) { + for (int z = bz - config.radius(); z <= bz + config.radius(); z++) { + if (!config.is3D()) { + double distance = ((bx - x) * (bx - x) + ((bz - z) * (bz - z))); + if (distance < radiusSquared) { + mutable.set(x, level.getHeight(Types.OCEAN_FLOOR, x, z) - 1, z); + double sample = EasyNoiseSampler.sample(sampler, mutable, config.noiseScale(), config.scaleY(), config.useY()); + if (sample > config.minThreshold() && sample < config.maxThreshold() && level.getBlockState(mutable).is(config.replaceableBlocks()) && checkSurroundingBlocks(level, mutable, predicate) && random.nextFloat() <= chance) { + generated = true; + BlockState setState = level.getFluidState(mutable.immutable().above()).is(FluidTags.WATER) ? config.underWaterState().getState(random, mutable) : config.state().getState(random, mutable); + level.setBlock(mutable, setState, 3); + } + } + } else { + for (int y = by - config.radius(); y <= by + config.radius(); y++) { + double distance = ((bx - x) * (bx - x) + ((bz - z) * (bz - z)) + ((by - y) * (by - y))); + if (distance < radiusSquared) { + mutable.set(x, y, z); + double sample = EasyNoiseSampler.sample(sampler, mutable, config.noiseScale(), config.scaleY(), config.useY()); + if (sample > config.minThreshold() && sample < config.maxThreshold() && level.getBlockState(mutable).is(config.replaceableBlocks()) && checkSurroundingBlocks(level, mutable, predicate) && random.nextFloat() <= chance) { + generated = true; + BlockState setState = level.getFluidState(mutable.immutable().above()).is(FluidTags.WATER) ? config.underWaterState().getState(random, mutable) : config.state().getState(random, mutable); + level.setBlock(mutable, setState, 3); + } + } + } + } + } + } + return generated; + } + + private static boolean checkSurroundingBlocks(WorldGenLevel level, BlockPos pos, BlockPredicate predicate) { + for (Direction direction : Direction.values()) { + if (predicate.test(level, pos.relative(direction))) { + return true; + } + } + return false; + } + +} diff --git a/src/main/java/net/frozenblock/lib/worldgen/feature/api/features/NoisePathTagFeature.java b/src/main/java/net/frozenblock/lib/worldgen/feature/api/features/NoisePathTagFeature.java new file mode 100644 index 0000000..cca5a8c --- /dev/null +++ b/src/main/java/net/frozenblock/lib/worldgen/feature/api/features/NoisePathTagFeature.java @@ -0,0 +1,100 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.worldgen.feature.api.features; + +import com.mojang.serialization.Codec; +import net.frozenblock.lib.math.api.EasyNoiseSampler; +import net.frozenblock.lib.worldgen.feature.api.features.config.PathTagFeatureConfig; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.util.RandomSource; +import net.minecraft.world.level.WorldGenLevel; +import net.minecraft.world.level.levelgen.Heightmap.Types; +import net.minecraft.world.level.levelgen.blockpredicates.BlockPredicate; +import net.minecraft.world.level.levelgen.feature.Feature; +import net.minecraft.world.level.levelgen.feature.FeaturePlaceContext; +import net.minecraft.world.level.levelgen.synth.ImprovedNoise; +import org.jetbrains.annotations.NotNull; + +public class NoisePathTagFeature extends Feature { + + public NoisePathTagFeature(Codec codec) { + super(codec); + } + + @Override + public boolean place(@NotNull FeaturePlaceContext context) { + boolean generated = false; + PathTagFeatureConfig config = context.config(); + BlockPos blockPos = context.origin(); + WorldGenLevel level = context.level(); + int radiusSquared = config.radius() * config.radius(); + RandomSource random = level.getRandom(); + long noiseSeed = level.getSeed(); + ImprovedNoise sampler = + config.noise() == 1 ? EasyNoiseSampler.createLocalNoise(noiseSeed) : + config.noise() == 2 ? EasyNoiseSampler.createCheckedNoise(noiseSeed) : + config.noise() == 3 ? EasyNoiseSampler.createLegacyThreadSafeNoise(noiseSeed) : + EasyNoiseSampler.createXoroNoise(noiseSeed); + float chance = config.placementChance(); + int bx = blockPos.getX(); + int by = blockPos.getY(); + int bz = blockPos.getZ(); + BlockPos.MutableBlockPos mutable = blockPos.mutable(); + BlockPredicate predicate = config.onlyPlaceWhenExposed() ? BlockPredicate.ONLY_IN_AIR_OR_WATER_PREDICATE : BlockPredicate.alwaysTrue(); + + for (int x = bx - config.radius(); x <= bx + config.radius(); x++) { + for (int z = bz - config.radius(); z <= bz + config.radius(); z++) { + if (!config.is3D()) { + double distance = ((bx - x) * (bx - x) + ((bz - z) * (bz - z))); + if (distance < radiusSquared) { + mutable.set(x, level.getHeight(Types.OCEAN_FLOOR, x, z) - 1, z); + double sample = EasyNoiseSampler.sample(sampler, mutable, config.noiseScale(), config.scaleY(), config.useY()); + if (sample > config.minThreshold() && sample < config.maxThreshold() && level.getBlockState(mutable).is(config.replaceableBlocks()) && checkSurroundingBlocks(level, mutable, predicate) && random.nextFloat() <= chance) { + generated = true; + level.setBlock(mutable, config.state().getState(random, mutable), 3); + } + } + } else { + for (int y = by - config.radius(); y <= by + config.radius(); y++) { + double distance = ((bx - x) * (bx - x) + ((bz - z) * (bz - z)) + ((by - y) * (by - y))); + if (distance < radiusSquared) { + mutable.set(x, y, z); + double sample = EasyNoiseSampler.sample(sampler, mutable, config.noiseScale(), config.scaleY(), config.useY()); + if (sample > config.minThreshold() && sample < config.maxThreshold() && level.getBlockState(mutable).is(config.replaceableBlocks()) && checkSurroundingBlocks(level, mutable, predicate) && random.nextFloat() <= chance) { + generated = true; + level.setBlock(mutable, config.state().getState(random, mutable), 3); + } + } + } + } + } + } + return generated; + } + + private static boolean checkSurroundingBlocks(WorldGenLevel level, BlockPos pos, BlockPredicate predicate) { + for (Direction direction : Direction.values()) { + if (predicate.test(level, pos.relative(direction))) { + return true; + } + } + return false; + } + +} diff --git a/src/main/java/net/frozenblock/lib/worldgen/feature/api/features/NoisePathTagUnderWaterFeature.java b/src/main/java/net/frozenblock/lib/worldgen/feature/api/features/NoisePathTagUnderWaterFeature.java new file mode 100644 index 0000000..4ca1c1e --- /dev/null +++ b/src/main/java/net/frozenblock/lib/worldgen/feature/api/features/NoisePathTagUnderWaterFeature.java @@ -0,0 +1,115 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.worldgen.feature.api.features; + +import com.mojang.serialization.Codec; +import net.frozenblock.lib.math.api.EasyNoiseSampler; +import net.frozenblock.lib.worldgen.feature.api.features.config.PathTagFeatureConfig; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.util.RandomSource; +import net.minecraft.world.level.WorldGenLevel; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.levelgen.Heightmap; +import net.minecraft.world.level.levelgen.blockpredicates.BlockPredicate; +import net.minecraft.world.level.levelgen.feature.Feature; +import net.minecraft.world.level.levelgen.feature.FeaturePlaceContext; +import net.minecraft.world.level.levelgen.synth.ImprovedNoise; +import org.jetbrains.annotations.NotNull; + +import java.util.Iterator; + +public class NoisePathTagUnderWaterFeature extends Feature { + + public NoisePathTagUnderWaterFeature(Codec codec) { + super(codec); + } + + @Override + public boolean place(@NotNull FeaturePlaceContext context) { + boolean generated = false; + PathTagFeatureConfig config = context.config(); + BlockPos blockPos = context.origin(); + WorldGenLevel level = context.level(); + long noiseSeed = level.getSeed(); + ImprovedNoise sampler = + config.noise() == 1 ? EasyNoiseSampler.createLocalNoise(noiseSeed) : + config.noise() == 2 ? EasyNoiseSampler.createCheckedNoise(noiseSeed) : + config.noise() == 3 ? EasyNoiseSampler.createLegacyThreadSafeNoise(noiseSeed) : + EasyNoiseSampler.createXoroNoise(noiseSeed); + float chance = config.placementChance(); + BlockPos.MutableBlockPos mutable = blockPos.mutable(); + int bx = mutable.getX(); + int by = mutable.getY(); + int bz = mutable.getZ(); + int radiusSquared = config.radius() * config.radius(); + RandomSource random = level.getRandom(); + BlockPredicate predicate = config.onlyPlaceWhenExposed() ? BlockPredicate.ONLY_IN_AIR_OR_WATER_PREDICATE : BlockPredicate.alwaysTrue(); + + for (int x = bx - config.radius(); x <= bx + config.radius(); x++) { + for (int z = bz - config.radius(); z <= bz + config.radius(); z++) { + if (!config.is3D()) { + double distance = ((bx - x) * (bx - x) + ((bz - z) * (bz - z))); + if (distance < radiusSquared) { + mutable.set(x, level.getHeight(Heightmap.Types.OCEAN_FLOOR, x, z) - 1, z); + double sample = EasyNoiseSampler.sample(sampler, mutable, config.noiseScale(), config.scaleY(), config.useY()); + if (sample > config.minThreshold() && sample < config.maxThreshold() && level.getBlockState(mutable).is(config.replaceableBlocks()) && checkSurroundingBlocks(level, mutable, predicate) && isWaterNearby(level, mutable, 2) && random.nextFloat() <= chance) { + generated = true; + level.setBlock(mutable, config.state().getState(random, mutable), 3); + } + } + } else { + for (int y = by - config.radius(); y <= by + config.radius(); y++) { + double distance = ((bx - x) * (bx - x) + ((bz - z) * (bz - z)) + ((by - y) * (by - y))); + if (distance < radiusSquared) { + mutable.set(x, y, z); + double sample = EasyNoiseSampler.sample(sampler, mutable, config.noiseScale(), config.scaleY(), config.useY()); + if (sample > config.minThreshold() && sample < config.maxThreshold() && level.getBlockState(mutable).is(config.replaceableBlocks()) && checkSurroundingBlocks(level, mutable, predicate) && isWaterNearby(level, mutable, 2) && random.nextFloat() <= chance) { + generated = true; + level.setBlock(mutable, config.state().getState(random, mutable), 3); + } + } + } + } + } + } + return generated; + } + + private static boolean checkSurroundingBlocks(WorldGenLevel level, BlockPos pos, BlockPredicate predicate) { + for (Direction direction : Direction.values()) { + if (predicate.test(level, pos.relative(direction))) { + return true; + } + } + return false; + } + + public static boolean isWaterNearby(WorldGenLevel level, @NotNull BlockPos blockPos, int x) { + Iterator var2 = BlockPos.betweenClosed(blockPos.offset(-x, -x, -x), blockPos.offset(x, x, x)).iterator(); + BlockPos blockPos2; + do { + if (!var2.hasNext()) { + return false; + } + blockPos2 = var2.next(); + } while (!level.getBlockState(blockPos2).is(Blocks.WATER)); + return true; + } + +} diff --git a/src/main/java/net/frozenblock/lib/worldgen/feature/api/features/NoisePathUnderWaterFeature.java b/src/main/java/net/frozenblock/lib/worldgen/feature/api/features/NoisePathUnderWaterFeature.java new file mode 100644 index 0000000..2b4139c --- /dev/null +++ b/src/main/java/net/frozenblock/lib/worldgen/feature/api/features/NoisePathUnderWaterFeature.java @@ -0,0 +1,115 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.worldgen.feature.api.features; + +import com.mojang.serialization.Codec; +import net.frozenblock.lib.math.api.EasyNoiseSampler; +import net.frozenblock.lib.worldgen.feature.api.features.config.PathFeatureConfig; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.util.RandomSource; +import net.minecraft.world.level.WorldGenLevel; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.levelgen.Heightmap; +import net.minecraft.world.level.levelgen.blockpredicates.BlockPredicate; +import net.minecraft.world.level.levelgen.feature.Feature; +import net.minecraft.world.level.levelgen.feature.FeaturePlaceContext; +import net.minecraft.world.level.levelgen.synth.ImprovedNoise; +import org.jetbrains.annotations.NotNull; + +import java.util.Iterator; + +public class NoisePathUnderWaterFeature extends Feature { + + public NoisePathUnderWaterFeature(Codec codec) { + super(codec); + } + + @Override + public boolean place(@NotNull FeaturePlaceContext context) { + boolean generated = false; + PathFeatureConfig config = context.config(); + BlockPos blockPos = context.origin(); + WorldGenLevel level = context.level(); + long noiseSeed = level.getSeed(); + ImprovedNoise sampler = + config.noise() == 1 ? EasyNoiseSampler.createLocalNoise(noiseSeed) : + config.noise() == 2 ? EasyNoiseSampler.createCheckedNoise(noiseSeed) : + config.noise() == 3 ? EasyNoiseSampler.createLegacyThreadSafeNoise(noiseSeed) : + EasyNoiseSampler.createXoroNoise(noiseSeed); + float chance = config.placementChance(); + BlockPos.MutableBlockPos mutable = blockPos.mutable(); + int bx = mutable.getX(); + int by = mutable.getY(); + int bz = mutable.getZ(); + int radiusSquared = config.radius() * config.radius(); + RandomSource random = level.getRandom(); + BlockPredicate predicate = config.onlyPlaceWhenExposed() ? BlockPredicate.ONLY_IN_AIR_OR_WATER_PREDICATE : BlockPredicate.alwaysTrue(); + + for (int x = bx - config.radius(); x <= bx + config.radius(); x++) { + for (int z = bz - config.radius(); z <= bz + config.radius(); z++) { + if (!config.is3D()) { + double distance = ((bx - x) * (bx - x) + ((bz - z) * (bz - z))); + if (distance < radiusSquared) { + mutable.set(x, level.getHeight(Heightmap.Types.OCEAN_FLOOR, x, z) - 1, z); + double sample = EasyNoiseSampler.sample(sampler, mutable, config.noiseScale(), config.scaleY(), config.useY()); + if (sample > config.minThreshold() && sample < config.maxThreshold() && level.getBlockState(mutable).is(config.replaceableBlocks()) && checkSurroundingBlocks(level, mutable, predicate) && isWaterNearby(level, mutable, 2) && random.nextFloat() <= chance) { + generated = true; + level.setBlock(mutable, config.state().getState(random, mutable), 3); + } + } + } else { + for (int y = by - config.radius(); y <= by + config.radius(); y++) { + double distance = ((bx - x) * (bx - x) + ((bz - z) * (bz - z)) + ((by - y) * (by - y))); + if (distance < radiusSquared) { + mutable.set(x, y, z); + double sample = EasyNoiseSampler.sample(sampler, mutable, config.noiseScale(), config.scaleY(), config.useY()); + if (sample > config.minThreshold() && sample < config.maxThreshold() && level.getBlockState(mutable).is(config.replaceableBlocks()) && checkSurroundingBlocks(level, mutable, predicate) && isWaterNearby(level, mutable, 2) && random.nextFloat() <= chance) { + generated = true; + level.setBlock(mutable, config.state().getState(random, mutable), 3); + } + } + } + } + } + } + return generated; + } + + private static boolean checkSurroundingBlocks(WorldGenLevel level, BlockPos pos, BlockPredicate predicate) { + for (Direction direction : Direction.values()) { + if (predicate.test(level, pos.relative(direction))) { + return true; + } + } + return false; + } + + public static boolean isWaterNearby(WorldGenLevel level, @NotNull BlockPos blockPos, int x) { + Iterator var2 = BlockPos.betweenClosed(blockPos.offset(-x, -x, -x), blockPos.offset(x, x, x)).iterator(); + BlockPos blockPos2; + do { + if (!var2.hasNext()) { + return false; + } + blockPos2 = var2.next(); + } while (!level.getBlockState(blockPos2).is(Blocks.WATER)); + return true; + } + +} diff --git a/src/main/java/net/frozenblock/lib/worldgen/feature/api/features/NoisePlantFeature.java b/src/main/java/net/frozenblock/lib/worldgen/feature/api/features/NoisePlantFeature.java new file mode 100644 index 0000000..dad8e70 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/worldgen/feature/api/features/NoisePlantFeature.java @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.worldgen.feature.api.features; + +import com.mojang.serialization.Codec; +import net.frozenblock.lib.math.api.EasyNoiseSampler; +import net.frozenblock.lib.worldgen.feature.api.features.config.PathFeatureConfig; +import net.minecraft.core.BlockPos; +import net.minecraft.tags.BlockTags; +import net.minecraft.util.RandomSource; +import net.minecraft.world.level.WorldGenLevel; +import net.minecraft.world.level.levelgen.Heightmap.Types; +import net.minecraft.world.level.levelgen.feature.Feature; +import net.minecraft.world.level.levelgen.feature.FeaturePlaceContext; +import net.minecraft.world.level.levelgen.synth.ImprovedNoise; +import org.jetbrains.annotations.NotNull; + +public class NoisePlantFeature extends Feature { + + public NoisePlantFeature(Codec codec) { + super(codec); + } + + @Override + public boolean place(@NotNull FeaturePlaceContext context) { + boolean generated = false; + PathFeatureConfig config = context.config(); + BlockPos blockPos = context.origin(); + WorldGenLevel level = context.level(); + int radiusSquared = config.radius() * config.radius(); + RandomSource random = level.getRandom(); + long noiseSeed = level.getSeed(); + ImprovedNoise sampler = + config.noise() == 1 ? EasyNoiseSampler.createLocalNoise(noiseSeed) : + config.noise() == 2 ? EasyNoiseSampler.createCheckedNoise(noiseSeed) : + config.noise() == 3 ? EasyNoiseSampler.createLegacyThreadSafeNoise(noiseSeed) : + EasyNoiseSampler.createXoroNoise(noiseSeed); + float chance = config.placementChance(); + int bx = blockPos.getX(); + int bz = blockPos.getZ(); + BlockPos.MutableBlockPos mutable = blockPos.mutable(); + + for (int x = bx - config.radius(); x <= bx + config.radius(); x++) { + for (int z = bz - config.radius(); z <= bz + config.radius(); z++) { + double distance = ((bx - x) * (bx - x) + ((bz - z) * (bz - z))); + if (distance < radiusSquared) { + mutable.set(x, level.getHeight(Types.OCEAN_FLOOR, x, z), z); + double sample = EasyNoiseSampler.sample(sampler, mutable, config.noiseScale(), config.scaleY(), config.useY()); + if (sample > config.minThreshold() && sample < config.maxThreshold() && level.getBlockState(mutable).is(config.replaceableBlocks()) && level.getBlockState(mutable.below()).is(BlockTags.DIRT) && random.nextFloat() <= chance) { + generated = true; + level.setBlock(mutable, config.state().getState(random, mutable), 3); + } + } + } + } + return generated; + } + +} diff --git a/src/main/java/net/frozenblock/lib/worldgen/feature/api/features/SimpleBlockScheduleTickFeature.java b/src/main/java/net/frozenblock/lib/worldgen/feature/api/features/SimpleBlockScheduleTickFeature.java new file mode 100644 index 0000000..3cb2dc4 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/worldgen/feature/api/features/SimpleBlockScheduleTickFeature.java @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.worldgen.feature.api.features; + +import com.mojang.serialization.Codec; +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.WorldGenLevel; +import net.minecraft.world.level.block.DoublePlantBlock; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.levelgen.feature.Feature; +import net.minecraft.world.level.levelgen.feature.FeaturePlaceContext; +import net.minecraft.world.level.levelgen.feature.configurations.SimpleBlockConfiguration; +import org.jetbrains.annotations.NotNull; + +public class SimpleBlockScheduleTickFeature extends Feature { + + public SimpleBlockScheduleTickFeature(Codec codec) { + super(codec); + } + + @Override + public boolean place(@NotNull FeaturePlaceContext featurePlaceContext) { + SimpleBlockConfiguration simpleBlockConfiguration = featurePlaceContext.config(); + WorldGenLevel worldGenLevel = featurePlaceContext.level(); + BlockPos blockPos = featurePlaceContext.origin(); + BlockState blockState = simpleBlockConfiguration.toPlace().getState(featurePlaceContext.random(), blockPos); + if (blockState.canSurvive(worldGenLevel, blockPos)) { + if (blockState.getBlock() instanceof DoublePlantBlock) { + if (!worldGenLevel.isEmptyBlock(blockPos.above())) { + return false; + } + DoublePlantBlock.placeAt(worldGenLevel, blockState, blockPos, 2); + worldGenLevel.scheduleTick(blockPos, blockState.getBlock(), 1); + } else { + worldGenLevel.setBlock(blockPos, blockState, 2); + worldGenLevel.scheduleTick(blockPos, blockState.getBlock(), 1); + } + + return true; + } else { + return false; + } + } + +} diff --git a/src/main/java/net/frozenblock/lib/worldgen/feature/api/features/UpwardsColumnFeature.java b/src/main/java/net/frozenblock/lib/worldgen/feature/api/features/UpwardsColumnFeature.java new file mode 100644 index 0000000..b2dd3e5 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/worldgen/feature/api/features/UpwardsColumnFeature.java @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.worldgen.feature.api.features; + +import com.mojang.serialization.Codec; +import net.frozenblock.lib.worldgen.feature.api.features.config.ColumnFeatureConfig; +import net.minecraft.core.BlockPos; +import net.minecraft.util.RandomSource; +import net.minecraft.world.level.WorldGenLevel; +import net.minecraft.world.level.levelgen.feature.Feature; +import net.minecraft.world.level.levelgen.feature.FeaturePlaceContext; +import net.minecraft.world.level.material.Fluids; +import org.jetbrains.annotations.NotNull; + +public class UpwardsColumnFeature extends Feature { + + public UpwardsColumnFeature(Codec codec) { + super(codec); + } + + @Override + public boolean place(@NotNull FeaturePlaceContext context) { + boolean bl = false; + BlockPos blockPos = context.origin(); + WorldGenLevel level = context.level(); + RandomSource random = level.getRandom(); + BlockPos.MutableBlockPos mutable = blockPos.mutable(); + int bx = blockPos.getX(); + int bz = blockPos.getZ(); + int by = blockPos.getY(); + int height = context.config().height().sample(random); + for (int y = 0; y < height; y++) { + if (context.config().replaceableBlocks().contains(level.getBlockState(mutable).getBlockHolder()) || level.getBlockState(mutable).isAir() || level.getBlockState(mutable).getFluidState() != Fluids.EMPTY.defaultFluidState()) { + bl = true; + level.setBlock(mutable, context.config().state(), 3); + mutable.set(bx, by + y, bz); + } else { + mutable.set(bx, by + y, bz); + } + } + return bl; + } + +} diff --git a/src/main/java/net/frozenblock/lib/worldgen/feature/api/features/config/ColumnFeatureConfig.java b/src/main/java/net/frozenblock/lib/worldgen/feature/api/features/config/ColumnFeatureConfig.java new file mode 100644 index 0000000..7ce7751 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/worldgen/feature/api/features/config/ColumnFeatureConfig.java @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.worldgen.feature.api.features.config; + +import com.mojang.serialization.Codec; +import com.mojang.serialization.codecs.RecordCodecBuilder; +import net.minecraft.core.HolderSet; +import net.minecraft.core.RegistryCodecs; +import net.minecraft.core.registries.Registries; +import net.minecraft.util.valueproviders.IntProvider; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.levelgen.feature.configurations.FeatureConfiguration; + +public record ColumnFeatureConfig(BlockState state, IntProvider height, + HolderSet replaceableBlocks) implements FeatureConfiguration { + public static final Codec CODEC = RecordCodecBuilder.create((instance) -> + instance.group( + BlockState.CODEC.fieldOf("state").forGetter((config) -> config.state), + IntProvider.NON_NEGATIVE_CODEC.fieldOf("height").forGetter((config) -> config.height), + RegistryCodecs.homogeneousList(Registries.BLOCK).fieldOf("replaceable_blocks").forGetter((config) -> config.replaceableBlocks) + ).apply(instance, ColumnFeatureConfig::new)); +} diff --git a/src/main/java/net/frozenblock/lib/worldgen/feature/api/features/config/ColumnWithDiskFeatureConfig.java b/src/main/java/net/frozenblock/lib/worldgen/feature/api/features/config/ColumnWithDiskFeatureConfig.java new file mode 100644 index 0000000..2570f06 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/worldgen/feature/api/features/config/ColumnWithDiskFeatureConfig.java @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.worldgen.feature.api.features.config; + +import com.mojang.serialization.Codec; +import com.mojang.serialization.codecs.RecordCodecBuilder; +import net.minecraft.core.HolderSet; +import net.minecraft.core.RegistryCodecs; +import net.minecraft.core.registries.Registries; +import net.minecraft.util.valueproviders.IntProvider; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.levelgen.feature.configurations.FeatureConfiguration; + +public record ColumnWithDiskFeatureConfig(BlockState state, IntProvider radius, IntProvider height, + IntProvider additionalHeight, HolderSet replaceableBlocks, + HolderSet diskBlocks) implements FeatureConfiguration { + public static final Codec CODEC = RecordCodecBuilder.create((instance) -> + instance.group( + BlockState.CODEC.fieldOf("state").forGetter((config) -> config.state), + IntProvider.NON_NEGATIVE_CODEC.fieldOf("radius").forGetter((config) -> config.radius), + IntProvider.NON_NEGATIVE_CODEC.fieldOf("height").forGetter((config) -> config.height), + IntProvider.NON_NEGATIVE_CODEC.fieldOf("additional_height").forGetter((config) -> config.additionalHeight), + RegistryCodecs.homogeneousList(Registries.BLOCK).fieldOf("replaceable_blocks").forGetter((config) -> config.replaceableBlocks), + RegistryCodecs.homogeneousList(Registries.BLOCK).fieldOf("disk_blocks").forGetter((config) -> config.diskBlocks) + ).apply(instance, ColumnWithDiskFeatureConfig::new)); +} diff --git a/src/main/java/net/frozenblock/lib/worldgen/feature/api/features/config/ComboFeatureConfig.java b/src/main/java/net/frozenblock/lib/worldgen/feature/api/features/config/ComboFeatureConfig.java new file mode 100644 index 0000000..fa922b6 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/worldgen/feature/api/features/config/ComboFeatureConfig.java @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.worldgen.feature.api.features.config; + +import com.mojang.serialization.Codec; +import com.mojang.serialization.codecs.RecordCodecBuilder; +import net.minecraft.core.Holder; +import net.minecraft.world.level.levelgen.feature.configurations.FeatureConfiguration; +import net.minecraft.world.level.levelgen.placement.PlacedFeature; + +import java.util.List; + +public record ComboFeatureConfig(List> features) implements FeatureConfiguration { + + public static final Codec CODEC = RecordCodecBuilder.create((instance) -> instance.group( + PlacedFeature.CODEC.listOf().fieldOf("features").forGetter(vegetationPatchConfiguration -> vegetationPatchConfiguration.features) + ).apply(instance, ComboFeatureConfig::new)); +} diff --git a/src/main/java/net/frozenblock/lib/worldgen/feature/api/features/config/CurvingTunnelFeatureConfig.java b/src/main/java/net/frozenblock/lib/worldgen/feature/api/features/config/CurvingTunnelFeatureConfig.java new file mode 100644 index 0000000..740c9ae --- /dev/null +++ b/src/main/java/net/frozenblock/lib/worldgen/feature/api/features/config/CurvingTunnelFeatureConfig.java @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.worldgen.feature.api.features.config; + +import com.mojang.serialization.Codec; +import com.mojang.serialization.codecs.RecordCodecBuilder; +import net.minecraft.core.registries.Registries; +import net.minecraft.tags.TagKey; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.levelgen.feature.configurations.FeatureConfiguration; +import net.minecraft.world.level.levelgen.feature.stateproviders.BlockStateProvider; + +public record CurvingTunnelFeatureConfig(BlockStateProvider state, int radius, double minCurvature, double maxCurvature, + TagKey replaceableBlocks) implements FeatureConfiguration { + public static final Codec CODEC = RecordCodecBuilder.create((instance) -> instance.group( + BlockStateProvider.CODEC.fieldOf("state").forGetter((config) -> config.state), + Codec.intRange(1, 64).fieldOf("radius").orElse(3).forGetter((config) -> config.radius), + Codec.doubleRange(0D, 15D).fieldOf("min_curvature").orElse(3D).forGetter((config) -> config.minCurvature), + Codec.doubleRange(0D, 15D).fieldOf("max_curvature").orElse(3D).forGetter((config) -> config.maxCurvature), + TagKey.codec(Registries.BLOCK).fieldOf("replaceable_blocks").forGetter((config) -> config.replaceableBlocks) + ).apply(instance, CurvingTunnelFeatureConfig::new)); +} diff --git a/src/main/java/net/frozenblock/lib/worldgen/feature/api/features/config/FadingDiskCarpetFeatureConfig.java b/src/main/java/net/frozenblock/lib/worldgen/feature/api/features/config/FadingDiskCarpetFeatureConfig.java new file mode 100644 index 0000000..02eb1c6 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/worldgen/feature/api/features/config/FadingDiskCarpetFeatureConfig.java @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.worldgen.feature.api.features.config; + +import com.mojang.serialization.Codec; +import com.mojang.serialization.codecs.RecordCodecBuilder; +import net.minecraft.util.valueproviders.IntProvider; +import net.minecraft.world.level.levelgen.Heightmap; +import net.minecraft.world.level.levelgen.feature.configurations.FeatureConfiguration; +import net.minecraft.world.level.levelgen.feature.stateproviders.BlockStateProvider; + +public record FadingDiskCarpetFeatureConfig(boolean useHeightmapInsteadOfCircularPlacement, + BlockStateProvider innerState, BlockStateProvider outerState, + IntProvider radius, float placementChance, float innerPercent, + float innerChance, float fadeStartDistancePercent, + Heightmap.Types heightmap) implements FeatureConfiguration { + public static final Codec CODEC = RecordCodecBuilder.create( + (instance) -> instance.group( + Codec.BOOL.fieldOf("use_heightmap_instead_of_circular_placement").forGetter(config -> config.useHeightmapInsteadOfCircularPlacement), + BlockStateProvider.CODEC.fieldOf("inner_state").forGetter(config -> config.innerState), + BlockStateProvider.CODEC.fieldOf("outer_state").forGetter(config -> config.outerState), + IntProvider.CODEC.fieldOf("radius").forGetter(config -> config.radius), + Codec.FLOAT.fieldOf("placement_chance").forGetter(config -> config.placementChance), + Codec.FLOAT.fieldOf("inner_chance").forGetter(config -> config.innerChance), + Codec.FLOAT.fieldOf("inner_percent").forGetter(config -> config.innerPercent), + Codec.FLOAT.fieldOf("fade_start_distance_percent").forGetter(config -> config.fadeStartDistancePercent), + Heightmap.Types.CODEC.fieldOf("heightmap").forGetter((config) -> config.heightmap) + ).apply(instance, FadingDiskCarpetFeatureConfig::new) + ); +} diff --git a/src/main/java/net/frozenblock/lib/worldgen/feature/api/features/config/FadingDiskFeatureConfig.java b/src/main/java/net/frozenblock/lib/worldgen/feature/api/features/config/FadingDiskFeatureConfig.java new file mode 100644 index 0000000..149f245 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/worldgen/feature/api/features/config/FadingDiskFeatureConfig.java @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.worldgen.feature.api.features.config; + +import com.mojang.serialization.Codec; +import com.mojang.serialization.codecs.RecordCodecBuilder; +import net.minecraft.core.HolderSet; +import net.minecraft.core.RegistryCodecs; +import net.minecraft.core.registries.Registries; +import net.minecraft.util.valueproviders.IntProvider; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.levelgen.Heightmap; +import net.minecraft.world.level.levelgen.feature.configurations.FeatureConfiguration; +import net.minecraft.world.level.levelgen.feature.stateproviders.BlockStateProvider; + +public record FadingDiskFeatureConfig(boolean useHeightmapInsteadOfCircularPlacement, BlockStateProvider innerState, + BlockStateProvider outerState, IntProvider radius, float placementChance, + float innerChance, float innerPercent, float fadeStartDistancePercent, + HolderSet innerReplaceableBlocks, HolderSet outerReplaceableBlocks, + Heightmap.Types heightmap) implements FeatureConfiguration { + public static final Codec CODEC = RecordCodecBuilder.create( + (instance) -> instance.group( + Codec.BOOL.fieldOf("use_heightmap_instead_of_circular_placement").forGetter(config -> config.useHeightmapInsteadOfCircularPlacement), + BlockStateProvider.CODEC.fieldOf("inner_state").forGetter(config -> config.innerState), + BlockStateProvider.CODEC.fieldOf("outer_state").forGetter(config -> config.outerState), + IntProvider.CODEC.fieldOf("radius").forGetter(config -> config.radius), + Codec.FLOAT.fieldOf("placement_chance").forGetter(config -> config.placementChance), + Codec.FLOAT.fieldOf("inner_chance").forGetter(config -> config.innerChance), + Codec.FLOAT.fieldOf("inner_percent").forGetter(config -> config.innerPercent), + Codec.FLOAT.fieldOf("fade_start_distance_percent").forGetter(config -> config.fadeStartDistancePercent), + RegistryCodecs.homogeneousList(Registries.BLOCK).fieldOf("inner_replaceable_blocks").forGetter((config) -> config.innerReplaceableBlocks), + RegistryCodecs.homogeneousList(Registries.BLOCK).fieldOf("outer_replaceable_blocks").forGetter((config) -> config.outerReplaceableBlocks), + Heightmap.Types.CODEC.fieldOf("heightmap").forGetter((config) -> config.heightmap) + ).apply(instance, FadingDiskFeatureConfig::new) + ); +} diff --git a/src/main/java/net/frozenblock/lib/worldgen/feature/api/features/config/FadingDiskTagBiomeFeatureConfig.java b/src/main/java/net/frozenblock/lib/worldgen/feature/api/features/config/FadingDiskTagBiomeFeatureConfig.java new file mode 100644 index 0000000..dba9cc2 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/worldgen/feature/api/features/config/FadingDiskTagBiomeFeatureConfig.java @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.worldgen.feature.api.features.config; + +import com.mojang.serialization.Codec; +import com.mojang.serialization.codecs.RecordCodecBuilder; +import net.minecraft.core.registries.Registries; +import net.minecraft.tags.TagKey; +import net.minecraft.util.valueproviders.IntProvider; +import net.minecraft.world.level.biome.Biome; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.levelgen.Heightmap; +import net.minecraft.world.level.levelgen.feature.configurations.FeatureConfiguration; +import net.minecraft.world.level.levelgen.feature.stateproviders.BlockStateProvider; + +public record FadingDiskTagBiomeFeatureConfig(boolean useHeightmapInsteadOfCircularPlacement, + BlockStateProvider innerState, BlockStateProvider outerState, + IntProvider radius, float placementChance, float innerChance, + float innerPercent, float fadeStartDistancePercent, + TagKey innerReplaceableBlocks, + TagKey outerReplaceableBlocks, Heightmap.Types heightmap, + TagKey excludedBiomes) implements FeatureConfiguration { + public static final Codec CODEC = RecordCodecBuilder.create( + (instance) -> instance.group( + Codec.BOOL.fieldOf("use_heightmap_instead_of_circular_placement").forGetter(config -> config.useHeightmapInsteadOfCircularPlacement), + BlockStateProvider.CODEC.fieldOf("inner_state").forGetter(config -> config.innerState), + BlockStateProvider.CODEC.fieldOf("outer_state").forGetter(config -> config.outerState), + IntProvider.CODEC.fieldOf("radius").forGetter(config -> config.radius), + Codec.FLOAT.fieldOf("placement_chance").forGetter(config -> config.placementChance), + Codec.FLOAT.fieldOf("inner_chance").forGetter(config -> config.innerChance), + Codec.FLOAT.fieldOf("inner_rercent").forGetter(config -> config.innerPercent), + Codec.FLOAT.fieldOf("fade_start_distance_percent").forGetter(config -> config.fadeStartDistancePercent), + TagKey.codec(Registries.BLOCK).fieldOf("inner_replaceable_blocks").forGetter((config) -> config.innerReplaceableBlocks), + TagKey.codec(Registries.BLOCK).fieldOf("outer_replaceable_blocks").forGetter((config) -> config.outerReplaceableBlocks), + Heightmap.Types.CODEC.fieldOf("heightmap").forGetter((config) -> config.heightmap), + TagKey.codec(Registries.BIOME).fieldOf("excluded_biomes").forGetter((config) -> config.excludedBiomes) + ).apply(instance, FadingDiskTagBiomeFeatureConfig::new) + ); +} diff --git a/src/main/java/net/frozenblock/lib/worldgen/feature/api/features/config/FadingDiskTagFeatureConfig.java b/src/main/java/net/frozenblock/lib/worldgen/feature/api/features/config/FadingDiskTagFeatureConfig.java new file mode 100644 index 0000000..6071bab --- /dev/null +++ b/src/main/java/net/frozenblock/lib/worldgen/feature/api/features/config/FadingDiskTagFeatureConfig.java @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.worldgen.feature.api.features.config; + +import com.mojang.serialization.Codec; +import com.mojang.serialization.codecs.RecordCodecBuilder; +import net.minecraft.core.registries.Registries; +import net.minecraft.tags.TagKey; +import net.minecraft.util.valueproviders.IntProvider; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.levelgen.Heightmap; +import net.minecraft.world.level.levelgen.feature.configurations.FeatureConfiguration; +import net.minecraft.world.level.levelgen.feature.stateproviders.BlockStateProvider; + +public record FadingDiskTagFeatureConfig(boolean useHeightmapInsteadOfCircularPlacement, BlockStateProvider innerState, + BlockStateProvider outerState, IntProvider radius, float placementChance, + float innerChance, float innerPercent, float fadeStartDistancePercent, + TagKey innerReplaceableBlocks, TagKey outerReplaceableBlocks, + Heightmap.Types heightmap) implements FeatureConfiguration { + public static final Codec CODEC = RecordCodecBuilder.create( + (instance) -> instance.group( + Codec.BOOL.fieldOf("use_heightmap_instead_of_circular_placement").forGetter(config -> config.useHeightmapInsteadOfCircularPlacement), + BlockStateProvider.CODEC.fieldOf("inner_state").forGetter(config -> config.innerState), + BlockStateProvider.CODEC.fieldOf("outer_state").forGetter(config -> config.outerState), + IntProvider.CODEC.fieldOf("radius").forGetter(config -> config.radius), + Codec.FLOAT.fieldOf("placement_chance").forGetter(config -> config.placementChance), + Codec.FLOAT.fieldOf("inner_chance").forGetter(config -> config.innerChance), + Codec.FLOAT.fieldOf("inner_percent").forGetter(config -> config.innerPercent), + Codec.FLOAT.fieldOf("fade_start_distance_percent").forGetter(config -> config.fadeStartDistancePercent), + TagKey.codec(Registries.BLOCK).fieldOf("inner_replaceable_blocks").forGetter((config) -> config.innerReplaceableBlocks), + TagKey.codec(Registries.BLOCK).fieldOf("outer_replaceable_blocks").forGetter((config) -> config.outerReplaceableBlocks), + Heightmap.Types.CODEC.fieldOf("heightmap").forGetter((config) -> config.heightmap) + ).apply(instance, FadingDiskTagFeatureConfig::new) + ); +} diff --git a/src/main/java/net/frozenblock/lib/worldgen/feature/api/features/config/PathFeatureConfig.java b/src/main/java/net/frozenblock/lib/worldgen/feature/api/features/config/PathFeatureConfig.java new file mode 100644 index 0000000..e8ab666 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/worldgen/feature/api/features/config/PathFeatureConfig.java @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.worldgen.feature.api.features.config; + +import com.mojang.serialization.Codec; +import com.mojang.serialization.codecs.RecordCodecBuilder; +import net.minecraft.core.HolderSet; +import net.minecraft.core.RegistryCodecs; +import net.minecraft.core.registries.Registries; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.levelgen.feature.configurations.FeatureConfiguration; +import net.minecraft.world.level.levelgen.feature.stateproviders.BlockStateProvider; + +public record PathFeatureConfig(BlockStateProvider state, int radius, int noise, double noiseScale, double minThreshold, + double maxThreshold, boolean useY, boolean scaleY, boolean is3D, + boolean onlyPlaceWhenExposed, HolderSet replaceableBlocks, + float placementChance) implements FeatureConfiguration { + public static final Codec CODEC = RecordCodecBuilder.create((instance) -> instance.group( + BlockStateProvider.CODEC.fieldOf("state").forGetter((config) -> config.state), + Codec.intRange(1, 64).fieldOf("radius").orElse(10).forGetter((config) -> config.radius), + Codec.intRange(1, 4).fieldOf("noise").orElse(4).forGetter((config) -> config.noise), + Codec.doubleRange(0.0001, 128).fieldOf("noise_scale").orElse(0.05).forGetter((config) -> config.noiseScale), + Codec.doubleRange(-1, 1).fieldOf("min_threshold").orElse(0.2).forGetter((config) -> config.minThreshold), + Codec.doubleRange(-1, 1).fieldOf("max_threshold").orElse(1D).forGetter((config) -> config.maxThreshold), + Codec.BOOL.fieldOf("use_y").orElse(false).forGetter((config) -> config.useY), + Codec.BOOL.fieldOf("scale_y").orElse(false).forGetter((config) -> config.scaleY), + Codec.BOOL.fieldOf("is_3d").orElse(false).forGetter((config) -> config.is3D), + Codec.BOOL.fieldOf("only_place_when_exposed").orElse(false).forGetter((config) -> config.onlyPlaceWhenExposed), + RegistryCodecs.homogeneousList(Registries.BLOCK).fieldOf("replaceable_blocks").forGetter((config) -> config.replaceableBlocks), + Codec.floatRange(0, 1).fieldOf("placement_chance").orElse(1F).forGetter((config) -> config.placementChance) + ).apply(instance, PathFeatureConfig::new)); +} diff --git a/src/main/java/net/frozenblock/lib/worldgen/feature/api/features/config/PathSwapUnderWaterFeatureConfig.java b/src/main/java/net/frozenblock/lib/worldgen/feature/api/features/config/PathSwapUnderWaterFeatureConfig.java new file mode 100644 index 0000000..95e3c26 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/worldgen/feature/api/features/config/PathSwapUnderWaterFeatureConfig.java @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.worldgen.feature.api.features.config; + +import com.mojang.serialization.Codec; +import com.mojang.serialization.codecs.RecordCodecBuilder; +import net.minecraft.core.HolderSet; +import net.minecraft.core.RegistryCodecs; +import net.minecraft.core.registries.Registries; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.levelgen.feature.configurations.FeatureConfiguration; +import net.minecraft.world.level.levelgen.feature.stateproviders.BlockStateProvider; + +public record PathSwapUnderWaterFeatureConfig(BlockStateProvider state, BlockStateProvider underWaterState, int radius, + int noise, double noiseScale, double minThreshold, double maxThreshold, + boolean useY, boolean scaleY, boolean is3D, boolean onlyPlaceWhenExposed, + HolderSet replaceableBlocks, + float placement_chance) implements FeatureConfiguration { + public static final Codec CODEC = RecordCodecBuilder.create((instance) -> instance.group( + BlockStateProvider.CODEC.fieldOf("state").forGetter((config) -> config.state), + BlockStateProvider.CODEC.fieldOf("under_water_state").forGetter((config) -> config.underWaterState), + Codec.intRange(1, 64).fieldOf("radius").orElse(10).forGetter((config) -> config.radius), + Codec.intRange(1, 4).fieldOf("noise").orElse(4).forGetter((config) -> config.noise), + Codec.doubleRange(0.0001, 128).fieldOf("noise_scale").orElse(0.05).forGetter((config) -> config.noiseScale), + Codec.doubleRange(-1, 1).fieldOf("min_threshold").orElse(0.2).forGetter((config) -> config.minThreshold), + Codec.doubleRange(-1, 1).fieldOf("max_threshold").orElse(1D).forGetter((config) -> config.maxThreshold), + Codec.BOOL.fieldOf("use_y").orElse(false).forGetter((config) -> config.useY), + Codec.BOOL.fieldOf("scale_y").orElse(false).forGetter((config) -> config.scaleY), + Codec.BOOL.fieldOf("is_3d").orElse(false).forGetter((config) -> config.is3D), + Codec.BOOL.fieldOf("only_place_when_exposed").orElse(false).forGetter((config) -> config.onlyPlaceWhenExposed), + RegistryCodecs.homogeneousList(Registries.BLOCK).fieldOf("replaceable_blocks").forGetter((config) -> config.replaceableBlocks), + Codec.floatRange(0, 1).fieldOf("placement_chance").orElse(1F).forGetter((config) -> config.placement_chance) + ).apply(instance, PathSwapUnderWaterFeatureConfig::new)); +} diff --git a/src/main/java/net/frozenblock/lib/worldgen/feature/api/features/config/PathSwapUnderWaterTagFeatureConfig.java b/src/main/java/net/frozenblock/lib/worldgen/feature/api/features/config/PathSwapUnderWaterTagFeatureConfig.java new file mode 100644 index 0000000..5ad4f6a --- /dev/null +++ b/src/main/java/net/frozenblock/lib/worldgen/feature/api/features/config/PathSwapUnderWaterTagFeatureConfig.java @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.worldgen.feature.api.features.config; + +import com.mojang.serialization.Codec; +import com.mojang.serialization.codecs.RecordCodecBuilder; +import net.minecraft.core.registries.Registries; +import net.minecraft.tags.TagKey; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.levelgen.feature.configurations.FeatureConfiguration; +import net.minecraft.world.level.levelgen.feature.stateproviders.BlockStateProvider; + +public record PathSwapUnderWaterTagFeatureConfig(BlockStateProvider state, BlockStateProvider underWaterState, + int radius, int noise, double noiseScale, double minThreshold, + double maxThreshold, boolean useY, boolean scaleY, boolean is3D, + boolean onlyPlaceWhenExposed, TagKey replaceableBlocks, + float placementChance) implements FeatureConfiguration { + public static final Codec CODEC = RecordCodecBuilder.create((instance) -> instance.group( + BlockStateProvider.CODEC.fieldOf("state").forGetter((config) -> config.state), + BlockStateProvider.CODEC.fieldOf("under_water_state").forGetter((config) -> config.underWaterState), + Codec.intRange(1, 64).fieldOf("radius").orElse(10).forGetter((config) -> config.radius), + Codec.intRange(1, 4).fieldOf("noise").orElse(4).forGetter((config) -> config.noise), + Codec.doubleRange(0.0001, 128).fieldOf("noise_scale").orElse(0.05).forGetter((config) -> config.noiseScale), + Codec.doubleRange(-1, 1).fieldOf("min_threshold").orElse(0.2).forGetter((config) -> config.minThreshold), + Codec.doubleRange(-1, 1).fieldOf("max_threshold").orElse(1D).forGetter((config) -> config.maxThreshold), + Codec.BOOL.fieldOf("use_y").orElse(false).forGetter((config) -> config.useY), + Codec.BOOL.fieldOf("scale_y").orElse(false).forGetter((config) -> config.scaleY), + Codec.BOOL.fieldOf("is_3d").orElse(false).forGetter((config) -> config.is3D), + Codec.BOOL.fieldOf("only_place_when_exposed").orElse(false).forGetter((config) -> config.onlyPlaceWhenExposed), + TagKey.codec(Registries.BLOCK).fieldOf("replaceable_blocks").forGetter((config) -> config.replaceableBlocks), + Codec.floatRange(0, 1).fieldOf("placement_chance").orElse(1F).forGetter((config) -> config.placementChance) + ).apply(instance, PathSwapUnderWaterTagFeatureConfig::new)); +} diff --git a/src/main/java/net/frozenblock/lib/worldgen/feature/api/features/config/PathTagFeatureConfig.java b/src/main/java/net/frozenblock/lib/worldgen/feature/api/features/config/PathTagFeatureConfig.java new file mode 100644 index 0000000..6b467f1 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/worldgen/feature/api/features/config/PathTagFeatureConfig.java @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.worldgen.feature.api.features.config; + +import com.mojang.serialization.Codec; +import com.mojang.serialization.codecs.RecordCodecBuilder; +import net.minecraft.core.registries.Registries; +import net.minecraft.tags.TagKey; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.levelgen.feature.configurations.FeatureConfiguration; +import net.minecraft.world.level.levelgen.feature.stateproviders.BlockStateProvider; + +public record PathTagFeatureConfig(BlockStateProvider state, int radius, int noise, double noiseScale, + double minThreshold, double maxThreshold, boolean useY, boolean scaleY, boolean is3D, + boolean onlyPlaceWhenExposed, TagKey replaceableBlocks, + float placementChance) implements FeatureConfiguration { + public static final Codec CODEC = RecordCodecBuilder.create((instance) -> instance.group( + BlockStateProvider.CODEC.fieldOf("state").forGetter((config) -> config.state), + Codec.intRange(1, 64).fieldOf("radius").orElse(10).forGetter((config) -> config.radius), + Codec.intRange(1, 4).fieldOf("noise").orElse(4).forGetter((config) -> config.noise), + Codec.doubleRange(0.0001, 128).fieldOf("noise_scale").orElse(0.05).forGetter((config) -> config.noiseScale), + Codec.doubleRange(-1, 1).fieldOf("min_threshold").orElse(0.2).forGetter((config) -> config.minThreshold), + Codec.doubleRange(-1, 1).fieldOf("max_threshold").orElse(1D).forGetter((config) -> config.maxThreshold), + Codec.BOOL.fieldOf("use_y").orElse(false).forGetter((config) -> config.useY), + Codec.BOOL.fieldOf("scale_y").orElse(false).forGetter((config) -> config.scaleY), + Codec.BOOL.fieldOf("is_3d").orElse(false).forGetter((config) -> config.is3D), + Codec.BOOL.fieldOf("only_place_when_exposed").orElse(false).forGetter((config) -> config.onlyPlaceWhenExposed), + TagKey.codec(Registries.BLOCK).fieldOf("replaceable_blocks").forGetter((config) -> config.replaceableBlocks), + Codec.floatRange(0, 1).fieldOf("placement_chance").orElse(1F).forGetter((config) -> config.placementChance) + ).apply(instance, PathTagFeatureConfig::new)); +} diff --git a/src/main/java/net/frozenblock/lib/worldgen/feature/api/placementmodifier/FrozenPlacementModifiers.java b/src/main/java/net/frozenblock/lib/worldgen/feature/api/placementmodifier/FrozenPlacementModifiers.java new file mode 100644 index 0000000..67de99e --- /dev/null +++ b/src/main/java/net/frozenblock/lib/worldgen/feature/api/placementmodifier/FrozenPlacementModifiers.java @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.worldgen.feature.api.placementmodifier; + +import com.mojang.serialization.MapCodec; +import net.frozenblock.lib.FrozenSharedConstants; +import net.minecraft.core.Registry; +import net.minecraft.world.level.levelgen.placement.PlacementModifier; +import net.minecraft.world.level.levelgen.placement.PlacementModifierType; +import net.neoforged.neoforge.registries.RegisterEvent; +import org.jetbrains.annotations.NotNull; + +public class FrozenPlacementModifiers { + public static final PlacementModifierType ACCURATE_HEIGHTMAP = () -> LowerHeightmapPlacement.CODEC; + public static final PlacementModifierType NOISE_FILTER = () -> NoisePlacementFilter.CODEC; + + public static void init(RegisterEvent.RegisterHelper> registry) { + registry.register(FrozenSharedConstants.id("improved_heightmap"), ACCURATE_HEIGHTMAP); + registry.register(FrozenSharedConstants.id("noise_filter"), NOISE_FILTER); + } +} diff --git a/src/main/java/net/frozenblock/lib/worldgen/feature/api/placementmodifier/LowerHeightmapPlacement.java b/src/main/java/net/frozenblock/lib/worldgen/feature/api/placementmodifier/LowerHeightmapPlacement.java new file mode 100644 index 0000000..1b2984c --- /dev/null +++ b/src/main/java/net/frozenblock/lib/worldgen/feature/api/placementmodifier/LowerHeightmapPlacement.java @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.worldgen.feature.api.placementmodifier; + +import com.mojang.serialization.MapCodec; +import com.mojang.serialization.codecs.RecordCodecBuilder; +import net.minecraft.core.BlockPos; +import net.minecraft.util.RandomSource; +import net.minecraft.world.level.levelgen.Heightmap; +import net.minecraft.world.level.levelgen.placement.PlacementContext; +import net.minecraft.world.level.levelgen.placement.PlacementModifier; +import net.minecraft.world.level.levelgen.placement.PlacementModifierType; + +import java.util.stream.Stream; + +public class LowerHeightmapPlacement extends PlacementModifier { + public static final MapCodec CODEC = RecordCodecBuilder.mapCodec(instance -> instance.group( + (Heightmap.Types.CODEC.fieldOf("heightmap")).forGetter(modifier -> modifier.heightmap) + ).apply(instance, LowerHeightmapPlacement::new)); + + private final Heightmap.Types heightmap; + + private LowerHeightmapPlacement(Heightmap.Types heightmap) { + this.heightmap = heightmap; + } + + public static LowerHeightmapPlacement onHeightmap(Heightmap.Types heightmap) { + return new LowerHeightmapPlacement(heightmap); + } + + @Override + public Stream getPositions(PlacementContext context, RandomSource random, BlockPos pos) { + int x = pos.getX(); + int z = pos.getZ(); + int y = context.getHeight(this.heightmap, x, z) - 1; + if (y > context.getMinBuildHeight()) { + return Stream.of(new BlockPos(x, y, z)); + } + return Stream.of(new BlockPos[0]); + } + + @Override + public PlacementModifierType type() { + return FrozenPlacementModifiers.ACCURATE_HEIGHTMAP; + } + + public static final PlacementModifier HEIGHTMAP_MOTION_BLOCKING = LowerHeightmapPlacement.onHeightmap(Heightmap.Types.MOTION_BLOCKING); + public static final PlacementModifier HEIGHTMAP_TOP_SOLID = LowerHeightmapPlacement.onHeightmap(Heightmap.Types.OCEAN_FLOOR_WG); + public static final PlacementModifier HEIGHTMAP_WORLD_SURFACE = LowerHeightmapPlacement.onHeightmap(Heightmap.Types.WORLD_SURFACE_WG); + public static final PlacementModifier HEIGHTMAP_OCEAN_FLOOR = LowerHeightmapPlacement.onHeightmap(Heightmap.Types.OCEAN_FLOOR); +} diff --git a/src/main/java/net/frozenblock/lib/worldgen/feature/api/placementmodifier/NoisePlacementFilter.java b/src/main/java/net/frozenblock/lib/worldgen/feature/api/placementmodifier/NoisePlacementFilter.java new file mode 100644 index 0000000..9d352dc --- /dev/null +++ b/src/main/java/net/frozenblock/lib/worldgen/feature/api/placementmodifier/NoisePlacementFilter.java @@ -0,0 +1,105 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.worldgen.feature.api.placementmodifier; + +import com.mojang.serialization.Codec; +import com.mojang.serialization.MapCodec; +import com.mojang.serialization.codecs.RecordCodecBuilder; +import net.frozenblock.lib.math.api.EasyNoiseSampler; +import net.minecraft.core.BlockPos; +import net.minecraft.util.RandomSource; +import net.minecraft.world.level.WorldGenLevel; +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.minecraft.world.level.levelgen.synth.ImprovedNoise; +import org.jetbrains.annotations.NotNull; + +public class NoisePlacementFilter extends PlacementFilter { + public static final MapCodec CODEC = RecordCodecBuilder.mapCodec((instance) -> instance.group( + Codec.intRange(1, 4).fieldOf("noise").orElse(4).forGetter((config) -> config.noise), + Codec.doubleRange(0.0001, 128).fieldOf("noise_scale").orElse(0.05).forGetter((config) -> config.noiseScale), + Codec.doubleRange(-1, 1).fieldOf("min_threshold").orElse(0.2).forGetter((config) -> config.minThreshold), + Codec.doubleRange(-1, 1).fieldOf("maxThresh").orElse(1D).forGetter((config) -> config.maxThreshold), + Codec.doubleRange(0, 1).fieldOf("fade_distance").orElse(0D).forGetter((config) -> config.fadeDistance), + Codec.BOOL.fieldOf("use_y").orElse(false).forGetter((config) -> config.useY), + Codec.BOOL.fieldOf("scale_y").orElse(false).forGetter((config) -> config.scaleY), + Codec.BOOL.fieldOf("must_be_inside").orElse(false).forGetter((config) -> config.mustBeInside) + ).apply(instance, NoisePlacementFilter::new)); + + private final int noise; + private final double noiseScale; + private final double minThreshold; + private final double minFadeThreshold; + private final double maxThreshold; + private final double maxFadeThreshold; + private final double fadeDistance; + private final boolean useY; + private final boolean scaleY; + private final boolean mustBeInside; + + public NoisePlacementFilter(int noise, double noiseScale, double minThreshold, double maxThreshold, double fadeDistance, boolean useY, boolean scaleY, boolean mustBeInside) { + this.noise = noise; + this.noiseScale = noiseScale; + this.minThreshold = minThreshold; + this.maxThreshold = maxThreshold; + this.fadeDistance = fadeDistance; + this.minFadeThreshold = minThreshold - fadeDistance; + this.maxFadeThreshold = maxThreshold + fadeDistance; + this.useY = useY; + this.scaleY = scaleY; + this.mustBeInside = mustBeInside; + if (this.minThreshold >= this.maxThreshold) { + throw new IllegalArgumentException("NoisePlacementFilter minThresh cannot be greater than or equal to maxThreshold!"); + } + if (this.fadeDistance < 0) { + throw new IllegalArgumentException("NoisePlacementFilter fadeDistance cannot be less than 0!"); + } + } + + @Override + protected boolean shouldPlace(@NotNull PlacementContext context, RandomSource random, BlockPos pos) { + WorldGenLevel level = context.level; + boolean isInside = false; + long noiseSeed = level.getSeed(); + ImprovedNoise sampler = + this.noise == 1 ? EasyNoiseSampler.createLocalNoise(noiseSeed) : + this.noise == 2 ? EasyNoiseSampler.createCheckedNoise(noiseSeed) : + this.noise == 3 ? EasyNoiseSampler.createLegacyThreadSafeNoise(noiseSeed) : + EasyNoiseSampler.createXoroNoise(noiseSeed); + double sample = EasyNoiseSampler.sample(sampler, pos, this.noiseScale, this.scaleY, this.useY); + if (sample > this.minThreshold && sample < this.maxThreshold) { + isInside = true; + } + if (this.fadeDistance > 0) { + if (sample > this.minFadeThreshold && sample < this.minThreshold) { + isInside = random.nextDouble() > Math.abs((this.minThreshold - sample) / this.fadeDistance); + } + if (sample < this.maxFadeThreshold && sample > this.maxThreshold) { + isInside = random.nextDouble() > Math.abs((this.maxThreshold - sample) / this.fadeDistance); + } + } + return this.mustBeInside == isInside; + } + + @Override + @NotNull + public PlacementModifierType type() { + return FrozenPlacementModifiers.NOISE_FILTER; + } +} diff --git a/src/main/java/net/frozenblock/lib/worldgen/heightmap/api/FrozenHeightmaps.java b/src/main/java/net/frozenblock/lib/worldgen/heightmap/api/FrozenHeightmaps.java new file mode 100644 index 0000000..418f51c --- /dev/null +++ b/src/main/java/net/frozenblock/lib/worldgen/heightmap/api/FrozenHeightmaps.java @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.worldgen.heightmap.api; + +import net.minecraft.world.level.levelgen.Heightmap; + +public class FrozenHeightmaps { + public static Heightmap.Types MOTION_BLOCKING_NO_LEAVES_SYNCED; + + static { + Heightmap.Types.values(); + } + +} diff --git a/src/main/java/net/frozenblock/lib/worldgen/heightmap/mixin/HeightmapTypesMixin.java b/src/main/java/net/frozenblock/lib/worldgen/heightmap/mixin/HeightmapTypesMixin.java new file mode 100644 index 0000000..c182cde --- /dev/null +++ b/src/main/java/net/frozenblock/lib/worldgen/heightmap/mixin/HeightmapTypesMixin.java @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.worldgen.heightmap.mixin; + +import net.frozenblock.lib.worldgen.heightmap.api.FrozenHeightmaps; +import net.minecraft.world.level.block.LeavesBlock; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.levelgen.Heightmap; +import org.objectweb.asm.Opcodes; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Mutable; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.gen.Invoker; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.function.Predicate; + +@Mixin(Heightmap.Types.class) +public class HeightmapTypesMixin { + + //CREDIT TO nyuppo/fabric-boat-example ON GITHUB + + @SuppressWarnings("ShadowTarget") + @Final + @Shadow + @Mutable + private static Heightmap.Types[] $VALUES; + + @SuppressWarnings("InvokerTarget") + @Invoker("") + private static Heightmap.Types frozenLib$newType(String internalName, int internalId, String serializationKey, Heightmap.Usage usage, Predicate isOpaque) { + throw new AssertionError("Mixin injection failed - FrozenLib HeightmapTypesMixin."); + } + + @Inject( + method = "", + at = @At( + value = "FIELD", + opcode = Opcodes.PUTSTATIC, + target = "Lnet/minecraft/world/level/levelgen/Heightmap$Types;$VALUES:[Lnet/minecraft/world/level/levelgen/Heightmap$Types;", + shift = At.Shift.AFTER + ) + ) + private static void frozenLib$addHeightmaps(CallbackInfo info) { + var types = new ArrayList<>(Arrays.asList($VALUES)); + var last = types.get(types.size() - 1); + int lastOrdinal = last.ordinal(); + + var motionBlockingOrFluidNoLeaves = frozenLib$newType( + "FROZENLIBMOTION_BLOCKING_NO_LEAVES_SYNCED", + lastOrdinal + 1, + "FROZENLIB_MOTION_BLOCKING_OR_NO_LEAVES_SYNCED", + Heightmap.Usage.CLIENT, + state -> (state.blocksMotion() || !state.getFluidState().isEmpty()) && !(state.getBlock() instanceof LeavesBlock) + ); + FrozenHeightmaps.MOTION_BLOCKING_NO_LEAVES_SYNCED = motionBlockingOrFluidNoLeaves; + types.add(motionBlockingOrFluidNoLeaves); + lastOrdinal += 1; + + $VALUES = types.toArray(new Heightmap.Types[0]); + } + +} diff --git a/src/main/java/net/frozenblock/lib/worldgen/structure/api/AppendSherds.java b/src/main/java/net/frozenblock/lib/worldgen/structure/api/AppendSherds.java new file mode 100644 index 0000000..ce08e19 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/worldgen/structure/api/AppendSherds.java @@ -0,0 +1,92 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.worldgen.structure.api; + +import com.mojang.serialization.Codec; +import com.mojang.serialization.MapCodec; +import com.mojang.serialization.codecs.RecordCodecBuilder; +import net.frozenblock.lib.worldgen.structure.impl.FrozenRuleBlockEntityModifiers; +import net.minecraft.Util; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.util.RandomSource; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.Items; +import net.minecraft.world.level.block.entity.PotDecorations; +import net.minecraft.world.level.levelgen.structure.templatesystem.rule.blockentity.RuleBlockEntityModifier; +import net.minecraft.world.level.levelgen.structure.templatesystem.rule.blockentity.RuleBlockEntityModifierType; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.List; + +public class AppendSherds implements RuleBlockEntityModifier { + public static final MapCodec CODEC = RecordCodecBuilder.mapCodec( + instance -> instance.group( + BuiltInRegistries.ITEM.byNameCodec().listOf().fieldOf("sherds").forGetter(modifier -> modifier.sherds), + Codec.FLOAT.fieldOf("chance_per_slot").orElse(0.75F).forGetter(modifier -> modifier.chancePerSlot), + Codec.BOOL.fieldOf("default_to_brick").orElse(true).forGetter(modifier -> modifier.defaultToBrick) + ).apply(instance, AppendSherds::new) + ); + private final List sherds; + private final float chancePerSlot; + private final boolean defaultToBrick; + + public AppendSherds(float chancePerSlot, boolean defaultToBrick, Item... sherd) { + this(List.of(sherd), chancePerSlot, defaultToBrick); + } + + public AppendSherds(List sherds, float chancePerSlot, boolean defaultToBrick) { + this.sherds = sherds; + this.chancePerSlot = chancePerSlot; + this.defaultToBrick = defaultToBrick; + if (this.sherds.isEmpty()) { + throw new IllegalArgumentException("AppendSherds requires at least one sherd!"); + } + } + + @Override + public CompoundTag apply(@NotNull RandomSource random, @Nullable CompoundTag nbt) { + CompoundTag compoundTag = nbt == null ? new CompoundTag() : nbt.copy(); + Item[] chosenSherds = new Item[4]; + List orderedDecorations = PotDecorations.load(nbt).ordered(); + for (int i = 0; i < chosenSherds.length; i++) { + if (random.nextFloat() <= this.chancePerSlot) { + chosenSherds[i] = this.getRandomSherd(random); + } else { + chosenSherds[i] = this.defaultToBrick ? Items.BRICK : orderedDecorations.get(i); + } + } + PotDecorations processedDecorations = new PotDecorations( + chosenSherds[0], + chosenSherds[1], + chosenSherds[2], + chosenSherds[3] + ); + return processedDecorations.save(compoundTag); + } + + public Item getRandomSherd(@NotNull RandomSource random) { + return Util.getRandom(this.sherds, random); + } + + @Override + public @NotNull RuleBlockEntityModifierType getType() { + return FrozenRuleBlockEntityModifiers.APPEND_SHERDS; + } +} diff --git a/src/main/java/net/frozenblock/lib/worldgen/structure/api/BlockStateRespectingProcessorRule.java b/src/main/java/net/frozenblock/lib/worldgen/structure/api/BlockStateRespectingProcessorRule.java new file mode 100644 index 0000000..c2e7be3 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/worldgen/structure/api/BlockStateRespectingProcessorRule.java @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.worldgen.structure.api; + +import com.mojang.serialization.Codec; +import com.mojang.serialization.codecs.RecordCodecBuilder; +import net.minecraft.core.BlockPos; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.util.RandomSource; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.levelgen.structure.templatesystem.PosAlwaysTrueTest; +import net.minecraft.world.level.levelgen.structure.templatesystem.PosRuleTest; +import net.minecraft.world.level.levelgen.structure.templatesystem.RuleTest; +import net.minecraft.world.level.levelgen.structure.templatesystem.rule.blockentity.Passthrough; +import net.minecraft.world.level.levelgen.structure.templatesystem.rule.blockentity.RuleBlockEntityModifier; +import org.jetbrains.annotations.Nullable; + +public class BlockStateRespectingProcessorRule { + public static final Passthrough DEFAULT_BLOCK_ENTITY_MODIFIER = Passthrough.INSTANCE; + public static final Codec CODEC = RecordCodecBuilder.create( + instance -> instance.group( + RuleTest.CODEC.fieldOf("input_predicate").forGetter(rule -> rule.inputPredicate), + RuleTest.CODEC.fieldOf("location_predicate").forGetter(rule -> rule.locPredicate), + PosRuleTest.CODEC.lenientOptionalFieldOf("position_predicate", PosAlwaysTrueTest.INSTANCE).forGetter(rule -> rule.posPredicate), + BuiltInRegistries.BLOCK.byNameCodec().fieldOf("output_block").forGetter(rule -> rule.outputBlock), + RuleBlockEntityModifier.CODEC.lenientOptionalFieldOf("block_entity_modifier", DEFAULT_BLOCK_ENTITY_MODIFIER).forGetter(rule -> rule.blockEntityModifier) + ) + .apply(instance, BlockStateRespectingProcessorRule::new) + ); + private final RuleTest inputPredicate; + private final RuleTest locPredicate; + private final PosRuleTest posPredicate; + private final Block outputBlock; + private final RuleBlockEntityModifier blockEntityModifier; + + public BlockStateRespectingProcessorRule(RuleTest inputPredicate, RuleTest locationPredicate, Block outputBlock) { + this(inputPredicate, locationPredicate, PosAlwaysTrueTest.INSTANCE, outputBlock); + } + + public BlockStateRespectingProcessorRule(RuleTest inputPredicate, RuleTest locationPredicate, PosRuleTest positionPredicate, Block outputBlock) { + this(inputPredicate, locationPredicate, positionPredicate, outputBlock, DEFAULT_BLOCK_ENTITY_MODIFIER); + } + + public BlockStateRespectingProcessorRule( + RuleTest inputPredicate, RuleTest locationPredicate, PosRuleTest positionPredicate, Block outputBlock, RuleBlockEntityModifier ruleBlockEntityModifier + ) { + this.inputPredicate = inputPredicate; + this.locPredicate = locationPredicate; + this.posPredicate = positionPredicate; + this.outputBlock = outputBlock; + this.blockEntityModifier = ruleBlockEntityModifier; + } + + public boolean test(BlockState input, BlockState location, BlockPos localPos, BlockPos absolutePos, BlockPos pivot, RandomSource random) { + return this.inputPredicate.test(input, random) && this.locPredicate.test(location, random) && this.posPredicate.test(localPos, absolutePos, pivot, random); + } + + public BlockState getOutputState(BlockState inputState) { + return this.outputBlock.withPropertiesOf(inputState); + } + + @Nullable + public CompoundTag getOutputTag(RandomSource random, @Nullable CompoundTag nbt) { + return this.blockEntityModifier.apply(random, nbt); + } +} diff --git a/src/main/java/net/frozenblock/lib/worldgen/structure/api/BlockStateRespectingRuleProcessor.java b/src/main/java/net/frozenblock/lib/worldgen/structure/api/BlockStateRespectingRuleProcessor.java new file mode 100644 index 0000000..5a25e98 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/worldgen/structure/api/BlockStateRespectingRuleProcessor.java @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.worldgen.structure.api; + +import com.google.common.collect.ImmutableList; +import com.mojang.serialization.MapCodec; +import net.frozenblock.lib.worldgen.structure.impl.FrozenStructureProcessorTypes; +import net.minecraft.core.BlockPos; +import net.minecraft.util.Mth; +import net.minecraft.util.RandomSource; +import net.minecraft.world.level.LevelReader; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.levelgen.structure.templatesystem.StructurePlaceSettings; +import net.minecraft.world.level.levelgen.structure.templatesystem.StructureProcessor; +import net.minecraft.world.level.levelgen.structure.templatesystem.StructureProcessorType; +import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplate; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.List; + +public class BlockStateRespectingRuleProcessor extends StructureProcessor { + public static final MapCodec CODEC = BlockStateRespectingProcessorRule.CODEC.listOf() + .fieldOf("rules").xmap(BlockStateRespectingRuleProcessor::new, processor -> processor.rules); + private final ImmutableList rules; + + public BlockStateRespectingRuleProcessor(List rules) { + this.rules = ImmutableList.copyOf(rules); + } + + @Nullable + @Override + public StructureTemplate.StructureBlockInfo processBlock( + LevelReader world, + BlockPos pos, + BlockPos pivot, + StructureTemplate.StructureBlockInfo localBlockInfo, + StructureTemplate.StructureBlockInfo absoluteBlockInfo, + StructurePlaceSettings placementData + ) { + BlockPos posInfo = absoluteBlockInfo.pos(); + RandomSource randomSource = RandomSource.create(Mth.getSeed(posInfo)); + BlockState blockState = world.getBlockState(posInfo); + BlockState inputState = absoluteBlockInfo.state(); + + for (BlockStateRespectingProcessorRule processorRule : this.rules) { + if (processorRule.test(inputState, blockState, localBlockInfo.pos(), absoluteBlockInfo.pos(), pivot, randomSource)) { + return new StructureTemplate.StructureBlockInfo( + absoluteBlockInfo.pos(), processorRule.getOutputState(inputState), processorRule.getOutputTag(randomSource, absoluteBlockInfo.nbt()) + ); + } + } + + return absoluteBlockInfo; + } + + @Override + protected @NotNull StructureProcessorType getType() { + return FrozenStructureProcessorTypes.BLOCK_STATE_RESPECTING_RULE_PROCESSOR; + } +} diff --git a/src/main/java/net/frozenblock/lib/worldgen/structure/api/RandomPoolAliasApi.java b/src/main/java/net/frozenblock/lib/worldgen/structure/api/RandomPoolAliasApi.java new file mode 100644 index 0000000..47697f7 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/worldgen/structure/api/RandomPoolAliasApi.java @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.worldgen.structure.api; + +import com.google.common.collect.ImmutableList; +import com.mojang.datafixers.util.Pair; +import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; +import net.minecraft.resources.ResourceLocation; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +public class RandomPoolAliasApi { + private static final Map>> ALIAS_TO_TARGETS = new Object2ObjectOpenHashMap<>(); + + public static void addTarget(ResourceLocation alias, ResourceLocation target, int weight) { + List> list = ALIAS_TO_TARGETS.getOrDefault(alias, null); + if (list == null) { + list = new ArrayList<>(); + } + list.add(Pair.of(target, weight)); + ALIAS_TO_TARGETS.put(alias, list); + } + + public static List> getAdditionalTargets(ResourceLocation alias) { + return ALIAS_TO_TARGETS.getOrDefault(alias, ImmutableList.of()); + } + +} diff --git a/src/main/java/net/frozenblock/lib/worldgen/structure/api/StructurePoolElementIdReplacements.java b/src/main/java/net/frozenblock/lib/worldgen/structure/api/StructurePoolElementIdReplacements.java new file mode 100644 index 0000000..651d852 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/worldgen/structure/api/StructurePoolElementIdReplacements.java @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.worldgen.structure.api; + +import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; +import net.minecraft.resources.ResourceLocation; + +import java.util.Map; + +public class StructurePoolElementIdReplacements { + + @Deprecated + public static final Map RESOURCE_LOCATION_REPLACEMENTS = new Object2ObjectOpenHashMap<>(); + +} diff --git a/src/main/java/net/frozenblock/lib/worldgen/structure/api/StructureProcessorApi.java b/src/main/java/net/frozenblock/lib/worldgen/structure/api/StructureProcessorApi.java new file mode 100644 index 0000000..031363a --- /dev/null +++ b/src/main/java/net/frozenblock/lib/worldgen/structure/api/StructureProcessorApi.java @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.worldgen.structure.api; + +import com.google.common.collect.ImmutableList; +import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.level.levelgen.structure.templatesystem.StructureProcessor; +import org.jetbrains.annotations.NotNull; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +public class StructureProcessorApi { + private static final List EMPTY = ImmutableList.of(); + + private static final Map> STRUCTURE_TO_PROCESSORS = new Object2ObjectOpenHashMap<>(); + + public static void addProcessor(ResourceLocation structureId, StructureProcessor processor) { + List processorList = STRUCTURE_TO_PROCESSORS.getOrDefault(structureId, new ArrayList<>()); + processorList.add(processor); + STRUCTURE_TO_PROCESSORS.put(structureId, processorList); + } + + public static @NotNull List getAdditionalProcessors(ResourceLocation structureId) { + List locationToProcessors = STRUCTURE_TO_PROCESSORS.getOrDefault(structureId, EMPTY); + return new ArrayList<>(locationToProcessors); + } + +} diff --git a/src/main/java/net/frozenblock/lib/worldgen/structure/api/WeightedProcessorRule.java b/src/main/java/net/frozenblock/lib/worldgen/structure/api/WeightedProcessorRule.java new file mode 100644 index 0000000..03a71fc --- /dev/null +++ b/src/main/java/net/frozenblock/lib/worldgen/structure/api/WeightedProcessorRule.java @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.worldgen.structure.api; + +import com.mojang.serialization.Codec; +import com.mojang.serialization.codecs.RecordCodecBuilder; +import net.minecraft.core.BlockPos; +import net.minecraft.util.RandomSource; +import net.minecraft.util.random.WeightedEntry; +import net.minecraft.util.random.WeightedRandomList; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.levelgen.structure.templatesystem.PosAlwaysTrueTest; +import net.minecraft.world.level.levelgen.structure.templatesystem.PosRuleTest; +import net.minecraft.world.level.levelgen.structure.templatesystem.RuleTest; + +public class WeightedProcessorRule { + public static final Codec CODEC = RecordCodecBuilder.create( + instance -> instance.group( + RuleTest.CODEC.fieldOf("input_predicate").forGetter(rule -> rule.inputPredicate), + RuleTest.CODEC.fieldOf("location_predicate").forGetter(rule -> rule.locPredicate), + PosRuleTest.CODEC.lenientOptionalFieldOf("position_predicate", PosAlwaysTrueTest.INSTANCE).forGetter(rule -> rule.posPredicate), + WeightedRandomList.codec(WeightedEntry.Wrapper.codec(BlockState.CODEC)).fieldOf("output_states").forGetter(rule -> rule.outputStates) + ) + .apply(instance, WeightedProcessorRule::new) + ); + private final RuleTest inputPredicate; + private final RuleTest locPredicate; + private final PosRuleTest posPredicate; + private final WeightedRandomList> outputStates; + + public WeightedProcessorRule(RuleTest inputPredicate, RuleTest locationPredicate, WeightedRandomList> states) { + this(inputPredicate, locationPredicate, PosAlwaysTrueTest.INSTANCE, states); + } + + public WeightedProcessorRule( + RuleTest inputPredicate, RuleTest locationPredicate, PosRuleTest positionPredicate, WeightedRandomList> states + ) { + this.inputPredicate = inputPredicate; + this.locPredicate = locationPredicate; + this.posPredicate = positionPredicate; + this.outputStates = states; + } + + public boolean test(BlockState input, BlockState location, BlockPos localPos, BlockPos absolutePos, BlockPos pivot, RandomSource random) { + return this.inputPredicate.test(input, random) && this.locPredicate.test(location, random) && this.posPredicate.test(localPos, absolutePos, pivot, random); + } + + public BlockState getOutputState(RandomSource random) { + return this.outputStates.getRandom(random).orElseThrow().data(); + } +} diff --git a/src/main/java/net/frozenblock/lib/worldgen/structure/api/WeightedRuleProcessor.java b/src/main/java/net/frozenblock/lib/worldgen/structure/api/WeightedRuleProcessor.java new file mode 100644 index 0000000..8d49595 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/worldgen/structure/api/WeightedRuleProcessor.java @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.worldgen.structure.api; + +import com.google.common.collect.ImmutableList; +import com.mojang.serialization.MapCodec; +import net.frozenblock.lib.worldgen.structure.impl.FrozenStructureProcessorTypes; +import net.minecraft.core.BlockPos; +import net.minecraft.util.Mth; +import net.minecraft.util.RandomSource; +import net.minecraft.world.level.LevelReader; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.levelgen.structure.templatesystem.StructurePlaceSettings; +import net.minecraft.world.level.levelgen.structure.templatesystem.StructureProcessor; +import net.minecraft.world.level.levelgen.structure.templatesystem.StructureProcessorType; +import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplate; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.List; + +public class WeightedRuleProcessor extends StructureProcessor { + public static final MapCodec CODEC = WeightedProcessorRule.CODEC.listOf() + .fieldOf("rules").xmap(WeightedRuleProcessor::new, processor -> processor.rules); + private final ImmutableList rules; + + public WeightedRuleProcessor(List rules) { + this.rules = ImmutableList.copyOf(rules); + } + + @Nullable + @Override + public StructureTemplate.StructureBlockInfo processBlock( + LevelReader world, + BlockPos pos, + BlockPos pivot, + StructureTemplate.StructureBlockInfo localBlockInfo, + StructureTemplate.StructureBlockInfo absoluteBlockInfo, + StructurePlaceSettings placementData + ) { + BlockPos posInfo = absoluteBlockInfo.pos(); + RandomSource randomSource = RandomSource.create(Mth.getSeed(posInfo)); + BlockState blockState = world.getBlockState(posInfo); + BlockState inputState = absoluteBlockInfo.state(); + + for (WeightedProcessorRule processorRule : this.rules) { + if (processorRule.test(inputState, blockState, localBlockInfo.pos(), absoluteBlockInfo.pos(), pivot, randomSource)) { + return new StructureTemplate.StructureBlockInfo( + absoluteBlockInfo.pos(), processorRule.getOutputState(randomSource), absoluteBlockInfo.nbt() + ); + } + } + + return absoluteBlockInfo; + } + + @Override + protected @NotNull StructureProcessorType getType() { + return FrozenStructureProcessorTypes.WEIGHTED_RULE_PROCESSOR; + } +} diff --git a/src/main/java/net/frozenblock/lib/worldgen/structure/impl/FrozenRuleBlockEntityModifiers.java b/src/main/java/net/frozenblock/lib/worldgen/structure/impl/FrozenRuleBlockEntityModifiers.java new file mode 100644 index 0000000..21f7639 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/worldgen/structure/impl/FrozenRuleBlockEntityModifiers.java @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.worldgen.structure.impl; + +import com.mojang.serialization.MapCodec; +import net.frozenblock.lib.FrozenSharedConstants; +import net.frozenblock.lib.worldgen.structure.api.AppendSherds; +import net.minecraft.core.Registry; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.world.level.levelgen.structure.templatesystem.rule.blockentity.RuleBlockEntityModifier; +import net.minecraft.world.level.levelgen.structure.templatesystem.rule.blockentity.RuleBlockEntityModifierType; +import net.neoforged.neoforge.registries.RegisterEvent; +import org.jetbrains.annotations.NotNull; + +public class FrozenRuleBlockEntityModifiers { + public static final RuleBlockEntityModifierType APPEND_SHERDS = () -> AppendSherds.CODEC; + + public static void init(RegisterEvent.RegisterHelper> registry) { + register(registry, "append_sherds", APPEND_SHERDS); + } + + private static void register(RegisterEvent.RegisterHelper> registry, String name, RuleBlockEntityModifierType value) { + registry.register(FrozenSharedConstants.id(name), value); + } +} diff --git a/src/main/java/net/frozenblock/lib/worldgen/structure/impl/FrozenStructureProcessorTypes.java b/src/main/java/net/frozenblock/lib/worldgen/structure/impl/FrozenStructureProcessorTypes.java new file mode 100644 index 0000000..3b02e7f --- /dev/null +++ b/src/main/java/net/frozenblock/lib/worldgen/structure/impl/FrozenStructureProcessorTypes.java @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.worldgen.structure.impl; + +import com.mojang.serialization.MapCodec; +import net.frozenblock.lib.FrozenSharedConstants; +import net.frozenblock.lib.worldgen.structure.api.BlockStateRespectingRuleProcessor; +import net.frozenblock.lib.worldgen.structure.api.WeightedRuleProcessor; +import net.minecraft.core.Registry; +import net.minecraft.world.level.levelgen.structure.templatesystem.StructureProcessor; +import net.minecraft.world.level.levelgen.structure.templatesystem.StructureProcessorType; +import net.neoforged.neoforge.registries.RegisterEvent; + +public class FrozenStructureProcessorTypes { + public static final StructureProcessorType BLOCK_STATE_RESPECTING_RULE_PROCESSOR = () -> BlockStateRespectingRuleProcessor.CODEC; + public static final StructureProcessorType WEIGHTED_RULE_PROCESSOR = () -> WeightedRuleProcessor.CODEC; + + public static void init(RegisterEvent.RegisterHelper> registry) { + registry.register(FrozenSharedConstants.id("block_state_respecting_rule"), BLOCK_STATE_RESPECTING_RULE_PROCESSOR); + registry.register(FrozenSharedConstants.id("weighted_rule"), WEIGHTED_RULE_PROCESSOR); + } +} diff --git a/src/main/java/net/frozenblock/lib/worldgen/structure/impl/InitialPieceProcessorInjectionInterface.java b/src/main/java/net/frozenblock/lib/worldgen/structure/impl/InitialPieceProcessorInjectionInterface.java new file mode 100644 index 0000000..5bc56a2 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/worldgen/structure/impl/InitialPieceProcessorInjectionInterface.java @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.worldgen.structure.impl; + +import net.minecraft.world.level.levelgen.structure.templatesystem.StructureProcessor; + +import java.util.List; + +public interface InitialPieceProcessorInjectionInterface { + + void frozenLib$addProcessors(List processors); +} diff --git a/src/main/java/net/frozenblock/lib/worldgen/structure/impl/StructurePoolElementInterface.java b/src/main/java/net/frozenblock/lib/worldgen/structure/impl/StructurePoolElementInterface.java new file mode 100644 index 0000000..bc99121 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/worldgen/structure/impl/StructurePoolElementInterface.java @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.worldgen.structure.impl; + +import net.minecraft.world.level.levelgen.structure.templatesystem.StructureProcessor; + +import java.util.List; + +public interface StructurePoolElementInterface { + + void frozenLib$addProcessors(List processors); +} diff --git a/src/main/java/net/frozenblock/lib/worldgen/structure/impl/StructureStartInterface.java b/src/main/java/net/frozenblock/lib/worldgen/structure/impl/StructureStartInterface.java new file mode 100644 index 0000000..96ff918 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/worldgen/structure/impl/StructureStartInterface.java @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.worldgen.structure.impl; + +import net.minecraft.resources.ResourceLocation; + +public interface StructureStartInterface { + + void frozenLib$setId(ResourceLocation id); +} diff --git a/src/main/java/net/frozenblock/lib/worldgen/structure/impl/StructureTemplateInterface.java b/src/main/java/net/frozenblock/lib/worldgen/structure/impl/StructureTemplateInterface.java new file mode 100644 index 0000000..a00b53d --- /dev/null +++ b/src/main/java/net/frozenblock/lib/worldgen/structure/impl/StructureTemplateInterface.java @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.worldgen.structure.impl; + +import net.minecraft.world.level.levelgen.structure.templatesystem.StructureProcessor; + +import java.util.List; + +public interface StructureTemplateInterface { + + void frozenLib$addProcessors(List processors); +} diff --git a/src/main/java/net/frozenblock/lib/worldgen/structure/mixin/ListPoolElementMixin.java b/src/main/java/net/frozenblock/lib/worldgen/structure/mixin/ListPoolElementMixin.java new file mode 100644 index 0000000..30a791b --- /dev/null +++ b/src/main/java/net/frozenblock/lib/worldgen/structure/mixin/ListPoolElementMixin.java @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.worldgen.structure.mixin; + +import net.frozenblock.lib.worldgen.structure.impl.StructurePoolElementInterface; +import net.minecraft.world.level.levelgen.structure.pools.ListPoolElement; +import net.minecraft.world.level.levelgen.structure.pools.StructurePoolElement; +import net.minecraft.world.level.levelgen.structure.templatesystem.StructureProcessor; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; + +import java.util.List; + +@Mixin(ListPoolElement.class) +public class ListPoolElementMixin implements StructurePoolElementInterface { + + @Shadow + @Final + private List elements; + + @Override + public void frozenLib$addProcessors(List processors) { + this.elements.forEach(element -> { + if (element instanceof StructurePoolElementInterface structurePoolElementInterface) { + structurePoolElementInterface.frozenLib$addProcessors(processors); + } + }); + } +} diff --git a/src/main/java/net/frozenblock/lib/worldgen/structure/mixin/PoolElementStructurePieceMixin.java b/src/main/java/net/frozenblock/lib/worldgen/structure/mixin/PoolElementStructurePieceMixin.java new file mode 100644 index 0000000..0008ed9 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/worldgen/structure/mixin/PoolElementStructurePieceMixin.java @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.worldgen.structure.mixin; + +import net.frozenblock.lib.worldgen.structure.impl.InitialPieceProcessorInjectionInterface; +import net.frozenblock.lib.worldgen.structure.impl.StructurePoolElementInterface; +import net.minecraft.world.level.levelgen.structure.PoolElementStructurePiece; +import net.minecraft.world.level.levelgen.structure.pools.StructurePoolElement; +import net.minecraft.world.level.levelgen.structure.templatesystem.StructureProcessor; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; + +import java.util.List; + +@Mixin(PoolElementStructurePiece.class) +public class PoolElementStructurePieceMixin implements InitialPieceProcessorInjectionInterface { + + @Shadow + @Final + protected StructurePoolElement element; + + @Override + public void frozenLib$addProcessors(List processors) { + if (this.element instanceof StructurePoolElementInterface structurePoolElementInterface) { + structurePoolElementInterface.frozenLib$addProcessors(processors); + } + } +} diff --git a/src/main/java/net/frozenblock/lib/worldgen/structure/mixin/RandomMixin.java b/src/main/java/net/frozenblock/lib/worldgen/structure/mixin/RandomMixin.java new file mode 100644 index 0000000..a92dc5d --- /dev/null +++ b/src/main/java/net/frozenblock/lib/worldgen/structure/mixin/RandomMixin.java @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.worldgen.structure.mixin; + +import com.mojang.datafixers.util.Pair; +import net.frozenblock.lib.worldgen.structure.api.RandomPoolAliasApi; +import net.minecraft.core.registries.Registries; +import net.minecraft.resources.ResourceKey; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.util.random.SimpleWeightedRandomList; +import net.minecraft.util.random.WeightedEntry; +import net.minecraft.world.level.levelgen.structure.pools.StructureTemplatePool; +import net.minecraft.world.level.levelgen.structure.pools.alias.Random; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Mutable; +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; + +import java.util.List; + +@Mixin(Random.class) +public class RandomMixin { + + @Shadow + @Final + @Mutable + private SimpleWeightedRandomList> targets; + + @Inject(method = "", at = @At("TAIL")) + public void frozenLib$addRandomPoolAliasTargets(ResourceKey alias, SimpleWeightedRandomList> targets, CallbackInfo info) { + ResourceLocation aliasLocation = alias.location(); + List> additions = RandomPoolAliasApi.getAdditionalTargets(aliasLocation); + + SimpleWeightedRandomList.Builder> builder = SimpleWeightedRandomList.builder(); + + for (WeightedEntry.Wrapper> wrapper : this.targets.unwrap()) { + builder.add(wrapper.data(), wrapper.weight().asInt()); + } + + for (Pair additionalTargets : additions) { + builder.add(ResourceKey.create(Registries.TEMPLATE_POOL, additionalTargets.getFirst()), additionalTargets.getSecond()); + } + + this.targets = builder.build(); + } + +} diff --git a/src/main/java/net/frozenblock/lib/worldgen/structure/mixin/SinglePoolElementMixin.java b/src/main/java/net/frozenblock/lib/worldgen/structure/mixin/SinglePoolElementMixin.java new file mode 100644 index 0000000..88d7fd7 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/worldgen/structure/mixin/SinglePoolElementMixin.java @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.worldgen.structure.mixin; + +import com.llamalad7.mixinextras.injector.ModifyExpressionValue; +import net.frozenblock.lib.worldgen.structure.impl.StructurePoolElementInterface; +import net.frozenblock.lib.worldgen.structure.impl.StructureTemplateInterface; +import net.minecraft.world.level.levelgen.structure.pools.SinglePoolElement; +import net.minecraft.world.level.levelgen.structure.templatesystem.StructureProcessor; +import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplate; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; + +import java.util.ArrayList; +import java.util.List; + +@Mixin(SinglePoolElement.class) +public class SinglePoolElementMixin implements StructurePoolElementInterface { + + @Unique + private final List frozenLib$additionalProcessors = new ArrayList<>(); + + @Override + public void frozenLib$addProcessors(List processors) { + this.frozenLib$additionalProcessors.addAll(processors); + } + + @ModifyExpressionValue( + method = "place", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/world/level/levelgen/structure/pools/SinglePoolElement;getTemplate(Lnet/minecraft/world/level/levelgen/structure/templatesystem/StructureTemplateManager;)Lnet/minecraft/world/level/levelgen/structure/templatesystem/StructureTemplate;" + ) + ) + public StructureTemplate frozenLib$place(StructureTemplate original) { + if (original instanceof StructureTemplateInterface structureTemplateInterface) { + structureTemplateInterface.frozenLib$addProcessors(this.frozenLib$additionalProcessors); + } + return original; + } + +} diff --git a/src/main/java/net/frozenblock/lib/worldgen/structure/mixin/StructureMixin.java b/src/main/java/net/frozenblock/lib/worldgen/structure/mixin/StructureMixin.java new file mode 100644 index 0000000..fd959dd --- /dev/null +++ b/src/main/java/net/frozenblock/lib/worldgen/structure/mixin/StructureMixin.java @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.worldgen.structure.mixin; + +import com.llamalad7.mixinextras.injector.ModifyExpressionValue; +import net.frozenblock.lib.worldgen.structure.impl.StructureStartInterface; +import net.minecraft.core.RegistryAccess; +import net.minecraft.core.registries.Registries; +import net.minecraft.world.level.levelgen.structure.Structure; +import net.minecraft.world.level.levelgen.structure.StructureStart; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; + +@Mixin(Structure.class) +public class StructureMixin { + + @ModifyExpressionValue( + method = "generate", + at = @At( + value = "NEW", + target = "(Lnet/minecraft/world/level/levelgen/structure/Structure;Lnet/minecraft/world/level/ChunkPos;ILnet/minecraft/world/level/levelgen/structure/pieces/PiecesContainer;)Lnet/minecraft/world/level/levelgen/structure/StructureStart;" + ) + ) + public StructureStart frozenLib$generate( + StructureStart original, + RegistryAccess registryManager + ) { + StructureStartInterface.class.cast(original).frozenLib$setId(registryManager.registryOrThrow(Registries.STRUCTURE).getKey(Structure.class.cast(this))); + return original; + } + +} diff --git a/src/main/java/net/frozenblock/lib/worldgen/structure/mixin/StructureStartMixin.java b/src/main/java/net/frozenblock/lib/worldgen/structure/mixin/StructureStartMixin.java new file mode 100644 index 0000000..64ee2ff --- /dev/null +++ b/src/main/java/net/frozenblock/lib/worldgen/structure/mixin/StructureStartMixin.java @@ -0,0 +1,129 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.worldgen.structure.mixin; + +import com.llamalad7.mixinextras.injector.ModifyExpressionValue; +import com.llamalad7.mixinextras.injector.wrapoperation.Operation; +import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; +import com.llamalad7.mixinextras.sugar.Share; +import com.llamalad7.mixinextras.sugar.ref.LocalRef; +import net.frozenblock.lib.worldgen.structure.api.StructureProcessorApi; +import net.frozenblock.lib.worldgen.structure.impl.InitialPieceProcessorInjectionInterface; +import net.frozenblock.lib.worldgen.structure.impl.StructureStartInterface; +import net.minecraft.core.BlockPos; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.util.RandomSource; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.StructureManager; +import net.minecraft.world.level.WorldGenLevel; +import net.minecraft.world.level.chunk.ChunkGenerator; +import net.minecraft.world.level.levelgen.structure.BoundingBox; +import net.minecraft.world.level.levelgen.structure.StructurePiece; +import net.minecraft.world.level.levelgen.structure.StructureStart; +import net.minecraft.world.level.levelgen.structure.templatesystem.StructureProcessor; +import org.jetbrains.annotations.Nullable; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +import java.util.List; + +@Mixin(StructureStart.class) +public class StructureStartMixin implements StructureStartInterface { + + @Unique + @Nullable + private ResourceLocation frozenLib$id; + + @ModifyExpressionValue( + method = "loadStaticStart", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/resources/ResourceLocation;parse(Ljava/lang/String;)Lnet/minecraft/resources/ResourceLocation;" + ) + ) + private static ResourceLocation frozenLib$loadStaticStartA( + ResourceLocation original, + @Share("frozenLib$resourceLocation") LocalRef resourceLocationRef + ) { + resourceLocationRef.set(original); + return original; + } + + @ModifyExpressionValue( + method = "loadStaticStart", + at = @At( + value = "NEW", + target = "(Lnet/minecraft/world/level/levelgen/structure/Structure;Lnet/minecraft/world/level/ChunkPos;ILnet/minecraft/world/level/levelgen/structure/pieces/PiecesContainer;)Lnet/minecraft/world/level/levelgen/structure/StructureStart;" + ) + ) + private static StructureStart frozenLib$loadStaticStartB( + StructureStart structureStart, @Share("frozenLib$resourceLocation") LocalRef resourceLocationRef + ) { + StructureStartInterface.class.cast(structureStart).frozenLib$setId(resourceLocationRef.get()); + return structureStart; + } + + @Inject( + method = "placeInChunk", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/world/level/levelgen/structure/BoundingBox;getCenter()Lnet/minecraft/core/BlockPos;" + ) + ) + public void frozenLib$placeInChunkA( + WorldGenLevel world, StructureManager structureManager, ChunkGenerator chunkGenerator, RandomSource random, BoundingBox boundingBox, ChunkPos chunkPos, CallbackInfo info, + @Share("frozenLib$additionalProcessors") LocalRef> additionalProcessors + ) { + additionalProcessors.set( + StructureProcessorApi.getAdditionalProcessors(this.frozenLib$id) + ); + } + + @WrapOperation( + method = "placeInChunk", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/world/level/levelgen/structure/StructurePiece;postProcess(Lnet/minecraft/world/level/WorldGenLevel;Lnet/minecraft/world/level/StructureManager;Lnet/minecraft/world/level/chunk/ChunkGenerator;Lnet/minecraft/util/RandomSource;Lnet/minecraft/world/level/levelgen/structure/BoundingBox;Lnet/minecraft/world/level/ChunkPos;Lnet/minecraft/core/BlockPos;)V" + ) + ) + public void frozenLib$placeInChunkB( + StructurePiece instance, + WorldGenLevel worldGenLevel, + StructureManager structureManager, + ChunkGenerator chunkGenerator, + RandomSource randomSource, + BoundingBox boundingBox, + ChunkPos chunkPos, + BlockPos blockPos, + Operation original, + @Share("frozenLib$additionalProcessors") LocalRef> additionalProcessors + ) { + if (instance instanceof InitialPieceProcessorInjectionInterface initialPieceProcessorInjectionInterface) { + initialPieceProcessorInjectionInterface.frozenLib$addProcessors(additionalProcessors.get()); + } + original.call(instance, worldGenLevel, structureManager, chunkGenerator, randomSource, boundingBox, chunkPos, blockPos); + } + + @Override + public void frozenLib$setId(ResourceLocation id) { + this.frozenLib$id = id; + } +} diff --git a/src/main/java/net/frozenblock/lib/worldgen/structure/mixin/StructureTemplateMixin.java b/src/main/java/net/frozenblock/lib/worldgen/structure/mixin/StructureTemplateMixin.java new file mode 100644 index 0000000..402f5e9 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/worldgen/structure/mixin/StructureTemplateMixin.java @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.worldgen.structure.mixin; + +import net.frozenblock.lib.worldgen.structure.impl.StructureTemplateInterface; +import net.minecraft.core.BlockPos; +import net.minecraft.util.RandomSource; +import net.minecraft.world.level.ServerLevelAccessor; +import net.minecraft.world.level.levelgen.structure.templatesystem.StructurePlaceSettings; +import net.minecraft.world.level.levelgen.structure.templatesystem.StructureProcessor; +import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplate; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +import java.util.ArrayList; +import java.util.List; + +@Mixin(StructureTemplate.class) +public class StructureTemplateMixin implements StructureTemplateInterface { + + @Unique + private final List frozenLib$additionalProcessors = new ArrayList<>(); + + @Inject( + method = "placeInWorld", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/world/level/levelgen/structure/templatesystem/StructurePlaceSettings;getBoundingBox()Lnet/minecraft/world/level/levelgen/structure/BoundingBox;", + shift = At.Shift.AFTER + ) + ) + public void frozenLib$placeInWorld( + ServerLevelAccessor serverLevel, BlockPos offset, BlockPos pos, StructurePlaceSettings settings, RandomSource random, int flags, + CallbackInfoReturnable info + ) { + this.frozenLib$additionalProcessors.forEach(settings::addProcessor); + this.frozenLib$additionalProcessors.clear(); + } + + @Override + public void frozenLib$addProcessors(List processors) { + this.frozenLib$additionalProcessors.addAll(processors); + } +} diff --git a/src/main/java/net/frozenblock/lib/worldgen/structure/mixin/TemplateStructurePieceMixin.java b/src/main/java/net/frozenblock/lib/worldgen/structure/mixin/TemplateStructurePieceMixin.java new file mode 100644 index 0000000..9b756ab --- /dev/null +++ b/src/main/java/net/frozenblock/lib/worldgen/structure/mixin/TemplateStructurePieceMixin.java @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.worldgen.structure.mixin; + +import net.frozenblock.lib.worldgen.structure.impl.InitialPieceProcessorInjectionInterface; +import net.minecraft.world.level.levelgen.structure.TemplateStructurePiece; +import net.minecraft.world.level.levelgen.structure.templatesystem.StructurePlaceSettings; +import net.minecraft.world.level.levelgen.structure.templatesystem.StructureProcessor; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; + +import java.util.List; + +@Mixin(TemplateStructurePiece.class) +public abstract class TemplateStructurePieceMixin implements InitialPieceProcessorInjectionInterface { + + @Shadow + protected StructurePlaceSettings placeSettings; + + @Override + public void frozenLib$addProcessors(List processors) { + processors.forEach(structureProcessor -> this.placeSettings.addProcessor(structureProcessor)); + } +} diff --git a/src/main/java/net/frozenblock/lib/worldgen/surface/api/FrozenDimensionBoundRuleSource.java b/src/main/java/net/frozenblock/lib/worldgen/surface/api/FrozenDimensionBoundRuleSource.java new file mode 100644 index 0000000..6c3c3ec --- /dev/null +++ b/src/main/java/net/frozenblock/lib/worldgen/surface/api/FrozenDimensionBoundRuleSource.java @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.worldgen.surface.api; + +import com.mojang.serialization.Codec; +import com.mojang.serialization.codecs.RecordCodecBuilder; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.level.levelgen.SurfaceRules; + +/** + * Holds both a {@link ResourceLocation} and {@link SurfaceRules.RuleSource}. + * The ResourceLocation denotes the dimension to be modified, and the RuleSource are the rules to be applied to it. + */ +public record FrozenDimensionBoundRuleSource(ResourceLocation dimension, SurfaceRules.RuleSource ruleSource) { + + public static final Codec CODEC = RecordCodecBuilder.create(instance -> + instance.group( + ResourceLocation.CODEC.fieldOf("dimension").forGetter(FrozenDimensionBoundRuleSource::dimension), + SurfaceRules.RuleSource.CODEC.fieldOf("rule_source").forGetter(FrozenDimensionBoundRuleSource::ruleSource) + ).apply(instance, FrozenDimensionBoundRuleSource::new) + ); +} diff --git a/src/main/java/net/frozenblock/lib/worldgen/surface/api/FrozenSurfaceRules.java b/src/main/java/net/frozenblock/lib/worldgen/surface/api/FrozenSurfaceRules.java new file mode 100644 index 0000000..cd13be5 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/worldgen/surface/api/FrozenSurfaceRules.java @@ -0,0 +1,202 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.worldgen.surface.api; + +import net.frozenblock.lib.worldgen.surface.impl.BiomeTagConditionSource; +import net.frozenblock.lib.worldgen.surface.impl.OptimizedBiomeTagConditionSource; +import net.minecraft.resources.ResourceKey; +import net.minecraft.tags.TagKey; +import net.minecraft.world.level.biome.Biome; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.dimension.BuiltinDimensionTypes; +import net.minecraft.world.level.dimension.DimensionType; +import net.minecraft.world.level.levelgen.SurfaceRules; +import net.neoforged.neoforge.common.NeoForge; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; + +public final class FrozenSurfaceRules { + + public static final SurfaceRules.RuleSource AIR = makeStateRule(Blocks.AIR); + public static final SurfaceRules.RuleSource BEDROCK = makeStateRule(Blocks.BEDROCK); + public static final SurfaceRules.RuleSource WHITE_TERRACOTTA = makeStateRule(Blocks.WHITE_TERRACOTTA); + public static final SurfaceRules.RuleSource ORANGE_TERRACOTTA = makeStateRule(Blocks.ORANGE_TERRACOTTA); + public static final SurfaceRules.RuleSource TERRACOTTA = makeStateRule(Blocks.TERRACOTTA); + public static final SurfaceRules.RuleSource RED_SAND = makeStateRule(Blocks.RED_SAND); + public static final SurfaceRules.RuleSource RED_SANDSTONE = makeStateRule(Blocks.RED_SANDSTONE); + public static final SurfaceRules.RuleSource STONE = makeStateRule(Blocks.STONE); + public static final SurfaceRules.RuleSource DEEPSLATE = makeStateRule(Blocks.DEEPSLATE); + public static final SurfaceRules.RuleSource DIRT = makeStateRule(Blocks.DIRT); + public static final SurfaceRules.RuleSource PODZOL = makeStateRule(Blocks.PODZOL); + public static final SurfaceRules.RuleSource COARSE_DIRT = makeStateRule(Blocks.COARSE_DIRT); + public static final SurfaceRules.RuleSource MYCELIUM = makeStateRule(Blocks.MYCELIUM); + public static final SurfaceRules.RuleSource GRASS_BLOCK = makeStateRule(Blocks.GRASS_BLOCK); + public static final SurfaceRules.RuleSource CALCITE = makeStateRule(Blocks.CALCITE); + public static final SurfaceRules.RuleSource GRAVEL = makeStateRule(Blocks.GRAVEL); + public static final SurfaceRules.RuleSource SAND = makeStateRule(Blocks.SAND); + public static final SurfaceRules.RuleSource SANDSTONE = makeStateRule(Blocks.SANDSTONE); + public static final SurfaceRules.RuleSource PACKED_ICE = makeStateRule(Blocks.PACKED_ICE); + public static final SurfaceRules.RuleSource SNOW_BLOCK = makeStateRule(Blocks.SNOW_BLOCK); + public static final SurfaceRules.RuleSource MUD = makeStateRule(Blocks.MUD); + public static final SurfaceRules.RuleSource POWDER_SNOW = makeStateRule(Blocks.POWDER_SNOW); + public static final SurfaceRules.RuleSource ICE = makeStateRule(Blocks.ICE); + public static final SurfaceRules.RuleSource WATER = makeStateRule(Blocks.WATER); + public static final SurfaceRules.RuleSource LAVA = makeStateRule(Blocks.LAVA); + public static final SurfaceRules.RuleSource NETHERRACK = makeStateRule(Blocks.NETHERRACK); + public static final SurfaceRules.RuleSource SOUL_SAND = makeStateRule(Blocks.SOUL_SAND); + public static final SurfaceRules.RuleSource SOUL_SOIL = makeStateRule(Blocks.SOUL_SOIL); + public static final SurfaceRules.RuleSource BASALT = makeStateRule(Blocks.BASALT); + public static final SurfaceRules.RuleSource BLACKSTONE = makeStateRule(Blocks.BLACKSTONE); + public static final SurfaceRules.RuleSource WARPED_WART_BLOCK = makeStateRule(Blocks.WARPED_WART_BLOCK); + public static final SurfaceRules.RuleSource WARPED_NYLIUM = makeStateRule(Blocks.WARPED_NYLIUM); + public static final SurfaceRules.RuleSource NETHER_WART_BLOCK = makeStateRule(Blocks.NETHER_WART_BLOCK); + public static final SurfaceRules.RuleSource CRIMSON_NYLIUM = makeStateRule(Blocks.CRIMSON_NYLIUM); + public static final SurfaceRules.RuleSource ENDSTONE = makeStateRule(Blocks.END_STONE); + + public static SurfaceRules.SequenceRuleSource sequence(@NotNull List list) { + return new SurfaceRules.SequenceRuleSource(list); + } + + public static SurfaceRules.ConditionSource isBiome(@NotNull List> biomes) { + return SurfaceRules.isBiome(biomes); + } + + public static SurfaceRules.ConditionSource isBiomeTag(@NotNull TagKey biomeTagKey) { + return new BiomeTagConditionSource(biomeTagKey); + } + + public static SurfaceRules.ConditionSource isBiomeTagOptimized(@NotNull TagKey biomeTagKey) { + return new OptimizedBiomeTagConditionSource(biomeTagKey); + } + + public static SurfaceRules.RuleSource makeStateRule(@NotNull Block block) { + return SurfaceRules.state(block.defaultBlockState()); + } + + @Nullable + public static SurfaceRules.RuleSource getSurfaceRules(ResourceKey dimension) { + if (dimension == null) return null; + + var location = dimension.location(); + SurfaceRules.RuleSource returnValue = null; + + if (location.equals(BuiltinDimensionTypes.OVERWORLD.location()) || location.equals(BuiltinDimensionTypes.OVERWORLD_CAVES.location())) { + returnValue = getOverworldSurfaceRules(); + } else if (location.equals(BuiltinDimensionTypes.NETHER.location())) { + returnValue = getNetherSurfaceRules(); + } else if (location.equals(BuiltinDimensionTypes.END.location())) { + returnValue = getEndSurfaceRules(); + } + + // get generic dimension surface rules + SurfaceRules.RuleSource generic = getGenericSurfaceRules(dimension); + + if (generic != null) { + if (returnValue == null) { + returnValue = generic; + } else { + returnValue = SurfaceRules.sequence(returnValue, generic); + } + } + + return returnValue; + } + + @Nullable + public static SurfaceRules.RuleSource getOverworldSurfaceRules() { + SurfaceRules.RuleSource newRule; + ArrayList sourceHolders = new ArrayList<>(); + + NeoForge.EVENT_BUS.post(new SurfaceRuleEvent.ModifyOverworld(sourceHolders)); + SurfaceRules.RuleSource newSource = sequence(sourceHolders); + + newSource = SurfaceRules.ifTrue(SurfaceRules.abovePreliminarySurface(), newSource); + newRule = newSource; + + // NO PRELIM + + ArrayList noPrelimSourceHolders = new ArrayList<>(); + NeoForge.EVENT_BUS.post(new SurfaceRuleEvent.ModifyOverworldNoPreliminarySurface(noPrelimSourceHolders)); + + + SurfaceRules.RuleSource noPrelimSource = sequence(noPrelimSourceHolders); + newRule = SurfaceRules.sequence(noPrelimSource, newRule); + + return newRule; + } + + @Nullable + public static SurfaceRules.RuleSource getNetherSurfaceRules() { + SurfaceRules.RuleSource newSource = null; + ArrayList sourceHolders = new ArrayList<>(); + + NeoForge.EVENT_BUS.post(new SurfaceRuleEvent.ModifyNether(sourceHolders)); + + for (SurfaceRules.RuleSource rule : sourceHolders) { + if (newSource == null) { + newSource = rule; + } else { + newSource = SurfaceRules.sequence(newSource, rule); + } + } + + return newSource; + } + + @Nullable + public static SurfaceRules.RuleSource getEndSurfaceRules() { + SurfaceRules.RuleSource newSource = null; + ArrayList sourceHolders = new ArrayList<>(); + + NeoForge.EVENT_BUS.post(new SurfaceRuleEvent.ModifyEnd(sourceHolders)); + + for (SurfaceRules.RuleSource rule : sourceHolders) { + if (newSource == null) { + newSource = rule; + } else { + newSource = SurfaceRules.sequence(newSource, rule); + } + } + + return newSource; + } + + @Nullable + public static SurfaceRules.RuleSource getGenericSurfaceRules(ResourceKey dimension) { + SurfaceRules.RuleSource newSource = null; + ArrayList sourceHolders = new ArrayList<>(); + + NeoForge.EVENT_BUS.post(new SurfaceRuleEvent.ModifyGeneric(sourceHolders)); + + for (FrozenDimensionBoundRuleSource dimRuleSource : sourceHolders) { + if (dimRuleSource.dimension().equals(dimension.location())) { + if (newSource == null) { + newSource = dimRuleSource.ruleSource(); + } else { + newSource = SurfaceRules.sequence(newSource, dimRuleSource.ruleSource()); + } + } + } + + return newSource; + } +} diff --git a/src/main/java/net/frozenblock/lib/worldgen/surface/api/SurfaceRuleEvent.java b/src/main/java/net/frozenblock/lib/worldgen/surface/api/SurfaceRuleEvent.java new file mode 100644 index 0000000..c25ee54 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/worldgen/surface/api/SurfaceRuleEvent.java @@ -0,0 +1,84 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.worldgen.surface.api; + +import net.minecraft.world.level.levelgen.SurfaceRules; +import net.neoforged.bus.api.Event; + +import java.util.ArrayList; +import java.util.List; + +/** + * Events that allows adding surface rules to dimensions. + *

+ * Defined with the {@code frozenlib:events} key in {@code fabric.mod.json}. + *

+ * Compatible with TerraBlender. + */ +public class SurfaceRuleEvent extends Event { + public ArrayList sourceHolders; + + private SurfaceRuleEvent(ArrayList sourceHolders) { + this.sourceHolders = sourceHolders; + } + + /** + * Lets you modify the Surface Rules of Overworld-based world presets. + */ + public static class ModifyOverworld extends SurfaceRuleEvent { + public ModifyOverworld(ArrayList sourceHolders) { + super(sourceHolders); + } + } + + /** + * Lets you modify the Surface Rules of Overworld-based world presets without checking the preliminary surface. + */ + public static class ModifyOverworldNoPreliminarySurface extends SurfaceRuleEvent { + public ModifyOverworldNoPreliminarySurface(ArrayList sourceHolders) { + super(sourceHolders); + } + } + + /** + * Lets you modify the Surface Rules of Nether-based world presets. + */ + public static class ModifyNether extends SurfaceRuleEvent { + public ModifyNether(ArrayList sourceHolders) { + super(sourceHolders); + } + } + + /** + * Lets you modify the Surface Rules of End-based world presets. + */ + public static class ModifyEnd extends SurfaceRuleEvent { + public ModifyEnd(ArrayList sourceHolders) { + super(sourceHolders); + } + } + + /** + * Lets you modify the Surface Rules of custom world presets. + */ + public static class ModifyGeneric extends SurfaceRuleEvent { + public ModifyGeneric(ArrayList sourceHolders) { + super(sourceHolders); + } + } +} diff --git a/src/main/java/net/frozenblock/lib/worldgen/surface/impl/BiomeTagConditionSource.java b/src/main/java/net/frozenblock/lib/worldgen/surface/impl/BiomeTagConditionSource.java new file mode 100644 index 0000000..2142e50 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/worldgen/surface/impl/BiomeTagConditionSource.java @@ -0,0 +1,90 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.worldgen.surface.impl; + +import com.mojang.serialization.codecs.RecordCodecBuilder; +import net.minecraft.core.registries.Registries; +import net.minecraft.tags.TagKey; +import net.minecraft.util.KeyDispatchDataCodec; +import net.minecraft.world.level.biome.Biome; +import net.minecraft.world.level.levelgen.SurfaceRules; +import org.jetbrains.annotations.NotNull; + +public final class BiomeTagConditionSource implements SurfaceRules.ConditionSource { + public static final KeyDispatchDataCodec CODEC = KeyDispatchDataCodec.of(RecordCodecBuilder.mapCodec((instance) -> + instance.group( + TagKey.codec(Registries.BIOME) + .fieldOf("biome_tag") + .forGetter(BiomeTagConditionSource::getBiomeTagKey)) + .apply(instance, BiomeTagConditionSource::new) + ) + ); + + private final TagKey biomeTagKey; + + public BiomeTagConditionSource(TagKey biomeTagKey) { + this.biomeTagKey = biomeTagKey; + } + + @Override + public KeyDispatchDataCodec codec() { + return CODEC; + } + + @Override + @NotNull + public SurfaceRules.Condition apply(@NotNull SurfaceRules.Context context) { + class BiomeTagCondition extends SurfaceRules.LazyYCondition { + BiomeTagCondition(SurfaceRules.Context context) { + super(context); + } + + protected boolean compute() { + return this.context.biome.get().is(BiomeTagConditionSource.this.biomeTagKey); + } + } + + return new BiomeTagCondition(context); + } + + @Override + public boolean equals(Object object) { + if (this == object) { + return true; + } else if (object instanceof BiomeTagConditionSource biomeConditionSource) { + return this.biomeTagKey.equals(biomeConditionSource.biomeTagKey); + } else { + return false; + } + } + + @Override + public int hashCode() { + return this.biomeTagKey.hashCode(); + } + + @Override + @NotNull + public String toString() { + return "BiomeConditionSource[biomeTagKey=" + this.biomeTagKey + "]"; + } + + private static TagKey getBiomeTagKey(@NotNull Object o) { + return ((BiomeTagConditionSource)o).biomeTagKey; + } +} diff --git a/src/main/java/net/frozenblock/lib/worldgen/surface/impl/NoiseGeneratorInterface.java b/src/main/java/net/frozenblock/lib/worldgen/surface/impl/NoiseGeneratorInterface.java new file mode 100644 index 0000000..0aceabd --- /dev/null +++ b/src/main/java/net/frozenblock/lib/worldgen/surface/impl/NoiseGeneratorInterface.java @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.worldgen.surface.impl; + +import net.minecraft.world.level.levelgen.SurfaceRules; + +public interface NoiseGeneratorInterface { + + void frozenLib$writeSurfaceRules(SurfaceRules.RuleSource surfaceRule); + +} diff --git a/src/main/java/net/frozenblock/lib/worldgen/surface/impl/OptimizedBiomeTagConditionSource.java b/src/main/java/net/frozenblock/lib/worldgen/surface/impl/OptimizedBiomeTagConditionSource.java new file mode 100644 index 0000000..358d2a0 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/worldgen/surface/impl/OptimizedBiomeTagConditionSource.java @@ -0,0 +1,134 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.worldgen.surface.impl; + +import com.mojang.serialization.codecs.RecordCodecBuilder; +import net.frozenblock.lib.FrozenLogUtils; +import net.frozenblock.lib.FrozenSharedConstants; +import net.minecraft.core.Holder; +import net.minecraft.core.Registry; +import net.minecraft.core.registries.Registries; +import net.minecraft.resources.ResourceKey; +import net.minecraft.tags.TagKey; +import net.minecraft.util.KeyDispatchDataCodec; +import net.minecraft.world.level.biome.Biome; +import net.minecraft.world.level.levelgen.SurfaceRules; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.function.Predicate; + +public final class OptimizedBiomeTagConditionSource implements SurfaceRules.ConditionSource { + public static final KeyDispatchDataCodec CODEC = KeyDispatchDataCodec.of(RecordCodecBuilder.mapCodec((instance) -> + instance.group( + TagKey.codec(Registries.BIOME) + .fieldOf("biome_tag") + .forGetter(OptimizedBiomeTagConditionSource::getBiomeTagKey)) + .apply(instance, OptimizedBiomeTagConditionSource::new) + ) + ); + + public final TagKey biomeTagKey; + @Nullable + public List> biomes; + @Nullable + public Predicate> biomeNameTest; + + public static final List INSTANCES = new ArrayList<>(); + + public static void optimizeAll(@NotNull Registry biomeRegistry) { + INSTANCES.forEach(optimizedBiomeTagConditionSource -> optimizedBiomeTagConditionSource.optimize(biomeRegistry)); + } + + public void optimize(@NotNull Registry biomeRegistry) { + this.biomes = null; + this.biomeNameTest = null; + ArrayList> biomeList = new ArrayList<>(); + + biomeRegistry.getTag(this.biomeTagKey).ifPresent((biomes -> { + for (Holder biomeHolder : biomes) { + biomeHolder.unwrapKey().ifPresent(biomeList::add); + } + this.biomes = biomeList; + })); + if (this.biomes != null) { + this.biomeNameTest = Set.copyOf(this.biomes)::contains; + FrozenLogUtils.log("OPTIMIZED A SOURCE :D", FrozenSharedConstants.UNSTABLE_LOGGING); + } else { + FrozenLogUtils.log("COULDN'T OPTIMIZE A SOURCE :(", FrozenSharedConstants.UNSTABLE_LOGGING); + } + } + + public OptimizedBiomeTagConditionSource(TagKey biomeTagKey) { + this.biomeTagKey = biomeTagKey; + INSTANCES.add(this); + } + + @Override + public KeyDispatchDataCodec codec() { + return CODEC; + } + + @Override + @NotNull + public SurfaceRules.Condition apply(@NotNull SurfaceRules.Context context) { + class BiomeTagCondition extends SurfaceRules.LazyYCondition { + BiomeTagCondition(SurfaceRules.Context context) { + super(context); + } + + protected boolean compute() { + if (OptimizedBiomeTagConditionSource.this.biomeNameTest != null) { + return this.context.biome.get().is(OptimizedBiomeTagConditionSource.this.biomeNameTest); + } + return this.context.biome.get().is(OptimizedBiomeTagConditionSource.this.biomeTagKey); + } + } + + return new BiomeTagCondition(context); + } + + @Override + public boolean equals(Object object) { + if (this == object) { + return true; + } else if (object instanceof OptimizedBiomeTagConditionSource biomeConditionSource) { + return this.biomeTagKey.equals(biomeConditionSource.biomeTagKey); + } else { + return false; + } + } + + @Override + public int hashCode() { + return this.biomeTagKey.hashCode(); + } + + @Override + @NotNull + public String toString() { + return "BiomeConditionSource[biomeTagKey=" + this.biomeTagKey + ", optimized]"; + } + + private static TagKey getBiomeTagKey(@NotNull Object o) { + return ((OptimizedBiomeTagConditionSource)o).biomeTagKey; + } +} diff --git a/src/main/java/net/frozenblock/lib/worldgen/surface/impl/SurfaceRuleUtil.java b/src/main/java/net/frozenblock/lib/worldgen/surface/impl/SurfaceRuleUtil.java new file mode 100644 index 0000000..5349796 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/worldgen/surface/impl/SurfaceRuleUtil.java @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.worldgen.surface.impl; + +import net.frozenblock.lib.worldgen.surface.api.FrozenSurfaceRules; +import net.minecraft.resources.ResourceKey; +import net.minecraft.world.level.dimension.DimensionType; +import net.minecraft.world.level.levelgen.NoiseGeneratorSettings; +import net.minecraft.world.level.levelgen.SurfaceRules; +import org.jetbrains.annotations.NotNull; + +public class SurfaceRuleUtil { + + public static void injectSurfaceRules(@NotNull NoiseGeneratorSettings settings, ResourceKey dimension) { + NoiseGeneratorInterface inter = NoiseGeneratorInterface.class.cast(settings); + SurfaceRules.RuleSource newRules = FrozenSurfaceRules.getSurfaceRules(dimension); + if (newRules != null) { + inter.frozenLib$writeSurfaceRules(newRules); + } + } +} diff --git a/src/main/java/net/frozenblock/lib/worldgen/surface/mixin/MinecraftServerMixin.java b/src/main/java/net/frozenblock/lib/worldgen/surface/mixin/MinecraftServerMixin.java new file mode 100644 index 0000000..9ef428e --- /dev/null +++ b/src/main/java/net/frozenblock/lib/worldgen/surface/mixin/MinecraftServerMixin.java @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.worldgen.surface.mixin; + +import net.frozenblock.lib.FrozenLogUtils; +import net.frozenblock.lib.FrozenSharedConstants; +import net.frozenblock.lib.worldgen.surface.impl.OptimizedBiomeTagConditionSource; +import net.frozenblock.lib.worldgen.surface.impl.SurfaceRuleUtil; +import net.minecraft.core.RegistryAccess; +import net.minecraft.core.registries.Registries; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.progress.ChunkProgressListener; +import net.minecraft.world.level.chunk.ChunkGenerator; +import net.minecraft.world.level.dimension.LevelStem; +import net.minecraft.world.level.levelgen.NoiseBasedChunkGenerator; +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(value = MinecraftServer.class, priority = 2010) // apply after bclib +public abstract class MinecraftServerMixin { + + @Shadow + public abstract RegistryAccess.Frozen registryAccess(); + + @Inject(method = "createLevels", at = @At("HEAD")) + private void frozenLib$addSurfaceRules(ChunkProgressListener worldGenerationProgressListener, CallbackInfo ci) { + var server = MinecraftServer.class.cast(this); + var registryAccess = server.registryAccess(); + var levelStems = registryAccess.registryOrThrow(Registries.LEVEL_STEM); + + OptimizedBiomeTagConditionSource.INSTANCES.clear(); + for (var entry : levelStems.entrySet()) { + LevelStem stem = entry.getValue(); + ChunkGenerator chunkGenerator = stem.generator(); + + if (chunkGenerator instanceof NoiseBasedChunkGenerator noiseGenerator) { + var noiseSettings = noiseGenerator.generatorSettings().value(); + var dimension = stem.type().unwrapKey().orElseThrow(); + + SurfaceRuleUtil.injectSurfaceRules(noiseSettings, dimension); + } + } + + OptimizedBiomeTagConditionSource.optimizeAll(this.registryAccess().registryOrThrow(Registries.BIOME)); + FrozenLogUtils.log("Optimized tag source count: " + OptimizedBiomeTagConditionSource.INSTANCES.size(), FrozenSharedConstants.UNSTABLE_LOGGING); + } + +} diff --git a/src/main/java/net/frozenblock/lib/worldgen/surface/mixin/NoiseGeneratorSettingsMixin.java b/src/main/java/net/frozenblock/lib/worldgen/surface/mixin/NoiseGeneratorSettingsMixin.java new file mode 100644 index 0000000..0ee82c5 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/worldgen/surface/mixin/NoiseGeneratorSettingsMixin.java @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.worldgen.surface.mixin; + +import com.llamalad7.mixinextras.injector.ModifyReturnValue; +import net.frozenblock.lib.worldgen.surface.impl.NoiseGeneratorInterface; +import net.minecraft.world.level.levelgen.NoiseGeneratorSettings; +import net.minecraft.world.level.levelgen.SurfaceRules; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; + +@Mixin(value = NoiseGeneratorSettings.class, priority = 990) // Apply before default mods +public class NoiseGeneratorSettingsMixin implements NoiseGeneratorInterface { + + /** + * Surface rules added by FrozenLib + */ + @Unique + private SurfaceRules.RuleSource frozenLib$frozenSurfaceRules; + + @ModifyReturnValue(method = "surfaceRule", at = @At("RETURN")) + private SurfaceRules.RuleSource frozenLib$modifyRules(SurfaceRules.RuleSource original) { + if (this.frozenLib$frozenSurfaceRules != null) { + return SurfaceRules.sequence(this.frozenLib$frozenSurfaceRules, original); + } + + return original; + } + + @Unique + @Override + public void frozenLib$writeSurfaceRules(SurfaceRules.RuleSource surfaceRule) { + if (surfaceRule == null || surfaceRule == this.frozenLib$frozenSurfaceRules) return; + + this.frozenLib$frozenSurfaceRules = surfaceRule; + } + +} diff --git a/src/main/java/net/frozenblock/lib/worldgen/surface/mixin/SurfaceRulePlugin.java b/src/main/java/net/frozenblock/lib/worldgen/surface/mixin/SurfaceRulePlugin.java new file mode 100644 index 0000000..6f1ab71 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/worldgen/surface/mixin/SurfaceRulePlugin.java @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.worldgen.surface.mixin; + +import net.frozenblock.lib.FrozenBools; +import org.jetbrains.annotations.NotNull; +import org.objectweb.asm.tree.ClassNode; +import org.spongepowered.asm.mixin.extensibility.IMixinConfigPlugin; +import org.spongepowered.asm.mixin.extensibility.IMixinInfo; + +import java.util.List; +import java.util.Set; + +public class SurfaceRulePlugin implements IMixinConfigPlugin { + @Override + public void onLoad(String mixinPackage) { + + } + + @Override + public String getRefMapperConfig() { + return null; + } + + @Override + public boolean shouldApplyMixin(String targetClassName, @NotNull String mixinClassName) { + if (mixinClassName.contains("terrablender")) + return FrozenBools.HAS_TERRABLENDER; + return true; + } + + @Override + public void acceptTargets(Set myTargets, Set otherTargets) { + + } + + @Override + public List getMixins() { + return null; + } + + @Override + public void preApply(String targetClassName, ClassNode targetClass, String mixinClassName, IMixinInfo mixinInfo) { + + } + + @Override + public void postApply(String targetClassName, ClassNode targetClass, String mixinClassName, IMixinInfo mixinInfo) { + + } +} diff --git a/src/main/java/net/frozenblock/lib/worldgen/surface/mixin/terrablender/SurfaceRuleManagerMixin.java b/src/main/java/net/frozenblock/lib/worldgen/surface/mixin/terrablender/SurfaceRuleManagerMixin.java new file mode 100644 index 0000000..54e3949 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/worldgen/surface/mixin/terrablender/SurfaceRuleManagerMixin.java @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.worldgen.surface.mixin.terrablender; + +import com.llamalad7.mixinextras.injector.ModifyReturnValue; +import net.frozenblock.lib.FrozenLogUtils; +import net.frozenblock.lib.FrozenSharedConstants; +import net.frozenblock.lib.worldgen.surface.api.FrozenSurfaceRules; +import net.minecraft.world.level.dimension.BuiltinDimensionTypes; +import net.minecraft.world.level.levelgen.SurfaceRules; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; + +@Mixin(targets = "terrablender.api.SurfaceRuleManager") +public class SurfaceRuleManagerMixin { + + /*@ModifyReturnValue(method = "getNamespacedRules", at = @At("RETURN")) + private static SurfaceRules.RuleSource frozenLib$getDefaultSurfaceRules( + SurfaceRules.RuleSource original, SurfaceRuleManager.RuleCategory category, SurfaceRules.RuleSource fallback + ) { + SurfaceRules.RuleSource newRules = FrozenSurfaceRules.getSurfaceRules( + category == SurfaceRuleManager.RuleCategory.OVERWORLD + ? BuiltinDimensionTypes.OVERWORLD + : BuiltinDimensionTypes.NETHER + ); + + if (newRules != null) { + FrozenLogUtils.log("Applying FrozenLib's surface rules to TerraBlender", FrozenSharedConstants.UNSTABLE_LOGGING); + return SurfaceRules.sequence(newRules, original, newRules); + } + return original; + }*/ +} diff --git a/src/main/java/net/frozenblock/lib/worldgen/vein/api/FrozenVeinTypes.java b/src/main/java/net/frozenblock/lib/worldgen/vein/api/FrozenVeinTypes.java new file mode 100644 index 0000000..335539b --- /dev/null +++ b/src/main/java/net/frozenblock/lib/worldgen/vein/api/FrozenVeinTypes.java @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.worldgen.vein.api; + +import net.minecraft.world.level.levelgen.OreVeinifier; + +import java.util.LinkedHashMap; +import java.util.Map; + +public class FrozenVeinTypes { + private static final Map NEW_VEIN_TYPES = new LinkedHashMap<>(); + + public static void addVeinType(String id, OreVeinifier.VeinType veinType) { + NEW_VEIN_TYPES.put(id, veinType); + } + + public static OreVeinifier.VeinType getVeinType(String modId, String name) { + return NEW_VEIN_TYPES.get(modId.toUpperCase() + name.toUpperCase()); + } +} diff --git a/src/main/java/net/frozenblock/lib/worldgen/vein/api/entrypoint/FrozenVeinTypeEntrypoint.java b/src/main/java/net/frozenblock/lib/worldgen/vein/api/entrypoint/FrozenVeinTypeEntrypoint.java new file mode 100644 index 0000000..96ae976 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/worldgen/vein/api/entrypoint/FrozenVeinTypeEntrypoint.java @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.worldgen.vein.api.entrypoint; + +import net.frozenblock.lib.worldgen.vein.impl.FrozenVeinType; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.level.block.state.BlockState; + +import java.util.ArrayList; + +public interface FrozenVeinTypeEntrypoint { + + void newCategories(ArrayList context); + + static FrozenVeinType createCategory(ResourceLocation key, BlockState ore, BlockState rawOreBlock, BlockState filler, int minY, int maxY) { + return new FrozenVeinType(key, ore, rawOreBlock, filler, minY, maxY); + } + +} + diff --git a/src/main/java/net/frozenblock/lib/worldgen/vein/impl/FrozenVeinType.java b/src/main/java/net/frozenblock/lib/worldgen/vein/impl/FrozenVeinType.java new file mode 100644 index 0000000..8b8cf16 --- /dev/null +++ b/src/main/java/net/frozenblock/lib/worldgen/vein/impl/FrozenVeinType.java @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.worldgen.vein.impl; + +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.level.block.state.BlockState; + +public record FrozenVeinType(ResourceLocation key, BlockState ore, BlockState rawOreBlock, BlockState filler, int minY, int maxY) { +} diff --git a/src/main/java/net/frozenblock/lib/worldgen/vein/mixin/VeinTypeMixin.java b/src/main/java/net/frozenblock/lib/worldgen/vein/mixin/VeinTypeMixin.java new file mode 100644 index 0000000..e90975e --- /dev/null +++ b/src/main/java/net/frozenblock/lib/worldgen/vein/mixin/VeinTypeMixin.java @@ -0,0 +1,94 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.frozenblock.lib.worldgen.vein.mixin; + +import net.frozenblock.lib.event.api.FrozenVeinEvent; +import net.frozenblock.lib.event.api.MobCategoryEvent; +import net.frozenblock.lib.worldgen.vein.api.FrozenVeinTypes; +import net.frozenblock.lib.worldgen.vein.api.entrypoint.FrozenVeinTypeEntrypoint; +import net.frozenblock.lib.worldgen.vein.impl.FrozenVeinType; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.levelgen.OreVeinifier; +import net.neoforged.neoforge.common.NeoForge; +import org.objectweb.asm.Opcodes; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Mutable; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.gen.Invoker; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +import java.util.ArrayList; +import java.util.Arrays; + +@Mixin(OreVeinifier.VeinType.class) +public class VeinTypeMixin { + + @SuppressWarnings("InvokerTarget") + @Invoker("") + private static OreVeinifier.VeinType newType(String internalName, int internalId, BlockState ore, BlockState rawOreBlock, BlockState filler, int minY, int maxY) { + throw new AssertionError("Mixin injection failed - FrozenLib VeinTypeMixin"); + } + + @SuppressWarnings("ShadowTarget") + @Shadow + @Final + @Mutable + private static OreVeinifier.VeinType[] $VALUES; + + @Inject( + method = "", + at = @At( + value = "FIELD", + opcode = Opcodes.PUTSTATIC, + target = "Lnet/minecraft/world/level/levelgen/OreVeinifier$VeinType;$VALUES:[Lnet/minecraft/world/level/levelgen/OreVeinifier$VeinType;", + shift = At.Shift.AFTER + ) + ) + private static void addCustomCategories(CallbackInfo ci) { + var categories = new ArrayList<>(Arrays.asList($VALUES)); + var last = categories.get(categories.size() - 1); + int currentOrdinal = last.ordinal(); + + ArrayList internalIds = new ArrayList<>(); + for (OreVeinifier.VeinType veinType : categories) { + internalIds.add(veinType.name()); + } + + ArrayList newVeinTypes = new ArrayList<>(); + NeoForge.EVENT_BUS.post(new FrozenVeinEvent(newVeinTypes)); + + for (FrozenVeinType frozenVeinType : newVeinTypes) { + var namespace = frozenVeinType.key().getNamespace(); + var path = frozenVeinType.key().getPath(); + StringBuilder internalId = new StringBuilder(namespace.toUpperCase()); + internalId.append(path.toUpperCase()); + if (internalIds.contains(internalId.toString())) { + throw new IllegalStateException("Cannot add duplicate VeinType " + internalId + "!"); + } + currentOrdinal += 1; + var addedVeinType = newType(internalId.toString(), currentOrdinal, frozenVeinType.ore(), frozenVeinType.rawOreBlock(), frozenVeinType.filler(), frozenVeinType.minY(), frozenVeinType.maxY()); + categories.add(addedVeinType); + FrozenVeinTypes.addVeinType(internalId.toString(), addedVeinType); + } + + $VALUES = categories.toArray(new OreVeinifier.VeinType[0]); + } +} diff --git a/src/main/java/org/quiltmc/qsl/frozenblock/core/registry/api/event/DynamicRegistryManagerSetupContext.java b/src/main/java/org/quiltmc/qsl/frozenblock/core/registry/api/event/DynamicRegistryManagerSetupContext.java new file mode 100644 index 0000000..7ec7cfc --- /dev/null +++ b/src/main/java/org/quiltmc/qsl/frozenblock/core/registry/api/event/DynamicRegistryManagerSetupContext.java @@ -0,0 +1,148 @@ +/* + * Copyright 2024 The Quilt Project + * Copyright 2024 FrozenBlock + * Modified to work on Fabric + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.quiltmc.qsl.frozenblock.core.registry.api.event; + +import it.unimi.dsi.fastutil.objects.Reference2ObjectOpenHashMap; +import net.minecraft.core.Registry; +import net.minecraft.core.RegistryAccess; +import net.minecraft.resources.ResourceKey; +import net.minecraft.resources.ResourceLocation; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.function.Consumer; +import java.util.function.Supplier; + +/** + * Represents the {@link RegistryAccess} setup context provided in the {@link RegistryEvent.DynamicRegistrySetup} event. + *

+ * Modified to work on Fabric + */ +@ApiStatus.NonExtendable +public interface DynamicRegistryManagerSetupContext { + /** + * {@return the registry access that is being currently setup} + */ + @Contract(pure = true) + @NotNull RegistryAccess registryManager(); + + /** + * Attempts to safely register a game object into the given registry. + *

+ * This method is preferred instead of {@link Registry#register(Registry, ResourceLocation, Object)} + * as it makes sure to not overwrite data-pack-provided entries, it also makes sure the registry exists. + * + * @param registryKey the key of the registry to register into + * @param id the identifier of the game object to register + * @param gameObjectSupplier the supplier of the game object to register + * @param the type of game object to register + * @return the optional game object, if the registry is present then the optional is filled, or empty otherwise + */ + default @NotNull Optional register(@NotNull ResourceKey> registryKey, @NotNull ResourceLocation id, + @NotNull Supplier gameObjectSupplier) { + return this.registryManager().registry(registryKey) + .map(registry -> registry.containsKey(id) ? registry.get(id) : Registry.register(registry, id, gameObjectSupplier.get())); + } + + /** + * Gets the registries requested by their keys. + *

+ * If one of the queried registries isn't found, then this method will return {@code null}. + * + * @param registryKeys the keys of the registries to get + * @return the registry map if all the queried registries have been found, or {@code null} otherwise + */ + @Contract(pure = true) + default @Nullable RegistryMap getRegistries(@NotNull Set>> registryKeys) { + if (registryKeys.isEmpty()) throw new IllegalArgumentException("Please provide at least one registry to gather."); + + Map>, Registry> foundRegistries = null; + + for (var key : registryKeys) { + var maybe = this.registryManager().registry(key); + + if (maybe.isPresent()) { + if (foundRegistries == null) { + foundRegistries = new Reference2ObjectOpenHashMap<>(); + } + + foundRegistries.put(key, maybe.get()); + } + } + + if (foundRegistries == null || foundRegistries.size() != registryKeys.size()) { + return null; + } + + return new RegistryMap(foundRegistries); + } + + /** + * Executes the given action if all the provided registry keys are present in the {@link RegistryAccess}. + * + * @param action the action + * @param registryKeys the registry keys to check + */ + default void withRegistries(@NotNull Consumer action, @NotNull Set>> registryKeys) { + var registries = this.getRegistries(registryKeys); + + if (registries != null) { + action.accept(registries); + } + } + + /** + * Represents a map of known registries. + * + * @param registries the map of registries + */ + record RegistryMap(Map>, Registry> registries) { + /** + * Gets the registry from its key in this map. + * + * @param registryKey the key of the registry + * @param the type of values held in the registry + * @return the registry if present, or {@code null} otherwise + */ + @Contract(pure = true) + @SuppressWarnings("unchecked") + public Registry get(ResourceKey> registryKey) { + return (Registry) this.registries.get(registryKey); + } + + /** + * Registers the given game object into the given registry. + * + * @param registryKey the key of the registry to register into + * @param id the identifier of the game object to register + * @param gameObject the game object to register + * @param the type of values held in the registry + * @return the game object + */ + public @NotNull V register(@NotNull ResourceKey> registryKey, @NotNull ResourceLocation id, + @NotNull V gameObject) { + return Registry.register(this.get(registryKey), id, gameObject); + } + } +} diff --git a/src/main/java/org/quiltmc/qsl/frozenblock/core/registry/api/event/ModProtocolEvent.java b/src/main/java/org/quiltmc/qsl/frozenblock/core/registry/api/event/ModProtocolEvent.java new file mode 100644 index 0000000..0c46cf7 --- /dev/null +++ b/src/main/java/org/quiltmc/qsl/frozenblock/core/registry/api/event/ModProtocolEvent.java @@ -0,0 +1,10 @@ +package org.quiltmc.qsl.frozenblock.core.registry.api.event; + +import net.neoforged.bus.api.Event; + +public class ModProtocolEvent extends Event { + + public static class Load extends ModProtocolEvent { + + } +} diff --git a/src/main/java/org/quiltmc/qsl/frozenblock/core/registry/api/event/RegistryEntryContext.java b/src/main/java/org/quiltmc/qsl/frozenblock/core/registry/api/event/RegistryEntryContext.java new file mode 100644 index 0000000..6e8bc9c --- /dev/null +++ b/src/main/java/org/quiltmc/qsl/frozenblock/core/registry/api/event/RegistryEntryContext.java @@ -0,0 +1,67 @@ +/* + * Copyright 2024 The Quilt Project + * Copyright 2024 FrozenBlock + * Modified to work on Fabric + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.quiltmc.qsl.frozenblock.core.registry.api.event; + +import net.minecraft.core.Registry; +import net.minecraft.resources.ResourceLocation; + +/** + * Represents information about a registry entry. + *

+ * Underlying implementations may be mutable; do not store this object in your own fields directly. + *

+ * Modified to work on Fabric + * + * @param the entry type used by the relevant {@link Registry} + */ +public interface RegistryEntryContext { + /** + * {@return the relevant registry for this entry} + */ + Registry registry(); + + /** + * {@return the entry's object} + */ + V value(); + + /** + * {@return the entry's namespaced identifier} + */ + ResourceLocation resourceLocation(); + + /** + * {@return the entry's raw int identifier} + */ + int rawId(); + + /** + * Safely registers a new entry in the registry of this context. + *

+ * Registration may be delayed when called from {@link RegistryMonitor#forAll(RegistryEvent.EntryAdd)}. + * + * @param id the identifier of the entry + * @param value the value to register + * @param the type of the value + * @return the registered value + */ + default T register(ResourceLocation id, T value) { + return Registry.register(this.registry(), id, value); + } +} diff --git a/src/main/java/org/quiltmc/qsl/frozenblock/core/registry/api/event/RegistryEvent.java b/src/main/java/org/quiltmc/qsl/frozenblock/core/registry/api/event/RegistryEvent.java new file mode 100644 index 0000000..129d466 --- /dev/null +++ b/src/main/java/org/quiltmc/qsl/frozenblock/core/registry/api/event/RegistryEvent.java @@ -0,0 +1,77 @@ +/* + * Copyright 2024 The Quilt Project + * Copyright 2024 FrozenBlock + * Modified to work on Fabric + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.quiltmc.qsl.frozenblock.core.registry.api.event; + +import lombok.experimental.UtilityClass; +import net.minecraft.core.RegistryAccess; +import net.neoforged.bus.api.Event; + +/** + * Events for listening to the manipulation of Minecraft's content registries. + *

+ * The events are to be used for very low-level purposes, and callbacks are only called on registry manipulations + * occurring after the event registration. This means that mod load order can affect what is picked up by these events. + *

+ * For more high-level monitoring of registries, including methods to ease the inconvenience of mod load order, + * use RegistryMonitor. + *

+ * Modified to work on Fabric + */ +@UtilityClass +public class RegistryEvent { + + /** + * This event gets triggered when a new {@link RegistryAccess} gets created, + * but before it gets filled. + *

+ * This event can be used to register callbacks to dynamic registries, or to pre-fill some values. + *

+ * Important Note: The passed dynamic registry manager might not + * contain the registry, as this event is invoked for each layer of + * the combined registry manager, and each layer holds different registries. + * Use {@link RegistryAccess#registry} to prevent crashes. + */ + public static class DynamicRegistrySetup extends Event { + public final DynamicRegistryManagerSetupContext context; + + public DynamicRegistrySetup(DynamicRegistryManagerSetupContext context) { + this.context = context; + } + } + + /** + * This event gets triggered when a new {@link RegistryAccess} gets created, + * after it has been filled with the registry entries specified by data packs. + *

+ * This event can be used to register callbacks to dynamic registries, or to inspect values. + *

+ * Important Note: The passed dynamic registry manager might not + * contain the registry, as this event is invoked for each layer of + * the combined registry manager, and each layer holds different registries. + * Use {@link RegistryAccess#registry} to prevent crashes. + */ + + public static class DynamicRegistryLoaded extends Event { + public final RegistryAccess registryAccess; + + public DynamicRegistryLoaded(final RegistryAccess access) { + registryAccess = access; + } + } +} diff --git a/src/main/java/org/quiltmc/qsl/frozenblock/core/registry/api/event/package-info.java b/src/main/java/org/quiltmc/qsl/frozenblock/core/registry/api/event/package-info.java new file mode 100644 index 0000000..85ddec9 --- /dev/null +++ b/src/main/java/org/quiltmc/qsl/frozenblock/core/registry/api/event/package-info.java @@ -0,0 +1,28 @@ +/* + * Copyright 2024 The Quilt Project + * Copyright 2024 FrozenBlock + * Modified to work on Fabric + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + *

The Registry-related Events.

+ *

+ * Some events to listen to registries such as additions or dynamic registries setup are provided + * by the {@linkplain org.quiltmc.qsl.frozenblock.core.registry.api.event.RegistryEvent RegistryEvents} class. + *

+ * In addition to that an API for finer {@link org.quiltmc.qsl.frozenblock.core.registry.api.event.RegistryMonitor registry monitoring} is also provided. + */ + +package org.quiltmc.qsl.frozenblock.core.registry.api.event; diff --git a/src/main/java/org/quiltmc/qsl/frozenblock/core/registry/api/sync/ModProtocol.java b/src/main/java/org/quiltmc/qsl/frozenblock/core/registry/api/sync/ModProtocol.java new file mode 100644 index 0000000..2d6d882 --- /dev/null +++ b/src/main/java/org/quiltmc/qsl/frozenblock/core/registry/api/sync/ModProtocol.java @@ -0,0 +1,161 @@ +/* + * Copyright 2024 The Quilt Project + * Copyright 2024 FrozenBlock + * Modified to work on Fabric + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.quiltmc.qsl.frozenblock.core.registry.api.sync; + +import com.mojang.logging.LogUtils; +import net.neoforged.neoforge.common.NeoForge; +import org.quiltmc.qsl.frozenblock.core.registry.api.event.ModProtocolEvent; +import org.slf4j.Logger; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class ModProtocol { + public static boolean enabled = false; + public static boolean disableQuery = false; + public static String prioritizedId = ""; + public static ModProtocolDef prioritizedEntry; + private static final Logger LOGGER = LogUtils.getLogger(); + + private static final Map PROTOCOL_VERSIONS = new HashMap<>(); + public static final List REQUIRED = new ArrayList<>(); + public static final List ALL = new ArrayList<>(); + + //@SuppressWarnings("ConstantConditions") + public static void loadVersions() { + NeoForge.EVENT_BUS.post(new ModProtocolEvent.Load()); + + /*for (IModInfo info : ModList.get().getMods()) { + ModContainer container = ModList.get(2).getModContainerById(info.getModId()).get(); + var data = info.; + var frozenRegistry = data.getCustomValue("frozenlib_registry"); + + if (frozenRegistry == null) { + continue; + } + + if (frozenRegistry.getType() != CustomValue.CvType.OBJECT) { + LOGGER.warn("Mod {} ({}) contains invalid 'frozenlib_registry' entry! Expected 'OBJECT', found '{}'", container.getMetadata().getName(), container.getMetadata().getId(), frozenRegistry.getType()); + continue; + } + + var value = frozenRegistry.getAsObject().get("mod_protocol"); + + if (value == null || value.getType() == CustomValue.CvType.NULL) { + continue; + } + + if (value.getType() == CustomValue.CvType.OBJECT) { + var object = value.getAsObject(); + + var optional = false; + var optVal = object.get("optional"); + + if (optVal != null) { + if (optVal.getType() != CustomValue.CvType.BOOLEAN) { + invalidEntryType(".optional", container, CustomValue.CvType.BOOLEAN, optVal.getType()); + continue; + } + + optional = optVal.getAsBoolean(); + } + + var version = decodeVersion(".value", container, object.get("value")); + + if (version != null) { + add(new ModProtocolDef("mod:" + data.getId(), data.getName(), version, optional)); + } + } else { + var version = decodeVersion("", container, value); + if (version != null) { + add(new ModProtocolDef("mod:" + data.getId(), data.getName(), version, false)); + } + } + }*/ + } + + /*private static IntList decodeVersion(String path, ModContainer container, CustomValue value) { + if (value == null) { + invalidEntryType(path, container, CustomValue.CvType.NUMBER, CustomValue.CvType.NULL); + return null; + } else if (value.getType() == CustomValue.CvType.NUMBER) { + var i = value.getAsNumber().intValue(); + if (i < 0) { + negativeEntry(path, container, i); + return null; + } + + return IntList.of(i); + } else if (value.getType() == CustomValue.CvType.ARRAY) { + var array = value.getAsArray(); + var versions = new IntArrayList(array.size()); + for (var i = 0; i < array.size(); i++) { + var entry = array.get(i); + if (entry.getType() == CustomValue.CvType.NUMBER) { + var version = entry.getAsNumber().intValue(); + if (version < 0) { + negativeEntry(path + "[" + i + "]", container, version); + return null; + } + + versions.add(version); + } else { + invalidEntryType(path + "[" + i + "]", container, CustomValue.CvType.NUMBER, entry.getType()); + return null; + } + } + + return versions; + } else { + invalidEntryType(path + ".optional", container, CustomValue.CvType.NUMBER, value.getType()); + return null; + } + } + + private static void invalidEntryType(String path, ModContainer c, Cu stomValue.CvType expected, CustomValue.CvType found) { + LOGGER.warn("Mod {} ({}) contains invalid 'frozenlib_registry.mod_protocol{}' entry! Expected '{}', found '{}'", path, c.getMetadata().getName(), c.getMetadata().getId(), expected.name(), found.name()); + } + + private static void negativeEntry(String path, ModContainer c, int i) { + LOGGER.warn("Mod {} ({}) contains invalid 'frozenlib_registry.mod_protocol{}' entry! Protocol requires non-negative integer, found '{}'!", path, c.getMetadata().getName(), c.getMetadata().getId(), i); + } + + public static IntList getVersion(String string) { + var x = PROTOCOL_VERSIONS.get(string); + return x == null ? IntList.of() : x.versions(); + } + + public static void add(ModProtocolDef def) { + PROTOCOL_VERSIONS.put(def.id(), def); + + if (!def.optional()) { + REQUIRED.add(def); + } + + ALL.add(def); + enabled = true; + } + + @FunctionalInterface + public interface LoadModProtocol extends CommonEventEntrypoint { + void load(); + }*/ +} diff --git a/src/main/java/org/quiltmc/qsl/frozenblock/core/registry/api/sync/ModProtocolDef.java b/src/main/java/org/quiltmc/qsl/frozenblock/core/registry/api/sync/ModProtocolDef.java new file mode 100644 index 0000000..b483a47 --- /dev/null +++ b/src/main/java/org/quiltmc/qsl/frozenblock/core/registry/api/sync/ModProtocolDef.java @@ -0,0 +1,45 @@ +/* + * Copyright 2024 The Quilt Project + * Copyright 2024 FrozenBlock + * Modified to work on Fabric + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.quiltmc.qsl.frozenblock.core.registry.api.sync; + +import it.unimi.dsi.fastutil.ints.IntCollection; +import it.unimi.dsi.fastutil.ints.IntList; +import net.minecraft.network.FriendlyByteBuf; +import org.quiltmc.qsl.frozenblock.core.registry.impl.sync.ProtocolVersions; + +public record ModProtocolDef(String id, String displayName, IntList versions, boolean optional) { + public static void write(FriendlyByteBuf buf, ModProtocolDef def) { + buf.writeUtf(def.id); + buf.writeUtf(def.displayName); + buf.writeIntIdList(def.versions); + buf.writeBoolean(def.optional); + } + + public static ModProtocolDef read(FriendlyByteBuf buf) { + var id = buf.readUtf(); + var name = buf.readUtf(); + var versions = buf.readIntIdList(); + var optional = buf.readBoolean(); + return new ModProtocolDef(id, name, versions, optional); + } + + public int latestMatchingVersion(IntCollection versions) { + return ProtocolVersions.getHighestSupported(versions, this.versions); + } +} diff --git a/src/main/java/org/quiltmc/qsl/frozenblock/core/registry/impl/DynamicRegistryManagerSetupContextImpl.java b/src/main/java/org/quiltmc/qsl/frozenblock/core/registry/impl/DynamicRegistryManagerSetupContextImpl.java new file mode 100644 index 0000000..575a7d8 --- /dev/null +++ b/src/main/java/org/quiltmc/qsl/frozenblock/core/registry/impl/DynamicRegistryManagerSetupContextImpl.java @@ -0,0 +1,70 @@ +/* + * Copyright 2024 The Quilt Project + * Copyright 2024 FrozenBlock + * Modified to work on Fabric + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.quiltmc.qsl.frozenblock.core.registry.impl; + +import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; +import net.minecraft.core.Registry; +import net.minecraft.core.RegistryAccess; +import net.minecraft.core.WritableRegistry; +import net.minecraft.resources.ResourceKey; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; +import org.quiltmc.qsl.frozenblock.core.registry.api.event.DynamicRegistryManagerSetupContext; +import org.quiltmc.qsl.frozenblock.core.registry.api.event.RegistryEvent; + +import java.util.Map; +import java.util.Optional; +import java.util.stream.Stream; + +/** + * Represents the context implementation for the {@link RegistryEvent#DYNAMIC_REGISTRY_SETUP} event. + *

+ * It is imperative that the passed registries are mutable to allow registration. + * + * @author LambdAurora + */ +@ApiStatus.Internal +public class DynamicRegistryManagerSetupContextImpl implements DynamicRegistryManagerSetupContext, RegistryAccess { + private final Map, WritableRegistry> registries; + + public DynamicRegistryManagerSetupContextImpl(Stream> registries) { + this.registries = new Object2ObjectOpenHashMap<>(); + + registries.forEach(registry -> this.registries.put(registry.key(), registry)); + } + + @Override + public @NotNull RegistryAccess registryManager() { + return this; + } + + @SuppressWarnings({"rawtypes", "unchecked"}) + @Override + @NotNull + public Optional> registry(ResourceKey> key) { + return Optional.ofNullable((Registry) this.registries.get(key)).map(registry -> registry); + } + + @SuppressWarnings({"rawtypes", "unchecked"}) + @Override + @NotNull + public Stream> registries() { + return this.registries.entrySet().stream().map(entry -> new RegistryEntry<>((ResourceKey) entry.getKey(), entry.getValue())); + } +} diff --git a/src/main/java/org/quiltmc/qsl/frozenblock/core/registry/impl/event/DelayedRegistry.java b/src/main/java/org/quiltmc/qsl/frozenblock/core/registry/impl/event/DelayedRegistry.java new file mode 100644 index 0000000..59aa6a3 --- /dev/null +++ b/src/main/java/org/quiltmc/qsl/frozenblock/core/registry/impl/event/DelayedRegistry.java @@ -0,0 +1,266 @@ +/* + * Copyright 2024 The Quilt Project + * Copyright 2024 FrozenBlock + * Modified to work on Fabric + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.quiltmc.qsl.frozenblock.core.registry.impl.event; + +import com.mojang.datafixers.util.Pair; +import com.mojang.serialization.Lifecycle; +import net.minecraft.core.*; +import net.minecraft.core.Holder.Reference; +import net.minecraft.core.HolderLookup.RegistryLookup; +import net.minecraft.core.HolderSet.Named; +import net.minecraft.resources.ResourceKey; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.tags.TagKey; +import net.minecraft.util.RandomSource; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.*; +import java.util.stream.Stream; + +/** + * Modified to work on Fabric + */ +@ApiStatus.Internal +public abstract class DelayedRegistry implements WritableRegistry { + private final WritableRegistry wrapped; + private final Queue> delayedEntries = new LinkedList<>(); + + DelayedRegistry(WritableRegistry registry) { + this.wrapped = registry; + } + + @Override + public @Nullable ResourceLocation getKey(T entry) { + return this.wrapped.getKey(entry); + } + + @Override + @NotNull + public Optional> getResourceKey(T entry) { + return this.wrapped.getResourceKey(entry); + } + + @Override + public int getId(@Nullable T entry) { + return this.wrapped.getId(entry); + } + + @Override + public @Nullable T get(@Nullable ResourceKey entry) { + return this.wrapped.get(entry); + } + + @Override + public @Nullable T get(@Nullable ResourceLocation id) { + return this.wrapped.get(id); + } + + @NotNull + @Override + public Optional registrationInfo(ResourceKey key) { + return this.wrapped.registrationInfo(key); + } + + @Override + @NotNull + public Lifecycle registryLifecycle() { + return this.wrapped.registryLifecycle(); + } + + @Override + @NotNull + public Optional> getAny() { + return this.wrapped.getAny(); + } + + @Override + @NotNull + public Set keySet() { + return this.wrapped.keySet(); + } + + @Override + @NotNull + public Set, T>> entrySet() { + return this.wrapped.entrySet(); + } + + @Override + @NotNull + public Set> registryKeySet() { + return this.wrapped.registryKeySet(); + } + + @Override + @NotNull + public Optional> getRandom(RandomSource random) { + return this.wrapped.getRandom(random); + } + + @Override + public boolean containsKey(ResourceLocation id) { + return this.wrapped.containsKey(id); + } + + @Override + public boolean containsKey(ResourceKey key) { + return this.wrapped.containsKey(key); + } + + @Override + @NotNull + public Registry freeze() { + // Refuse freezing. + return this; + } + + @Override + @NotNull + public Reference createIntrusiveHolder(T holder) { + return this.wrapped.createIntrusiveHolder(holder); + } + + @Override + @NotNull + public Optional> getHolder(int index) { + return this.wrapped.getHolder(index); + } + + @Override + @NotNull + public Optional> getHolder(ResourceLocation key) { + return this.wrapped.getHolder(key); + } + + @Override + @NotNull + public Optional> getHolder(ResourceKey key) { + return this.wrapped.getHolder(key); + } + + @Override + @NotNull + public Holder wrapAsHolder(T object) { + return this.wrapped.wrapAsHolder(object); + } + + @Override + @NotNull + public Stream> holders() { + return this.wrapped.holders(); + } + + @Override + @NotNull + public Optional> getTag(TagKey tag) { + return this.wrapped.getTag(tag); + } + + @Override + @NotNull + public Named getOrCreateTag(TagKey key) { + return this.wrapped.getOrCreateTag(key); + } + + @Override + @NotNull + public Stream, Named>> getTags() { + return this.wrapped.getTags(); + } + + @Override + @NotNull + public Stream> getTagNames() { + return this.wrapped.getTagNames(); + } + + @Override + public void resetTags() { + throw new UnsupportedOperationException("DelayedRegistry does not support resetTags."); + } + + @Override + public void bindTags(Map, List>> tags) { + throw new UnsupportedOperationException("DelayedRegistry does not support bindTags."); + } + + @Override + @NotNull + public HolderOwner holderOwner() { + return this.wrapped.holderOwner(); + } + + @Override + @NotNull + public RegistryLookup asLookup() { + return this.wrapped.asLookup(); + } + + @Override + @NotNull + public Iterator iterator() { + return this.wrapped.iterator(); + } + + @Override + public @Nullable T byId(int index) { + return this.wrapped.byId(index); + } + + @Override + @NotNull + public ResourceKey> key() { + return this.wrapped.key(); + } + + @Override + public int size() { + return this.wrapped.size(); + } + + @NotNull + @Override + public Reference register(ResourceKey key, T entry, RegistrationInfo registrationInfo) { + this.delayedEntries.add(new DelayedEntry<>(key, entry, registrationInfo)); + return Reference.createStandAlone(this.wrapped.holderOwner(), key); + } + + @Override + public boolean isEmpty() { + return this.wrapped.isEmpty(); + } + + @Override + @NotNull + public HolderGetter createRegistrationLookup() { + return this.wrapped.createRegistrationLookup(); + } + + void applyDelayed() { + DelayedEntry entry; + + while ((entry = this.delayedEntries.poll()) != null) { + this.wrapped.register(entry.key(), entry.entry(), entry.registrationInfo); + } + } + + record DelayedEntry(ResourceKey key, T entry, RegistrationInfo registrationInfo) {} +} diff --git a/src/main/java/org/quiltmc/qsl/frozenblock/core/registry/impl/event/MutableRegistryEntryContextImpl.java b/src/main/java/org/quiltmc/qsl/frozenblock/core/registry/impl/event/MutableRegistryEntryContextImpl.java new file mode 100644 index 0000000..09d90a9 --- /dev/null +++ b/src/main/java/org/quiltmc/qsl/frozenblock/core/registry/impl/event/MutableRegistryEntryContextImpl.java @@ -0,0 +1,97 @@ +/* + * Copyright 2024 The Quilt Project + * Copyright 2024 FrozenBlock + * Modified to work on Fabric + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.quiltmc.qsl.frozenblock.core.registry.impl.event; + +import net.minecraft.core.Registry; +import net.minecraft.resources.ResourceLocation; +import org.jetbrains.annotations.ApiStatus; +import org.quiltmc.qsl.frozenblock.core.registry.api.event.RegistryEntryContext; + +/** + * The default implementation for {@link RegistryEntryContext}. + *

+ * In order to minimize allocations during event invocation, especially during registry iteration, this class is + * mutable. The api interface only allows accessing fields of the class, whereas modification methods are reserved for the + * impl. + *

+ * Modified to work on Fabric + * + * @param the type of the relevant {@link Registry}'s entries + */ +@ApiStatus.Internal +public class MutableRegistryEntryContextImpl implements RegistryEntryContext { + private final Registry registry; + private V value; + private ResourceLocation resourceLocation; + private int raw = -1; + + public MutableRegistryEntryContextImpl(Registry registry) { + this.registry = registry; + } + + /** + * Changes the current entry information. + *

+ * Raw identifier is set to -1 to signify that it should be lazily looked up. + * + * @param id the namespaced identifier of the new entry + * @param entry the new entry's object + */ + public void set(ResourceLocation id, V entry) { + this.set(id, entry, -1); + } + + /** + * Changes the current entry information. + * + * @param id the namespaced identifier of the new entry + * @param entry the new entry's object + * @param rawId the raw int identifier of the new entry + */ + public void set(ResourceLocation id, V entry, int rawId) { + this.resourceLocation = id; + this.value = entry; + this.raw = rawId; + } + + @Override + public Registry registry() { + return this.registry; + } + + @Override + public V value() { + return this.value; + } + + @Override + public ResourceLocation resourceLocation() { + return this.resourceLocation; + } + + @Override + public int rawId() { + if (this.raw < 0) { + this.raw = this.registry.getId(this.value); + } + + return this.raw; + } +} + diff --git a/src/main/java/org/quiltmc/qsl/frozenblock/core/registry/impl/sync/ClientPackets.java b/src/main/java/org/quiltmc/qsl/frozenblock/core/registry/impl/sync/ClientPackets.java new file mode 100644 index 0000000..30891af --- /dev/null +++ b/src/main/java/org/quiltmc/qsl/frozenblock/core/registry/impl/sync/ClientPackets.java @@ -0,0 +1,124 @@ +/* + * Copyright 2024 The Quilt Project + * Copyright 2024 FrozenBlock + * Modified to work on Fabric + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.quiltmc.qsl.frozenblock.core.registry.impl.sync; + +import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap; +import net.frozenblock.lib.FrozenSharedConstants; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.codec.ByteBufCodecs; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; + +/** + * Identifiers of packets sent by server. + */ +@ApiStatus.Internal +public final class ClientPackets { + /** + * Response for {@link ServerPackets.Handshake#PACKET_TYPE}. Selects the registry sync version to be used from the server's supported options. + * + *


+	 * {
+	 *     Supported Version: VarInt
+	 * }
+	 * 
+ */ + public record Handshake(int version) implements CustomPacketPayload { + public static final Type PACKET_TYPE = new Type<>(FrozenSharedConstants.id("registry_sync/handshake_client")); + public static final StreamCodec CODEC = ByteBufCodecs.VAR_INT.map(Handshake::new, Handshake::version).cast(); + + @Override + @NotNull + public Type type() { + return PACKET_TYPE; + } + } + + /** + * Sent after receiving Mod Protocol request packet from server. + * Returns all latest supported by client version of requested Mod Protocols see {@link ServerPackets.ModProtocol#PACKET_TYPE} + * + *

+	 * {
+	 *   Count of Entries: VarInt
+	 *   [
+	 *     Id: String
+	 *     Highest Supported Version: VarInt
+	 *   ]
+	 * }
+	 * 
+ */ + public record ModProtocol(Object2IntOpenHashMap protocols) implements CustomPacketPayload { + public static final Type PACKET_TYPE = new Type<>(FrozenSharedConstants.id("registry_sync/mod_protocol")); + public static final StreamCodec CODEC = StreamCodec.ofMember(ModProtocol::write, ModProtocol::new); + + public ModProtocol(FriendlyByteBuf buf) { + this(read(buf)); + } + + private static @NotNull Object2IntOpenHashMap read(@NotNull FriendlyByteBuf buf) { + Object2IntOpenHashMap protocols = new Object2IntOpenHashMap<>(); + + int count = buf.readVarInt(); + + while (count-- > 0) { + protocols.put(buf.readUtf(), buf.readVarInt()); + } + + return protocols; + } + + public void write(@NotNull FriendlyByteBuf buf) { + buf.writeVarInt(this.protocols.size()); + for (var entry : this.protocols.object2IntEntrySet()) { + buf.writeUtf(entry.getKey()); + buf.writeVarInt(entry.getIntValue()); + } + } + + @Override + @NotNull + public Type type() { + return PACKET_TYPE; + } + } + + /** + * Ends registry sync. No data + */ + public record End() implements CustomPacketPayload { + public static final Type PACKET_TYPE = new Type<>(FrozenSharedConstants.id("registry_sync/end")); + public static final StreamCodec CODEC = StreamCodec.ofMember(End::write, End::new); + + public End(FriendlyByteBuf buf) { + this(); + } + + public void write(FriendlyByteBuf buf) { + } + + @Override + @NotNull + public Type type() { + return PACKET_TYPE; + } + } +} diff --git a/src/main/java/org/quiltmc/qsl/frozenblock/core/registry/impl/sync/ProtocolVersions.java b/src/main/java/org/quiltmc/qsl/frozenblock/core/registry/impl/sync/ProtocolVersions.java new file mode 100644 index 0000000..2aaf398 --- /dev/null +++ b/src/main/java/org/quiltmc/qsl/frozenblock/core/registry/impl/sync/ProtocolVersions.java @@ -0,0 +1,52 @@ +/* + * Copyright 2024 The Quilt Project + * Copyright 2024 FrozenBlock + * Modified to work on Fabric + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.quiltmc.qsl.frozenblock.core.registry.impl.sync; + +import it.unimi.dsi.fastutil.ints.IntCollection; +import it.unimi.dsi.fastutil.ints.IntList; +import it.unimi.dsi.fastutil.ints.IntSet; + +import java.util.stream.IntStream; + +public class ProtocolVersions { + public static final int CURRENT_VERSION = 1; + public static final int OLDEST_SUPPORTED_VERSION = 1; + public static final IntSet IMPL_SUPPORTED_VERSIONS = IntSet.of(IntStream.rangeClosed(OLDEST_SUPPORTED_VERSION, CURRENT_VERSION).toArray()); + + public static final int NO_PROTOCOL = -1; + public static final int FAPI_PROTOCOL = -2; + + public static int getHighestSupportedLocal(IntList supportedRemote) { + return getHighestSupported(IMPL_SUPPORTED_VERSIONS, supportedRemote); + } + + public static int getHighestSupported(IntCollection supportedLocal, IntList supportedRemote) { + int highestSupported = NO_PROTOCOL; + + for (var i = 0; i < supportedRemote.size(); i++) { + int version = supportedRemote.getInt(i); + + if (version > highestSupported && supportedLocal.contains(version)) { + highestSupported = version; + } + } + + return highestSupported; + } +} diff --git a/src/main/java/org/quiltmc/qsl/frozenblock/core/registry/impl/sync/RegistrySyncText.java b/src/main/java/org/quiltmc/qsl/frozenblock/core/registry/impl/sync/RegistrySyncText.java new file mode 100644 index 0000000..3b38eb9 --- /dev/null +++ b/src/main/java/org/quiltmc/qsl/frozenblock/core/registry/impl/sync/RegistrySyncText.java @@ -0,0 +1,120 @@ +/* + * Copyright 2024 The Quilt Project + * Copyright 2024 FrozenBlock + * Modified to work on Fabric + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.quiltmc.qsl.frozenblock.core.registry.impl.sync; + +import net.minecraft.ChatFormatting; +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.ComponentUtils; +import net.minecraft.network.chat.MutableComponent; +import org.quiltmc.qsl.frozenblock.core.registry.api.sync.ModProtocol; +import org.quiltmc.qsl.frozenblock.core.registry.api.sync.ModProtocolDef; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.function.Function; + +public class RegistrySyncText { + + private static MutableComponent entryList(List namespacesList, Function toText) { + var namespaceText = Component.empty(); + + var textLength = 0; + var lines = 0; + + while (lines < 2 && !namespacesList.isEmpty()) { + var max = lines == 0 ? 38 : 30; + while (textLength < max && !namespacesList.isEmpty()) { + var t = toText.apply(namespacesList.remove(0)); + namespaceText.append(t); + + textLength += t.getString().length(); + + if (!namespacesList.isEmpty()) { + var alt = (lines + toText.apply(namespacesList.get(0)).getString().length() < max && lines == 1); + if (namespacesList.size() == 1 || alt) { + namespaceText.append(Component.translatableWithFallback("frozenlib.core.registry_sync.and", " and ").withStyle(alt ? ChatFormatting.GRAY : ChatFormatting.DARK_GRAY)); + textLength += 6; + } else { + namespaceText.append(Component.literal(", ").withStyle(ChatFormatting.DARK_GRAY)); + textLength += 2; + } + } + } + + if (!namespacesList.isEmpty() && lines != 1) { + namespaceText.append("\n"); + } + + textLength = 0; + lines++; + } + + if (!namespacesList.isEmpty()) { + namespaceText.append(Component.translatableWithFallback("frozenlib.core.registry_sync.more", "%s more...", namespacesList.size())); + } + + return namespaceText; + } + + public static Component unsupportedModVersion(List unsupported, ModProtocolDef missingPrioritized) { + if (missingPrioritized != null && !missingPrioritized.versions().isEmpty()) { + var x = Component.translatableWithFallback("frozenlib.core.registry_sync.require_main_mod_protocol", "This server requires %s with protocol version of %s!", + Component.literal(missingPrioritized.displayName()).withStyle(ChatFormatting.YELLOW), + missingPrioritized.versions().getInt(0) + ); + + if (ModProtocol.enabled && ModProtocol.prioritizedEntry != null) { + x.append("\n").append( + Component.translatableWithFallback("frozenlib.core.registry_sync.main_mod_protocol", "You are on %s with protocol version of %s.", + Component.literal(ModProtocol.prioritizedEntry.displayName()).withStyle(ChatFormatting.GOLD), ModProtocol.prioritizedEntry.versions().getInt(0) + ) + ); + } + + return x; + } else { + System.out.println(unsupported.size()); + var namespacesList = new ArrayList<>(unsupported); + namespacesList.sort(Comparator.comparing(ModProtocolDef::displayName)); + var namespaceText = entryList(namespacesList, RegistrySyncText::protocolDefEntryText).withStyle(ChatFormatting.GRAY); + + return Component.translatableWithFallback("frozenlib.core.registry_sync.unsupported_mod_protocol", "Unsupported mod protocol versions for:\n%s", + namespaceText + ); + } + } + + private static Component protocolDefEntryText(ModProtocolDef def) { + MutableComponent version; + var x = def.versions(); + if (x.isEmpty()) { + version = Component.literal("WHAT??? HOW???"); + } else if (x.size() == 1) { + version = Component.literal("" + x.getInt(0)); + } else { + version = (MutableComponent) ComponentUtils.formatList(x.subList(0, Math.min(x.size(), 4)), n -> Component.literal(n.toString())); + if (x.size() > 4) { + version = version.append(ComponentUtils.DEFAULT_SEPARATOR).append("..."); + } + } + + return Component.translatableWithFallback("quilt.core.registry_sync.protocol_entry", "%s (%s)", def.displayName(), version); + } +} diff --git a/src/main/java/org/quiltmc/qsl/frozenblock/core/registry/impl/sync/ServerPackets.java b/src/main/java/org/quiltmc/qsl/frozenblock/core/registry/impl/sync/ServerPackets.java new file mode 100644 index 0000000..9e3c9fa --- /dev/null +++ b/src/main/java/org/quiltmc/qsl/frozenblock/core/registry/impl/sync/ServerPackets.java @@ -0,0 +1,161 @@ +/* + * Copyright 2024 The Quilt Project + * Copyright 2024 FrozenBlock + * Modified to work on Fabric + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.quiltmc.qsl.frozenblock.core.registry.impl.sync; + +import it.unimi.dsi.fastutil.ints.IntList; +import net.frozenblock.lib.FrozenSharedConstants; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.ComponentSerialization; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.resources.ResourceLocation; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; +import org.quiltmc.qsl.frozenblock.core.registry.api.sync.ModProtocolDef; + +import java.util.Collection; + +/** + * Identifiers of packets sent by server. + */ +@ApiStatus.Internal +public final class ServerPackets { + /** + * Starts registry sync. + * + *

+	 * {
+	 *   Supported Versions: IntList
+	 * }
+	 * 
+ */ + public record Handshake(IntList supportedVersions) implements CustomPacketPayload { + public static final Type PACKET_TYPE = new Type<>(ServerPackets.id("registry_sync/handshake")); + public static final StreamCodec CODEC = StreamCodec.ofMember(Handshake::write, Handshake::new); + + public Handshake(@NotNull FriendlyByteBuf buf) { + this(buf.readIntIdList()); + } + + public void write(@NotNull FriendlyByteBuf buf) { + buf.writeIntIdList(this.supportedVersions); + } + + @Override + public Type type() { + return PACKET_TYPE; + } + } + + /** + * Ends registry sync. No data + */ + public record End() implements CustomPacketPayload { + public static final Type PACKET_TYPE = new Type<>(ServerPackets.id("registry_sync/end")); + public static final StreamCodec CODEC = StreamCodec.ofMember(End::write, End::new); + + public End(FriendlyByteBuf buf) { + this(); + } + + public void write(FriendlyByteBuf buf) { + } + + @Override + @NotNull + public Type type() { + return PACKET_TYPE; + } + } + + /** + * This packet sets failure text look/properties. + * Requires protocol version 3 or newer. + * + *

+	 * {
+	 *   Text Header: Text (String)
+	 *   Text Footer: Text (String)
+	 *   Show Details: bool
+	 *
+	 * }
+	 * 
+ */ + public record ErrorStyle(Component errorHeader, Component errorFooter, boolean showError) implements CustomPacketPayload { + public static final Type PACKET_TYPE = new Type<>(ServerPackets.id("registry_sync/error_style")); + public static final StreamCodec CODEC = StreamCodec.ofMember(ErrorStyle::write, ErrorStyle::new); + + public ErrorStyle(@NotNull FriendlyByteBuf buf) { + this(ComponentSerialization.TRUSTED_CONTEXT_FREE_STREAM_CODEC.decode(buf), ComponentSerialization.TRUSTED_CONTEXT_FREE_STREAM_CODEC.decode(buf), buf.readBoolean()); + } + + public void write(@NotNull FriendlyByteBuf buf) { + ComponentSerialization.TRUSTED_CONTEXT_FREE_STREAM_CODEC.encode(buf, this.errorHeader); + ComponentSerialization.TRUSTED_CONTEXT_FREE_STREAM_CODEC.encode(buf, this.errorFooter); + buf.writeBoolean(this.showError); + } + + @Override + @NotNull + public Type type() { + return PACKET_TYPE; + } + } + + /** + * This packet requests client to validate and return supported Mod Protocol versions. + * + *

+	 * {
+	 *   Prioritized Id: String
+	 *   Count of Entries: VarInt
+	 *   [
+	 *     Id: String
+	 *     Name: String
+	 *     Supported Versions: IntList
+	 *     Optional: boolean
+	 *   ]
+	 * }
+	 * 
+ */ + public record ModProtocol(String prioritizedId, Collection protocols) implements CustomPacketPayload { + public static final Type PACKET_TYPE = new Type<>(ServerPackets.id("registry_sync/mod_protocol")); + public static final StreamCodec CODEC = StreamCodec.ofMember(ModProtocol::write, ModProtocol::new); + + public ModProtocol(@NotNull FriendlyByteBuf buf) { + this(buf.readUtf(), buf.readList(ModProtocolDef::read)); + } + + public void write(@NotNull FriendlyByteBuf buf) { + buf.writeUtf(this.prioritizedId); + buf.writeCollection(this.protocols, ModProtocolDef::write); + } + + @Override + @NotNull + public Type type() { + return PACKET_TYPE; + } + } + + private static ResourceLocation id(String path) { + return FrozenSharedConstants.id(path); + } +} diff --git a/src/main/java/org/quiltmc/qsl/frozenblock/core/registry/impl/sync/client/ClientRegistrySync.java b/src/main/java/org/quiltmc/qsl/frozenblock/core/registry/impl/sync/client/ClientRegistrySync.java new file mode 100644 index 0000000..6535b08 --- /dev/null +++ b/src/main/java/org/quiltmc/qsl/frozenblock/core/registry/impl/sync/client/ClientRegistrySync.java @@ -0,0 +1,191 @@ +/* + * Copyright 2024 The Quilt Project + * Copyright 2024 FrozenBlock + * Modified to work on Fabric + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.quiltmc.qsl.frozenblock.core.registry.impl.sync.client; + +import com.mojang.logging.LogUtils; +import it.unimi.dsi.fastutil.ints.IntList; +import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap; +import net.minecraft.ChatFormatting; +import net.minecraft.client.Minecraft; +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.contents.PlainTextContents; +import net.neoforged.api.distmarker.Dist; +import net.neoforged.api.distmarker.OnlyIn; +import org.jetbrains.annotations.ApiStatus; +import org.quiltmc.qsl.frozenblock.core.registry.api.sync.ModProtocol; +import org.quiltmc.qsl.frozenblock.core.registry.api.sync.ModProtocolDef; +import org.quiltmc.qsl.frozenblock.core.registry.impl.sync.ClientPackets; +import org.quiltmc.qsl.frozenblock.core.registry.impl.sync.ProtocolVersions; +import org.quiltmc.qsl.frozenblock.core.registry.impl.sync.RegistrySyncText; +import org.quiltmc.qsl.frozenblock.core.registry.impl.sync.ServerPackets; +import org.quiltmc.qsl.frozenblock.core.registry.impl.sync.server.ServerRegistrySync; +import org.slf4j.Logger; + +import java.util.ArrayList; + + +@ApiStatus.Internal +@OnlyIn(Dist.CLIENT) +public final class ClientRegistrySync { + private static final Logger LOGGER = LogUtils.getLogger(); + + @SuppressWarnings({"FieldCanBeLocal", "unused"}) + private static int syncVersion = -1; + @SuppressWarnings("unused") + private static int currentCount; + @SuppressWarnings("unused") + private static byte currentFlags; + + private static Component errorStyleHeader = ServerRegistrySync.errorStyleHeader; + private static Component errorStyleFooter = ServerRegistrySync.errorStyleFooter; + private static boolean showErrorDetails = ServerRegistrySync.showErrorDetails; + + private static Component disconnectMainReason = null; + + private static LogBuilder builder = new LogBuilder(); + private static boolean mustDisconnect; + + public static void registerHandlers() { + /* + ClientConfigurationNetworking.registerGlobalReceiver(ServerPackets.Handshake.PACKET_TYPE, ClientRegistrySync::handleHelloPacket); + ClientConfigurationNetworking.registerGlobalReceiver(ServerPackets.End.PACKET_TYPE, ClientRegistrySync::handleEndPacket); + ClientConfigurationNetworking.registerGlobalReceiver(ServerPackets.ErrorStyle.PACKET_TYPE, ClientRegistrySync::handleErrorStylePacket); + ClientConfigurationNetworking.registerGlobalReceiver(ServerPackets.ModProtocol.PACKET_TYPE, ClientRegistrySync::handleModProtocol);*/ + //TODO: Useless? + } + + /*private static void handleModProtocol(ServerPackets.ModProtocol modProtocol, Context ctx) { + var prioritizedId = modProtocol.prioritizedId(); + var protocols = modProtocol.protocols(); + + var values = new Object2IntOpenHashMap(protocols.size()); + var unsupportedList = new ArrayList(); + ModProtocolDef missingPrioritized = null; + + boolean disconnect = false; + + for (var protocol : protocols) { + var local = ModProtocol.getVersion(protocol.id()); + var latest = protocol.latestMatchingVersion(local); + LOGGER.info(String.valueOf(latest)); + if (latest != ProtocolVersions.NO_PROTOCOL) { + values.put(protocol.id(), latest); + } else if (!protocol.optional()) { + unsupportedList.add(protocol); + disconnect = true; + if (prioritizedId.equals(protocol.id())) { + missingPrioritized = protocol; + } + } + } + + if (disconnect) { + markDisconnected(RegistrySyncText.unsupportedModVersion(unsupportedList, missingPrioritized)); + + builder.pushT("unsupported_protocol", "Unsupported Mod Protocol"); + + for (var entry : unsupportedList) { + builder.textEntry(Component.literal(entry.displayName()).append(Component.literal(" (" + entry.id() + ")").withStyle(ChatFormatting.DARK_GRAY)).append(" | Server: ").append(stringifyVersions(entry.versions())).append(", Client: ").append(stringifyVersions(ModProtocol.getVersion(entry.id())))); + } + } else { + sendSupportedModProtocol(ctx.responseSender(), values); + } + } + + private static void handleEndPacket(ServerPackets.End end, Context ctx) { + syncVersion = -1; + + if (mustDisconnect) { + var entry = Component.empty(); + entry.append(errorStyleHeader); + + if (disconnectMainReason != null && showErrorDetails && !isTextEmpty(disconnectMainReason)) { + entry.append("\n"); + entry.append(disconnectMainReason); + } + + if (!isTextEmpty(errorStyleFooter)) { + entry.append("\n"); + entry.append(errorStyleFooter); + } + + ctx.responseSender().disconnect(entry); + + LOGGER.warn(builder.asString()); + } else { + ctx.responseSender().sendPacket(new ClientPackets.End()); + } + }*/ + + private static String stringifyVersions(IntList versions) { + if (versions == null || versions.isEmpty()) { + return "Missing!"; + } + + var b = new StringBuilder().append('['); + + var iter = versions.iterator(); + + while (iter.hasNext()) { + b.append(iter.nextInt()); + + if (iter.hasNext()) { + b.append(", "); + } + } + + return b.append(']').toString(); + } + + /*private static void sendSupportedModProtocol(PacketSender sender, Object2IntOpenHashMap values) { + sender.sendPacket(new ClientPackets.ModProtocol(values)); + } + + private static void handleErrorStylePacket(ServerPackets.ErrorStyle errorStyle, Context ctx) { + errorStyleHeader = errorStyle.errorHeader(); + errorStyleFooter = errorStyle.errorFooter(); + showErrorDetails = errorStyle.showError(); + } + + private static void handleHelloPacket(ServerPackets.Handshake handshake, Context ctx) { + syncVersion = ProtocolVersions.getHighestSupportedLocal(handshake.supportedVersions()); + + ctx.responseSender().sendPacket(new ClientPackets.Handshake(syncVersion)); + builder.clear(); + }*/ + + private static void markDisconnected(Component reason) { + if (disconnectMainReason == null) { + disconnectMainReason = reason; + } + + mustDisconnect = true; + } + + private static boolean isTextEmpty(Component text) { + return (text.getContents().equals(PlainTextContents.EMPTY) || (text.getContents() instanceof PlainTextContents literalContents && literalContents.text().isEmpty())) && text.getSiblings().isEmpty(); + } + + public static void disconnectCleanup(Minecraft client) { + errorStyleHeader = ServerRegistrySync.errorStyleHeader; + errorStyleFooter = ServerRegistrySync.errorStyleFooter; + showErrorDetails = ServerRegistrySync.showErrorDetails; + disconnectMainReason = null; + } +} diff --git a/src/main/java/org/quiltmc/qsl/frozenblock/core/registry/impl/sync/client/LogBuilder.java b/src/main/java/org/quiltmc/qsl/frozenblock/core/registry/impl/sync/client/LogBuilder.java new file mode 100644 index 0000000..7707a24 --- /dev/null +++ b/src/main/java/org/quiltmc/qsl/frozenblock/core/registry/impl/sync/client/LogBuilder.java @@ -0,0 +1,133 @@ +/* + * Copyright 2024 The Quilt Project + * Copyright 2024 FrozenBlock + * Modified to work on Fabric + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.quiltmc.qsl.frozenblock.core.registry.impl.sync.client; + +import net.minecraft.ChatFormatting; +import net.minecraft.network.chat.Component; +import net.neoforged.api.distmarker.Dist; +import net.neoforged.api.distmarker.OnlyIn; +import org.jetbrains.annotations.ApiStatus; + +import java.util.ArrayList; +import java.util.List; + +@ApiStatus.Internal +@OnlyIn(Dist.CLIENT) +public class LogBuilder { + private Component title; + private List entriesCurrent; + private List
sections = new ArrayList<>(); + private Component currentText = null; + private int duplicateCount = 0; + + public void pushT(String id, String lang, Object... args) { + this.push(Component.translatableWithFallback("frozenlib.core.registry_sync.log." + id, lang, args)); + } + + public void push(Component title) { + if (this.title != null) { + this.sections.add(new Section(this.title, this.entriesCurrent)); + } + + this.title = title; + this.entriesCurrent = new ArrayList<>(); + } + + public void textEntry(Component text) { + this.text(Component.literal("- ").append(Component.empty().append(text)).withStyle(ChatFormatting.GRAY)); + } + + public void text(Component text) { + if (this.currentText != null && !text.equals(this.currentText)) { + this.entriesCurrent.add(duplicatedText(this.currentText, this.duplicateCount)); + this.duplicateCount = 1; + } else { + this.duplicateCount++; + } + + this.currentText = text; + } + + public List
finish() { + if (this.title != null) { + var y = new ArrayList<>(this.entriesCurrent); + if (this.currentText != null) { + y.add(duplicatedText(this.currentText, this.duplicateCount)); + } + + this.sections.add(new Section(this.title, y)); + } + + var x = this.sections; + this.clear(); + return x; + } + + private static Component duplicatedText(Component currentText, int duplicateCount) { + if (duplicateCount < 2) { + return currentText; + } else { + return Component.empty().append(currentText).append(Component.literal(" (" + duplicateCount + ")").withStyle(ChatFormatting.BLUE)); + } + } + + public void clear() { + this.sections = new ArrayList<>(); + this.title = null; + this.entriesCurrent = null; + } + + public String asString() { + var sections = new ArrayList<>(this.sections); + if (this.title != null) { + sections.add(new Section(this.title, this.entriesCurrent)); + } + + return stringify(sections); + } + + public record Section(Component title, List entries) {}; + + public static String stringify(List
sections) { + var builder = new StringBuilder(); + var iter = sections.iterator(); + + while (iter.hasNext()) { + var entry = iter.next(); + builder.append("## " + entry.title().getString()); + + if (entry.entries.size() > 0) { + builder.append("\n"); + var eIter = entry.entries.iterator(); + while (eIter.hasNext()) { + builder.append(" " + eIter.next().getString()); + if (eIter.hasNext()) { + builder.append("\n"); + } + } + } + + if (iter.hasNext()) { + builder.append("\n"); + } + } + + return builder.toString(); + } +} diff --git a/src/main/java/org/quiltmc/qsl/frozenblock/core/registry/impl/sync/mod_protocol/ModProtocolContainer.java b/src/main/java/org/quiltmc/qsl/frozenblock/core/registry/impl/sync/mod_protocol/ModProtocolContainer.java new file mode 100644 index 0000000..74efb12 --- /dev/null +++ b/src/main/java/org/quiltmc/qsl/frozenblock/core/registry/impl/sync/mod_protocol/ModProtocolContainer.java @@ -0,0 +1,79 @@ +/* + * Copyright 2024 The Quilt Project + * Copyright 2024 FrozenBlock + * Modified to work on Fabric + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.quiltmc.qsl.frozenblock.core.registry.impl.sync.mod_protocol; + +import com.mojang.datafixers.util.Pair; +import com.mojang.serialization.Codec; +import com.mojang.serialization.DataResult; +import com.mojang.serialization.DynamicOps; +import it.unimi.dsi.fastutil.ints.IntArrayList; +import it.unimi.dsi.fastutil.ints.IntList; +import net.frozenblock.lib.FrozenSharedConstants; +import org.jetbrains.annotations.NotNull; + +import java.util.ArrayList; +import java.util.Map; + +public interface ModProtocolContainer { + Codec> MAP_CODEC = Codec.unboundedMap(Codec.STRING, Codec.list(Codec.INT).xmap(IntArrayList::new, ArrayList::new)); + + @NotNull + static Codec createCodec(Codec codec) { + final String modProtocol = FrozenSharedConstants.string("mod_protocol"); + + return new Codec<>() { + @Override + public DataResult> decode(DynamicOps ops, T input) { + var value = codec.decode(ops, input); + + ops.get(input, modProtocol).ifSuccess((x) -> { + var versionData = MAP_CODEC.decode(ops, x); + versionData.ifSuccess(y -> + ((ModProtocolContainer) value.result().orElseThrow().getFirst()).frozenLib$setModProtocol(y.getFirst()) + ); + }); + + return value; + } + + @Override + public DataResult encode(E input, DynamicOps ops, T prefix) { + var value = codec.encode(input, ops, prefix); + var modProto = ModProtocolContainer.of(input).frozenLib$getModProtocol(); + + if (value.isSuccess() && modProto != null) { + var x = MAP_CODEC.encodeStart(ops, modProto); + + if (x.isSuccess()) { + return DataResult.success(ops.set(value.result().orElseThrow(), modProtocol, x.result().orElseThrow())); + } + } + + return value; + } + }; + } + + void frozenLib$setModProtocol(Map map); + Map frozenLib$getModProtocol(); + + static ModProtocolContainer of(Object object) { + return (ModProtocolContainer) object; + } +} diff --git a/src/main/java/org/quiltmc/qsl/frozenblock/core/registry/impl/sync/server/ExtendedConnection.java b/src/main/java/org/quiltmc/qsl/frozenblock/core/registry/impl/sync/server/ExtendedConnection.java new file mode 100644 index 0000000..7239dc6 --- /dev/null +++ b/src/main/java/org/quiltmc/qsl/frozenblock/core/registry/impl/sync/server/ExtendedConnection.java @@ -0,0 +1,30 @@ +/* + * Copyright 2024 The Quilt Project + * Copyright 2024 FrozenBlock + * Modified to work on Fabric + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.quiltmc.qsl.frozenblock.core.registry.impl.sync.server; + +import net.minecraft.server.network.ServerConfigurationPacketListenerImpl; + +public interface ExtendedConnection { + void frozenLib$setModProtocol(String id, int version); + int frozenLib$getModProtocol(String id); + + static ExtendedConnection from(ServerConfigurationPacketListenerImpl handler) { + return (ExtendedConnection) handler.connection; + } +} diff --git a/src/main/java/org/quiltmc/qsl/frozenblock/core/registry/impl/sync/server/QuiltSyncTask.java b/src/main/java/org/quiltmc/qsl/frozenblock/core/registry/impl/sync/server/QuiltSyncTask.java new file mode 100644 index 0000000..664c570 --- /dev/null +++ b/src/main/java/org/quiltmc/qsl/frozenblock/core/registry/impl/sync/server/QuiltSyncTask.java @@ -0,0 +1,74 @@ +/* + * Copyright 2024 The Quilt Project + * Copyright 2024 FrozenBlock + * Modified to work on Fabric + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.quiltmc.qsl.frozenblock.core.registry.impl.sync.server; + +import net.frozenblock.lib.FrozenSharedConstants; +import net.minecraft.network.Connection; +import net.minecraft.network.protocol.Packet; +import net.minecraft.server.network.ConfigurationTask; +import net.minecraft.server.network.ServerConfigurationPacketListenerImpl; +import org.jetbrains.annotations.NotNull; +import org.quiltmc.qsl.frozenblock.core.registry.impl.sync.ClientPackets; +import org.quiltmc.qsl.frozenblock.core.registry.impl.sync.ProtocolVersions; + +import java.util.function.Consumer; + +public class QuiltSyncTask implements ConfigurationTask { + public static final Type TYPE = new Type(FrozenSharedConstants.string("registry_sync")); + private final ServerConfigurationPacketListenerImpl packetHandler; + private final ExtendedConnection extendedConnection; + private Consumer> sender; + private int syncVersion = ProtocolVersions.NO_PROTOCOL; + + public QuiltSyncTask(ServerConfigurationPacketListenerImpl packetHandler, Connection connection) { + this.packetHandler = packetHandler; + this.extendedConnection = (ExtendedConnection) connection; + } + + @Override + public void start(Consumer> sender) { + //ServerRegistrySync.sendHelloPacket(ServerConfigurationNetworking.getSender(this.packetHandler)); + } + + @Override + public Type type() { + return TYPE; + } + + /*private void sendSyncPackets(PacketSender sender) { + ServerRegistrySync.sendSyncPackets(sender, this.syncVersion); + }*/ + + public void handleHandshake(ClientPackets.@NotNull Handshake handshake) { + this.syncVersion = handshake.version(); + //this.sendSyncPackets(ServerConfigurationNetworking.getSender(this.packetHandler)); + } + + /*public void handleModProtocol(ClientPackets.@NotNull ModProtocol modProtocol, PacketSender sender) { + modProtocol.protocols().forEach(this.extendedConnection::frozenLib$setModProtocol); + }*/ + + public void handleEnd(ClientPackets.End end) { + /*if (this.syncVersion == ProtocolVersions.NO_PROTOCOL && ServerRegistrySync.requiresSync()) { + this.packetHandler.disconnect(ServerRegistrySync.noRegistrySyncMessage); + } else { + this.packetHandler.completeTask(TYPE); + }*/ + } +} diff --git a/src/main/java/org/quiltmc/qsl/frozenblock/core/registry/impl/sync/server/ServerRegistrySync.java b/src/main/java/org/quiltmc/qsl/frozenblock/core/registry/impl/sync/server/ServerRegistrySync.java new file mode 100644 index 0000000..8741bd8 --- /dev/null +++ b/src/main/java/org/quiltmc/qsl/frozenblock/core/registry/impl/sync/server/ServerRegistrySync.java @@ -0,0 +1,137 @@ +/* + * Copyright 2024 The Quilt Project + * Copyright 2024 FrozenBlock + * Modified to work on Fabric + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.quiltmc.qsl.frozenblock.core.registry.impl.sync.server; + +import it.unimi.dsi.fastutil.ints.IntArrayList; +import it.unimi.dsi.fastutil.ints.IntList; +import net.minecraft.network.chat.Component; +import org.jetbrains.annotations.ApiStatus; +import org.quiltmc.qsl.frozenblock.core.registry.impl.sync.ProtocolVersions; + +@ApiStatus.Internal +public final class ServerRegistrySync { + private static final int MAX_SAFE_PACKET_SIZE = 734003; + + public static Component noRegistrySyncMessage = Component.empty(); + public static Component errorStyleHeader = Component.empty(); + public static Component errorStyleFooter = Component.empty(); + public static boolean forceDisable = false; + public static boolean showErrorDetails = true; + + public static IntList SERVER_SUPPORTED_PROTOCOL = new IntArrayList(ProtocolVersions.IMPL_SUPPORTED_VERSIONS); + + public static void registerHandlers() { + /*ServerConfigurationConnectionEvents.CONFIGURE.register(((handler, server) -> { + // You must check to see if the client can handle your config task + if ( + ServerConfigurationNetworking.canSend(handler, ServerPackets.Handshake.PACKET_TYPE) + && ServerConfigurationNetworking.canSend(handler, ServerPackets.ErrorStyle.PACKET_TYPE) + && ServerConfigurationNetworking.canSend(handler, ServerPackets.ModProtocol.PACKET_TYPE) + && ServerConfigurationNetworking.canSend(handler, ServerPackets.End.PACKET_TYPE) + ) { + handler.addTask(new QuiltSyncTask(handler, handler.connection)); + } + })); + var registryClient = PayloadTypeRegistry.configurationC2S(); + registryClient.register(ClientPackets.Handshake.PACKET_TYPE, ClientPackets.Handshake.CODEC); + registryClient.register(ClientPackets.ModProtocol.PACKET_TYPE, ClientPackets.ModProtocol.CODEC); + registryClient.register(ClientPackets.End.PACKET_TYPE, ClientPackets.End.CODEC); + + ServerConfigurationNetworking.registerGlobalReceiver(ClientPackets.Handshake.PACKET_TYPE, ServerRegistrySync::handleHandshake); + ServerConfigurationNetworking.registerGlobalReceiver(ClientPackets.ModProtocol.PACKET_TYPE, ServerRegistrySync::handleModProtocol); + ServerConfigurationNetworking.registerGlobalReceiver(ClientPackets.End.PACKET_TYPE, ServerRegistrySync::handleEnd); + + var registry = PayloadTypeRegistry.configurationS2C(); + registry.register(ServerPackets.Handshake.PACKET_TYPE, ServerPackets.Handshake.CODEC); + registry.register(ServerPackets.ModProtocol.PACKET_TYPE, ServerPackets.ModProtocol.CODEC); + registry.register(ServerPackets.End.PACKET_TYPE, ServerPackets.End.CODEC); + registry.register(ServerPackets.ErrorStyle.PACKET_TYPE, ServerPackets.ErrorStyle.CODEC);*/ + } + + /*public static void handleHandshake(ClientPackets.Handshake handshake, Context ctx) { + ((QuiltSyncTask) ctx.networkHandler().currentTask).handleHandshake(handshake); + } + + public static void handleModProtocol(ClientPackets.ModProtocol modProtocol, Context ctx) { + ((QuiltSyncTask) ctx.networkHandler().currentTask).handleModProtocol(modProtocol, ctx.responseSender()); + } + + public static void handleEnd(ClientPackets.End end, Context ctx) { + ((QuiltSyncTask) ctx.networkHandler().currentTask).handleEnd(end); + } + + public static Component text(String string) { + if (string == null || string.isEmpty()) { + return Component.empty(); + } + + Component text = null; + try { + text = Component.Serializer.fromJson(string, RegistryAccess.EMPTY); + } catch (Exception ignored) {} + + return text != null ? text : Component.literal(string); + } + + public static boolean isNamespaceVanilla(String namespace) { + return namespace.equals(ResourceLocation.DEFAULT_NAMESPACE) || namespace.equals("brigadier"); + } + + public static boolean shouldSync() { + if (forceDisable) { + return false; + } + + return ModProtocol.enabled; + } + + public static boolean requiresSync() { + if (forceDisable) { + return false; + } + + if (!ModProtocol.REQUIRED.isEmpty()) { + return true; + } + + return false; + } + + public static void sendSyncPackets(PacketSender sender, int syncVersion) { + sendErrorStylePacket(sender); + + if (ModProtocol.enabled) { + sendModProtocol(sender); + } + + sender.sendPacket(new ServerPackets.End()); + } + + public static void sendHelloPacket(PacketSender sender) { + sender.sendPacket(new ServerPackets.Handshake(SERVER_SUPPORTED_PROTOCOL)); + } + + public static void sendModProtocol(PacketSender sender) { + sender.sendPacket(new ServerPackets.ModProtocol(ModProtocol.prioritizedId, ModProtocol.ALL)); + } + + private static void sendErrorStylePacket(PacketSender sender) { + sender.sendPacket(new ServerPackets.ErrorStyle(errorStyleHeader, errorStyleFooter, showErrorDetails)); + }*/ +} diff --git a/src/main/java/org/quiltmc/qsl/frozenblock/core/registry/impl/sync/server/SyncTaskHolder.java b/src/main/java/org/quiltmc/qsl/frozenblock/core/registry/impl/sync/server/SyncTaskHolder.java new file mode 100644 index 0000000..398a889 --- /dev/null +++ b/src/main/java/org/quiltmc/qsl/frozenblock/core/registry/impl/sync/server/SyncTaskHolder.java @@ -0,0 +1,23 @@ +/* + * Copyright 2024 The Quilt Project + * Copyright 2024 FrozenBlock + * Modified to work on Fabric + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.quiltmc.qsl.frozenblock.core.registry.impl.sync.server; + +public interface SyncTaskHolder { + QuiltSyncTask frozenLib$getQuiltSyncTask(); +} diff --git a/src/main/java/org/quiltmc/qsl/frozenblock/core/registry/mixin/BuiltInRegistriesMixin.java b/src/main/java/org/quiltmc/qsl/frozenblock/core/registry/mixin/BuiltInRegistriesMixin.java new file mode 100644 index 0000000..1147629 --- /dev/null +++ b/src/main/java/org/quiltmc/qsl/frozenblock/core/registry/mixin/BuiltInRegistriesMixin.java @@ -0,0 +1,50 @@ +/* + * Copyright 2024 The Quilt Project + * Copyright 2024 FrozenBlock + * Modified to work on Fabric + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.quiltmc.qsl.frozenblock.core.registry.mixin; + +import net.frozenblock.lib.worldgen.biome.mixin.DebugLevelSourceAccessor; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.util.Mth; +import net.minecraft.world.level.block.state.BlockState; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +import java.util.List; + +@Mixin(BuiltInRegistries.class) +public class BuiltInRegistriesMixin { + + @Inject(method = "freeze", at = @At("RETURN")) + private static void onFreezeBuiltins(CallbackInfo ci) { + //region Fix MC-197259 + final List states = BuiltInRegistries.BLOCK.stream() + .flatMap(block -> block.getStateDefinition().getPossibleStates().stream()) + .toList(); + + final int xLength = Mth.ceil(Mth.sqrt(states.size())); + final int zLength = Mth.ceil(states.size() / (float) xLength); + + DebugLevelSourceAccessor.setALL_BLOCKS(states); + DebugLevelSourceAccessor.setGRID_WIDTH(xLength); + DebugLevelSourceAccessor.setGRID_HEIGHT(zLength); + //endregion + } +} diff --git a/src/main/java/org/quiltmc/qsl/frozenblock/core/registry/mixin/ConnectionMixin.java b/src/main/java/org/quiltmc/qsl/frozenblock/core/registry/mixin/ConnectionMixin.java new file mode 100644 index 0000000..73b44dd --- /dev/null +++ b/src/main/java/org/quiltmc/qsl/frozenblock/core/registry/mixin/ConnectionMixin.java @@ -0,0 +1,52 @@ +/* + * Copyright 2024 The Quilt Project + * Copyright 2024 FrozenBlock + * Modified to work on Fabric + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.quiltmc.qsl.frozenblock.core.registry.mixin; + +import it.unimi.dsi.fastutil.objects.Object2IntMap; +import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap; +import net.minecraft.network.Connection; +import net.minecraft.network.protocol.PacketFlow; +import org.quiltmc.qsl.frozenblock.core.registry.impl.sync.ProtocolVersions; +import org.quiltmc.qsl.frozenblock.core.registry.impl.sync.server.ExtendedConnection; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(Connection.class) +public class ConnectionMixin implements ExtendedConnection { + @Unique + private Object2IntMap frozenLib$modProtocol = new Object2IntOpenHashMap<>(); + + @Inject(method = "", at = @At("TAIL")) + private void setDefault(PacketFlow receiving, CallbackInfo ci) { + this.frozenLib$modProtocol.defaultReturnValue(ProtocolVersions.NO_PROTOCOL); + } + + @Override + public void frozenLib$setModProtocol(String id, int version) { + this.frozenLib$modProtocol.put(id, version); + } + + @Override + public int frozenLib$getModProtocol(String id) { + return this.frozenLib$modProtocol.getInt(id); + } +} diff --git a/src/main/java/org/quiltmc/qsl/frozenblock/core/registry/mixin/RegistryDataLoaderMixin.java b/src/main/java/org/quiltmc/qsl/frozenblock/core/registry/mixin/RegistryDataLoaderMixin.java new file mode 100644 index 0000000..150baa6 --- /dev/null +++ b/src/main/java/org/quiltmc/qsl/frozenblock/core/registry/mixin/RegistryDataLoaderMixin.java @@ -0,0 +1,78 @@ +/* + * Copyright 2024 The Quilt Project + * Copyright 2024 FrozenBlock + * Modified to work on Fabric + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.quiltmc.qsl.frozenblock.core.registry.mixin; + +import net.minecraft.core.RegistryAccess; +import net.minecraft.resources.RegistryDataLoader; +import net.minecraft.resources.RegistryOps; +import net.minecraft.resources.ResourceKey; +import net.neoforged.neoforge.common.NeoForge; +import org.quiltmc.qsl.frozenblock.core.registry.api.event.RegistryEvent; +import org.quiltmc.qsl.frozenblock.core.registry.impl.DynamicRegistryManagerSetupContextImpl; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; +import org.spongepowered.asm.mixin.injection.callback.LocalCapture; + +import java.util.List; +import java.util.Map; + +/** + * Modified to work on Fabric + */ +@Mixin(RegistryDataLoader.class) +public class RegistryDataLoaderMixin { + + @Inject( + method = "load(Lnet/minecraft/resources/RegistryDataLoader$LoadingFunction;Lnet/minecraft/core/RegistryAccess;Ljava/util/List;)Lnet/minecraft/core/RegistryAccess$Frozen;", + at = @At( + value = "INVOKE", + target = "Ljava/util/List;forEach(Ljava/util/function/Consumer;)V", + ordinal = 1, + shift = At.Shift.BEFORE + ), + locals = LocalCapture.CAPTURE_FAILHARD + ) + private static void onDynamicSetup( + RegistryDataLoader.LoadingFunction loadingFunction, + RegistryAccess registryManager, + List> decodingData, + CallbackInfoReturnable cir, + Map, Exception> map, + List> registries, + RegistryOps.RegistryInfoLookup registryInfoLookup + ) { + NeoForge.EVENT_BUS.post(new RegistryEvent.DynamicRegistrySetup(new DynamicRegistryManagerSetupContextImpl(registries.stream().map(RegistryDataLoader.Loader::registry)))); + } + + @Inject( + method = "load(Lnet/minecraft/resources/RegistryDataLoader$LoadingFunction;Lnet/minecraft/core/RegistryAccess;Ljava/util/List;)Lnet/minecraft/core/RegistryAccess$Frozen;", + at = @At( + value = "INVOKE", + target = "Ljava/util/List;forEach(Ljava/util/function/Consumer;)V", + ordinal = 1, + shift = At.Shift.AFTER + ), + locals = LocalCapture.CAPTURE_FAILHARD + ) + private static void onDynamicLoaded(RegistryDataLoader.LoadingFunction loadingFunction, RegistryAccess registryAccess, List> list, CallbackInfoReturnable cir, Map, Exception> map, List> list2, RegistryOps.RegistryInfoLookup registryInfoLookup) { + NeoForge.EVENT_BUS.post(new RegistryEvent.DynamicRegistryLoaded(registryAccess)); + } +} diff --git a/src/main/java/org/quiltmc/qsl/frozenblock/core/registry/mixin/ServerStatusVersionMixin.java b/src/main/java/org/quiltmc/qsl/frozenblock/core/registry/mixin/ServerStatusVersionMixin.java new file mode 100644 index 0000000..2e1d0e9 --- /dev/null +++ b/src/main/java/org/quiltmc/qsl/frozenblock/core/registry/mixin/ServerStatusVersionMixin.java @@ -0,0 +1,74 @@ +/* + * Copyright 2024 The Quilt Project + * Copyright 2024 FrozenBlock + * Modified to work on Fabric + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.quiltmc.qsl.frozenblock.core.registry.mixin; + +import com.llamalad7.mixinextras.injector.ModifyReturnValue; +import com.mojang.serialization.Codec; +import it.unimi.dsi.fastutil.ints.IntList; +import net.minecraft.network.protocol.status.ServerStatus; +import org.quiltmc.qsl.frozenblock.core.registry.api.sync.ModProtocol; +import org.quiltmc.qsl.frozenblock.core.registry.impl.sync.mod_protocol.ModProtocolContainer; +import org.spongepowered.asm.mixin.*; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +import java.util.HashMap; +import java.util.Map; + +@Mixin(ServerStatus.Version.class) +public class ServerStatusVersionMixin implements ModProtocolContainer { + @Mutable + @Shadow + @Final + public static Codec CODEC; + + @Unique + private Map frozenLib$modProtocol; + + @ModifyReturnValue(method = "current", at = @At("RETURN")) + private static ServerStatus.Version quilt$addProtocolVersions(ServerStatus.Version original) { + if (ModProtocol.disableQuery) { + return null; + } + + var map = new HashMap(); + for (var protocol : ModProtocol.REQUIRED) { + map.put(protocol.id(), protocol.versions()); + } + + ((ModProtocolContainer) (Object) original).frozenLib$setModProtocol(map); + return original; + } + + @Inject(method = "", at = @At("TAIL")) + private static void quilt$extendCodec(CallbackInfo ci) { + CODEC = ModProtocolContainer.createCodec(CODEC); + } + + @Override + public void frozenLib$setModProtocol(Map map) { + this.frozenLib$modProtocol = map; + } + + @Override + public Map frozenLib$getModProtocol() { + return this.frozenLib$modProtocol; + } +} diff --git a/src/main/java/org/quiltmc/qsl/frozenblock/core/registry/mixin/client/MinecraftMixin.java b/src/main/java/org/quiltmc/qsl/frozenblock/core/registry/mixin/client/MinecraftMixin.java new file mode 100644 index 0000000..aeb7fdf --- /dev/null +++ b/src/main/java/org/quiltmc/qsl/frozenblock/core/registry/mixin/client/MinecraftMixin.java @@ -0,0 +1,37 @@ +/* + * Copyright 2024 The Quilt Project + * Copyright 2024 FrozenBlock + * Modified to work on Fabric + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.quiltmc.qsl.frozenblock.core.registry.mixin.client; + +import net.minecraft.client.Minecraft; +import net.neoforged.api.distmarker.Dist; +import net.neoforged.api.distmarker.OnlyIn; +import org.quiltmc.qsl.frozenblock.core.registry.impl.sync.client.ClientRegistrySync; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@OnlyIn(Dist.CLIENT) +@Mixin(Minecraft.class) +public class MinecraftMixin { + @Inject(method = "disconnect(Lnet/minecraft/client/gui/screens/Screen;)V", at = @At("TAIL")) + private void quilt$restoreRegistries(CallbackInfo ci) { + ClientRegistrySync.disconnectCleanup((Minecraft) (Object) this); + } +} diff --git a/src/main/java/org/quiltmc/qsl/frozenblock/core/registry/mixin/client/ServerDataMixin.java b/src/main/java/org/quiltmc/qsl/frozenblock/core/registry/mixin/client/ServerDataMixin.java new file mode 100644 index 0000000..b743836 --- /dev/null +++ b/src/main/java/org/quiltmc/qsl/frozenblock/core/registry/mixin/client/ServerDataMixin.java @@ -0,0 +1,46 @@ +/* + * Copyright 2024 The Quilt Project + * Copyright 2024 FrozenBlock + * Modified to work on Fabric + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.quiltmc.qsl.frozenblock.core.registry.mixin.client; + +import it.unimi.dsi.fastutil.ints.IntList; +import net.minecraft.client.multiplayer.ServerData; +import net.neoforged.api.distmarker.Dist; +import net.neoforged.api.distmarker.OnlyIn; +import org.quiltmc.qsl.frozenblock.core.registry.impl.sync.mod_protocol.ModProtocolContainer; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Unique; + +import java.util.Map; + +@OnlyIn(Dist.CLIENT) +@Mixin(ServerData.class) +public class ServerDataMixin implements ModProtocolContainer { + @Unique + private Map frozenLib$modProtocol; + + @Override + public void frozenLib$setModProtocol(Map map) { + this.frozenLib$modProtocol = map; + } + + @Override + public Map frozenLib$getModProtocol() { + return this.frozenLib$modProtocol; + } +} diff --git a/src/main/java/org/quiltmc/qsl/frozenblock/misc/datafixerupper/api/EmptySchema.java b/src/main/java/org/quiltmc/qsl/frozenblock/misc/datafixerupper/api/EmptySchema.java new file mode 100644 index 0000000..8641501 --- /dev/null +++ b/src/main/java/org/quiltmc/qsl/frozenblock/misc/datafixerupper/api/EmptySchema.java @@ -0,0 +1,56 @@ +/* + * Copyright 2024 The Quilt Project + * Copyright 2024 FrozenBlock + * Modified to work on Fabric + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.quiltmc.qsl.frozenblock.misc.datafixerupper.api; + +import com.mojang.datafixers.DSL; +import com.mojang.datafixers.schemas.Schema; +import com.mojang.datafixers.types.Type; +import com.mojang.datafixers.types.templates.TypeTemplate; +import it.unimi.dsi.fastutil.objects.Object2ObjectMaps; +import org.jetbrains.annotations.Range; + +import java.util.Map; +import java.util.function.Supplier; + +/** + * Represents an empty {@link Schema}, having no parent and containing no type definitions. + *

+ * Modified to work on Fabric + */ +public final class EmptySchema extends FirstSchema { + /** + * Constructs an empty schema. + * + * @param versionKey the data version key + */ + public EmptySchema(@Range(from = 0, to = Integer.MAX_VALUE) int versionKey) { + super(versionKey); + } + + // Ensure the schema stays empty. + @Override + public void registerType(boolean recursive, DSL.TypeReference type, Supplier template) { + throw new UnsupportedOperationException(); + } + + @Override + protected Map> buildTypes() { + return Object2ObjectMaps.emptyMap(); + } +} diff --git a/src/main/java/org/quiltmc/qsl/frozenblock/misc/datafixerupper/api/FirstSchema.java b/src/main/java/org/quiltmc/qsl/frozenblock/misc/datafixerupper/api/FirstSchema.java new file mode 100644 index 0000000..97cb553 --- /dev/null +++ b/src/main/java/org/quiltmc/qsl/frozenblock/misc/datafixerupper/api/FirstSchema.java @@ -0,0 +1,56 @@ +/* + * Copyright 2024 The Quilt Project + * Copyright 2024 FrozenBlock + * Modified to work on Fabric + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.quiltmc.qsl.frozenblock.misc.datafixerupper.api; + +import com.mojang.datafixers.schemas.Schema; +import com.mojang.datafixers.types.templates.TypeTemplate; +import org.jetbrains.annotations.Range; + +import java.util.Map; +import java.util.function.Supplier; + +/** + * Represents a {@link Schema} that has no parent. + *

+ * Modified to work on Fabric + */ +public class FirstSchema extends Schema { + /** + * Creates a schema. + * @param versionKey the data version key + */ + public FirstSchema(@Range(from = 0, to = Integer.MAX_VALUE) int versionKey) { + super(versionKey, null); + } + + // all of these methods refer to this.parent without checking if its null + @Override + public void registerTypes(Schema schema, Map> entityTypes, + Map> blockEntityTypes) {} + + @Override + public Map> registerEntities(Schema schema) { + return Map.of(); + } + + @Override + public Map> registerBlockEntities(Schema schema) { + return Map.of(); + } +} diff --git a/src/main/java/org/quiltmc/qsl/frozenblock/misc/datafixerupper/api/QuiltDataFixerBuilder.java b/src/main/java/org/quiltmc/qsl/frozenblock/misc/datafixerupper/api/QuiltDataFixerBuilder.java new file mode 100644 index 0000000..809a36d --- /dev/null +++ b/src/main/java/org/quiltmc/qsl/frozenblock/misc/datafixerupper/api/QuiltDataFixerBuilder.java @@ -0,0 +1,76 @@ +/* + * Copyright 2024 The Quilt Project + * Copyright 2024 FrozenBlock + * Modified to work on Fabric + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.quiltmc.qsl.frozenblock.misc.datafixerupper.api; + +import com.mojang.datafixers.DSL; +import com.mojang.datafixers.DataFixer; +import com.mojang.datafixers.DataFixerBuilder; +import net.minecraft.Util; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Range; + +import java.util.Set; +import java.util.concurrent.Executor; +import java.util.function.Supplier; + +/** + * An extended variant of the {@link DataFixerBuilder} class, which provides an extra method. + *

+ * Modified to work on Fabric + */ +public class QuiltDataFixerBuilder extends DataFixerBuilder { + protected final int dataVersion; + + /** + * Creates a new {@code QuiltDataFixerBuilder}. + * + * @param dataVersion the current data version + */ + public QuiltDataFixerBuilder(@Range(from = 0, to = Integer.MAX_VALUE) int dataVersion) { + super(dataVersion); + this.dataVersion = dataVersion; + } + + /** + * {@return the current data version} + */ + @Range(from = 0, to = Integer.MAX_VALUE) + public int getDataVersion() { + return this.dataVersion; + } + + /** + * Builds the final {@code DataFixer}. + *

+ * This will build either an {@linkplain #build() unoptimized fixer} or an + * {@linkplain #build(Set, Supplier) optimized fixer}, depending on the vanilla game's settings. + * + * @param executorGetter the executor supplier, only invoked if the game is using optimized data fixers + * @return the newly built data fixer + */ + @Contract(value = "_, _ -> new") + public @NotNull DataFixer build(Set types, @NotNull Supplier executorGetter) { + return types.isEmpty() ? this.build().fixer() : Util.make(() -> { + var result = this.build(); + result.optimize(types, executorGetter.get()).join(); + return result.fixer(); + }); + } +} diff --git a/src/main/java/org/quiltmc/qsl/frozenblock/misc/datafixerupper/api/QuiltDataFixes.java b/src/main/java/org/quiltmc/qsl/frozenblock/misc/datafixerupper/api/QuiltDataFixes.java new file mode 100644 index 0000000..bc7a504 --- /dev/null +++ b/src/main/java/org/quiltmc/qsl/frozenblock/misc/datafixerupper/api/QuiltDataFixes.java @@ -0,0 +1,267 @@ +/* + * Copyright 2024 The Quilt Project + * Copyright 2024 FrozenBlock + * Modified to work on Fabric + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.quiltmc.qsl.frozenblock.misc.datafixerupper.api; + +import com.google.common.util.concurrent.ThreadFactoryBuilder; +import com.mojang.datafixers.DataFixer; +import com.mojang.datafixers.DataFixerBuilder; +import com.mojang.datafixers.schemas.Schema; +import lombok.experimental.UtilityClass; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.util.datafix.DataFixTypes; +import net.neoforged.fml.ModContainer; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Range; +import org.quiltmc.qsl.frozenblock.misc.datafixerupper.impl.QuiltDataFixesInternals; + +import java.util.Optional; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; +import java.util.function.BiFunction; +import java.util.function.Supplier; + +import static com.google.common.base.Preconditions.checkArgument; +import static java.util.Objects.requireNonNull; + +/** + * Provides methods to register custom {@link DataFixer}s. + *

+ * Modified to work on Fabric + */ +@UtilityClass +public class QuiltDataFixes { + + /** + * A "base" version {@code 0} schema, for use by all mods. + *

+ * This schema must be the first one added! + * + * @see DataFixerBuilder#addSchema(int, BiFunction) + */ + public static final BiFunction BASE_SCHEMA = (version, parent) -> { + checkArgument(version == 0, "version must be 0"); + checkArgument(parent == null, "parent must be null"); + return QuiltDataFixesInternals.get().createBaseSchema(); + }; + + /** + * Registers a new data fixer. + * + * @param modId the mod identifier + * @param currentVersion the current version of the mod's data + * @param dataFixer the data fixer + */ + public static void registerFixer( + @NotNull String modId, + @Range(from = 0, to = Integer.MAX_VALUE) int currentVersion, + @NotNull DataFixer dataFixer + ) { + requireNonNull(modId, "modId cannot be null"); + //noinspection ConstantConditions + checkArgument(currentVersion >= 0, "currentVersion must be positive"); + requireNonNull(dataFixer, "dataFixer cannot be null"); + + if (isFrozen()) { + throw new IllegalStateException("Can't register data fixer after registry is frozen"); + } + + QuiltDataFixesInternals.get().registerFixer(modId, currentVersion, dataFixer); + } + + /** + * Registers a new data fixer. + * + * @param mod the mod container + * @param currentVersion the current version of the mod's data + * @param dataFixer the data fixer + */ + /*public static void registerFixer( + @NotNull ModContainer mod, + @Range(from = 0, to = Integer.MAX_VALUE) int currentVersion, + @NotNull DataFixer dataFixer + ) { + requireNonNull(mod, "mod cannot be null"); + + registerFixer(mod.getMetadata().getId(), currentVersion, dataFixer); + } + + /** + * Builds and registers a new data fixer. + * + * @param mod the mod container + * @param dataFixerBuilder the data fixer builder + */ + /*public static void buildAndRegisterFixer( + @NotNull ModContainer mod, + @NotNull QuiltDataFixerBuilder dataFixerBuilder + ) { + requireNonNull(mod, "mod cannot be null"); + requireNonNull(dataFixerBuilder, "data fixer builder cannot be null"); + + registerFixer(mod.getMetadata().getId(), dataFixerBuilder.getDataVersion(), buildFixer(dataFixerBuilder)); + } + + /** + * Registers a new data fixer for use with Minecraft version-specific datafixing. + * + * @param modId the mod identifier + * @param currentVersion the current version of the mod's data + * @param dataFixer the data fixer + */ + /*public static void registerMinecraftFixer( + @NotNull String modId, + @Range(from = 0, to = Integer.MAX_VALUE) int currentVersion, + @NotNull DataFixer dataFixer + ) { + requireNonNull(modId, "modId cannot be null"); + //noinspection ConstantConditions + checkArgument(currentVersion >= 0, "currentVersion must be positive"); + requireNonNull(dataFixer, "dataFixer cannot be null"); + + if (isFrozen()) { + throw new IllegalStateException("Can't register data fixer after registry is frozen"); + } + + QuiltDataFixesInternals.get().registerMinecraftFixer(modId, currentVersion, dataFixer); + } + + /** + * Registers a new data fixer for use with Minecraft version-specific datafixing. + * + * @param mod the mod container + * @param currentVersion the current version of the mod's data + * @param dataFixer the data fixer + */ + /*public static void registerMinecraftFixer( + @NotNull ModContainer mod, + @Range(from = 0, to = Integer.MAX_VALUE) int currentVersion, + @NotNull DataFixer dataFixer + ) { + requireNonNull(mod, "mod cannot be null"); + + registerMinecraftFixer(mod.getMetadata().getId(), currentVersion, dataFixer); + } + + /** + * Builds and registers a new data fixer for use with Minecraft version-specific datafixing. + * + * @param mod the mod container + * @param dataFixerBuilder the data fixer builder + */ + /*public static void buildAndRegisterMinecraftFixer( + @NotNull ModContainer mod, + @NotNull QuiltDataFixerBuilder dataFixerBuilder + ) { + requireNonNull(mod, "mod cannot be null"); + requireNonNull(dataFixerBuilder, "data fixer builder cannot be null"); + + registerMinecraftFixer(mod.getMetadata().getId(), dataFixerBuilder.getDataVersion(), buildFixer(dataFixerBuilder)); + }*/ + + /** + * Builds a new data fixer. + * + * @param dataFixerBuilder the data fixer builder + * @return The built data fixer. + */ + public static DataFixer buildFixer(@NotNull QuiltDataFixerBuilder dataFixerBuilder) { + requireNonNull(dataFixerBuilder, "data fixer builder cannot be null"); + + Supplier executor = () -> Executors.newSingleThreadExecutor( + new ThreadFactoryBuilder().setNameFormat("FrozenLib Quilt Datafixer Bootstrap").setDaemon(true).setPriority(1).build() + ); + + return dataFixerBuilder.build(DataFixTypes.TYPES_FOR_LEVEL_LIST, executor); + } + + /** + * Gets a mod's Minecraft version-specificdata fixer. + * + * @param modId the mod identifier + * @return the mod's data fixer, or empty if the mod hasn't registered one + */ + public static @NotNull Optional getFixer(@NotNull String modId) { + requireNonNull(modId, "modId cannot be null"); + + QuiltDataFixesInternals.DataFixerEntry entry = QuiltDataFixesInternals.get().getFixerEntry(modId); + if (entry == null) { + return Optional.empty(); + } + return Optional.of(entry.dataFixer()); + } + + /** + * Gets a mod's Minecraft version-specific data fixer. + * + * @param modId the mod identifier + * @return the mod's data fixer, or empty if the mod hasn't registered one + */ + public static @NotNull Optional getMinecraftFixer(@NotNull String modId) { + requireNonNull(modId, "modId cannot be null"); + + QuiltDataFixesInternals.DataFixerEntry entry = QuiltDataFixesInternals.get().getMinecraftFixerEntry(modId); + if (entry == null) { + return Optional.empty(); + } + return Optional.of(entry.dataFixer()); + } + + /** + * Gets a mod's Minecraft version-specific data version from a {@link CompoundTag}. + * + * @param compound the compound + * @param modId the mod identifier + * @return the mod's data version, or {@code 0} if the compound has no data for that mod + */ + @Contract(pure = true) + @Range(from = 0, to = Integer.MAX_VALUE) + public static int getModDataVersion(@NotNull CompoundTag compound, @NotNull String modId) { + requireNonNull(compound, "compound cannot be null"); + requireNonNull(modId, "modId cannot be null"); + + return QuiltDataFixesInternals.getModDataVersion(compound, modId); + } + + /** + * Gets a mod's Minecraft version-specific data version from a {@link CompoundTag}. + * + * @param compound the compound + * @param modId the mod identifier + * @return the mod's data version, or {@code 0} if the compound has no data for that mod + */ + @Contract(pure = true) + @Range(from = 0, to = Integer.MAX_VALUE) + public static int getModMinecraftDataVersion(@NotNull CompoundTag compound, @NotNull String modId) { + requireNonNull(compound, "compound cannot be null"); + requireNonNull(modId, "modId cannot be null"); + + return QuiltDataFixesInternals.getModMinecraftDataVersion(compound, modId); + } + + /** + * Checks if the data fixer registry is frozen. + * + * @return {@code true} if frozen, or {@code false} otherwise. + */ + @Contract(pure = true) + public static boolean isFrozen() { + return QuiltDataFixesInternals.get().isFrozen(); + } +} diff --git a/src/main/java/org/quiltmc/qsl/frozenblock/misc/datafixerupper/api/SimpleFixes.java b/src/main/java/org/quiltmc/qsl/frozenblock/misc/datafixerupper/api/SimpleFixes.java new file mode 100644 index 0000000..00104f0 --- /dev/null +++ b/src/main/java/org/quiltmc/qsl/frozenblock/misc/datafixerupper/api/SimpleFixes.java @@ -0,0 +1,174 @@ +/* + * Copyright 2024 The Quilt Project + * Copyright 2024 FrozenBlock + * Modified to work on Fabric + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.quiltmc.qsl.frozenblock.misc.datafixerupper.api; + +import com.google.common.collect.ImmutableMap; +import com.mojang.datafixers.DataFix; +import com.mojang.datafixers.DataFixerBuilder; +import com.mojang.datafixers.schemas.Schema; +import net.frozenblock.lib.datafix.api.BlockStateRenameFix; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.util.datafix.DataFixers; +import net.minecraft.util.datafix.fixes.*; +import net.minecraft.util.datafix.schemas.NamespacedSchema; +import org.jetbrains.annotations.NotNull; + +import java.util.Map; +import java.util.Objects; + +import static java.util.Objects.requireNonNull; + +/** + * Provides methods to add common {@link DataFix}es to {@link DataFixerBuilder}s. + *

+ * Modified to work on Fabric + */ +public final class SimpleFixes { + private SimpleFixes() { + throw new RuntimeException("SimpleFixes contains only static declarations."); + } + + /** + * Adds a block rename fix to the builder, in case a block's identifier is changed. + * + * @param builder the builder + * @param name the fix's name + * @param oldId the block's old identifier + * @param newId the block's new identifier + * @param schema the schema this fixer should be a part of + * @see BlockRenameFix + */ + public static void addBlockRenameFix(@NotNull DataFixerBuilder builder, @NotNull String name, + @NotNull ResourceLocation oldId, @NotNull ResourceLocation newId, + @NotNull Schema schema) { + requireNonNull(builder, "DataFixerBuilder cannot be null"); + requireNonNull(name, "Fix name cannot be null"); + requireNonNull(oldId, "Old identifier cannot be null"); + requireNonNull(newId, "New identifier cannot be null"); + requireNonNull(schema, "Schema cannot be null"); + + final String oldIdStr = oldId.toString(), newIdStr = newId.toString(); + builder.addFixer(BlockRenameFix.create(schema, name, (inputName) -> + Objects.equals(NamespacedSchema.ensureNamespaced(inputName), oldIdStr) ? newIdStr : inputName)); + } + + /** + * Adds an entity rename fix to the builder, in case an entity's identifier is changed. + * + * @param builder the builder + * @param name the fix's name + * @param oldId the entity's old identifier + * @param newId the entity's new identifier + * @param schema the schema this fix should be a part of + * @see SimplestEntityRenameFix + */ + public static void addEntityRenameFix(@NotNull DataFixerBuilder builder, @NotNull String name, + @NotNull ResourceLocation oldId, @NotNull ResourceLocation newId, + @NotNull Schema schema) { + requireNonNull(builder, "DataFixerBuilder cannot be null"); + requireNonNull(name, "Fix name cannot be null"); + requireNonNull(oldId, "Old identifier cannot be null"); + requireNonNull(newId, "New identifier cannot be null"); + requireNonNull(schema, "Schema cannot be null"); + + final String oldIdStr = oldId.toString(), newIdStr = newId.toString(); + builder.addFixer(new SimplestEntityRenameFix(name, schema, false) { + @Override + protected String rename(String inputName) { + return Objects.equals(NamespacedSchema.ensureNamespaced(inputName), oldIdStr) ? newIdStr : inputName; + } + }); + } + + /** + * Adds an item rename fix to the builder, in case an item's identifier is changed. + * + * @param builder the builder + * @param name the fix's name + * @param oldId the item's old identifier + * @param newId the item's new identifier + * @param schema the schema this fix should be a part of + * @see ItemRenameFix + */ + public static void addItemRenameFix(@NotNull DataFixerBuilder builder, @NotNull String name, + @NotNull ResourceLocation oldId, @NotNull ResourceLocation newId, + @NotNull Schema schema) { + requireNonNull(builder, "DataFixerBuilder cannot be null"); + requireNonNull(name, "Fix name cannot be null"); + requireNonNull(oldId, "Old identifier cannot be null"); + requireNonNull(newId, "New identifier cannot be null"); + requireNonNull(schema, "Schema cannot be null"); + + final String oldIdStr = oldId.toString(), newIdStr = newId.toString(); + builder.addFixer(ItemRenameFix.create(schema, name, (inputName) -> + Objects.equals(NamespacedSchema.ensureNamespaced(inputName), oldIdStr) ? newIdStr : inputName)); + } + + /** + * Adds a blockstate rename fix to the builder, in case a blockstate's name is changed. + * + * @param builder the builder + * @param name the fix's name + * @param blockId the block's identifier + * @param oldState the blockstate's old name + * @param defaultValue the blockstate's default value + * @param newState the blockstates's new name + * @param schema the schema this fixer should be a part of + * @see BlockStateRenameFix + */ + public static void addBlockStateRenameFix(@NotNull DataFixerBuilder builder, @NotNull String name, + @NotNull ResourceLocation blockId, @NotNull String oldState, + @NotNull String defaultValue, @NotNull String newState, + @NotNull Schema schema) { + requireNonNull(builder, "DataFixerBuilder cannot be null"); + requireNonNull(name, "Fix name cannot be null"); + requireNonNull(blockId, "Block Id cannot be null"); + requireNonNull(oldState, "Old BlockState cannot be null"); + requireNonNull(defaultValue, "Default value cannot be null"); + requireNonNull(newState, "New BlockState cannot be null"); + requireNonNull(schema, "Schema cannot be null"); + + final String blockIdStr = blockId.toString(); + builder.addFixer(new BlockStateRenameFix(schema, name, blockIdStr, oldState, defaultValue, newState)); + } + + /** + * Adds a biome rename fix to the builder, in case biome identifiers are changed. + * + * @param builder the builder + * @param name the fix's name + * @param changes a map of old biome identifiers to new biome identifiers + * @param schema the schema this fixer should be a part of + * @see NamespacedTypeRenameFix + */ + public static void addBiomeRenameFix(@NotNull DataFixerBuilder builder, @NotNull String name, + @NotNull Map changes, + @NotNull Schema schema) { + requireNonNull(builder, "DataFixerBuilder cannot be null"); + requireNonNull(name, "Fix name cannot be null"); + requireNonNull(changes, "Changes cannot be null"); + requireNonNull(schema, "Schema cannot be null"); + + var mapBuilder = ImmutableMap.builder(); + for (var entry : changes.entrySet()) { + mapBuilder.put(entry.getKey().toString(), entry.getValue().toString()); + } + builder.addFixer(new NamespacedTypeRenameFix(schema, name, References.BIOME, DataFixers.createRenamer(mapBuilder.build()))); + } +} diff --git a/src/main/java/org/quiltmc/qsl/frozenblock/misc/datafixerupper/api/package-info.java b/src/main/java/org/quiltmc/qsl/frozenblock/misc/datafixerupper/api/package-info.java new file mode 100644 index 0000000..76a7690 --- /dev/null +++ b/src/main/java/org/quiltmc/qsl/frozenblock/misc/datafixerupper/api/package-info.java @@ -0,0 +1,59 @@ +/* + * Copyright 2024 The Quilt Project + * Copyright 2024 FrozenBlock + * Modified to work on Fabric + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + *

Custom DataFixerUpper API

+ *

+ * This API lets you register a {@code DataFixer} for your own mod, letting you use Minecraft's built-in + * "old save compatibility" system without bodges! + *

+ * Here is an example simple use of this API: + *


+ * // the latest version of the mod's data
+ * // this should match the version of the last schema added!
+ * // note that the default data version is 0, meaning that you can upgrade
+ * //  from a version that did not have a fixer
+ * //  (by registering a schema for upgrading from version 0 to version 1)
+ * public static final int CURRENT_DATA_VERSION = 1;
+ *
+ * public static void initialize(ModContainer mod) {
+ *     // create a builder
+ *     var builder = new QuiltDataFixerBuilder(CURRENT_DATA_VERSION);
+ *     // add the "base" version 0 schema
+ *     builder.addSchema(0, QuiltDataFixes.BASE_SCHEMA);
+ *     // add a schema for upgrading from version 0 to version 1
+ *     Schema schemaV1 = builder.addSchema(1, IdentifierNormalizingSchema::new)
+ *     // add fixes to the schema - for example, an item rename (identifier change)
+ *     SimpleFixes.addItemRenameFix(builder, "Rename cool_item to awesome_item",
+ *         new Identifier("mymod", "cool_item"),
+ *         new Identifier("mymod", "awesome_item"),
+ *         schemaV1);
+ *
+ *     // register the fixer!
+ *     // this will create either an unoptimized fixer or an optimized fixer,
+ *     //  depending on the game configuration
+ *     QuiltDataFixes.buildAndRegisterFixer(mod, builder);
+ * }
+ * 
+ * + * @see org.quiltmc.qsl.frozenblock.misc.datafixerupper.api.QuiltDataFixes + * @see org.quiltmc.qsl.frozenblock.misc.datafixerupper.api.SimpleFixes + * @see org.quiltmc.qsl.frozenblock.misc.datafixerupper.api.QuiltDataFixerBuilder + */ + +package org.quiltmc.qsl.frozenblock.misc.datafixerupper.api; diff --git a/src/main/java/org/quiltmc/qsl/frozenblock/misc/datafixerupper/impl/NoOpQuiltDataFixesInternals.java b/src/main/java/org/quiltmc/qsl/frozenblock/misc/datafixerupper/impl/NoOpQuiltDataFixesInternals.java new file mode 100644 index 0000000..7903c75 --- /dev/null +++ b/src/main/java/org/quiltmc/qsl/frozenblock/misc/datafixerupper/impl/NoOpQuiltDataFixesInternals.java @@ -0,0 +1,93 @@ +/* + * Copyright 2024 The Quilt Project + * Copyright 2024 FrozenBlock + * Modified to work on Fabric + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.quiltmc.qsl.frozenblock.misc.datafixerupper.impl; + +import com.mojang.datafixers.DataFixer; +import com.mojang.datafixers.schemas.Schema; +import com.mojang.serialization.Dynamic; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.Tag; +import net.minecraft.util.datafix.DataFixTypes; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.annotations.Range; +import org.quiltmc.qsl.frozenblock.misc.datafixerupper.api.EmptySchema; + +/** + * Modified to work on Fabric + */ +@ApiStatus.Internal +public final class NoOpQuiltDataFixesInternals extends QuiltDataFixesInternals { + private final Schema schema; + + private boolean frozen; + + public NoOpQuiltDataFixesInternals() { + this.schema = new EmptySchema(0); + + this.frozen = false; + } + + @Override + public void registerFixer(@NotNull String modId, @Range(from = 0, to = Integer.MAX_VALUE) int currentVersion, @NotNull DataFixer dataFixer) {} + + @Override + public boolean isEmpty() { + return true; + } + + @Override + public @Nullable DataFixerEntry getFixerEntry(@NotNull String modId) { + return null; + } + + @Override + public void registerMinecraftFixer(@NotNull String modId, @Range(from = 0, to = Integer.MAX_VALUE) int currentVersion, @NotNull DataFixer dataFixer) {} + + @Override + public @Nullable DataFixerEntry getMinecraftFixerEntry(@NotNull String modId) { + return null; + } + + @Override + public @NotNull Schema createBaseSchema() { + return this.schema; + } + + @Override + public @NotNull Dynamic updateWithAllFixers(@NotNull DataFixTypes dataFixTypes, @NotNull Dynamic dynamic) { + return new Dynamic<>(dynamic.getOps(), dynamic.getValue().copy()); + } + + @Override + public @NotNull CompoundTag addModDataVersions(@NotNull CompoundTag compound) { + return compound; + } + + @Override + public void freeze() { + this.frozen = true; + } + + @Override + public boolean isFrozen() { + return this.frozen; + } +} diff --git a/src/main/java/org/quiltmc/qsl/frozenblock/misc/datafixerupper/impl/QuiltDataFixesInternals.java b/src/main/java/org/quiltmc/qsl/frozenblock/misc/datafixerupper/impl/QuiltDataFixesInternals.java new file mode 100644 index 0000000..4437c80 --- /dev/null +++ b/src/main/java/org/quiltmc/qsl/frozenblock/misc/datafixerupper/impl/QuiltDataFixesInternals.java @@ -0,0 +1,101 @@ +/* + * Copyright 2024 The Quilt Project + * Copyright 2024 FrozenBlock + * Modified to work on Fabric + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.quiltmc.qsl.frozenblock.misc.datafixerupper.impl; + +import com.mojang.datafixers.DataFixUtils; +import com.mojang.datafixers.DataFixer; +import com.mojang.datafixers.schemas.Schema; +import com.mojang.logging.LogUtils; +import com.mojang.serialization.Dynamic; +import net.minecraft.SharedConstants; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.Tag; +import net.minecraft.util.datafix.DataFixTypes; +import net.minecraft.util.datafix.DataFixers; +import org.jetbrains.annotations.*; +import org.slf4j.Logger; + +/** + * Modified to work on Fabric + */ +@ApiStatus.Internal +public abstract class QuiltDataFixesInternals { + private static final Logger LOGGER = LogUtils.getLogger(); + + public record DataFixerEntry(DataFixer dataFixer, int currentVersion) {} + + @Contract(pure = true) + @Range(from = 0, to = Integer.MAX_VALUE) + public static int getModDataVersion(@NotNull CompoundTag compound, @NotNull String modId) { + return compound.getInt(modId + "_DataVersion"); + } + + @Contract(pure = true) + @Range(from = 0, to = Integer.MAX_VALUE) + public static int getModMinecraftDataVersion(@NotNull CompoundTag compound, @NotNull String modId) { + return compound.getInt(modId + "_DataVersion_Minecraft"); + } + + private static QuiltDataFixesInternals instance; + + public static @NotNull QuiltDataFixesInternals get() { + if (instance == null) { + Schema latestVanillaSchema; + try { + latestVanillaSchema = DataFixers.getDataFixer() + .getSchema(DataFixUtils.makeKey(SharedConstants.getCurrentVersion().getDataVersion().getVersion())); + } catch (Exception e) { + latestVanillaSchema = null; + } + + if (latestVanillaSchema == null) { + LOGGER.warn("[Quilt DFU API] Failed to initialize! Either someone stopped DFU from initializing,"); + LOGGER.warn("[Quilt DFU API] or this Minecraft build is hosed."); + LOGGER.warn("[Quilt DFU API] Using no-op implementation."); + instance = new NoOpQuiltDataFixesInternals(); + } else { + instance = new QuiltDataFixesInternalsImpl(latestVanillaSchema); + } + } + + return instance; + } + + public abstract void registerFixer(@NotNull String modId, @Range(from = 0, to = Integer.MAX_VALUE) int currentVersion, @NotNull DataFixer dataFixer); + + public abstract boolean isEmpty(); + + public abstract @Nullable DataFixerEntry getFixerEntry(@NotNull String modId); + + public abstract void registerMinecraftFixer(@NotNull String modId, @Range(from = 0, to = Integer.MAX_VALUE) int currentVersion, @NotNull DataFixer dataFixer); + + public abstract @Nullable DataFixerEntry getMinecraftFixerEntry(@NotNull String modId); + + @Contract(value = "-> new", pure = true) + public abstract @NotNull Schema createBaseSchema(); + + public abstract @NotNull Dynamic updateWithAllFixers(@NotNull DataFixTypes dataFixTypes, @NotNull Dynamic dynamic); + + public abstract @NotNull CompoundTag addModDataVersions(@NotNull CompoundTag compound); + + public abstract void freeze(); + + @Contract(pure = true) + public abstract boolean isFrozen(); +} diff --git a/src/main/java/org/quiltmc/qsl/frozenblock/misc/datafixerupper/impl/QuiltDataFixesInternalsImpl.java b/src/main/java/org/quiltmc/qsl/frozenblock/misc/datafixerupper/impl/QuiltDataFixesInternalsImpl.java new file mode 100644 index 0000000..4ce39c4 --- /dev/null +++ b/src/main/java/org/quiltmc/qsl/frozenblock/misc/datafixerupper/impl/QuiltDataFixesInternalsImpl.java @@ -0,0 +1,151 @@ +/* + * Copyright 2024 The Quilt Project + * Copyright 2024 FrozenBlock + * Modified to work on Fabric + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.quiltmc.qsl.frozenblock.misc.datafixerupper.impl; + +import com.mojang.datafixers.DataFixer; +import com.mojang.datafixers.schemas.Schema; +import com.mojang.serialization.Dynamic; +import it.unimi.dsi.fastutil.objects.Object2ReferenceOpenHashMap; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.Tag; +import net.minecraft.util.datafix.DataFixTypes; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.annotations.Range; +import org.quiltmc.qsl.frozenblock.misc.datafixerupper.mixin.DataFixTypesAccessor; + +import java.util.Collections; +import java.util.Map; + +/** + * Modified to work on Fabric + */ +@ApiStatus.Internal +public final class QuiltDataFixesInternalsImpl extends QuiltDataFixesInternals { + private final @NotNull Schema latestVanillaSchema; + + private Map modDataFixers; + private Map modMinecraftDataFixers; + private boolean frozen; + + public QuiltDataFixesInternalsImpl(@NotNull Schema latestVanillaSchema) { + this.latestVanillaSchema = latestVanillaSchema; + + this.modDataFixers = new Object2ReferenceOpenHashMap<>(); + this.modMinecraftDataFixers = new Object2ReferenceOpenHashMap<>(); + this.frozen = false; + } + + @Override + public void registerFixer(@NotNull String modId, @Range(from = 0, to = Integer.MAX_VALUE) int currentVersion, @NotNull DataFixer dataFixer) { + if (this.modDataFixers.containsKey(modId)) { + throw new IllegalArgumentException("Mod '" + modId + "' already has a registered data fixer"); + } + + this.modDataFixers.put(modId, new DataFixerEntry(dataFixer, currentVersion)); + } + + @Override + public boolean isEmpty() { + return this.modDataFixers.isEmpty() && this.modMinecraftDataFixers.isEmpty(); + } + + @Override + public @Nullable DataFixerEntry getFixerEntry(@NotNull String modId) { + return modDataFixers.get(modId); + } + + @Override + public void registerMinecraftFixer(@NotNull String modId, @Range(from = 0, to = Integer.MAX_VALUE) int currentVersion, @NotNull DataFixer dataFixer) { + if (this.modMinecraftDataFixers.containsKey(modId)) { + throw new IllegalArgumentException("Mod '" + modId + "' already has a registered Minecraft-version-based data fixer"); + } + + this.modMinecraftDataFixers.put(modId, new DataFixerEntry(dataFixer, currentVersion)); + } + + @Override + public @Nullable DataFixerEntry getMinecraftFixerEntry(@NotNull String modId) { + return modMinecraftDataFixers.get(modId); + } + + @Override + public @NotNull Schema createBaseSchema() { + return new Schema(0, this.latestVanillaSchema); + } + + @Override + public @NotNull Dynamic updateWithAllFixers(@NotNull DataFixTypes dataFixTypes, @NotNull Dynamic current) { + var compound = (CompoundTag) current.getValue(); + + for (Map.Entry entry : this.modMinecraftDataFixers.entrySet()) { + int modDataVersion = getModMinecraftDataVersion(compound, entry.getKey()); + DataFixerEntry dataFixerEntry = entry.getValue(); + current = dataFixerEntry.dataFixer().update( + DataFixTypesAccessor.class.cast(dataFixTypes).getType(), + current, + modDataVersion, + dataFixerEntry.currentVersion() + ); + } + + for (Map.Entry entry : this.modDataFixers.entrySet()) { + int modDataVersion = getModDataVersion(compound, entry.getKey()); + DataFixerEntry dataFixerEntry = entry.getValue(); + + current = dataFixerEntry.dataFixer().update( + DataFixTypesAccessor.class.cast(dataFixTypes).getType(), + current, + modDataVersion, + dataFixerEntry.currentVersion() + ); + } + + return current; + } + + @Override + public @NotNull CompoundTag addModDataVersions(@NotNull CompoundTag compound) { + for (Map.Entry entry : this.modDataFixers.entrySet()) { + compound.putInt(entry.getKey() + "_DataVersion", entry.getValue().currentVersion()); + } + for (Map.Entry entry : this.modMinecraftDataFixers.entrySet()) { + compound.putInt(entry.getKey() + "_DataVersion_Minecraft", entry.getValue().currentVersion()); + } + + return compound; + } + + @Override + public void freeze() { + if (!this.frozen) { + this.modDataFixers = Collections.unmodifiableMap(this.modDataFixers); + this.modMinecraftDataFixers = Collections.unmodifiableMap(this.modMinecraftDataFixers); + } + + this.frozen = true; + } + + @Override + public boolean isFrozen() { + return this.frozen; + } + +} diff --git a/src/main/java/org/quiltmc/qsl/frozenblock/misc/datafixerupper/impl/ServerFreezer.java b/src/main/java/org/quiltmc/qsl/frozenblock/misc/datafixerupper/impl/ServerFreezer.java new file mode 100644 index 0000000..c720145 --- /dev/null +++ b/src/main/java/org/quiltmc/qsl/frozenblock/misc/datafixerupper/impl/ServerFreezer.java @@ -0,0 +1,35 @@ +/* + * Copyright 2024 The Quilt Project + * Copyright 2024 FrozenBlock + * Modified to work on Fabric + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.quiltmc.qsl.frozenblock.misc.datafixerupper.impl; + +import net.frozenblock.lib.FrozenLogUtils; +import org.jetbrains.annotations.ApiStatus; + +/** + * Modified to work on Fabric + */ +@ApiStatus.Internal +public final class ServerFreezer { + + public static void onInitialize() { + FrozenLogUtils.log("[Quilt DFU API] Serverside DataFixer Registry is about to freeze", true); + QuiltDataFixesInternals.get().freeze(); + FrozenLogUtils.log("[Quilt DFU API] Serverside DataFixer Registry was frozen", true); + } +} diff --git a/src/main/java/org/quiltmc/qsl/frozenblock/misc/datafixerupper/mixin/ChunkStorageMixin.java b/src/main/java/org/quiltmc/qsl/frozenblock/misc/datafixerupper/mixin/ChunkStorageMixin.java new file mode 100644 index 0000000..f4dcc58 --- /dev/null +++ b/src/main/java/org/quiltmc/qsl/frozenblock/misc/datafixerupper/mixin/ChunkStorageMixin.java @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2024 FrozenBlock + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.quiltmc.qsl.frozenblock.misc.datafixerupper.mixin; + +import com.llamalad7.mixinextras.injector.wrapoperation.Operation; +import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; +import net.minecraft.world.level.chunk.storage.ChunkStorage; +import net.minecraft.world.level.storage.DataVersion; +import org.quiltmc.qsl.frozenblock.misc.datafixerupper.impl.QuiltDataFixesInternals; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; + +@Mixin(ChunkStorage.class) +public class ChunkStorageMixin { + + @WrapOperation(method = "upgradeChunkTag", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/level/storage/DataVersion;getVersion()I")) + private int bypassCheck(DataVersion instance, Operation original) { + if (!QuiltDataFixesInternals.get().isEmpty()) { + return -1; + } + + return original.call(instance); + } +} diff --git a/src/main/java/org/quiltmc/qsl/frozenblock/misc/datafixerupper/mixin/DataFixTypesAccessor.java b/src/main/java/org/quiltmc/qsl/frozenblock/misc/datafixerupper/mixin/DataFixTypesAccessor.java new file mode 100644 index 0000000..89b9d90 --- /dev/null +++ b/src/main/java/org/quiltmc/qsl/frozenblock/misc/datafixerupper/mixin/DataFixTypesAccessor.java @@ -0,0 +1,31 @@ +/* + * Copyright 2024 The Quilt Project + * Copyright 2024 FrozenBlock + * Modified to work on Fabric + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.quiltmc.qsl.frozenblock.misc.datafixerupper.mixin; + +import com.mojang.datafixers.DSL; +import net.minecraft.util.datafix.DataFixTypes; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Accessor; + +@Mixin(DataFixTypes.class) +public interface DataFixTypesAccessor { + + @Accessor + DSL.TypeReference getType(); +} diff --git a/src/main/java/org/quiltmc/qsl/frozenblock/misc/datafixerupper/mixin/DataFixTypesMixin.java b/src/main/java/org/quiltmc/qsl/frozenblock/misc/datafixerupper/mixin/DataFixTypesMixin.java new file mode 100644 index 0000000..598a1aa --- /dev/null +++ b/src/main/java/org/quiltmc/qsl/frozenblock/misc/datafixerupper/mixin/DataFixTypesMixin.java @@ -0,0 +1,59 @@ +/* + * Copyright 2024 The Quilt Project + * Copyright 2024 FrozenBlock + * Modified to work on Fabric + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.quiltmc.qsl.frozenblock.misc.datafixerupper.mixin; + +import com.llamalad7.mixinextras.injector.ModifyReturnValue; +import com.mojang.datafixers.DSL; +import com.mojang.datafixers.DataFixer; +import com.mojang.serialization.Dynamic; +import net.frozenblock.lib.config.frozenlib_config.FrozenLibConfig; +import net.minecraft.nbt.Tag; +import net.minecraft.util.datafix.DataFixTypes; +import org.quiltmc.qsl.frozenblock.misc.datafixerupper.impl.QuiltDataFixesInternals; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; + +/** + * Modified to work on Fabric + * Original name was NbtHelperMixin + */ +@Mixin(value = DataFixTypes.class, priority = 1001) +public class DataFixTypesMixin { + + @Shadow + @Final + private DSL.TypeReference type; + + @ModifyReturnValue( + method = "update(Lcom/mojang/datafixers/DataFixer;Lcom/mojang/serialization/Dynamic;II)Lcom/mojang/serialization/Dynamic;", + at = @At("RETURN") + ) + private Dynamic updateDataWithFixers(Dynamic original, DataFixer fixer, Dynamic dynamic, int oldVersion, int targetVersion) { + var type = DataFixTypes.class.cast(this); + var value = original.getValue(); + + if (value instanceof Tag && !FrozenLibConfig.get().dataFixer.disabledDataFixTypes.contains(this.type.typeName())) { + //noinspection unchecked + return (Dynamic) QuiltDataFixesInternals.get().updateWithAllFixers(type, (Dynamic) original); + } + return original; + } +} diff --git a/src/main/java/org/quiltmc/qsl/frozenblock/misc/datafixerupper/mixin/NbtUtilsMixin.java b/src/main/java/org/quiltmc/qsl/frozenblock/misc/datafixerupper/mixin/NbtUtilsMixin.java new file mode 100644 index 0000000..65ac781 --- /dev/null +++ b/src/main/java/org/quiltmc/qsl/frozenblock/misc/datafixerupper/mixin/NbtUtilsMixin.java @@ -0,0 +1,35 @@ +/* + * Copyright 2024 The Quilt Project + * Copyright 2024 FrozenBlock + * Modified to work on Fabric + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.quiltmc.qsl.frozenblock.misc.datafixerupper.mixin; + +import com.llamalad7.mixinextras.injector.ModifyReturnValue; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.NbtUtils; +import org.quiltmc.qsl.frozenblock.misc.datafixerupper.impl.QuiltDataFixesInternals; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; + +@Mixin(NbtUtils.class) +public class NbtUtilsMixin { + + @ModifyReturnValue(method = "addDataVersion", at = @At("RETURN")) + private static CompoundTag addDataVersion(CompoundTag original) { + return QuiltDataFixesInternals.get().addModDataVersions(original); + } +} diff --git a/src/main/java/org/quiltmc/qsl/frozenblock/resource/loader/api/ResourceLoaderEvent.java b/src/main/java/org/quiltmc/qsl/frozenblock/resource/loader/api/ResourceLoaderEvent.java new file mode 100644 index 0000000..6a31757 --- /dev/null +++ b/src/main/java/org/quiltmc/qsl/frozenblock/resource/loader/api/ResourceLoaderEvent.java @@ -0,0 +1,62 @@ +/* + * Copyright 2024 The Quilt Project + * Copyright 2024 FrozenBlock + * Modified to work on Fabric + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.quiltmc.qsl.frozenblock.resource.loader.api; + +import net.frozenblock.lib.entrypoint.api.CommonEventEntrypoint; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.packs.resources.ResourceManager; +import net.neoforged.bus.api.Event; +import org.jetbrains.annotations.Nullable; + + +public class ResourceLoaderEvent extends Event { + + @Nullable public final MinecraftServer server; + @Nullable public final ResourceManager resourceManager; + + private ResourceLoaderEvent(final @Nullable MinecraftServer server, final @Nullable ResourceManager resourceManager) { + this.server = server; + this.resourceManager = resourceManager; + } + + /** + * An event indicating the start of the reloading of data packs on a Minecraft server. + *

+ * This event should not be used to load resources.*//*, use {@link ResourceLoader#registerReloader(IdentifiableResourceReloader)} instead. + */ + public static class StartDataPackReload extends ResourceLoaderEvent { + + public StartDataPackReload(@Nullable MinecraftServer server, @Nullable ResourceManager resourceManager) { + super(server, resourceManager); + } + } + + /** + * An event indicating the end of the reloading of data packs on a Minecraft server. + *

+ * This event should not be used to load resources.*//*, use {@link ResourceLoader#registerReloader(IdentifiableResourceReloader)} instead. + */ + public static class EndDataPackReload extends ResourceLoaderEvent { + @Nullable public final Throwable throwable; + public EndDataPackReload(@Nullable final MinecraftServer server, @Nullable final ResourceManager resourceManager, @Nullable final Throwable throwable) { + super(server, resourceManager); + this.throwable = throwable; + } + } +} diff --git a/src/main/java/org/quiltmc/qsl/frozenblock/resource/loader/mixin/MinecraftServerMixin.java b/src/main/java/org/quiltmc/qsl/frozenblock/resource/loader/mixin/MinecraftServerMixin.java new file mode 100644 index 0000000..893f60c --- /dev/null +++ b/src/main/java/org/quiltmc/qsl/frozenblock/resource/loader/mixin/MinecraftServerMixin.java @@ -0,0 +1,56 @@ +/* + * Copyright 2024 The Quilt Project + * Copyright 2024 FrozenBlock + * Modified to work on Fabric + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.quiltmc.qsl.frozenblock.resource.loader.mixin; + +import com.llamalad7.mixinextras.injector.ModifyReturnValue; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.packs.resources.ResourceManager; +import net.neoforged.neoforge.common.NeoForge; +import org.quiltmc.qsl.frozenblock.resource.loader.api.ResourceLoaderEvent; +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.CallbackInfoReturnable; + +import java.util.Collection; +import java.util.concurrent.CompletableFuture; + +/** + * Modified to work on Fabric + */ +@Mixin(MinecraftServer.class) +public abstract class MinecraftServerMixin { + @Shadow + public abstract ResourceManager getResourceManager(); + + @Inject(method = "reloadResources", at = @At("HEAD")) + private void onReloadResourcesStart(Collection collection, CallbackInfoReturnable> cir) { + NeoForge.EVENT_BUS.post(new ResourceLoaderEvent.StartDataPackReload((MinecraftServer) (Object) this, this.getResourceManager())); + } + + @ModifyReturnValue(method = "reloadResources", at = @At("RETURN")) + private CompletableFuture onReloadResourcesEnd(CompletableFuture original) { + original.handleAsync((value, throwable) -> { + NeoForge.EVENT_BUS.post(new ResourceLoaderEvent.EndDataPackReload((MinecraftServer) (Object) this, this.getResourceManager(), throwable)); + return value; + }, (MinecraftServer) (Object) this); + return original; + } +} diff --git a/src/main/java/org/quiltmc/qsl/frozenblock/resource/loader/mixin/client/CreateWorldScreenMixin.java b/src/main/java/org/quiltmc/qsl/frozenblock/resource/loader/mixin/client/CreateWorldScreenMixin.java new file mode 100644 index 0000000..f50a671 --- /dev/null +++ b/src/main/java/org/quiltmc/qsl/frozenblock/resource/loader/mixin/client/CreateWorldScreenMixin.java @@ -0,0 +1,107 @@ +/* + * Copyright 2024 The Quilt Project + * Copyright 2024 FrozenBlock + * Modified to work on Fabric + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.quiltmc.qsl.frozenblock.resource.loader.mixin.client; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.client.gui.screens.worldselection.CreateWorldScreen; +import net.minecraft.client.gui.screens.worldselection.WorldCreationContext; +import net.minecraft.core.LayeredRegistryAccess; +import net.minecraft.server.ReloadableServerResources; +import net.minecraft.server.packs.repository.PackRepository; +import net.minecraft.server.packs.resources.CloseableResourceManager; +import net.minecraft.world.level.WorldDataConfiguration; +import net.neoforged.api.distmarker.Dist; +import net.neoforged.api.distmarker.OnlyIn; +import net.neoforged.neoforge.common.NeoForge; +import org.quiltmc.qsl.frozenblock.resource.loader.api.ResourceLoaderEvent; +import org.spongepowered.asm.mixin.Dynamic; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Coerce; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +import java.util.function.Consumer; + +/** + * Modified to work on Fabric + */ +@OnlyIn(Dist.CLIENT) +@Mixin(CreateWorldScreen.class) +public abstract class CreateWorldScreenMixin { + + @Dynamic + @Inject( + method = {"m_qcsfhvrb", "method_41851", "lambda$openFresh$2"}, + at = @At("HEAD"), + require = 1 + ) + private static void onEndDataPackLoadOnOpen(CloseableResourceManager resourceManager, ReloadableServerResources resources, + LayeredRegistryAccess layeredRegistryAccess, @Coerce Object object, CallbackInfoReturnable cir) { + NeoForge.EVENT_BUS.post(new ResourceLoaderEvent.EndDataPackReload(null, resourceManager, null)); + } + + @Inject( + method = "applyNewPackConfig", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/server/WorldLoader;load(Lnet/minecraft/server/WorldLoader$InitConfig;Lnet/minecraft/server/WorldLoader$WorldDataSupplier;Lnet/minecraft/server/WorldLoader$ResultFactory;Ljava/util/concurrent/Executor;Ljava/util/concurrent/Executor;)Ljava/util/concurrent/CompletableFuture;" + ) + ) + private void onDataPackLoadStart(PackRepository packRepository, WorldDataConfiguration worldDataConfiguration, Consumer consumer, CallbackInfo ci) { + NeoForge.EVENT_BUS.post(new ResourceLoaderEvent.StartDataPackReload(null, null)); + } + + @Inject( + method = "openFresh", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/server/WorldLoader;load(Lnet/minecraft/server/WorldLoader$InitConfig;Lnet/minecraft/server/WorldLoader$WorldDataSupplier;Lnet/minecraft/server/WorldLoader$ResultFactory;Ljava/util/concurrent/Executor;Ljava/util/concurrent/Executor;)Ljava/util/concurrent/CompletableFuture;" + ) + ) + private static void onDataPackLoadStart(Minecraft client, Screen parent, CallbackInfo ci) { + NeoForge.EVENT_BUS.post(new ResourceLoaderEvent.StartDataPackReload(null, null)); + } + + @Dynamic + @Inject( + method = { "lambda$applyNewPackConfig$14", "lambda$applyNewPackConfig$13"}, + at = @At("HEAD"), + require = 1 + ) + private static void onCreateDataPackLoadEnd(CloseableResourceManager resourceManager, ReloadableServerResources resources, + LayeredRegistryAccess layeredRegistryAccess, @Coerce Object object, CallbackInfoReturnable cir) { + NeoForge.EVENT_BUS.post(new ResourceLoaderEvent.EndDataPackReload(null, resourceManager, null)); + } + + @Inject( + method = "lambda$applyNewPackConfig$17(Ljava/util/function/Consumer;Ljava/lang/Void;Ljava/lang/Throwable;)Ljava/lang/Object;", + at = @At( + value = "INVOKE", + target = "Lorg/slf4j/Logger;warn(Ljava/lang/String;Ljava/lang/Throwable;)V", + shift = At.Shift.AFTER, + remap = false + ) + ) + private static void onFailDataPackLoading(Consumer consumer, Void unused, Throwable throwable, CallbackInfoReturnable cir) { + NeoForge.EVENT_BUS.post(new ResourceLoaderEvent.EndDataPackReload(null, null, throwable)); + } +} diff --git a/src/main/java/org/quiltmc/qsl/frozenblock/resource/loader/mixin/client/IntegratedServerLoaderMixin.java b/src/main/java/org/quiltmc/qsl/frozenblock/resource/loader/mixin/client/IntegratedServerLoaderMixin.java new file mode 100644 index 0000000..3917f54 --- /dev/null +++ b/src/main/java/org/quiltmc/qsl/frozenblock/resource/loader/mixin/client/IntegratedServerLoaderMixin.java @@ -0,0 +1,71 @@ +/* + * Copyright 2024 The Quilt Project + * Copyright 2024 FrozenBlock + * Modified to work on Fabric + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.quiltmc.qsl.frozenblock.resource.loader.mixin.client; + +import com.llamalad7.mixinextras.injector.ModifyReturnValue; +import net.minecraft.client.gui.screens.worldselection.WorldOpenFlows; +import net.minecraft.server.WorldLoader; +import net.minecraft.server.WorldStem; +import net.neoforged.neoforge.common.NeoForge; +import org.quiltmc.qsl.frozenblock.resource.loader.api.ResourceLoaderEvent; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.ModifyArg; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +/** + * Modified to work on Fabric + */ +@Mixin(WorldOpenFlows.class) +public abstract class IntegratedServerLoaderMixin { + + + @Inject( + method = "loadWorldDataBlocking", + at = @At("HEAD") + ) + private void onStartDataPackLoad(WorldLoader.PackConfig dataPackConfig, WorldLoader.WorldDataSupplier savePropertiesSupplier, + WorldLoader.ResultFactory resultFactory, + CallbackInfoReturnable cir) { + NeoForge.EVENT_BUS.post(new ResourceLoaderEvent.StartDataPackReload(null, null)); + } + + @ModifyReturnValue( + method = "loadWorldDataBlocking", + at = @At("RETURN") + ) + private R onEndDataPackLoad(R original, WorldLoader.PackConfig dataPackConfig, WorldLoader.WorldDataSupplier savePropertiesSupplier, + WorldLoader.ResultFactory resultFactory) { + if (original instanceof WorldStem worldStem) { + NeoForge.EVENT_BUS.post(new ResourceLoaderEvent.EndDataPackReload(null, worldStem.resourceManager(), null)); + } + return original; + } + + @ModifyArg( + method = {"createFreshLevel", "loadLevel"}, + at = @At(value = "INVOKE", target = "Lorg/slf4j/Logger;warn(Ljava/lang/String;Ljava/lang/Throwable;)V", remap = false), + index = 1 + ) + private Throwable onFailedDataPackLoad(Throwable throwable) { + NeoForge.EVENT_BUS.post(new ResourceLoaderEvent.EndDataPackReload(null, null, throwable)); + return throwable; // noop + } +} diff --git a/src/main/java/org/quiltmc/qsl/frozenblock/resource/loader/mixin/server/MainMixin.java b/src/main/java/org/quiltmc/qsl/frozenblock/resource/loader/mixin/server/MainMixin.java new file mode 100644 index 0000000..64a06e2 --- /dev/null +++ b/src/main/java/org/quiltmc/qsl/frozenblock/resource/loader/mixin/server/MainMixin.java @@ -0,0 +1,76 @@ +/* + * Copyright 2024 The Quilt Project + * Copyright 2024 FrozenBlock + * Modified to work on Fabric + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.quiltmc.qsl.frozenblock.resource.loader.mixin.server; + +import net.minecraft.server.Main; +import net.minecraft.server.WorldStem; +import net.neoforged.neoforge.common.NeoForge; +import org.quiltmc.qsl.frozenblock.resource.loader.api.ResourceLoaderEvent; +import org.slf4j.Logger; +import org.spongepowered.asm.mixin.Final; +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.ModifyArg; +import org.spongepowered.asm.mixin.injection.ModifyVariable; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +/** + * Modified to work on Fabric + */ +@Mixin(Main.class) +public class MainMixin { + @Shadow + @Final + private static Logger LOGGER; + + @Inject( + method = "main", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/Util;blockUntilDone(Ljava/util/function/Function;)Ljava/util/concurrent/CompletableFuture;", + remap = true + ), + remap = false + ) + private static void onStartReloadResources(String[] strings, CallbackInfo ci) { + NeoForge.EVENT_BUS.post(new ResourceLoaderEvent.StartDataPackReload(null, null)); + } + + @ModifyVariable(method = "main", at = @At(value = "STORE"), remap = false) + private static WorldStem onSuccessfulReloadResources(WorldStem resources) { + NeoForge.EVENT_BUS.post(new ResourceLoaderEvent.EndDataPackReload(null, resources.resourceManager(), null)); + return resources; // noop + } + + @ModifyArg( + method = "main", + at = @At( + value = "INVOKE", + target = "Lorg/slf4j/Logger;warn(Ljava/lang/String;Ljava/lang/Throwable;)V" + ), + index = 1, + remap = false + ) + private static Throwable onFailedReloadResources(Throwable exception) { + NeoForge.EVENT_BUS.post(new ResourceLoaderEvent.EndDataPackReload(null, null, exception)); + return exception; // noop + } +} diff --git a/src/main/resources/META-INF/accesstransformer.cfg b/src/main/resources/META-INF/accesstransformer.cfg new file mode 100644 index 0000000..fb5b6c5 --- /dev/null +++ b/src/main/resources/META-INF/accesstransformer.cfg @@ -0,0 +1,195 @@ +# AccessTransformer generated with liukrast translator + +# Advancement +public-f net.minecraft.advancements.critereon.EffectsChangedTrigger$TriggerInstance effects +public-f net.minecraft.advancements.critereon.MobEffectsPredicate effectMap + +# Network +public-f net.minecraft.client.multiplayer.ClientCommonPacketListenerImpl connection +public-f net.minecraft.server.network.ServerCommonPacketListenerImpl connection +public-f net.minecraft.server.network.ServerConfigurationPacketListenerImpl finishCurrentTask(Lnet/minecraft/server/network/ConfigurationTask$Type;)V +public-f net.minecraft.server.network.ServerConfigurationPacketListenerImpl currentTask + +# Command +public-f net.minecraft.commands.synchronization.ArgumentTypeInfos register(Lnet/minecraft/core/Registry;Ljava/lang/String;Ljava/lang/Class;Lnet/minecraft/commands/synchronization/ArgumentTypeInfo;)Lnet/minecraft/commands/synchronization/ArgumentTypeInfo; +public-f net.minecraft.commands.synchronization.ArgumentTypeInfos fixClassType(Ljava/lang/Class;)Ljava/lang/Class; + +# Recipe +public-f net.minecraft.world.item.crafting.Ingredient$ItemValue (Lnet/minecraft/world/item/ItemStack;)V +public-f net.minecraft.world.item.ItemStack ITEM_NON_AIR_CODEC + +# Sculk Spreading + +public net.minecraft.world.level.block.SculkVeinBlock$SculkVeinSpreaderConfig +# Tree Features +public-f net.minecraft.world.level.levelgen.feature.treedecorators.TreeDecoratorType (Lcom/mojang/serialization/MapCodec;)V +public-f net.minecraft.world.level.levelgen.feature.trunkplacers.TrunkPlacerType (Lcom/mojang/serialization/MapCodec;)V +public-f net.minecraft.world.level.levelgen.feature.foliageplacers.FoliagePlacerType (Lcom/mojang/serialization/MapCodec;)V + +# Items +public-f net.minecraft.world.item.ItemCooldowns cooldowns +public-f net.minecraft.world.item.ItemCooldowns tickCount +public-f net.minecraft.world.item.ItemCooldowns$CooldownInstance startTime +public-f net.minecraft.world.item.ItemCooldowns$CooldownInstance endTime +public-f net.minecraft.world.item.ItemCooldowns$CooldownInstance (II)V +public-f net.minecraft.world.item.ItemStack components +public-f net.minecraft.world.item.CreativeModeTabs generateInstrumentTypes(Lnet/minecraft/world/item/CreativeModeTab$Output;Lnet/minecraft/core/HolderLookup;Lnet/minecraft/world/item/Item;Lnet/minecraft/tags/TagKey;Lnet/minecraft/world/item/CreativeModeTab$TabVisibility;)V +public-f net.minecraft.client.renderer.item.ItemProperties register(Lnet/minecraft/world/item/Item;Lnet/minecraft/resources/ResourceLocation;Lnet/minecraft/client/renderer/item/ClampedItemPropertyFunction;)V + +public net.minecraft.world.item.CreativeModeTab$ItemDisplayBuilder +public net.minecraft.world.item.CreativeModeTab$TabVisibility +# Structures +public-f net.minecraft.world.level.levelgen.structure.StructurePiece getWorldPos(III)Lnet/minecraft/core/BlockPos$MutableBlockPos; +public-f net.minecraft.world.level.levelgen.structure.pools.SinglePoolElement (Lcom/mojang/datafixers/util/Either;Lnet/minecraft/core/Holder;Lnet/minecraft/world/level/levelgen/structure/pools/StructureTemplatePool$Projection;Ljava/util/Optional;)V +public-f net.minecraft.world.level.levelgen.structure.ScatteredFeaturePiece updateAverageGroundHeight(Lnet/minecraft/world/level/LevelAccessor;Lnet/minecraft/world/level/levelgen/structure/BoundingBox;I)Z +public-f net.minecraft.world.level.levelgen.structure.StructurePiece generateBox(Lnet/minecraft/world/level/WorldGenLevel;Lnet/minecraft/world/level/levelgen/structure/BoundingBox;IIIIIILnet/minecraft/world/level/block/state/BlockState;Lnet/minecraft/world/level/block/state/BlockState;Z)V +public-f net.minecraft.world.level.levelgen.structure.StructurePiece placeBlock(Lnet/minecraft/world/level/WorldGenLevel;Lnet/minecraft/world/level/block/state/BlockState;IIILnet/minecraft/world/level/levelgen/structure/BoundingBox;)V + +public net.minecraft.world.level.levelgen.structure.pools.alias.Random +# Entities +public-f net.minecraft.world.entity.Entity updateInWaterStateAndDoWaterCurrentPushing()V +public-f net.minecraft.server.level.ServerLevel entityManager +public-f net.minecraft.world.entity.ai.navigation.WallClimberNavigation pathToPosition +public-f net.minecraft.world.entity.LivingEntity canBreatheUnderwater()Z + +public net.minecraft.world.entity.monster.warden.Warden$VibrationUser +# Entity Models +public-f net.minecraft.world.entity.Entity DATA_POSE +public-f net.minecraft.client.model.HierarchicalModel animate(Lnet/minecraft/world/entity/AnimationState;Lnet/minecraft/client/animation/AnimationDefinition;F)V +public-f net.minecraft.world.entity.LivingEntity dead + +# Entity Spawns + +public net.minecraft.world.entity.SpawnPlacements$Data +# Mobs +public-f net.minecraft.world.effect.MobEffect (Lnet/minecraft/world/effect/MobEffectCategory;I)V +public-f net.minecraft.world.entity.Mob goalSelector +public-f net.minecraft.world.entity.Mob targetSelector +public-f net.minecraft.world.entity.monster.warden.Warden isDiggingOrEmerging()Z + +# Damage Sources +public-f net.minecraft.world.damagesource.DamageSources source(Lnet/minecraft/resources/ResourceKey;)Lnet/minecraft/world/damagesource/DamageSource; +public-f net.minecraft.world.damagesource.DamageSources source(Lnet/minecraft/resources/ResourceKey;Lnet/minecraft/world/entity/Entity;Lnet/minecraft/world/entity/Entity;)Lnet/minecraft/world/damagesource/DamageSource; + +# Blocks +public-f net.minecraft.world.level.block.Block registerDefaultState(Lnet/minecraft/world/level/block/state/BlockState;)V +public-f net.minecraft.client.renderer.Sheets chestMaterial(Ljava/lang/String;)Lnet/minecraft/client/resources/model/Material; +public-f net.minecraft.world.CompoundContainer container1 +public-f net.minecraft.world.CompoundContainer container2 +public-f net.minecraft.world.level.block.PointedDripstoneBlock$FluidInfo (Lnet/minecraft/core/BlockPos;Lnet/minecraft/world/level/material/Fluid;Lnet/minecraft/world/level/block/state/BlockState;)V +public-f net.minecraft.world.level.block.MultifaceSpreader config +public-f net.minecraft.data.BlockFamilies familyBuilder(Lnet/minecraft/world/level/block/Block;)Lnet/minecraft/data/BlockFamily$Builder; + +# Block Entities +public-f net.minecraft.world.level.block.entity.RandomizableContainerBlockEntity lootTable +public-f net.minecraft.world.level.block.entity.BrushableBlockEntity lootTable + +# Render Types +public-f net.minecraft.client.renderer.RenderStateShard RENDERTYPE_ENTITY_TRANSLUCENT_EMISSIVE_SHADER +public-f net.minecraft.client.renderer.RenderStateShard NO_CULL +public-f net.minecraft.client.renderer.RenderStateShard COLOR_DEPTH_WRITE +public-f net.minecraft.client.renderer.RenderStateShard COLOR_WRITE +public-f net.minecraft.client.renderer.RenderStateShard DEPTH_WRITE +public-f net.minecraft.client.renderer.RenderStateShard NO_LAYERING +public-f net.minecraft.client.renderer.RenderStateShard POLYGON_OFFSET_LAYERING +public-f net.minecraft.client.renderer.RenderStateShard VIEW_OFFSET_Z_LAYERING +public-f net.minecraft.client.renderer.RenderStateShard OVERLAY +public-f net.minecraft.client.renderer.RenderType$CompositeRenderType (Ljava/lang/String;Lcom/mojang/blaze3d/vertex/VertexFormat;Lcom/mojang/blaze3d/vertex/VertexFormat$Mode;IZZLnet/minecraft/client/renderer/RenderType$CompositeState;)V +public-f net.minecraft.client.renderer.RenderStateShard$TextureStateShard (Lnet/minecraft/resources/ResourceLocation;ZZ)V +public-f net.minecraft.client.renderer.RenderStateShard TRANSLUCENT_TRANSPARENCY +public-f net.minecraft.client.renderer.RenderStateShard RENDERTYPE_TRANSLUCENT_SHADER +public-f net.minecraft.client.renderer.RenderStateShard$ShaderStateShard (Ljava/util/function/Supplier;)V +public-f net.minecraft.client.renderer.RenderStateShard RENDERTYPE_TEXT_INTENSITY_SEE_THROUGH_SHADER +public-f net.minecraft.client.renderer.RenderStateShard LIGHTMAP +public-f net.minecraft.client.renderer.RenderStateShard NO_DEPTH_TEST +public-f net.minecraft.client.renderer.RenderStateShard EQUAL_DEPTH_TEST +public-f net.minecraft.client.renderer.RenderStateShard TRANSLUCENT_TARGET +public-f net.minecraft.client.renderer.RenderType create(Ljava/lang/String;Lcom/mojang/blaze3d/vertex/VertexFormat;Lcom/mojang/blaze3d/vertex/VertexFormat$Mode;ILnet/minecraft/client/renderer/RenderType$CompositeState;)Lnet/minecraft/client/renderer/RenderType$CompositeRenderType; +public-f net.minecraft.client.renderer.RenderStateShard POSITION_COLOR_SHADER + +public net.minecraft.client.renderer.RenderType$CompositeState +public net.minecraft.client.renderer.RenderStateShard$OutputStateShard +# Biomes +public-f net.minecraft.data.worldgen.biome.OverworldBiomes calculateSkyColor(F)I +public-f net.minecraft.world.level.levelgen.feature.stateproviders.SimpleStateProvider (Lnet/minecraft/world/level/block/state/BlockState;)V +public-f net.minecraft.world.level.biome.OverworldBiomeBuilder addBiomes(Ljava/util/function/Consumer;)V + +# Noise +public-f net.minecraft.data.worldgen.NoiseData register(Lnet/minecraft/data/worldgen/BootstrapContext;Lnet/minecraft/resources/ResourceKey;ID[D)V + +# Surface Rules +public-f net.minecraft.world.level.levelgen.SurfaceRules$SequenceRuleSource (Ljava/util/List;)V +public-f net.minecraft.world.level.levelgen.SurfaceRules isBiome(Ljava/util/List;)Lnet/minecraft/world/level/levelgen/SurfaceRules$BiomeConditionSource; +public-f net.minecraft.world.level.levelgen.SurfaceRules$Context biome + +public net.minecraft.world.level.levelgen.SurfaceRules$Condition +public net.minecraft.world.level.levelgen.SurfaceRules$Context +public net.minecraft.world.level.levelgen.SurfaceRules$LazyYCondition +# Placement Modifiers +public-f net.minecraft.world.level.levelgen.placement.PlacementContext level + +# QuiltMC Resource Reloaders +public-f net.minecraft.resources.RegistryDataLoader$Loader registry + +public net.minecraft.client.gui.screens.worldselection.CreateWorldScreen$DataPackReloadCookie +public net.minecraft.resources.RegistryDataLoader$LoadingFunction +# QuiltMC Surface Rules +public-f net.minecraft.world.level.levelgen.SurfaceRules$SequenceRule (Ljava/util/List;)V + +public net.minecraft.world.level.levelgen.SurfaceRules$SurfaceRule +public net.minecraft.world.level.levelgen.SurfaceRules$SequenceRule +# Ore Veins + +public net.minecraft.world.level.levelgen.OreVeinifier$VeinType +# Registry +public-f net.minecraft.data.registries.VanillaRegistries validateThatAllBiomeFeaturesHaveBiomeFilter(Lnet/minecraft/core/HolderLookup$Provider;)V +public-f net.minecraft.data.Main bindRegistries(Ljava/util/function/BiFunction;Ljava/util/concurrent/CompletableFuture;)Lnet/minecraft/data/DataProvider$Factory; +public-f net.minecraft.core.MappedRegistry getOrCreateHolderOrThrow(Lnet/minecraft/resources/ResourceKey;)Lnet/minecraft/core/Holder$Reference; +public-f net.minecraft.core.registries.BuiltInRegistries freeze()V +public net.minecraft.core.registries.BuiltInRegistries$RegistryBootstrap +# Sounds +public-f net.minecraft.client.sounds.SoundManager soundEngine +public-f net.minecraft.client.sounds.SoundEngine soundDeleteTime +public-f net.minecraft.client.sounds.SoundEngine tickCount +public-f net.minecraft.client.sounds.SoundEngine instanceToChannel +public-f net.minecraft.client.sounds.SoundEngine channelAccess +public-f net.minecraft.sounds.SoundEvent (Lnet/minecraft/resources/ResourceLocation;FZ)V + +#Camera +public-f net.minecraft.client.Camera setRotation(FF)V +public-f net.minecraft.client.Camera xRot +public-f net.minecraft.client.Camera yRot + +# Data Generation +public-f net.minecraft.data.DataGenerator vanillaPackOutput +public-f net.minecraft.data.DataGenerator$PackGenerator (Lnet/minecraft/data/DataGenerator;ZLjava/lang/String;Lnet/minecraft/data/PackOutput;)V + +public net.minecraft.data.DataGenerator$PackGenerator +# Particle +public-f net.minecraft.client.particle.Particle x +public-f net.minecraft.client.particle.Particle y +public-f net.minecraft.client.particle.Particle z + +# Minecraft Instance +public-f net.minecraft.client.Minecraft isMultiplayerServer()Z + +# Feature Flags + +# DataFixerUpper +public-f net.minecraft.util.datafix.DataFixers createRenamer(Ljava/util/Map;)Ljava/util/function/UnaryOperator; + +# World Loading +public-f net.minecraft.client.gui.screens.BackupConfirmScreen onProceed + +# Screens +public-f net.minecraft.client.gui.screens.Screen PANORAMA + +# Extra +public net.minecraft.client.model.geom.ModelPart$Vertex +protected-f net.minecraft.client.resources.sounds.AbstractTickableSoundInstance stop()V +public net.minecraft.world.level.block.PointedDripstoneBlock$FluidInfo +public net.minecraft.world.item.ItemCooldowns$CooldownInstance +public net.minecraft.resources.RegistryDataLoader$Loader +public net.minecraft.world.level.levelgen.SurfaceRules$SequenceRuleSource +public-f net.minecraft.world.flag.FeatureFlags REGISTRY +public-f net.minecraft.world.flag.FeatureFlags CODEC \ No newline at end of file diff --git a/src/main/resources/frozenlib_logo.png b/src/main/resources/frozenlib_logo.png index a7a007a..f1e0b0a 100644 Binary files a/src/main/resources/frozenlib_logo.png and b/src/main/resources/frozenlib_logo.png differ diff --git a/src/main/resources/mixin/frozenlib.core.mixins.json b/src/main/resources/mixin/frozenlib.core.mixins.json index dbaed57..3a31c3a 100644 --- a/src/main/resources/mixin/frozenlib.core.mixins.json +++ b/src/main/resources/mixin/frozenlib.core.mixins.json @@ -12,6 +12,5 @@ "WardenSpawnTrackerCommandMixin" ], "client": [ - "client.NoExperimentalMixin" ] } diff --git a/src/main/resources/mixin/frozenlib.event.mixins.json b/src/main/resources/mixin/frozenlib.event.mixins.json index 94f81f3..a737505 100644 --- a/src/main/resources/mixin/frozenlib.event.mixins.json +++ b/src/main/resources/mixin/frozenlib.event.mixins.json @@ -4,12 +4,17 @@ "package": "net.frozenblock.lib.event.mixin", "compatibilityLevel": "JAVA_21", "injectors": { - "defaultRequire": 1 + "defaultRequire": 1 }, "mixins": [ "BuiltInRegistriesMixin", "MappedRegistryMixin", "PlayerListMixin", "ServerLevelMixin" + ], + "client": [ + "ConnectionMixin", + "ClientLevelMixin", + "MinecraftMixin" ] } diff --git a/src/main/resources/mixin/frozenlib.item.mixins.json b/src/main/resources/mixin/frozenlib.item.mixins.json index 972622e..dcd7e2c 100644 --- a/src/main/resources/mixin/frozenlib.item.mixins.json +++ b/src/main/resources/mixin/frozenlib.item.mixins.json @@ -9,11 +9,12 @@ "mixins": [ "AbstractContainerMenuMixin", "ItemCooldownsMixin", - "ItemEntityMixin", "ItemStackMixin", "LivingEntityMixin", "ServerItemCooldownsMixin", "ServerPlayerMixin", - "bonemeal.BoneMealItemMixin" + "axe.AxeItemMixin", + "bonemeal.BoneMealItemMixin", + "shovel.ShovelItemMixin" ] } diff --git a/src/main/resources/mixin/frozenlib.axe.mixins.json b/src/main/resources/mixin/frozenlib.jankson.mixins.json similarity index 51% rename from src/main/resources/mixin/frozenlib.axe.mixins.json rename to src/main/resources/mixin/frozenlib.jankson.mixins.json index f5de467..87d3cf5 100644 --- a/src/main/resources/mixin/frozenlib.axe.mixins.json +++ b/src/main/resources/mixin/frozenlib.jankson.mixins.json @@ -1,12 +1,13 @@ { "required": true, "minVersion": "0.8", - "package": "net.frozenblock.lib.axe.mixin", + "package": "net.frozenblock.lib.jankson.mixin", "compatibilityLevel": "JAVA_21", "injectors": { - "defaultRequire": 1 + "defaultRequire": 1 }, "mixins": [ - "AxeItemMixin" + "JanksonMixin", + "JsonObjectMixin" ] } diff --git a/src/main/resources/mixin/frozenlib.modmenu.mixins.json b/src/main/resources/mixin/frozenlib.modmenu.mixins.json deleted file mode 100644 index af4738e..0000000 --- a/src/main/resources/mixin/frozenlib.modmenu.mixins.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "required": true, - "minVersion": "0.8", - "package": "net.frozenblock.lib.modmenu.mixin", - "compatibilityLevel": "JAVA_21", - "plugin": "net.frozenblock.lib.modmenu.mixin.plugin.RequiresModMenuMixinPlugin", - "injectors": { - "defaultRequire": 1 - }, - "client": [ - "client.BadgeMixin" - ] -} diff --git a/src/main/resources/mixin/frozenlib.shovel.mixins.json b/src/main/resources/mixin/frozenlib.shovel.mixins.json deleted file mode 100644 index a64d2bd..0000000 --- a/src/main/resources/mixin/frozenlib.shovel.mixins.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "required": true, - "minVersion": "0.8", - "package": "net.frozenblock.lib.shovel.mixin", - "compatibilityLevel": "JAVA_21", - "injectors": { - "defaultRequire": 1 - }, - "mixins": [ - "ShovelItemMixin" - ] -} diff --git a/src/main/resources/mixin/frozenlib.tag.mixins.json b/src/main/resources/mixin/frozenlib.tag.mixins.json index 70331da..4cdfd79 100644 --- a/src/main/resources/mixin/frozenlib.tag.mixins.json +++ b/src/main/resources/mixin/frozenlib.tag.mixins.json @@ -7,7 +7,6 @@ "defaultRequire": 1 }, "mixins": [ - "CreeperMixin", - "ServerPlayerGameModeMixin" + "CreeperMixin" ] } diff --git a/src/main/resources/mixin/frozenlib.worldgen.biome.mixins.json b/src/main/resources/mixin/frozenlib.worldgen.biome.mixins.json index 56cf0c3..01b9218 100644 --- a/src/main/resources/mixin/frozenlib.worldgen.biome.mixins.json +++ b/src/main/resources/mixin/frozenlib.worldgen.biome.mixins.json @@ -7,6 +7,7 @@ "defaultRequire": 1 }, "mixins": [ + "DebugLevelSourceAccessor", "MultiNoiseBiomeSourceMixin", "OverworldBiomeBuilderMixin", "OverworldBiomePresetMixin" diff --git a/src/main/resources/mixin/frozenlib_quiltmc_registry.mixins.json b/src/main/resources/mixin/frozenlib_quiltmc_registry.mixins.json index bb284d4..53c5f8d 100644 --- a/src/main/resources/mixin/frozenlib_quiltmc_registry.mixins.json +++ b/src/main/resources/mixin/frozenlib_quiltmc_registry.mixins.json @@ -6,7 +6,6 @@ "mixins": [ "BuiltInRegistriesMixin", "ConnectionMixin", - "MappedRegistryMixin", "RegistryDataLoaderMixin", "ServerStatusVersionMixin" ], diff --git a/src/main/resources/mixin/frozenlib_quiltmc_resource_loader.mixins.json b/src/main/resources/mixin/frozenlib_quiltmc_resource_loader.mixins.json index 31a3396..3b320ea 100644 --- a/src/main/resources/mixin/frozenlib_quiltmc_resource_loader.mixins.json +++ b/src/main/resources/mixin/frozenlib_quiltmc_resource_loader.mixins.json @@ -9,7 +9,6 @@ ], "client": [ "client.CreateWorldScreenMixin", - "client.IntegratedServerLoaderMixin" ], "injectors": { "defaultRequire": 1 diff --git a/src/main/templates/META-INF/accesstransformer.cfg b/src/main/templates/META-INF/accesstransformer.cfg deleted file mode 100644 index e69de29..0000000 diff --git a/src/main/templates/META-INF/neoforge.mods.toml b/src/main/templates/META-INF/neoforge.mods.toml index 9086890..19840f2 100644 --- a/src/main/templates/META-INF/neoforge.mods.toml +++ b/src/main/templates/META-INF/neoforge.mods.toml @@ -47,8 +47,63 @@ authors="${mod_authors}" #optional description='''${mod_description}''' # The [[mixins]] block allows you to declare your mixin config to FML so that it gets loaded. + +[[mixins]] +config="mixin/${mod_id}.advancement.mixins.json" +[[mixins]] +config="mixin/${mod_id}.block.mixins.json" +[[mixins]] +config="mixin/${mod_id}.clothconfig.mixins.json" +[[mixins]] +config="mixin/${mod_id}.core.mixins.json" +[[mixins]] +config="mixin/${mod_id}.entity.mixins.json" +[[mixins]] +config="mixin/${mod_id}.event.mixins.json" +[[mixins]] +config="mixin/${mod_id}.feature_flag.mixins.json" +[[mixins]] +config="mixin/${mod_id}.gravity.mixins.json" +[[mixins]] +config="mixin/${mod_id}.item.mixins.json" +[[mixins]] +config="mixin/${mod_id}.jankson.mixins.json" +[[mixins]] +config="mixin/${mod_id}.menu.mixins.json" +[[mixins]] +config="mixin/${mod_id}.mobcategory.mixins.json" +[[mixins]] +config="mixin/${mod_id}.recipe.mixins.json" +[[mixins]] +config="mixin/${mod_id}.screenshake.mixins.json" +[[mixins]] +config="mixin/${mod_id}.sound.mixins.json" +[[mixins]] +config="mixin/${mod_id}.spotting_icons.mixins.json" +[[mixins]] +config="mixin/${mod_id}.storage.mixins.json" +[[mixins]] +config="mixin/${mod_id}.tag.mixins.json" +[[mixins]] +config="mixin/${mod_id}.weather.mixins.json" +[[mixins]] +config="mixin/${mod_id}.wind.mixins.json" +[[mixins]] +config="mixin/${mod_id}.worldgen.biome.mixins.json" +[[mixins]] +config="mixin/${mod_id}.worldgen.heightmap.mixins.json" +[[mixins]] +config="mixin/${mod_id}.worldgen.structure.mixins.json" +[[mixins]] +config="mixin/${mod_id}.worldgen.surface.mixins.json" +[[mixins]] +config="mixin/${mod_id}.worldgen.vein.mixins.json" +[[mixins]] +config="mixin/${mod_id}_quiltmc_datafixerupper.mixins.json" +[[mixins]] +config="mixin/${mod_id}_quiltmc_registry.mixins.json" [[mixins]] -config="${mod_id}.mixins.json" +config="mixin/${mod_id}_quiltmc_resource_loader.mixins.json" # The [[accessTransformers]] block allows you to declare where your AT file is. # If this block is omitted, a fallback attempt will be made to load an AT from META-INF/accesstransformer.cfg