Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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 <name>`, replacing `<name>` with the name of your pack. Your resourcepack and Geyser mappings will be exported in the `.minecraft/rainbow/<name>` 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 <target>` - maps the custom block at `<target>`, if any.
- `/rainbow map item` - maps the custom item or skull you're currently holding in your hand, if any.
- `/rainbow map sound <namespace>` - 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/<name>` folder. You can also click on the `Wrote pack to disk` in chat to open this folder.
Expand All @@ -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!

Expand Down Expand Up @@ -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.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -103,6 +106,11 @@ public Optional<TextureResource> getTexture(@Nullable Identifier atlasId, Identi
accessor.getFrameRowSize(), accessor.getInterpolateFrames()));
}

@Override
public Optional<Resource> getSound(Identifier sound) {
return resourceManager.getResource(Sound.SOUND_LISTER.idToFile(sound));
}

@Override
public Map<String, Map<String, String>> getForeignLanguages() {
// Ideally we'd not load the language keys again each time, but it's not possible to make use
Expand Down Expand Up @@ -143,4 +151,9 @@ public Map<String, Map<String, String>> getForeignLanguages() {
}
return foreignLanguages;
}

@Override
public Map<String, Map<String, SoundEventRegistration>> getSoundRegistrations() {
return ((SoundManagerAccessor) Minecraft.getInstance().getSoundManager()).rainbow$getRawRegistrations();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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"));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down
Original file line number Diff line number Diff line change
@@ -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<String, Map<String, SoundEventRegistration>> rainbow$getRawRegistrations();
}
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,21 @@ public static void register(CommandDispatcher<FabricClientCommandSource> 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) -> {
Expand Down Expand Up @@ -154,6 +169,15 @@ public static void register(CommandDispatcher<FabricClientCommandSource> 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);
Expand Down
Original file line number Diff line number Diff line change
@@ -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<FabricClientCommandSource> {
public static final SoundNamespaceSuggestionProvider INSTANCE = new SoundNamespaceSuggestionProvider();

protected SoundNamespaceSuggestionProvider() {}

@Override
public CompletableFuture<Suggestions> getSuggestions(CommandContext<FabricClientCommandSource> context, SuggestionsBuilder builder) {

return SharedSuggestionProvider.suggest(((SoundManagerAccessor) context.getSource().getClient().getSoundManager()).rainbow$getRawRegistrations().keySet().stream()
.filter(namespace -> !namespace.equals(Identifier.DEFAULT_NAMESPACE)), builder);
}
}
Original file line number Diff line number Diff line change
@@ -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<Object> implements SoundManagerAccessor {
@Unique
private final Map<String, Map<String, SoundEventRegistration>> 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<Object> callbackInfoReturnable,
@Local(name = "namespace") String namespace, @Local(name = "map") Map<String, SoundEventRegistration> map) {
rawRegistrations.put(namespace, map);
}

@Override
public Map<String, Map<String, SoundEventRegistration>> rainbow$getRawRegistrations() {
return rawRegistrations;
}
}
2 changes: 1 addition & 1 deletion client/src/main/resources/assets/rainbow_lang
Submodule rainbow_lang updated 1 files
+3 −0 lang/en_us.json
3 changes: 2 additions & 1 deletion client/src/main/resources/rainbow-client.mixins.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
"defaultRequire": 1
},
"mixins": [
"ResolvableProfileAccessor"
"ResolvableProfileAccessor",
"SoundManagerMixin"
]
}
Loading
Loading