1
0
mirror of https://github.com/GeyserMC/Rainbow.git synced 2025-12-19 14:59:16 +00:00

Split the mod in half

This commit is contained in:
Eclipse
2025-10-14 08:33:38 +00:00
parent 73bd8222be
commit 466427e974
32 changed files with 307 additions and 160 deletions

View File

@@ -1,3 +1,9 @@
plugins {
id("rainbow.base-conventions")
}
dependencies {
// Implement namedElements so IntelliJ can use it correctly, but include the remapped build
implementation(project(path = ":rainbow", configuration = "namedElements"))
include(project(":rainbow"))
}

View File

@@ -0,0 +1,41 @@
package org.geysermc.rainbow.client;
import net.minecraft.client.renderer.item.ClientItem;
import net.minecraft.client.resources.model.EquipmentClientInfo;
import net.minecraft.client.resources.model.ResolvedModel;
import net.minecraft.core.HolderLookup;
import net.minecraft.resources.ResourceKey;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.item.equipment.EquipmentAsset;
import org.geysermc.rainbow.mapping.AssetResolver;
import java.io.InputStream;
import java.util.Optional;
public class MinecraftAssetResolver implements AssetResolver {
@Override
public Optional<ResolvedModel> getResolvedModel(ResourceLocation location) {
return Optional.empty();
}
@Override
public Optional<ClientItem> getClientItem(ResourceLocation location) {
return Optional.empty();
}
@Override
public EquipmentClientInfo getEquipmentInfo(ResourceKey<EquipmentAsset> key) {
return null;
}
@Override
public InputStream getTexture(ResourceLocation location) {
return null;
}
@Override
public HolderLookup.Provider registries() {
return null;
}
}

View File

@@ -0,0 +1,39 @@
package org.geysermc.rainbow.client;
import org.geysermc.rainbow.pack.BedrockPack;
import java.io.IOException;
import java.nio.file.Path;
import java.util.Optional;
import java.util.function.Consumer;
public final class PackManager {
private Optional<BedrockPack> currentPack = Optional.empty();
public void startPack(String name) throws IOException {
if (currentPack.isPresent()) {
throw new IllegalStateException("Already started a pack (" + currentPack.get().name() + ")");
}
currentPack = Optional.of(new BedrockPack(name, new MinecraftAssetResolver()));
}
public void run(Consumer<BedrockPack> consumer) {
currentPack.ifPresent(consumer);
}
public void runOrElse(Consumer<BedrockPack> consumer, Runnable runnable) {
currentPack.ifPresentOrElse(consumer, runnable);
}
public Optional<Path> getExportPath() {
return currentPack.map(BedrockPack::getExportPath);
}
public Optional<Boolean> finish() {
Optional<Boolean> success = currentPack.map(BedrockPack::save);
currentPack = Optional.empty();
return success;
}
}

View File

@@ -0,0 +1,27 @@
package org.geysermc.rainbow.client;
import net.fabricmc.api.ClientModInitializer;
import net.fabricmc.fabric.api.client.command.v2.ClientCommandRegistrationCallback;
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
import net.fabricmc.fabric.api.command.v2.ArgumentTypeRegistry;
import net.minecraft.commands.synchronization.SingletonArgumentInfo;
import org.geysermc.rainbow.Rainbow;
import org.geysermc.rainbow.client.command.CommandSuggestionsArgumentType;
import org.geysermc.rainbow.client.command.PackGeneratorCommand;
import org.geysermc.rainbow.client.mapper.PackMapper;
public class RainbowClient implements ClientModInitializer {
private final PackManager packManager = new PackManager();
private final PackMapper packMapper = new PackMapper(packManager);
// TODO export language overrides
@Override
public void onInitializeClient() {
ClientCommandRegistrationCallback.EVENT.register((dispatcher, buildContext) -> PackGeneratorCommand.register(dispatcher, packManager, packMapper));
ClientTickEvents.START_CLIENT_TICK.register(packMapper::tick);
ArgumentTypeRegistry.registerArgumentType(Rainbow.getModdedLocation("command_suggestions"),
CommandSuggestionsArgumentType.class, SingletonArgumentInfo.contextFree(CommandSuggestionsArgumentType::new));
}
}

View File

