diff --git a/README.md b/README.md index 9470ecc..b0f9e35 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ for use with Geyser's [custom block API](https://geysermc.org/wiki/geyser/custom [custom skulls API](https://geysermc.org/wiki/geyser/custom-skulls), and [custom item API (v2)](https://geysermc.org/wiki/geyser/custom-items). Rainbow is available for Minecraft 26.1. Rainbow is currently experimental, and capable of generating Geyser block/item mappings, a `custom-skulls.yml` file, and a bedrock resourcepack for -somewhat simple blocks, 2D and 3D items. For a moderately complete list of Rainbow's capabilities, see further below. +somewhat simple blocks, 2D and 3D items, and sounds. For a moderately complete list of Rainbow's capabilities, see further below. Rainbow works by detecting custom blocks (using block state overrides) from loaded resourcepacks, and custom items in your inventory, or a container/inventory menu you have opened. It analyses the components of detected items, and uses assets from loaded Java resourcepacks to gather information about block and item models, textures, @@ -22,14 +22,16 @@ To use Rainbow, you must install it on your Minecraft client. Rainbow adds a few you use them as follows: 1. First, start a new pack by running `/rainbow create `, replacing `` with the name of your pack. Your resourcepack and Geyser mappings will be exported in the `.minecraft/rainbow/` folder. Anything in here can be overwritten! -2. Once you have created a pack, you can start mapping custom blocks, skulls, and items. Mapped custom content will be included in the exported resourcepack and Geyser mappings. There are 5 ways to map custom content: +2. Once you have created a pack, you can start mapping custom content. Mapped custom content will be included in the exported resourcepack and Geyser mappings. There are multiple ways to map custom content: - `/rainbow map block ` - maps the custom block at ``, if any. - `/rainbow map item` - maps the custom item or skull you're currently holding in your hand, if any. + - `/rainbow map sound ` - maps all the custom sounds of the given namespace. - `/rainbow mapinventory` - scans your inventory for custom items or skulls, and maps all that are found. - `/rainbow auto blocks` - scans through all loaded resourcepacks for custom blocks created using block state overrides, and maps all that are found. This may shortly freeze your client. - `/rainbow auto inventory` - scans all inventory menus and containers you open for custom items and skulls, and maps all that are found. This is handy for plugins that offer an inventory menu listing all custom items. - Use `/rainbow auto stop` to stop the mapping of custom items. - `/rainbow auto recipes` - scans through all known recipes for results that are custom items or skulls, and maps all that are found. This is handy for datapacks that contain recipes with custom items as result. You can use the vanilla `/recipe give @s *` command to give yourself all recipes, which helps Rainbow recognise custom items with this command. + - `/rainbow auto sounds` - scans through all loaded resourcepacks for custom sounds, and maps all that are found. 3. Once you have mapped all of your custom items, use `/rainbow finish` to finish the pack. Rainbow will then export the resourcepack and Geyser mappings it has created. When you've finished your pack, navigate to the `.minecraft/rainbow/` folder. You can also click on the `Wrote pack to disk` in chat to open this folder. @@ -43,7 +45,7 @@ In this folder, you'll find 5 important files/folders: - `report.txt`: you don't need to do anything with this file, but it contains information about generated assets and possible problems that occurred. Once you have taken these steps, restart your server. Bedrock players should then download the generated pack upon joining, -and if everything went well, they should be able to see custom content! +and if everything went well, they should be able to see (and hear) custom content! If you have any questions or run into any problems, please do feel free to ask for support in the Geyser Discord! @@ -79,6 +81,7 @@ Rainbow is currently capable of the following: - Custom elytra items also work, but only visually, due to bedrock limitations. - 3D items, by converting the Java model to a bedrock one, and generating an attachable and animations for it, as well as rendering a custom GUI icon. - Is able to translate display transformations for the head, first-person and third-person item slots. + - Custom sounds. - Generating working animated (flipbook) textures for 2D items and 3D items that make use of a single texture only. - Exporting merged language files from loaded resourcepacks to a folder, for easy copying to Geyser's `locales/overrides` folder. - Files from different resourcepacks for the same language are merged together. diff --git a/client/src/main/java/org/geysermc/rainbow/client/ClientAssetResolver.java b/client/src/main/java/org/geysermc/rainbow/client/ClientAssetResolver.java index e4f563c..c164599 100644 --- a/client/src/main/java/org/geysermc/rainbow/client/ClientAssetResolver.java +++ b/client/src/main/java/org/geysermc/rainbow/client/ClientAssetResolver.java @@ -15,6 +15,8 @@ import net.minecraft.client.resources.model.ModelManager; import net.minecraft.client.resources.model.ResolvedModel; import net.minecraft.client.resources.model.sprite.AtlasManager; +import net.minecraft.client.resources.sounds.Sound; +import net.minecraft.client.resources.sounds.SoundEventRegistration; import net.minecraft.resources.Identifier; import net.minecraft.resources.ResourceKey; import net.minecraft.server.packs.resources.Resource; @@ -24,6 +26,7 @@ import org.geysermc.rainbow.Rainbow; import org.geysermc.rainbow.RainbowIO; import org.geysermc.rainbow.client.accessor.ResolvedModelAccessor; +import org.geysermc.rainbow.client.accessor.SoundManagerAccessor; import org.geysermc.rainbow.client.mixin.EntityRenderDispatcherAccessor; import org.geysermc.rainbow.client.mixin.SpriteContentsAnimatedTextureAccessor; import org.geysermc.rainbow.client.mixin.SpriteContentsClientAccessor; @@ -103,6 +106,11 @@ public Optional getTexture(@Nullable Identifier atlasId, Identi accessor.getFrameRowSize(), accessor.getInterpolateFrames())); } + @Override + public Optional getSound(Identifier sound) { + return resourceManager.getResource(Sound.SOUND_LISTER.idToFile(sound)); + } + @Override public Map> getForeignLanguages() { // Ideally we'd not load the language keys again each time, but it's not possible to make use @@ -143,4 +151,9 @@ public Map> getForeignLanguages() { } return foreignLanguages; } + + @Override + public Map> getSoundRegistrations() { + return ((SoundManagerAccessor) Minecraft.getInstance().getSoundManager()).rainbow$getRawRegistrations(); + } } diff --git a/client/src/main/java/org/geysermc/rainbow/client/ClientPackSerializer.java b/client/src/main/java/org/geysermc/rainbow/client/ClientPackSerializer.java index 4aeda1f..1ce6da7 100644 --- a/client/src/main/java/org/geysermc/rainbow/client/ClientPackSerializer.java +++ b/client/src/main/java/org/geysermc/rainbow/client/ClientPackSerializer.java @@ -6,13 +6,16 @@ import com.mojang.serialization.JsonOps; import net.minecraft.core.HolderLookup; import net.minecraft.resources.RegistryOps; +import net.minecraft.server.packs.resources.Resource; import net.minecraft.util.Util; +import org.apache.commons.io.IOUtils; import org.geysermc.rainbow.CodecUtil; import org.geysermc.rainbow.RainbowIO; import org.geysermc.rainbow.mapping.PackSerializer; import org.jspecify.annotations.Nullable; import java.io.FileOutputStream; +import java.io.InputStream; import java.io.OutputStream; import java.nio.file.Path; import java.util.concurrent.CompletableFuture; @@ -58,4 +61,16 @@ public CompletableFuture saveTexture(byte[] texture, Path path) { } }), Util.backgroundExecutor().forName("PackSerializer-saveTexture")); } + + @Override + public CompletableFuture saveResource(Resource resource, Path path) { + return CompletableFuture.runAsync(() -> RainbowIO.safeIO(() -> { + CodecUtil.ensureDirectoryExists(path.getParent()); + try (InputStream input = resource.open()) { + try (OutputStream output = new FileOutputStream(path.toFile())) { + IOUtils.copy(input, output); + } + } + }), Util.backgroundExecutor().forName("PackSerializer-saveResource")); + } } diff --git a/client/src/main/java/org/geysermc/rainbow/client/PackManager.java b/client/src/main/java/org/geysermc/rainbow/client/PackManager.java index ad10679..ca8c85e 100644 --- a/client/src/main/java/org/geysermc/rainbow/client/PackManager.java +++ b/client/src/main/java/org/geysermc/rainbow/client/PackManager.java @@ -141,6 +141,7 @@ private static String createPackSummary(RainbowPack pack, ClientPackSerializer p report.append("\nItem texture atlas size: ").append(stats.itemAtlas()); report.append("\nTerrain texture atlas size: ").append(stats.terrainAtlas()); report.append("\nFlipbook texture definitions: ").append(stats.flipbookTextures()); + report.append("\nSound definitions: ").append(stats.soundDefinitions()); report.append("\n"); report.append("\nItem attachables exported: ").append(attachables); report.append("\n"); diff --git a/client/src/main/java/org/geysermc/rainbow/client/accessor/SoundManagerAccessor.java b/client/src/main/java/org/geysermc/rainbow/client/accessor/SoundManagerAccessor.java new file mode 100644 index 0000000..e729928 --- /dev/null +++ b/client/src/main/java/org/geysermc/rainbow/client/accessor/SoundManagerAccessor.java @@ -0,0 +1,10 @@ +package org.geysermc.rainbow.client.accessor; + +import net.minecraft.client.resources.sounds.SoundEventRegistration; + +import java.util.Map; + +public interface SoundManagerAccessor { + + Map> rainbow$getRawRegistrations(); +} diff --git a/client/src/main/java/org/geysermc/rainbow/client/command/PackGeneratorCommand.java b/client/src/main/java/org/geysermc/rainbow/client/command/PackGeneratorCommand.java index 527cc94..2928765 100644 --- a/client/src/main/java/org/geysermc/rainbow/client/command/PackGeneratorCommand.java +++ b/client/src/main/java/org/geysermc/rainbow/client/command/PackGeneratorCommand.java @@ -83,6 +83,21 @@ public static void register(CommandDispatcher dispatc } })) ) + .then(ClientCommands.literal("sounds") + .then(ClientCommands.argument("namespace", StringArgumentType.word()) + .suggests(SoundNamespaceSuggestionProvider.INSTANCE) + .executes(context -> { + String namespace = StringArgumentType.getString(context, "namespace"); + return runWithPack(packManager, (source, pack) -> { + if (pack.resources().mapSounds(namespace)) { + source.sendFeedback(Component.translatable("commands.rainbow.mapped_sounds_of_namespace", namespace)); + } else { + source.sendError(Component.translatable("commands.rainbow.no_sounds_mapped")); + } + }).run(context); + }) + ) + ) ) .then(ClientCommands.literal("mapinventory") .executes(runWithPack(packManager, (source, pack) -> { @@ -154,6 +169,15 @@ public static void register(CommandDispatcher dispatc packMapper.setItemProvider(RecipeMapper.INSTANCE); })) ) + .then(ClientCommands.literal("sounds") + .executes(runWithPack(packManager, (source, pack) -> { + if (pack.resources().mapAllSounds()) { + source.sendFeedback(Component.translatable("commands.rainbow.mapped_all_sounds")); + } else { + source.sendError(Component.translatable("commands.rainbow.no_sounds_mapped")); + } + })) + ) .then(ClientCommands.literal("stop") .executes(runWithPack(packManager, (source, _) -> { packMapper.setItemProvider(null); diff --git a/client/src/main/java/org/geysermc/rainbow/client/command/SoundNamespaceSuggestionProvider.java b/client/src/main/java/org/geysermc/rainbow/client/command/SoundNamespaceSuggestionProvider.java new file mode 100644 index 0000000..866706b --- /dev/null +++ b/client/src/main/java/org/geysermc/rainbow/client/command/SoundNamespaceSuggestionProvider.java @@ -0,0 +1,25 @@ +package org.geysermc.rainbow.client.command; + +import com.mojang.brigadier.context.CommandContext; +import com.mojang.brigadier.suggestion.SuggestionProvider; +import com.mojang.brigadier.suggestion.Suggestions; +import com.mojang.brigadier.suggestion.SuggestionsBuilder; +import net.fabricmc.fabric.api.client.command.v2.FabricClientCommandSource; +import net.minecraft.commands.SharedSuggestionProvider; +import net.minecraft.resources.Identifier; +import org.geysermc.rainbow.client.accessor.SoundManagerAccessor; + +import java.util.concurrent.CompletableFuture; + +public class SoundNamespaceSuggestionProvider implements SuggestionProvider { + public static final SoundNamespaceSuggestionProvider INSTANCE = new SoundNamespaceSuggestionProvider(); + + protected SoundNamespaceSuggestionProvider() {} + + @Override + public CompletableFuture getSuggestions(CommandContext context, SuggestionsBuilder builder) { + + return SharedSuggestionProvider.suggest(((SoundManagerAccessor) context.getSource().getClient().getSoundManager()).rainbow$getRawRegistrations().keySet().stream() + .filter(namespace -> !namespace.equals(Identifier.DEFAULT_NAMESPACE)), builder); + } +} diff --git a/client/src/main/java/org/geysermc/rainbow/client/mixin/SoundManagerMixin.java b/client/src/main/java/org/geysermc/rainbow/client/mixin/SoundManagerMixin.java new file mode 100644 index 0000000..62786c4 --- /dev/null +++ b/client/src/main/java/org/geysermc/rainbow/client/mixin/SoundManagerMixin.java @@ -0,0 +1,34 @@ +package org.geysermc.rainbow.client.mixin; + +import com.llamalad7.mixinextras.sugar.Local; +import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; +import net.minecraft.client.resources.sounds.SoundEventRegistration; +import net.minecraft.client.sounds.SoundManager; +import net.minecraft.server.packs.resources.ResourceManager; +import net.minecraft.server.packs.resources.SimplePreparableReloadListener; +import net.minecraft.util.profiling.ProfilerFiller; +import org.geysermc.rainbow.client.accessor.SoundManagerAccessor; +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.Map; + +@Mixin(SoundManager.class) +public abstract class SoundManagerMixin extends SimplePreparableReloadListener implements SoundManagerAccessor { + @Unique + private final Map> rawRegistrations = new Object2ObjectOpenHashMap<>(); + + @Inject(method = "prepare(Lnet/minecraft/server/packs/resources/ResourceManager;Lnet/minecraft/util/profiling/ProfilerFiller;)Lnet/minecraft/client/sounds/SoundManager$Preparations;", at = @At(value = "INVOKE", target = "Ljava/util/Map;entrySet()Ljava/util/Set;")) + public void storeRawRegistrations(ResourceManager manager, ProfilerFiller profiler, CallbackInfoReturnable callbackInfoReturnable, + @Local(name = "namespace") String namespace, @Local(name = "map") Map map) { + rawRegistrations.put(namespace, map); + } + + @Override + public Map> rainbow$getRawRegistrations() { + return rawRegistrations; + } +} diff --git a/client/src/main/resources/assets/rainbow_lang b/client/src/main/resources/assets/rainbow_lang index 4e83fc8..36998e6 160000 --- a/client/src/main/resources/assets/rainbow_lang +++ b/client/src/main/resources/assets/rainbow_lang @@ -1 +1 @@ -Subproject commit 4e83fc8c0710388f39263b21e892d472fe24131f +Subproject commit 36998e65f2f8181a7feea284c936e5f3452cd142 diff --git a/client/src/main/resources/rainbow-client.mixins.json b/client/src/main/resources/rainbow-client.mixins.json index cb9a1f8..3ebc536 100644 --- a/client/src/main/resources/rainbow-client.mixins.json +++ b/client/src/main/resources/rainbow-client.mixins.json @@ -17,6 +17,7 @@ "defaultRequire": 1 }, "mixins": [ - "ResolvableProfileAccessor" + "ResolvableProfileAccessor", + "SoundManagerMixin" ] } diff --git a/datagen/src/main/java/org/geysermc/rainbow/datagen/RainbowDataProvider.java b/datagen/src/main/java/org/geysermc/rainbow/datagen/RainbowDataProvider.java index 3c6068a..1133737 100644 --- a/datagen/src/main/java/org/geysermc/rainbow/datagen/RainbowDataProvider.java +++ b/datagen/src/main/java/org/geysermc/rainbow/datagen/RainbowDataProvider.java @@ -4,9 +4,12 @@ import com.mojang.blaze3d.platform.NativeImage; import com.mojang.serialization.Codec; import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; +import net.fabricmc.fabric.api.client.datagen.v1.builder.SoundTypeBuilder; import net.fabricmc.fabric.api.client.datagen.v1.provider.FabricModelProvider; +import net.fabricmc.fabric.api.client.datagen.v1.provider.FabricSoundsProvider; import net.fabricmc.fabric.api.datagen.v1.FabricPackOutput; import net.fabricmc.fabric.api.datagen.v1.provider.FabricLanguageProvider; +import net.fabricmc.fabric.impl.datagen.client.SoundTypeBuilderImpl; import net.minecraft.client.data.models.blockstates.BlockModelDefinitionGenerator; import net.minecraft.client.data.models.model.ModelInstance; import net.minecraft.client.renderer.block.dispatch.BlockStateModel; @@ -18,6 +21,8 @@ import net.minecraft.client.resources.model.UnbakedModel; import net.minecraft.client.resources.model.cuboid.CuboidModel; import net.minecraft.client.resources.model.cuboid.ItemModelGenerator; +import net.minecraft.client.resources.sounds.Sound; +import net.minecraft.client.resources.sounds.SoundEventRegistration; import net.minecraft.core.HolderLookup; import net.minecraft.core.component.DataComponentInitializers; import net.minecraft.core.component.DataComponentMap; @@ -28,15 +33,18 @@ import net.minecraft.data.PackOutput; import net.minecraft.resources.Identifier; import net.minecraft.resources.ResourceKey; +import net.minecraft.server.packs.resources.Resource; import net.minecraft.server.packs.resources.ResourceManager; import net.minecraft.util.ProblemReporter; import net.minecraft.util.Util; +import net.minecraft.util.valueproviders.ConstantFloat; import net.minecraft.world.item.Item; import net.minecraft.world.item.equipment.EquipmentAsset; import net.minecraft.world.level.block.Block; import net.minecraft.world.level.block.state.BlockState; import org.geysermc.rainbow.Rainbow; import org.geysermc.rainbow.RainbowIO; +import org.geysermc.rainbow.datagen.accessor.SoundsProviderDataAccessor; import org.geysermc.rainbow.datagen.mixin.FabricLanguageProviderAccessor; import org.geysermc.rainbow.datagen.accessor.LanguageProviderDataAccessor; import org.geysermc.rainbow.datagen.accessor.ModelProviderDataAccessor; @@ -60,6 +68,7 @@ import java.util.Set; import java.util.TreeSet; import java.util.concurrent.CompletableFuture; +import java.util.stream.Collectors; public abstract class RainbowDataProvider implements DataProvider { private static final Logger PROBLEM_LOGGER = LoggerFactory.getLogger(Rainbow.MOD_ID); @@ -94,7 +103,7 @@ protected RainbowDataProvider(FabricPackOutput output, CompletableFuture registries, String packName, FabricModelProvider modelProvider) { - this(output, registries, packName, new Providers(modelProvider, Optional.empty(), Map.of())); + this(output, registries, packName, new Providers(modelProvider, Optional.empty(), Optional.empty(), Map.of())); } protected RainbowDataProvider(FabricPackOutput output, CompletableFuture registries, FabricModelProvider modelProvider) { @@ -109,6 +118,8 @@ public CompletableFuture run(CachedOutput output) { .map(provider -> provider.run(output)) .collect(() -> CompletableFuture.completedFuture(null), CompletableFuture::allOf, CompletableFuture::allOf)) .orElseGet(() -> CompletableFuture.completedFuture(null)); + CompletableFuture sounds = providers.sounds.map(provider -> provider.run(output)) + .orElseGet(() -> CompletableFuture.completedFuture(null)); CompletableFuture bedrockPack = ClientPackLoader.openClientResources() .thenCompose(resourceManager -> registries.thenApply(registries -> { @@ -127,7 +138,7 @@ public CompletableFuture run(CachedOutput output) { } })); - return CompletableFuture.allOf(models, languages, bedrockPack.thenCompose(BedrockPack::save)); + return CompletableFuture.allOf(models, languages, sounds, bedrockPack.thenCompose(BedrockPack::save)); } protected BedrockPack.Builder createBedrockPack(PackSerializer serializer, AssetResolver resolver) { @@ -147,18 +158,22 @@ protected DataComponentPatch getVanillaDataComponentPatch(Item modded) { public static class Providers { private final FabricModelProvider models; private final Optional> languages; + private final Optional sounds; private final Map, EquipmentClientInfo> equipmentInfos; public Providers(FabricModelProvider models, Optional> languages, + Optional sounds, Map, EquipmentClientInfo> equipmentInfos) { this.models = models; this.languages = languages; + this.sounds = sounds; this.equipmentInfos = equipmentInfos; } public static Providers create(FabricModelProvider models, Optional languages, + Optional sounds, Map, EquipmentClientInfo> equipmentInfos) { - return new Providers(models, languages.map(List::of), equipmentInfos); + return new Providers(models, languages.map(List::of), sounds, equipmentInfos); } private Map getItemInfos() { @@ -207,6 +222,18 @@ public CompletableFuture saveTexture(byte[] texture, Path path) { } }, Util.backgroundExecutor().forName("PackSerializer-saveTexture")); } + + @Override + public CompletableFuture saveResource(Resource resource, Path path) { + return CompletableFuture.runAsync(() -> { + try (InputStream input = resource.open()) { + byte[] bytes = input.readAllBytes(); + output.writeIfNeeded(path, bytes, Hashing.sha1().hashBytes(bytes)); + } catch (IOException exception) { + LOGGER.error("Failed to save resource to {}", path, exception); + } + }, Util.backgroundExecutor().forName("PackSerializer-saveResource")); + } } private static class DatagenResolver implements AssetResolver { @@ -218,6 +245,7 @@ private static class DatagenResolver implements AssetResolver { private final Map itemInfos; private final Map models; private final Map> foreignLanguages; + private final Map> soundRegistrations; private DatagenResolver(ResourceManager resourceManager, Providers providers) { this.resourceManager = resourceManager; @@ -240,6 +268,15 @@ private DatagenResolver(ResourceManager resourceManager, Providers providers) { return existingKeys; }); })); + + // Same as above, CompletableFuture should have been finished by now + soundRegistrations = providers.sounds.map(sounds -> ((SoundsProviderDataAccessor) sounds).rainbow$getData()) + .map(data -> data.entrySet().stream() + .map(registrations -> Map.entry(registrations.getKey(), registrations.getValue().entrySet().stream() + .map(registration -> Map.entry(registration.getKey(), mapFabricToVanillaRegistration(registration.getValue()))) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)))) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue))) + .orElseGet(Map::of); } @Override @@ -314,9 +351,29 @@ public Optional getTexture(@Nullable Identifier atlas, Identifi })); } + @Override + public Optional getSound(Identifier sound) { + // TODO code duplication with ClientAssetResolver + return resourceManager.getResource(Sound.SOUND_LISTER.idToFile(sound)); + } + @Override public Map> getForeignLanguages() { return foreignLanguages; } + + @Override + public Map> getSoundRegistrations() { + return soundRegistrations; + } + + // Cursed? Yes. Works? Also yes + private static SoundEventRegistration mapFabricToVanillaRegistration(SoundTypeBuilderImpl.SoundType registration) { + return new SoundEventRegistration(registration.sounds().stream() + .map(entry -> new Sound(entry.name(), ConstantFloat.of(entry.volume()), ConstantFloat.of(entry.pitch()), + entry.weight(), entry.type() == SoundTypeBuilder.RegistrationType.FILE? Sound.Type.FILE : Sound.Type.SOUND_EVENT, + entry.stream(), entry.preload(), entry.attenuationDistance())) + .toList(), registration.replace(), registration.subtitle().orElse(null)); + } } } diff --git a/datagen/src/main/java/org/geysermc/rainbow/datagen/accessor/SoundsProviderDataAccessor.java b/datagen/src/main/java/org/geysermc/rainbow/datagen/accessor/SoundsProviderDataAccessor.java new file mode 100644 index 0000000..ae28042 --- /dev/null +++ b/datagen/src/main/java/org/geysermc/rainbow/datagen/accessor/SoundsProviderDataAccessor.java @@ -0,0 +1,10 @@ +package org.geysermc.rainbow.datagen.accessor; + +import net.fabricmc.fabric.impl.datagen.client.SoundTypeBuilderImpl; + +import java.util.Map; + +public interface SoundsProviderDataAccessor { + + Map> rainbow$getData(); +} diff --git a/datagen/src/main/java/org/geysermc/rainbow/datagen/mixin/FabricSoundsProviderMixin.java b/datagen/src/main/java/org/geysermc/rainbow/datagen/mixin/FabricSoundsProviderMixin.java new file mode 100644 index 0000000..66afc1f --- /dev/null +++ b/datagen/src/main/java/org/geysermc/rainbow/datagen/mixin/FabricSoundsProviderMixin.java @@ -0,0 +1,38 @@ +package org.geysermc.rainbow.datagen.mixin; + +import com.llamalad7.mixinextras.sugar.Local; +import net.fabricmc.fabric.api.client.datagen.v1.provider.FabricSoundsProvider; +import net.fabricmc.fabric.impl.datagen.client.SoundTypeBuilderImpl; +import net.minecraft.core.HolderLookup; +import net.minecraft.data.CachedOutput; +import net.minecraft.data.DataProvider; +import org.geysermc.rainbow.datagen.accessor.SoundsProviderDataAccessor; +import org.jspecify.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.CallbackInfoReturnable; + +import java.util.Map; +import java.util.concurrent.CompletionStage; + +@Mixin(FabricSoundsProvider.class) +public abstract class FabricSoundsProviderMixin implements DataProvider, SoundsProviderDataAccessor { + @Unique + private @Nullable Map> data = null; + + @Inject(method = "lambda$run$0", at = @At("RETURN")) + public void setData(CachedOutput output, HolderLookup.Provider lookup, CallbackInfoReturnable cir, + @Local(name = "data") Map> data) { + this.data = data; + } + + @Override + public Map> rainbow$getData() { + if (data == null) { + throw new IllegalStateException("FabricSoundsProvider has not run yet"); + } + return data; + } +} diff --git a/datagen/src/main/resources/rainbow-datagen.mixins.json b/datagen/src/main/resources/rainbow-datagen.mixins.json index b3657b7..5a89631 100644 --- a/datagen/src/main/resources/rainbow-datagen.mixins.json +++ b/datagen/src/main/resources/rainbow-datagen.mixins.json @@ -14,6 +14,7 @@ }, "mixins": [ "FabricLanguageProviderAccessor", - "FabricLanguageProviderMixin" + "FabricLanguageProviderMixin", + "FabricSoundsProviderMixin" ] } diff --git a/rainbow/src/main/java/org/geysermc/rainbow/mapping/AssetResolver.java b/rainbow/src/main/java/org/geysermc/rainbow/mapping/AssetResolver.java index 6581df5..9ce3c44 100644 --- a/rainbow/src/main/java/org/geysermc/rainbow/mapping/AssetResolver.java +++ b/rainbow/src/main/java/org/geysermc/rainbow/mapping/AssetResolver.java @@ -5,9 +5,11 @@ import net.minecraft.client.renderer.texture.MissingTextureAtlasSprite; import net.minecraft.client.resources.model.EquipmentClientInfo; import net.minecraft.client.resources.model.ResolvedModel; +import net.minecraft.client.resources.sounds.SoundEventRegistration; import net.minecraft.data.AtlasIds; import net.minecraft.resources.Identifier; import net.minecraft.resources.ResourceKey; +import net.minecraft.server.packs.resources.Resource; import net.minecraft.world.item.equipment.EquipmentAsset; import net.minecraft.world.level.block.state.BlockState; import org.geysermc.rainbow.mapping.texture.TextureResource; @@ -28,6 +30,8 @@ public interface AssetResolver { Optional getTexture(@Nullable Identifier atlas, Identifier identifier); + Optional getSound(Identifier sound); + default Optional getPossibleAtlasTextureSafely(Identifier identifier) { if (identifier.equals(MissingTextureAtlasSprite.getLocation())) { return Optional.empty(); @@ -40,4 +44,6 @@ default Optional getPossibleAtlasTextureSafely(Identifier ident } Map> getForeignLanguages(); + + Map> getSoundRegistrations(); } diff --git a/rainbow/src/main/java/org/geysermc/rainbow/mapping/PackSerializer.java b/rainbow/src/main/java/org/geysermc/rainbow/mapping/PackSerializer.java index bacc855..adef54d 100644 --- a/rainbow/src/main/java/org/geysermc/rainbow/mapping/PackSerializer.java +++ b/rainbow/src/main/java/org/geysermc/rainbow/mapping/PackSerializer.java @@ -1,6 +1,7 @@ package org.geysermc.rainbow.mapping; import com.mojang.serialization.Codec; +import net.minecraft.server.packs.resources.Resource; import org.geysermc.rainbow.pack.PackPaths; import org.jspecify.annotations.Nullable; @@ -17,6 +18,8 @@ public interface PackSerializer { CompletableFuture saveTexture(byte[] texture, Path path); + CompletableFuture saveResource(Resource resource, Path path); + @FunctionalInterface interface Serializable { diff --git a/rainbow/src/main/java/org/geysermc/rainbow/mapping/PackStats.java b/rainbow/src/main/java/org/geysermc/rainbow/mapping/PackStats.java index de87c49..e1e642e 100644 --- a/rainbow/src/main/java/org/geysermc/rainbow/mapping/PackStats.java +++ b/rainbow/src/main/java/org/geysermc/rainbow/mapping/PackStats.java @@ -1,4 +1,4 @@ package org.geysermc.rainbow.mapping; public record PackStats(AssetCacheStats cacheStats, int blockMappings, int itemMappings, - int itemAtlas, int terrainAtlas, int flipbookTextures) {} + int itemAtlas, int terrainAtlas, int flipbookTextures, int soundDefinitions) {} diff --git a/rainbow/src/main/java/org/geysermc/rainbow/mapping/texture/ModelTextures.java b/rainbow/src/main/java/org/geysermc/rainbow/mapping/texture/ModelTextures.java index 8763984..59c6fd9 100644 --- a/rainbow/src/main/java/org/geysermc/rainbow/mapping/texture/ModelTextures.java +++ b/rainbow/src/main/java/org/geysermc/rainbow/mapping/texture/ModelTextures.java @@ -51,6 +51,6 @@ static String sanitizeMaterialReference(String reference) { } private static boolean areAllMaterialsTheSame(Map materials) { - return materials.values().stream().distinct().count() <= 1L; + return materials.values().stream().distinct().count() == 1L; } } diff --git a/rainbow/src/main/java/org/geysermc/rainbow/pack/BedrockPack.java b/rainbow/src/main/java/org/geysermc/rainbow/pack/BedrockPack.java index 75cb048..6bd572a 100644 --- a/rainbow/src/main/java/org/geysermc/rainbow/pack/BedrockPack.java +++ b/rainbow/src/main/java/org/geysermc/rainbow/pack/BedrockPack.java @@ -1,6 +1,7 @@ package org.geysermc.rainbow.pack; import com.mojang.datafixers.util.Pair; +import net.minecraft.client.resources.sounds.SoundEventRegistration; import net.minecraft.core.Holder; import net.minecraft.core.component.DataComponentPatch; import net.minecraft.core.component.DataComponents; @@ -28,6 +29,7 @@ import org.geysermc.rainbow.mapping.PackStats; import org.geysermc.rainbow.mapping.geometry.GeometryRenderer; import org.geysermc.rainbow.definition.item.GeyserItemMappings; +import org.geysermc.rainbow.pack.sound.BedrockSoundDefinitions; import org.geysermc.rainbow.pack.texture.BedrockFlipbookTextures; import org.geysermc.rainbow.pack.texture.BedrockTextureAtlas; import org.geysermc.rainbow.pack.texture.BedrockTextures; @@ -36,6 +38,7 @@ import java.nio.file.Path; import java.util.Collections; import java.util.HashSet; +import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.Set; @@ -55,6 +58,7 @@ public class BedrockPack implements BedrockAssetConsumer, PackSerializer.Seriali private final BedrockTextures.Builder itemTextures = BedrockTextures.builder(); private final BedrockTextures.Builder terrainTextures = BedrockTextures.builder(); private final BedrockFlipbookTextures.Builder flipbookTextures = BedrockFlipbookTextures.builder(); + private final BedrockSoundDefinitions.Builder soundDefinitions = BedrockSoundDefinitions.builder(); private final Set bedrockBlocks = new HashSet<>(); private final Set bedrockItems = new HashSet<>(); private final Set modelsMapped = new HashSet<>(); @@ -142,6 +146,24 @@ public MappingResult mapItem(Holder item, DataComponentPatch patch) { return mapItem(new ItemStackTemplate(item, 1, patch)); } + public boolean mapSounds(String namespace) { + Map sounds = context.assetResolver().getSoundRegistrations().get(namespace); + if (sounds == null) { + return false; + } + soundDefinitions.withRegistrations(namespace, sounds); + return true; + } + + public boolean mapAllSounds() { + Map> sounds = context.assetResolver().getSoundRegistrations(); + if (sounds.isEmpty()) { + return false; + } + soundDefinitions.withRegistrations(sounds); + return true; + } + public CompletableFuture save() { CompletableFuture baseSerialization = save(createSerializingContext()); if (reporter instanceof AutoCloseable closeable) { @@ -177,6 +199,7 @@ public CompletableFuture save(PackSerializingContext serializingContext) { .with(BedrockTextureAtlas.CODEC, BedrockTextureAtlas.itemAtlas(name, itemTextures), PackPaths::itemAtlas) .with(BedrockTextureAtlas.CODEC, BedrockTextureAtlas.terrainAtlas(name, terrainTextures), PackPaths::terrainAtlas) .with(BedrockFlipbookTextures.CODEC, flipbookTextures.build(), PackPaths::flipbookTextures) + .with(soundDefinitions.build()) .with(bedrockBlocks) .with(bedrockItems) .with(paths.languageOutput().map(languageFolder -> context -> LanguageUtil.saveLanguages(context, languageFolder))) @@ -185,7 +208,7 @@ public CompletableFuture save(PackSerializingContext serializingContext) { public PackStats stats() { return new PackStats(context.cacheStats(), context.mappings().blocks().size(), context.mappings().items().size(), - itemTextures.build().size(), terrainTextures.build().size(), flipbookTextures.build().size()); + itemTextures.build().size(), terrainTextures.build().size(), flipbookTextures.build().size(), soundDefinitions.build().size()); } public Set getBedrockItems() { diff --git a/rainbow/src/main/java/org/geysermc/rainbow/pack/PackPaths.java b/rainbow/src/main/java/org/geysermc/rainbow/pack/PackPaths.java index 8bcde11..4cab071 100644 --- a/rainbow/src/main/java/org/geysermc/rainbow/pack/PackPaths.java +++ b/rainbow/src/main/java/org/geysermc/rainbow/pack/PackPaths.java @@ -1,5 +1,6 @@ package org.geysermc.rainbow.pack; +import net.minecraft.client.resources.sounds.Sound; import net.minecraft.resources.Identifier; import org.geysermc.rainbow.Rainbow; import org.geysermc.rainbow.mapping.texture.TextureHolder; @@ -21,6 +22,7 @@ public record PackPaths(Path mappingsRoot, Path packRoot, Optional zipOutp private static final Path ITEM_ATLAS = Path.of("textures/item_texture.json"); private static final Path TERRAIN_ATLAS = Path.of("textures/terrain_texture.json"); private static final Path FLIPBOOK_TEXTURES = Path.of("textures/flipbook_textures.json"); + private static final Path SOUND_DEFINITIONS = Path.of("sounds/sound_definitions.json"); public Path blockMappings() { return mappingsRoot.resolve(BLOCK_MAPPINGS); @@ -51,6 +53,10 @@ public Path texturePath(TextureHolder texture) { return packRoot.resolve(textureIdentifier.getPath()); } + public Path sound(Sound sound) { + return packRoot.resolve(sound.getPath().getPath()); + } + public Path manifest() { return packRoot.resolve(MANIFEST); } @@ -66,4 +72,8 @@ public Path terrainAtlas() { public Path flipbookTextures() { return packRoot.resolve(FLIPBOOK_TEXTURES); } + + public Path soundDefinitions() { + return packRoot.resolve(SOUND_DEFINITIONS); + } } diff --git a/rainbow/src/main/java/org/geysermc/rainbow/pack/sound/BedrockSoundDefinitions.java b/rainbow/src/main/java/org/geysermc/rainbow/pack/sound/BedrockSoundDefinitions.java new file mode 100644 index 0000000..eb6311b --- /dev/null +++ b/rainbow/src/main/java/org/geysermc/rainbow/pack/sound/BedrockSoundDefinitions.java @@ -0,0 +1,114 @@ +package org.geysermc.rainbow.pack.sound; + +import com.mojang.datafixers.util.Pair; +import com.mojang.serialization.Codec; +import com.mojang.serialization.MapCodec; +import com.mojang.serialization.codecs.RecordCodecBuilder; +import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; +import net.minecraft.client.resources.sounds.Sound; +import net.minecraft.client.resources.sounds.SoundEventRegistration; +import net.minecraft.resources.Identifier; +import net.minecraft.util.RandomSource; +import net.minecraft.util.valueproviders.ConstantFloat; +import net.minecraft.util.valueproviders.SampledFloat; +import org.geysermc.rainbow.CodecUtil; +import org.geysermc.rainbow.mapping.PackSerializer; +import org.geysermc.rainbow.mapping.PackSerializingContext; +import org.geysermc.rainbow.pack.BedrockVersion; +import org.geysermc.rainbow.pack.PackPaths; + +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.stream.Stream; + +public record BedrockSoundDefinitions(Map definitions) implements PackSerializer.Serializable { + public static final BedrockVersion FORMAT_VERSION = BedrockVersion.of(1, 20, 20); + public static final Codec BEDROCK_SOUND_CODEC = RecordCodecBuilder.create(instance -> + instance.group( + Codec.STRING.fieldOf("name").forGetter(sound -> "sounds/" + sound.getLocation().getPath()), + Codec.BOOL.optionalFieldOf("stream", false).forGetter(Sound::shouldStream), + Codec.BOOL.optionalFieldOf("is3D", false).forGetter(_ -> true), + createOptionalStaticSampledFloatCodec("volume", 1.0F).forGetter(Sound::getVolume), + createOptionalStaticSampledFloatCodec("pitch", 1.0F).forGetter(Sound::getPitch), + Codec.INT.optionalFieldOf("weight", 1).forGetter(Sound::getWeight) + ).apply(instance, (name, stream, _, volume, pitch, weight) -> + new Sound(Identifier.withDefaultNamespace(name.replaceFirst("^sounds/", "")), volume, pitch, weight, + Sound.Type.FILE, stream, false, 16)) + ); + public static final Codec BEDROCK_REGISTRATION_CODEC = RecordCodecBuilder.create(instance -> + instance.group( + Codec.STRING.optionalFieldOf("category", "sound").forGetter(_ -> "sound"), + Codec.FLOAT.optionalFieldOf("min_distance", 0.0F).forGetter(registration -> Float.valueOf(registration.getSounds().stream().map(Sound::getAttenuationDistance).findFirst().orElse(0))), + Codec.FLOAT.optionalFieldOf("max_distance", 0.0F).forGetter(registration -> Float.valueOf(registration.getSounds().stream().map(Sound::getAttenuationDistance).findFirst().orElse(0)) * 2.0F), + BEDROCK_SOUND_CODEC.listOf().fieldOf("sounds").forGetter(SoundEventRegistration::getSounds), + Codec.STRING.optionalFieldOf("subtitle", null).forGetter(SoundEventRegistration::getSubtitle) + ).apply(instance, (_, _, _, sounds, subtitle) -> new SoundEventRegistration(sounds, false, subtitle)) + ); + public static final Codec CODEC = RecordCodecBuilder.create(instance -> + instance.group( + CodecUtil.unitVerifyCodec(BedrockVersion.STRING_CODEC, "format_version", FORMAT_VERSION), + Codec.compoundList(Identifier.CODEC, BEDROCK_REGISTRATION_CODEC) + .xmap(pairs -> pairs.stream().collect(Pair.toMap()), + registrations -> registrations.entrySet().stream().map(entry -> Pair.of(entry.getKey(), entry.getValue())).toList()) + .fieldOf("sound_definitions").forGetter(BedrockSoundDefinitions::definitions) + ).apply(instance, (_, definitions) -> new BedrockSoundDefinitions(definitions)) + ); + + public int size() { + return definitions.size(); + } + + public Stream flatten() { + return definitions.values().stream() + .flatMap(registration -> registration.getSounds().stream()); + } + + @Override + public CompletableFuture save(PackSerializingContext context) { + return PackSerializer.Serializable.wrapCodec(CODEC, this, PackPaths::soundDefinitions) + .with(flatten() + .map(BedrockSoundDefinitions::serializableSound) + .toList()) + .save(context); + } + + private static PackSerializer.Serializable serializableSound(Sound sound) { + return context -> context.assetResolver().getSound(sound.getLocation()) + .map(resource -> context.serializer().saveResource(resource, context.paths().sound(sound))) + .orElseGet(PackSerializer::noop); + } + + private static MapCodec createOptionalStaticSampledFloatCodec(String field, float defaultValue) { + return Codec.FLOAT.optionalFieldOf(field, defaultValue).xmap(ConstantFloat::of, f -> f.sample(RandomSource.create(0L))); + } + + public static Builder builder() { + return new Builder(); + } + + public static class Builder { + private final Map definitions = new Object2ObjectOpenHashMap<>(); + + public Builder withRegistrations(String namespace, Map registrations) { + for (Map.Entry registration : registrations.entrySet()) { + definitions.put(Identifier.fromNamespaceAndPath(namespace, registration.getKey()), registration.getValue()); + } + return this; + } + + public Builder withRegistrations(Map> registrations) { + for (String namespace : registrations.keySet()) { + if (namespace.equals(Identifier.DEFAULT_NAMESPACE)) { + continue; + } + + withRegistrations(namespace, registrations.get(namespace)); + } + return this; + } + + public BedrockSoundDefinitions build() { + return new BedrockSoundDefinitions(Map.copyOf(definitions)); + } + } +} diff --git a/rainbow/src/main/java/org/geysermc/rainbow/pack/sound/package-info.java b/rainbow/src/main/java/org/geysermc/rainbow/pack/sound/package-info.java new file mode 100644 index 0000000..a63ab7d --- /dev/null +++ b/rainbow/src/main/java/org/geysermc/rainbow/pack/sound/package-info.java @@ -0,0 +1,4 @@ +@NullMarked +package org.geysermc.rainbow.pack.sound; + +import org.jspecify.annotations.NullMarked;