Skip to content

Commit

Permalink
Add /packcontrol to enable/disable/hide resource packs
Browse files Browse the repository at this point in the history
  • Loading branch information
Gegy committed Nov 18, 2024
1 parent c07d35b commit 748f850
Show file tree
Hide file tree
Showing 8 changed files with 319 additions and 2 deletions.
107 changes: 107 additions & 0 deletions src/main/java/com/lovetropics/extras/client/ClientPackControl.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
package com.lovetropics.extras.client;

import com.google.common.collect.Sets;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonElement;
import com.google.gson.JsonParser;
import com.lovetropics.extras.data.packcontrol.PackControl;
import com.mojang.logging.LogUtils;
import com.mojang.serialization.JsonOps;
import net.minecraft.Util;
import net.minecraft.client.Minecraft;
import net.minecraft.server.packs.repository.Pack;
import net.minecraft.server.packs.repository.PackRepository;
import net.neoforged.fml.loading.FMLLoader;
import org.slf4j.Logger;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;

public class ClientPackControl {
private static final Logger LOGGER = LogUtils.getLogger();

private static final Path STATE_PATH = FMLLoader.getGamePath().resolve("config/client_pack_control.json");
private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();

private static CompletableFuture<PackControl.State> stateFuture;

static {
stateFuture = CompletableFuture.supplyAsync(ClientPackControl::readState).exceptionally(throwable -> {
LOGGER.error("Failed to read pack control state", throwable);
return PackControl.State.DEFAULT;
});
}

public static void updatePacks(PackControl.State state) {
PackControl.State oldState = state();
if (oldState.equals(state)) {
return;
}
handleStateChange(Minecraft.getInstance(), oldState, state);
stateFuture = CompletableFuture.completedFuture(state);
Util.ioPool().submit(() -> storeState(state));
}

private static void handleStateChange(Minecraft minecraft, PackControl.State oldState, PackControl.State newState) {
PackRepository packRepository = minecraft.getResourcePackRepository();

Set<String> newlyEnabled = Sets.difference(newState.enabled(), oldState.enabled());
Set<String> newlyDisabled = Sets.difference(oldState.enabled(), newState.enabled());

newlyEnabled.forEach(packRepository::addPack);
newlyDisabled.forEach(packRepository::removePack);

minecraft.options.updateResourcePacks(packRepository);
}

private static PackControl.State readState() {
if (!Files.exists(STATE_PATH)) {
return PackControl.State.DEFAULT;
}
try (BufferedReader reader = Files.newBufferedReader(STATE_PATH)) {
return PackControl.State.CODEC.parse(JsonOps.INSTANCE, JsonParser.parseReader(reader)).getOrThrow();
} catch (IOException e) {
throw new CompletionException(e);
}
}

private static void storeState(PackControl.State state) {
try (BufferedWriter writer = Files.newBufferedWriter(STATE_PATH)) {
JsonElement json = PackControl.State.CODEC.encodeStart(JsonOps.INSTANCE, state).getOrThrow();
GSON.toJson(json, writer);
} catch (Exception e) {
LOGGER.error("Failed to write pack control state", e);
}
}

public static PackControl.State state() {
return stateFuture.join();
}

public static Collection<Pack> removeHidden(Collection<Pack> packs, PackRepository repository) {
PackControl.State state = state();
if (state.hidden().isEmpty()) {
return packs;
}
List<Pack> newPacks = new ArrayList<>(packs);
Collection<String> selectedIds = repository.getSelectedIds();
if (newPacks.removeIf(pack -> {
// Let the player keep it if they already had it enabled
String id = pack.getId();
return state.hidden().contains(id) && !selectedIds.contains(id);
})) {
return newPacks;
}
return packs;
}
}
138 changes: 138 additions & 0 deletions src/main/java/com/lovetropics/extras/data/packcontrol/PackControl.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
package com.lovetropics.extras.data.packcontrol;

import com.lovetropics.extras.LTExtras;
import com.lovetropics.extras.network.message.ClientboundUpdatePackControl;
import com.mojang.brigadier.builder.LiteralArgumentBuilder;
import com.mojang.serialization.Codec;
import com.mojang.serialization.codecs.RecordCodecBuilder;
import io.netty.buffer.ByteBuf;
import net.minecraft.commands.CommandSourceStack;
import net.minecraft.commands.Commands;
import net.minecraft.core.HolderLookup;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.nbt.NbtOps;
import net.minecraft.network.codec.ByteBufCodecs;
import net.minecraft.network.codec.StreamCodec;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.level.saveddata.SavedData;
import net.neoforged.bus.api.SubscribeEvent;
import net.neoforged.fml.common.EventBusSubscriber;
import net.neoforged.neoforge.event.RegisterCommandsEvent;
import net.neoforged.neoforge.event.entity.player.PlayerEvent;

import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.function.BiFunction;
import java.util.function.UnaryOperator;