@@ -0,0 +1,16 @@
package org.geysermc.rainbow.client.accessor;
import net.minecraft.client.renderer.item.ClientItem;
import net.minecraft.client.resources.model.ResolvedModel;
import net.minecraft.resources.ResourceLocation;
import java.util.Optional;
// Implemented on ModelManager, since this class doesn't keep the resolved models or unbaked client items after baking, we have to store them manually.
// This comes with some extra memory usage, but Rainbow should only be used to convert packs, so it should be fine
public interface ResolvedModelAccessor {
Optional<ResolvedModel> rainbow$getResolvedModel(ResourceLocation location);
Optional<ClientItem> rainbow$getClientItem(ResourceLocation location);
}

View File

@@ -0,0 +1,54 @@
package org.geysermc.rainbow.client.command;
import com.mojang.brigadier.StringReader;
import com.mojang.brigadier.arguments.ArgumentType;
import com.mojang.brigadier.context.CommandContext;
import com.mojang.brigadier.context.StringRange;
import com.mojang.brigadier.suggestion.Suggestion;
import com.mojang.brigadier.suggestion.Suggestions;
import com.mojang.brigadier.suggestion.SuggestionsBuilder;
import com.mojang.datafixers.util.Pair;
import net.minecraft.client.Minecraft;
import java.util.concurrent.CompletableFuture;
public class CommandSuggestionsArgumentType implements ArgumentType<Pair<String, CompletableFuture<Suggestions>>> {
public static final CommandSuggestionsArgumentType TYPE = new CommandSuggestionsArgumentType();
@Override
public Pair<String, CompletableFuture<Suggestions>> parse(StringReader reader) {
String command = reader.getRemaining();
reader.setCursor(reader.getTotalLength());
return Pair.of(command, Minecraft.getInstance().getConnection().getSuggestionsProvider().customSuggestion(createCommandSuggestionsContext(command)));
}
@Override
public <S> CompletableFuture<Suggestions> listSuggestions(CommandContext<S> context, SuggestionsBuilder builder) {
int offset = builder.getStart();
return Minecraft.getInstance().getConnection().getSuggestionsProvider().customSuggestion(createCommandSuggestionsContext(builder.getRemaining()))
.thenApply(suggestions -> addOffset(suggestions, offset));
}
public static Pair<String, CompletableFuture<Suggestions>> getSuggestions(CommandContext<?> context, String argument) {
return context.getArgument(argument, Pair.class);
}
private Suggestions addOffset(Suggestions suggestions, int offset) {
StringRange offsetRange = addOffset(suggestions.getRange(), offset);
return new Suggestions(offsetRange, suggestions.getList().stream()
.map(suggestion -> new Suggestion(addOffset(suggestion.getRange(), offset), suggestion.getText(), suggestion.getTooltip()))
.toList());
}
private StringRange addOffset(StringRange range, int offset) {
return new StringRange(range.getStart() + offset, range.getEnd() + offset);
}
private static CommandContext<?> createCommandSuggestionsContext(String string) {
// hack
return new CommandContext<>(null,
string,
null, null, null, null, null, null, null, false);
}
}

View File

