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:
@@ -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"))
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package org.geysermc.rainbow.client.render;
|
||||
|
||||
public interface PictureInPictureCopyRenderer {
|
||||
|
||||
void rainbow$allowTextureCopy();
|
||||
}
|
||||
BIN
client/src/main/resources/assets/rainbow/icon.png
Normal file
BIN
client/src/main/resources/assets/rainbow/icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 282 B |
17
client/src/main/resources/assets/rainbow/lang/en_us.json
Normal file
17
client/src/main/resources/assets/rainbow/lang/en_us.json
Normal 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"
|
||||
}
|
||||
38
client/src/main/resources/fabric.mod.json
Normal file
38
client/src/main/resources/fabric.mod.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
18
client/src/main/resources/rainbow-client.mixins.json
Normal file
18
client/src/main/resources/rainbow-client.mixins.json
Normal 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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user