import static com.mojang.brigadier.arguments.StringArgumentType.getString;
import static com.mojang.brigadier.arguments.StringArgumentType.string;
import static net.minecraft.commands.Commands.argument;
import static net.minecraft.commands.Commands.literal;

@EventBusSubscriber(modid = LTExtras.MODID)
public class PackControl extends SavedData {
private static final Factory<PackControl> FACTORY = new Factory<>(PackControl::new, PackControl::load);

private static final String STORAGE_ID = LTExtras.MODID + "_pack_control";

private State state = State.DEFAULT;

@Override
public CompoundTag save(CompoundTag tag, HolderLookup.Provider registries) {
tag.put("state", State.CODEC.encodeStart(NbtOps.INSTANCE, state).getOrThrow());
return tag;
}

public static PackControl get(MinecraftServer server) {
return server.overworld().getDataStorage().computeIfAbsent(FACTORY, STORAGE_ID);
}

private static PackControl load(CompoundTag tag, HolderLookup.Provider registries) {
PackControl packControl = new PackControl();
State.CODEC.parse(NbtOps.INSTANCE, tag.get("state")).ifSuccess(state -> packControl.state = state);
return packControl;
}

@SubscribeEvent
public static void onPlayerLoggedIn(PlayerEvent.PlayerLoggedInEvent event) {
if (event.getEntity() instanceof ServerPlayer player) {
PackControl packControl = PackControl.get(player.getServer());
player.connection.send(new ClientboundUpdatePackControl(packControl.state));
}
}

@SubscribeEvent
public static void onRegisterCommands(RegisterCommandsEvent event) {
event.getDispatcher().register(literal("packcontrol")
.requires(source -> source.hasPermission(Commands.LEVEL_GAMEMASTERS))
.then(packUpdater("enable", (state, packId) -> state.setEnabled(packId, true)))
.then(packUpdater("disable", (state, packId) -> state.setEnabled(packId, false)))
.then(packUpdater("hide", (state, packId) -> state.setHidden(packId, true)))
.then(packUpdater("show", (state, packId) -> state.setHidden(packId, false)))
);
}

private static LiteralArgumentBuilder<CommandSourceStack> packUpdater(String name, BiFunction<State, String, State> updater) {
return literal(name)
.then(argument("pack", string())
.executes(context -> {
String pack = getString(context, "pack");
updateState(context.getSource().getServer(), state1 -> updater.apply(state1, pack));
return 1;
})
);
}

public static void updateState(MinecraftServer server, UnaryOperator<State> operator) {
PackControl packControl = PackControl.get(server);
State newState = operator.apply(packControl.state);
if (packControl.state.equals(newState)) {
return;
}
packControl.state = newState;
for (ServerPlayer player : server.getPlayerList().getPlayers()) {
player.connection.send(new ClientboundUpdatePackControl(newState));
}
packControl.setDirty();
}

public record State(Set<String> hidden, Set<String> enabled) {
public static final State DEFAULT = new State(Set.of(), Set.of());

private static final Codec<Set<String>> PACK_SET_CODEC = Codec.STRING.listOf().xmap(Set::copyOf, List::copyOf);

public static final Codec<State> CODEC = RecordCodecBuilder.create(i -> i.group(
PACK_SET_CODEC.fieldOf("hidden").forGetter(State::hidden),
PACK_SET_CODEC.fieldOf("enabled").forGetter(State::enabled)
).apply(i, State::new));

private static final StreamCodec<ByteBuf, Set<String>> PACK_SET_STREAM_CODEC = ByteBufCodecs.STRING_UTF8.apply(ByteBufCodecs.collection(HashSet::new));

public static final StreamCodec<ByteBuf, State> STREAM_CODEC = StreamCodec.composite(
PACK_SET_STREAM_CODEC, State::hidden,
PACK_SET_STREAM_CODEC, State::enabled,
State::new
);

public State setHidden(String packId, boolean hidden) {
return new State(setInSet(this.hidden, packId, hidden), enabled);
}

public State setEnabled(String packId, boolean enabled) {
return new State(this.hidden, setInSet(this.enabled, packId, enabled));
}

private static <T> Set<T> setInSet(Set<T> set, T value, boolean inSet) {
Set<T> newSet = new HashSet<>(set);
if (inSet) {
newSet.add(value);
} else {
newSet.remove(value);
}
return newSet;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
@ParametersAreNonnullByDefault
@MethodsReturnNonnullByDefault
@FieldsAreNonnullByDefault
package com.lovetropics.extras.data.packcontrol;

import com.mojang.blaze3d.FieldsAreNonnullByDefault;
import com.mojang.blaze3d.MethodsReturnNonnullByDefault;

import javax.annotation.ParametersAreNonnullByDefault;
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.lovetropics.extras.mixin.client.packcontrol;

import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
import com.lovetropics.extras.client.ClientPackControl;
import net.minecraft.client.gui.screens.packs.PackSelectionModel;
import net.minecraft.server.packs.repository.Pack;
import net.minecraft.server.packs.repository.PackRepository;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;

import java.util.Collection;

@Mixin(PackSelectionModel.class)
public class PackSelectionModelMixin {
@WrapOperation(method = "<init>", at = @At(value = "INVOKE", target = "Lnet/minecraft/server/packs/repository/PackRepository;getAvailablePacks()Ljava/util/Collection;"))
private Collection<Pack> initGetAvailablePacks(PackRepository repository, Operation<Collection<Pack>> original) {
return ClientPackControl.removeHidden(original.call(repository), repository);
}

@WrapOperation(method = "findNewPacks", at = @At(value = "INVOKE", target = "Lnet/minecraft/server/packs/repository/PackRepository;getAvailablePacks()Ljava/util/Collection;"))
private Collection<Pack> findNewPacksGetAvailablePacks(PackRepository repository, Operation<Collection<Pack>> original) {
return ClientPackControl.removeHidden(original.call(repository), repository);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
@ParametersAreNonnullByDefault
@MethodsReturnNonnullByDefault
@FieldsAreNonnullByDefault
package com.lovetropics.extras.mixin.client.packcontrol;

import com.mojang.blaze3d.FieldsAreNonnullByDefault;
import com.mojang.blaze3d.MethodsReturnNonnullByDefault;

import javax.annotation.ParametersAreNonnullByDefault;
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import com.lovetropics.extras.network.message.ClientboundOpenCollectibleBasketPacket;
import com.lovetropics.extras.network.message.ClientboundPoiFacesPacket;
import com.lovetropics.extras.network.message.ClientboundRemovePoiPacket;
import com.lovetropics.extras.network.message.ClientboundUpdatePackControl;
import com.lovetropics.extras.network.message.ClientboundUpdatePoiPacket;
import com.lovetropics.extras.network.message.ClientboundSetAutoRejoinIntent;
import com.lovetropics.extras.network.message.ClientboundSetDisplayTextPacket;
Expand Down Expand Up @@ -36,5 +37,6 @@ public static void registerPackets(RegisterPayloadHandlersEvent event) {
registrar.playToClient(ClientboundRemovePoiPacket.TYPE, ClientboundRemovePoiPacket.STREAM_CODEC, ClientboundRemovePoiPacket::handle);
registrar.playToClient(ClientboundPoiFacesPacket.TYPE, ClientboundPoiFacesPacket.STREAM_CODEC, ClientboundPoiFacesPacket::handle);
registrar.playToClient(ClientboundOpenCollectibleBasketPacket.TYPE, ClientboundOpenCollectibleBasketPacket.STREAM_CODEC, ClientboundOpenCollectibleBasketPacket::handle);
registrar.playToClient(ClientboundUpdatePackControl.TYPE, ClientboundUpdatePackControl.STREAM_CODEC, ClientboundUpdatePackControl::handle);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.lovetropics.extras.network.message;

import com.lovetropics.extras.LTExtras;
import com.lovetropics.extras.client.ClientPackControl;
import com.lovetropics.extras.data.packcontrol.PackControl;
import io.netty.buffer.ByteBuf;
import net.minecraft.network.codec.StreamCodec;
import net.minecraft.network.protocol.common.custom.CustomPacketPayload;
import net.neoforged.neoforge.network.handling.IPayloadContext;

public record ClientboundUpdatePackControl(
PackControl.State state
) implements CustomPacketPayload {
public static final StreamCodec<ByteBuf, ClientboundUpdatePackControl> STREAM_CODEC = PackControl.State.STREAM_CODEC.map(ClientboundUpdatePackControl::new, ClientboundUpdatePackControl::state);

public static final Type<ClientboundUpdatePackControl> TYPE = new Type<>(LTExtras.location("update_resource_packs"));

public static void handle(ClientboundUpdatePackControl packet, IPayloadContext context) {
ClientPackControl.updatePacks(packet.state);
}

@Override
public Type<ClientboundUpdatePackControl> type() {
return TYPE;
}
}
5 changes: 3 additions & 2 deletions src/main/resources/ltextras.mixins.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,15 +39,16 @@
"client.collectible.MultiPlayerGameModeMixin",
"client.menu.LogoRendererMixin",
"client.menu.TitleScreenMixin",
"client.packcontrol.PackSelectionModelMixin",
"client.perf.BlockModelRendererCacheMixin",
"client.perf.ChunkRenderMixin",
"client.perf.ClientChunkCacheStorageMixin",
"client.perf.ClientWorldMixin",
"client.perf.TagCollectorMixin",
"client.perf.ViewFrustumAccess",
"client.perf.WorldRendererAccess",
"client.translation.MinecraftMixin",
"client.world_effect.ClientLevelMixin",
"client.perf.TagCollectorMixin"
"client.world_effect.ClientLevelMixin"
],
"injectors": {
"defaultRequire": 1
Expand Down

0 comments on commit 748f850

Please sign in to comment.