@@ -0,0 +1,137 @@
package org.geysermc.rainbow.client.command;
import com.mojang.brigadier.Command;
import com.mojang.brigadier.CommandDispatcher;
import com.mojang.brigadier.arguments.StringArgumentType;
import net.fabricmc.fabric.api.client.command.v2.ClientCommandManager;
import net.fabricmc.fabric.api.client.command.v2.FabricClientCommandSource;
import net.minecraft.ChatFormatting;
import net.minecraft.network.chat.ClickEvent;
import net.minecraft.network.chat.Component;
import net.minecraft.world.entity.player.Inventory;
import net.minecraft.world.item.ItemStack;
import org.geysermc.rainbow.client.PackManager;
import org.geysermc.rainbow.client.mapper.InventoryMapper;
import org.geysermc.rainbow.client.mapper.PackMapper;
import org.geysermc.rainbow.pack.BedrockPack;
import java.nio.file.Path;
import java.util.Optional;
import java.util.function.BiConsumer;
public class PackGeneratorCommand {
private static final Component NO_PACK_CREATED = Component.translatable("commands.rainbow.no_pack", Component.literal("/rainbow create <name>")
.withStyle(style -> style.withColor(ChatFormatting.BLUE).withUnderlined(true)
.withClickEvent(new ClickEvent.SuggestCommand("/rainbow create "))));
public static void register(CommandDispatcher<FabricClientCommandSource> dispatcher, PackManager packManager, PackMapper packMapper) {
dispatcher.register(ClientCommandManager.literal("rainbow")
.then(ClientCommandManager.literal("create")
.then(ClientCommandManager.argument("name", StringArgumentType.word())
.executes(context -> {
String name = StringArgumentType.getString(context, "name");
try {
packManager.startPack(name);
} catch (Exception exception) {
context.getSource().sendError(Component.translatable("commands.rainbow.create_pack_failed"));
throw new RuntimeException(exception);
}
context.getSource().sendFeedback(Component.translatable("commands.rainbow.pack_created", name));
return 0;
})
)
)
.then(ClientCommandManager.literal("map")
.executes(runWithPack(packManager, (source, pack) -> {
ItemStack heldItem = source.getPlayer().getMainHandItem();
switch (pack.map(heldItem)) {
case NONE_MAPPED -> source.sendError(Component.translatable("commands.rainbow.no_item_mapped"));
case PROBLEMS_OCCURRED -> source.sendFeedback(Component.translatable("commands.rainbow.mapped_held_item_problems"));
case MAPPED_SUCCESSFULLY -> source.sendFeedback(Component.translatable("commands.rainbow.mapped_held_item"));
}
}))
)
.then(ClientCommandManager.literal("mapinventory")
.executes(runWithPack(packManager, (source, pack) -> {
int mapped = 0;
boolean errors = false;
Inventory inventory = source.getPlayer().getInventory();
for (ItemStack stack : inventory) {
BedrockPack.MappingResult result = pack.map(stack);
if (result != BedrockPack.MappingResult.NONE_MAPPED) {
mapped++;
if (result == BedrockPack.MappingResult.PROBLEMS_OCCURRED) {
errors = true;
}
}
}
if (mapped > 0) {
source.sendFeedback(Component.translatable("commands.rainbow.mapped_items_from_inventory", mapped));
if (errors) {
source.sendFeedback(Component.translatable("commands.rainbow.mapped_items_problems"));
}
} else {
source.sendError(Component.translatable("commands.rainbow.no_items_mapped"));
}
}))
)
.then(ClientCommandManager.literal("auto")
/* This is disabled for now.
.then(ClientCommandManager.literal("command")
.then(ClientCommandManager.argument("suggestions", CommandSuggestionsArgumentType.TYPE)
.executes(context -> {
Pair<String, CompletableFuture<Suggestions>> suggestions = CommandSuggestionsArgumentType.getSuggestions(context, "suggestions");
String baseCommand = suggestions.getFirst();
suggestions.getSecond().thenAccept(completed -> {
ItemSuggestionProvider provider = new ItemSuggestionProvider(completed.getList().stream()
.map(suggestion -> baseCommand.substring(0, suggestion.getRange().getStart()) + suggestion.getText())
.toList());
packMapper.setItemProvider(provider);
context.getSource().sendFeedback(Component.literal("Running " + provider.queueSize() + " commands to obtain custom items to map"));
});
return 0;
})
)
)
*/
.then(ClientCommandManager.literal("inventory")
.executes(runWithPack(packManager, (source, pack) -> {
packMapper.setItemProvider(InventoryMapper.INSTANCE);
source.sendFeedback(Component.translatable("commands.rainbow.automatic_inventory_mapping"));
}))
)
.then(ClientCommandManager.literal("stop")
.executes(runWithPack(packManager, (source, pack) -> {
packMapper.setItemProvider(null);
source.sendFeedback(Component.translatable("commands.rainbow.stopped_automatic_mapping"));
}))
)
)
.then(ClientCommandManager.literal("finish")
.executes(context -> {
Optional<Path> exportPath = packManager.getExportPath();
packManager.finish().ifPresentOrElse(success -> {
if (!success) {
context.getSource().sendError(Component.translatable("commands.rainbow.pack_finished_error"));
} else {
context.getSource().sendFeedback(Component.translatable("commands.rainbow.pack_finished_successfully")
.withStyle(style -> style.withUnderlined(true).withClickEvent(new ClickEvent.OpenFile(exportPath.orElseThrow()))));
}
}, () -> context.getSource().sendError(NO_PACK_CREATED));
return 0;
})
)
);
}
private static Command<FabricClientCommandSource> runWithPack(PackManager manager, BiConsumer<FabricClientCommandSource, BedrockPack> executor) {
return context -> {
manager.runOrElse(pack -> executor.accept(context.getSource(), pack),
() -> context.getSource().sendError(NO_PACK_CREATED));
return 0;
};
}
}

View File

@@ -0,0 +1,14 @@
package org.geysermc.rainbow.client.mapper;
import net.minecraft.client.multiplayer.ClientPacketListener;
import net.minecraft.client.player.LocalPlayer;
import net.minecraft.world.item.ItemStack;
import java.util.stream.Stream;
public interface CustomItemProvider {
Stream<ItemStack> nextItems(LocalPlayer player, ClientPacketListener connection);
boolean isDone();
}

View File

@@ -0,0 +1,23 @@
package org.geysermc.rainbow.client.mapper;
import net.minecraft.client.multiplayer.ClientPacketListener;
import net.minecraft.client.player.LocalPlayer;
import net.minecraft.world.item.ItemStack;
import java.util.stream.Stream;
public class InventoryMapper implements CustomItemProvider {
public static final InventoryMapper INSTANCE = new InventoryMapper();
private InventoryMapper() {}
@Override
public Stream<ItemStack> nextItems(LocalPlayer player, ClientPacketListener connection) {
return player.containerMenu.getItems().stream();
}
@Override
public boolean isDone() {
return false;
}
}

View File

@@ -0,0 +1,53 @@
package org.geysermc.rainbow.client.mapper;
import net.minecraft.client.multiplayer.ClientPacketListener;
import net.minecraft.client.player.LocalPlayer;
import net.minecraft.network.protocol.game.ServerboundChatCommandPacket;
import net.minecraft.world.item.ItemStack;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Stream;
// TODO safety
public class ItemSuggestionProvider implements CustomItemProvider {
private final List<String> remainingCommands;
private boolean waitingOnItem = false;
private boolean waitingOnClear = false;
public ItemSuggestionProvider(List<String> commands) {
remainingCommands = new ArrayList<>(commands);
}
public Stream<ItemStack> nextItems(LocalPlayer player, ClientPacketListener connection) {
if (!remainingCommands.isEmpty() || waitingOnItem) {
if (waitingOnClear && player.getInventory().isEmpty()) {
waitingOnClear = false;
} else if (!waitingOnItem) {
connection.send(new ServerboundChatCommandPacket(remainingCommands.removeFirst()));
waitingOnItem = true;
} else {
if (!player.getInventory().isEmpty()) {
Stream<ItemStack> items = player.getInventory().getNonEquipmentItems().stream();
connection.send(new ServerboundChatCommandPacket("clear"));
waitingOnItem = false;
if (!remainingCommands.isEmpty()) {
waitingOnClear = true;
}
return items;
}
}
}
return Stream.empty();
}
public int queueSize() {
return remainingCommands.size();
}
@Override
public boolean isDone() {
return remainingCommands.isEmpty() && !waitingOnItem && !waitingOnClear;
}
}

View File

@@ -0,0 +1,45 @@
package org.geysermc.rainbow.client.mapper;
import net.minecraft.client.Minecraft;
import net.minecraft.client.multiplayer.ClientPacketListener;
import net.minecraft.client.player.LocalPlayer;
import net.minecraft.network.chat.Component;
import org.geysermc.rainbow.client.PackManager;
import org.geysermc.rainbow.pack.BedrockPack;
import java.util.Objects;
public class PackMapper {
private final PackManager packManager;
private CustomItemProvider itemProvider;
public PackMapper(PackManager packManager) {
this.packManager = packManager;
}
public void setItemProvider(CustomItemProvider itemProvider) {
this.itemProvider = itemProvider;
}
public void tick(Minecraft minecraft) {
if (itemProvider != null) {
LocalPlayer player = Objects.requireNonNull(minecraft.player);
ClientPacketListener connection = Objects.requireNonNull(minecraft.getConnection());
packManager.runOrElse(pack -> {
// TODO maybe report problems here... probably better to do so in pack class though
long mapped = itemProvider.nextItems(player, connection)
.map(pack::map)
.filter(result -> result != BedrockPack.MappingResult.NONE_MAPPED)
.count();
if (mapped != 0) {
player.displayClientMessage(Component.translatable("chat.rainbow.mapped_items", mapped), false);
}
if (itemProvider.isDone()) {
player.displayClientMessage(Component.translatable("chat.rainbow.automatic_mapping_finished"), false);
itemProvider = null;
}
}, () -> itemProvider = null);
}
}
}

View File

@@ -0,0 +1,13 @@
package org.geysermc.rainbow.client.mixin;
import net.minecraft.client.renderer.entity.EntityRenderDispatcher;
import net.minecraft.client.resources.model.EquipmentAssetManager;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.gen.Accessor;
@Mixin(EntityRenderDispatcher.class)
public interface EntityRenderDispatcherAccessor {
@Accessor
EquipmentAssetManager getEquipmentAssets();
}

View File

@@ -0,0 +1,16 @@
package org.geysermc.rainbow.client.mixin;
import net.minecraft.client.gui.render.state.GuiItemRenderState;
import net.minecraft.client.gui.render.state.ScreenArea;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.Constant;
import org.spongepowered.asm.mixin.injection.ModifyConstant;
@Mixin(GuiItemRenderState.class)
public abstract class GuiItemRenderStateMixin implements ScreenArea {
@ModifyConstant(method = "calculateOversizedItemBounds", constant = @Constant(intValue = 16))
public int neverReturnNull(int i) {
return -1;
}
}

View File

@@ -0,0 +1,59 @@
package org.geysermc.rainbow.client.mixin;
import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
import net.minecraft.client.Minecraft;
import net.minecraft.client.renderer.item.ClientItem;
import net.minecraft.client.resources.model.ClientItemInfoLoader;
import net.minecraft.client.resources.model.ModelManager;
import net.minecraft.client.resources.model.ResolvedModel;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.packs.resources.PreparableReloadListener;
import org.geysermc.rainbow.client.accessor.ResolvedModelAccessor;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Unique;
import org.spongepowered.asm.mixin.injection.At;
import java.lang.reflect.InvocationTargetException;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
@Mixin(ModelManager.class)
public abstract class ModelManagerMixin implements PreparableReloadListener, AutoCloseable, ResolvedModelAccessor {
@Unique
private Map<ResourceLocation, ResolvedModel> unbakedResolvedModels;
@Unique
private Map<ResourceLocation, ClientItem> clientItems;
@WrapOperation(method = "method_65753", at = @At(value = "INVOKE", target = "Ljava/util/concurrent/CompletableFuture;join()Ljava/lang/Object;", ordinal = 1))
private static Object setResolvedModels(CompletableFuture<?> instance, Operation<Object> original) {
Object resolved = original.call(instance);
try {
// Couldn't be bothered setting up access wideners, this resolves the second component of the ResolvedModels record, which is called "models"
// Ideally we'd somehow use the "this" instance, but that's not possible here since the lambda we inject into is a static one
((ModelManagerMixin) (Object) Minecraft.getInstance().getModelManager()).unbakedResolvedModels = (Map<ResourceLocation, ResolvedModel>) resolved.getClass().getRecordComponents()[1].getAccessor().invoke(resolved);
} catch (IllegalAccessException | InvocationTargetException | ClassCastException exception) {
throw new RuntimeException(exception);
}
return resolved;
}
@WrapOperation(method = "method_65753", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/resources/model/ClientItemInfoLoader$LoadedClientInfos;contents()Ljava/util/Map;"))
private static Map<ResourceLocation, ClientItem> setClientItems(ClientItemInfoLoader.LoadedClientInfos instance, Operation<Map<ResourceLocation, ClientItem>> original) {
// Same note as above for not using "this"
ModelManagerMixin thiz = ((ModelManagerMixin) (Object) Minecraft.getInstance().getModelManager());
thiz.clientItems = original.call(instance);
return thiz.clientItems;
}
@Override
public Optional<ResolvedModel> rainbow$getResolvedModel(ResourceLocation location) {
return unbakedResolvedModels == null ? Optional.empty() : Optional.ofNullable(unbakedResolvedModels.get(location));
}
@Override
public Optional<ClientItem> rainbow$getClientItem(ResourceLocation location) {
return clientItems == null ? Optional.empty() : Optional.ofNullable(clientItems.get(location));
}
}

View File

@@ -0,0 +1,13 @@
package org.geysermc.rainbow.client.mixin;
import com.mojang.blaze3d.textures.GpuTexture;
import net.minecraft.client.gui.render.pip.PictureInPictureRenderer;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.gen.Accessor;
@Mixin(PictureInPictureRenderer.class)
public interface PictureInPictureRendererAccessor {
@Accessor
GpuTexture getTexture();
}

View File

@@ -0,0 +1,34 @@
package org.geysermc.rainbow.client.mixin;
import com.mojang.blaze3d.textures.GpuTexture;
import net.minecraft.client.gui.render.pip.PictureInPictureRenderer;
import org.geysermc.rainbow.client.render.PictureInPictureCopyRenderer;
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.Constant;
import org.spongepowered.asm.mixin.injection.ModifyConstant;
@Mixin(PictureInPictureRenderer.class)
public abstract class PictureInPictureRendererMixin implements AutoCloseable, PictureInPictureCopyRenderer {
@Shadow
private @Nullable GpuTexture texture;
@Unique
private boolean allowTextureCopy = false;
@Override
public void rainbow$allowTextureCopy() {
if (texture != null) {
throw new IllegalStateException("texture already created");
}
allowTextureCopy = true;
}
@ModifyConstant(method = "prepareTexturesAndProjection", constant = @Constant(intValue = 12))
public int allowUsageCopySrc(int usage) {
return allowTextureCopy ? usage | GpuTexture.USAGE_COPY_SRC : usage;
}
}

View File

@@ -0,0 +1,12 @@
package org.geysermc.rainbow.client.mixin;
import net.minecraft.client.gui.components.SplashRenderer;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.gen.Accessor;
@Mixin(SplashRenderer.class)
public interface SplashRendererAccessor {
@Accessor
String getSplash();
}

View File

@@ -0,0 +1,81 @@
package org.geysermc.rainbow.client.render;
import com.mojang.blaze3d.buffers.GpuBuffer;
import com.mojang.blaze3d.platform.NativeImage;
import com.mojang.blaze3d.systems.CommandEncoder;
import com.mojang.blaze3d.systems.RenderSystem;
import com.mojang.blaze3d.textures.GpuTexture;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.navigation.ScreenRectangle;
import net.minecraft.client.gui.render.pip.OversizedItemRenderer;
import net.minecraft.client.gui.render.state.GuiItemRenderState;
import net.minecraft.client.gui.render.state.GuiRenderState;
import net.minecraft.client.gui.render.state.pip.OversizedItemRenderState;
import net.minecraft.client.renderer.item.TrackingItemStackRenderState;
import net.minecraft.world.item.ItemDisplayContext;
import net.minecraft.world.item.ItemStack;
import org.geysermc.rainbow.CodecUtil;
import org.geysermc.rainbow.mapping.geometry.GeometryRenderer;
import org.geysermc.rainbow.client.mixin.PictureInPictureRendererAccessor;
import org.joml.Matrix3x2fStack;
import java.io.IOException;
import java.nio.file.Path;
import java.util.Objects;
// TODO maybe just use this even for normal 2D items, not sure, could be useful for composite models and stuff
// TODO output in a size bedrock likes
public class MinecraftGeometryRenderer implements GeometryRenderer {
@Override
public boolean render(ItemStack stack, Path path) {
TrackingItemStackRenderState itemRenderState = new TrackingItemStackRenderState();
Minecraft.getInstance().getItemModelResolver().updateForTopItem(itemRenderState, stack, ItemDisplayContext.GUI, null, null, 0);
itemRenderState.setOversizedInGui(true);
GuiItemRenderState guiItemRenderState = new GuiItemRenderState("geometry_render", new Matrix3x2fStack(16), itemRenderState, 0, 0, null);
ScreenRectangle sizeBounds = guiItemRenderState.oversizedItemBounds();
Objects.requireNonNull(sizeBounds);
OversizedItemRenderState oversizedRenderState = new OversizedItemRenderState(guiItemRenderState, sizeBounds.left(), sizeBounds.top(), sizeBounds.right() + 4, sizeBounds.bottom() + 4);
try (OversizedItemRenderer itemRenderer = new OversizedItemRenderer(Minecraft.getInstance().renderBuffers().bufferSource())) {
//noinspection DataFlowIssue
((PictureInPictureCopyRenderer) itemRenderer).rainbow$allowTextureCopy();
itemRenderer.prepare(oversizedRenderState, new GuiRenderState(), 4);
writeAsPNG(path, ((PictureInPictureRendererAccessor) itemRenderer).getTexture());
}
return true;
}
// Simplified TextureUtil#writeAsPNG with some modifications to flip the image and just generate it at full size
private static void writeAsPNG(Path path, GpuTexture texture) {
RenderSystem.assertOnRenderThread();
int width = texture.getWidth(0);
int height = texture.getHeight(0);
int bufferSize = texture.getFormat().pixelSize() * width * height;
GpuBuffer buffer = RenderSystem.getDevice().createBuffer(() -> "Texture output buffer", GpuBuffer.USAGE_COPY_DST | GpuBuffer.USAGE_MAP_READ, bufferSize);
CommandEncoder commandEncoder = RenderSystem.getDevice().createCommandEncoder();
Runnable writer = () -> {
try (GpuBuffer.MappedView mappedView = commandEncoder.mapBuffer(buffer, true, false)) {
try (NativeImage nativeImage = new NativeImage(width, height, false)) {
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
int colour = mappedView.data().getInt((x + y * width) * texture.getFormat().pixelSize());
nativeImage.setPixelABGR(x, height - y - 1, colour);
}
}
CodecUtil.ensureDirectoryExists(path.getParent());
nativeImage.writeToFile(path);
} catch (IOException var19) {
// TODO
}
}
buffer.close();
};
commandEncoder.copyTextureToBuffer(texture, buffer, 0, writer, 0);
}
}

View File

@@ -0,0 +1,6 @@
package org.geysermc.rainbow.client.render;
public interface PictureInPictureCopyRenderer {
void rainbow$allowTextureCopy();
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 282 B

View File

@@ -0,0 +1,17 @@
{
"chat.rainbow.automatic_mapping_finished": "Finished mapping items from provider",
"chat.rainbow.mapped_items": "Mapped %d items",
"commands.rainbow.automatic_inventory_mapping": "Now watching inventories for custom items to map",
"commands.rainbow.create_pack_failed": "Failed to create new pack!",
"commands.rainbow.mapped_held_item": "The held item was mapped",
"commands.rainbow.mapped_held_item_problems": "The held item was mapped, however problems occurred whilst doing so. Read the pack report after finishing the pack for more information",
"commands.rainbow.mapped_items_from_inventory": "Mapped %d items from your inventory",
"commands.rainbow.mapped_items_problems": "Problems occurred whilst mapping items. Read the pack report after finishing the pack for more information",
"commands.rainbow.no_item_mapped": "No item was mapped. Either no custom item was found, or it was already included in the pack",
"commands.rainbow.no_items_mapped": "No items were mapped. Either no custom items were found, or they were already included in the pack",
"commands.rainbow.no_pack": "Create a pack first: %s",
"commands.rainbow.pack_created": "Created pack with name %s",
"commands.rainbow.pack_finished_error": "Errors occurred whilst writing the pack to disk!",
"commands.rainbow.pack_finished_successfully": "Wrote pack to disk",
"commands.rainbow.stopped_automatic_mapping": "Stopped automatic mapping of custom items"
}

View File

@@ -0,0 +1,38 @@
{
"schemaVersion": 1,
"id": "rainbow-client",
"version": "${version}",
"name": "Rainbow",
"description": "Rainbow is a mod to generate Geyser item mappings and bedrock resourcepacks for use with Geyser's custom item API (v2)",
"authors": [
"GeyserMC contributors"
],
"contact": {
"homepage": "https://github.com/GeyserMC/rainbow",
"issues": "https://github.com/GeyserMC/rainbow/issues",
"sources": "https://github.com/GeyserMC/rainbow"
},
"license": "MIT",
"icon": "assets/rainbow/icon.png",
"environment": "client",
"entrypoints": {
"client": [
"org.geysermc.rainbow.client.RainbowClient"
]
},
"mixins": [
"rainbow-client.mixins.json"
],
"depends": {
"fabricloader": ">=${loader_version}",
"fabric-api": "*",
"minecraft": "${supported_versions}"
},
"custom": {
"modmenu": {
"links": {
"modmenu.discord": "https://discord.gg/GeyserMC"
}
}
}
}

View File

@@ -0,0 +1,18 @@
{
"required": true,
"minVersion": "0.8",
"package": "org.geysermc.rainbow.client.mixin",
"compatibilityLevel": "JAVA_21",
"mixins": [],
"client": [
"EntityRenderDispatcherAccessor",
"GuiItemRenderStateMixin",
"ModelManagerMixin",
"PictureInPictureRendererAccessor",
"PictureInPictureRendererMixin",
"SplashRendererAccessor"
],
"injectors": {
"defaultRequire": 1
}
}