diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 508322917..4e86b9270 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.2-bin.zip networkTimeout=10000 zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/src/main/java/com/simibubi/create/AllPackets.java b/src/main/java/com/simibubi/create/AllPackets.java index e6765afbe..6b27f594b 100644 --- a/src/main/java/com/simibubi/create/AllPackets.java +++ b/src/main/java/com/simibubi/create/AllPackets.java @@ -93,6 +93,8 @@ import com.simibubi.create.foundation.utility.ServerSpeedProvider; import com.simibubi.create.infrastructure.command.HighlightPacket; import com.simibubi.create.infrastructure.command.SConfigureConfigPacket; +import com.simibubi.create.infrastructure.debugInfo.ServerDebugInfoPacket; + import net.minecraft.core.BlockPos; import net.minecraft.network.FriendlyByteBuf; import net.minecraft.resources.ResourceLocation; @@ -203,7 +205,7 @@ public enum AllPackets { CONTRAPTION_ACTOR_TOGGLE(ContraptionDisableActorPacket.class, ContraptionDisableActorPacket::new, PLAY_TO_CLIENT), CONTRAPTION_COLLIDER_LOCK(ContraptionColliderLockPacket.class, ContraptionColliderLockPacket::new, PLAY_TO_CLIENT), ATTACHED_COMPUTER(AttachedComputerPacket.class, AttachedComputerPacket::new, PLAY_TO_CLIENT), - + SERVER_DEBUG_INFO(ServerDebugInfoPacket.class, ServerDebugInfoPacket::new, PLAY_TO_CLIENT) ; public static final ResourceLocation CHANNEL_NAME = Create.asResource("main"); diff --git a/src/main/java/com/simibubi/create/foundation/mixin/accessor/SystemReportAccessor.java b/src/main/java/com/simibubi/create/foundation/mixin/accessor/SystemReportAccessor.java new file mode 100644 index 000000000..3c46a1b6e --- /dev/null +++ b/src/main/java/com/simibubi/create/foundation/mixin/accessor/SystemReportAccessor.java @@ -0,0 +1,24 @@ +package com.simibubi.create.foundation.mixin.accessor; + +import net.minecraft.SystemReport; + +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Accessor; + +import java.util.Map; + +@Mixin(SystemReport.class) +public interface SystemReportAccessor { + @Accessor + static String getOPERATING_SYSTEM() { + throw new AssertionError(); + } + + @Accessor + static String getJAVA_VERSION() { + throw new AssertionError(); + } + + @Accessor + Map getEntries(); +} diff --git a/src/main/java/com/simibubi/create/infrastructure/command/AllCommands.java b/src/main/java/com/simibubi/create/infrastructure/command/AllCommands.java index ce5809dee..6e1381977 100644 --- a/src/main/java/com/simibubi/create/infrastructure/command/AllCommands.java +++ b/src/main/java/com/simibubi/create/infrastructure/command/AllCommands.java @@ -30,6 +30,7 @@ public class AllCommands { .then(OverlayConfigCommand.register()) .then(DumpRailwaysCommand.register()) .then(FixLightingCommand.register()) + .then(DebugInfoCommand.register()) .then(HighlightCommand.register()) .then(KillTrainCommand.register()) .then(PassengerCommand.register()) @@ -39,6 +40,7 @@ public class AllCommands { .then(CloneCommand.register()) .then(GlueCommand.register()) + // utility .then(util); diff --git a/src/main/java/com/simibubi/create/infrastructure/command/DebugInfoCommand.java b/src/main/java/com/simibubi/create/infrastructure/command/DebugInfoCommand.java new file mode 100644 index 000000000..ff6f58627 --- /dev/null +++ b/src/main/java/com/simibubi/create/infrastructure/command/DebugInfoCommand.java @@ -0,0 +1,35 @@ +package com.simibubi.create.infrastructure.command; + +import com.mojang.brigadier.Command; +import com.mojang.brigadier.builder.ArgumentBuilder; + +import com.simibubi.create.AllPackets; +import com.simibubi.create.foundation.utility.Components; + +import com.simibubi.create.infrastructure.debugInfo.DebugInformation; +import com.simibubi.create.infrastructure.debugInfo.ServerDebugInfoPacket; + +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.network.chat.ClickEvent; +import net.minecraft.network.chat.Component; +import net.minecraft.server.level.ServerPlayer; +import net.minecraftforge.network.PacketDistributor; + +import static net.minecraft.commands.Commands.literal; + +public class DebugInfoCommand { + public static ArgumentBuilder register() { + return literal("debuginfo").executes(ctx -> { + CommandSourceStack source = ctx.getSource(); + ServerPlayer player = source.getPlayerOrException(); + source.sendSuccess( + Components.literal("Sending server debug information to your client..."), true + ); + AllPackets.getChannel().send( + PacketDistributor.PLAYER.with(() -> player), + new ServerDebugInfoPacket(player) + ); + return Command.SINGLE_SUCCESS; + }); + } +} diff --git a/src/main/java/com/simibubi/create/infrastructure/debugInfo/DebugInformation.java b/src/main/java/com/simibubi/create/infrastructure/debugInfo/DebugInformation.java new file mode 100644 index 000000000..fde7ab2d1 --- /dev/null +++ b/src/main/java/com/simibubi/create/infrastructure/debugInfo/DebugInformation.java @@ -0,0 +1,162 @@ +package com.simibubi.create.infrastructure.debugInfo; + +import com.google.common.collect.ImmutableMap; +import com.jozufozu.flywheel.Flywheel; +import com.jozufozu.flywheel.backend.Backend; +import com.simibubi.create.Create; +import com.simibubi.create.foundation.mixin.accessor.SystemReportAccessor; +import com.simibubi.create.infrastructure.debugInfo.element.DebugInfoSection; + +import net.minecraft.SharedConstants; +import net.minecraft.SystemReport; +import net.minecraft.Util; +import net.minecraft.client.Minecraft; + +import com.simibubi.create.infrastructure.debugInfo.element.InfoElement; + +import com.simibubi.create.infrastructure.debugInfo.element.InfoEntry; + +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.fml.DistExecutor; +import net.minecraftforge.fml.ModList; +import net.minecraftforge.forgespi.language.IModInfo; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import javax.annotation.Nullable; + +import com.mojang.blaze3d.platform.GlUtil; + +/** + * Allows for providing easily accessible debugging information. + * This info can be retrieved with the "/create debuginfo" command. + * This command copies all information to the clipboard, formatted for a GitHub issue. + * Addons are welcome to add their own sections. Registration must occur synchronously. + */ +public class DebugInformation { + private static final List client = new ArrayList<>(); + private static final List server = new ArrayList<>(); + + private static final ImmutableMap mcSystemInfo = Util.make(() -> { + SystemReport systemReport = new SystemReport(); + SystemReportAccessor access = (SystemReportAccessor) systemReport; + return ImmutableMap.copyOf(access.getEntries()); + }); + + public static void registerClientInfo(DebugInfoSection section) { + client.add(section); + } + + public static void registerServerInfo(DebugInfoSection section) { + server.add(section); + } + + public static void registerBothInfo(DebugInfoSection section) { + registerClientInfo(section); + registerServerInfo(section); + } + + public static List getClientInfo() { + return client; + } + + public static List getServerInfo() { + return server; + } + + static { + DebugInfoSection.builder(Create.NAME) + .put("Mod Version", Create.VERSION) + .put("Forge Version", getVersionOfMod("forge")) + .put("Minecraft Version", SharedConstants.getCurrentVersion().getName()) + .buildTo(DebugInformation::registerBothInfo); + + DistExecutor.unsafeRunWhenOn(Dist.CLIENT, () -> () -> { + DebugInfoSection.builder("Graphics") + .put("Flywheel Version", Flywheel.getVersion().toString()) + .put("Flywheel Backend", () -> Backend.getBackendType().toString()) + .put("OpenGL Renderer", GlUtil::getRenderer) + .put("OpenGL Version", GlUtil::getOpenGLVersion) + .put("Graphics Mode", () -> Minecraft.getInstance().options.graphicsMode.toString()) + .buildTo(DebugInformation::registerClientInfo); + }); + + DebugInfoSection.builder("System Information") + .put("Operating System", SystemReportAccessor.getOPERATING_SYSTEM()) + .put("Java Version", SystemReportAccessor.getJAVA_VERSION()) + .put("JVM Flags", getMcSystemInfo("JVM Flags")) + .put("Memory", () -> getMcSystemInfo("Memory")) + .put("CPU", getCpuInfo()) + .putAll(listAllGraphicsCards()) + .buildTo(DebugInformation::registerBothInfo); + + DebugInfoSection.builder("Other Mods") + .putAll(listAllOtherMods()) + .buildTo(DebugInformation::registerBothInfo); + } + + public static String getVersionOfMod(String id) { + return ModList.get().getModContainerById(id) + .map(mod -> mod.getModInfo().getVersion().toString()) + .orElse("None"); + } + + public static Collection listAllOtherMods() { + List mods = new ArrayList<>(); + ModList.get().forEachModContainer((id, mod) -> { + if (!id.equals(Create.ID) && !id.equals("forge") && !id.equals("minecraft") && !id.equals("flywheel")) { + IModInfo info = mod.getModInfo(); + String name = info.getDisplayName(); + String version = info.getVersion().toString(); + mods.add(new InfoEntry(name, version)); + } + }); + return mods; + } + + public static Collection listAllGraphicsCards() { + List cards = new ArrayList<>(); + for (int i = 0; i < 10; i++) { // there won't be more than 10, right? right?? + String name = getMcSystemInfo("Graphics card #" + i + " name"); + String vendor = getMcSystemInfo("Graphics card #" + i + " vendor"); + String vram = getMcSystemInfo("Graphics card #" + i + " VRAM (MB)"); + if (name == null || vendor == null || vram == null) + break; + String key = "Graphics card #" + i; + String value = String.format("%s (%s); %s MB of VRAM", name, vendor, vram); + cards.add(new InfoEntry(key, value)); + } + return cards.isEmpty() ? List.of(new InfoEntry("Graphics cards", "none")) : cards; + } + + public static String getCpuInfo() { + String name = tryTrim(getMcSystemInfo("Processor Name")); + String freq = getMcSystemInfo("Frequency (GHz)"); + String sockets = getMcSystemInfo("Number of physical packages"); + String cores = getMcSystemInfo("Number of physical CPUs"); + String threads = getMcSystemInfo("Number of logical CPUs"); + return String.format("%s @ %s GHz; %s cores / %s threads on %s socket(s)", name, freq, cores, threads, sockets); + } + + /** + * Get a system attribute provided by Minecraft. + * They can be found in the constructor of {@link SystemReport}. + */ + @Nullable + public static String getMcSystemInfo(String key) { + return mcSystemInfo.get(key); + } + + public static String getIndent(int depth) { + return Stream.generate(() -> "\t").limit(depth).collect(Collectors.joining()); + } + + @Nullable + public static String tryTrim(@Nullable String s) { + return s == null ? null : s.trim(); + } +} diff --git a/src/main/java/com/simibubi/create/infrastructure/debugInfo/InfoProvider.java b/src/main/java/com/simibubi/create/infrastructure/debugInfo/InfoProvider.java new file mode 100644 index 000000000..3a8540f60 --- /dev/null +++ b/src/main/java/com/simibubi/create/infrastructure/debugInfo/InfoProvider.java @@ -0,0 +1,32 @@ +package com.simibubi.create.infrastructure.debugInfo; + +import java.util.Objects; + +import net.minecraft.world.entity.player.Player; + +import javax.annotation.Nullable; + +/** + * A supplier of debug information. May be queried on the client or server. + */ +@FunctionalInterface +public interface InfoProvider { + /** + * @param player the player requesting the data. May be null + */ + @Nullable + String getInfo(@Nullable Player player); + + default String getInfoSafe(Player player) { + try { + return Objects.toString(getInfo(player)); + } catch (Throwable t) { + StringBuilder builder = new StringBuilder("Error getting information!"); + builder.append(' ').append(t.getMessage()); + for (StackTraceElement element : t.getStackTrace()) { + builder.append('\n').append("\t").append(element.toString()); + } + return builder.toString(); + } + } +} diff --git a/src/main/java/com/simibubi/create/infrastructure/debugInfo/ServerDebugInfoPacket.java b/src/main/java/com/simibubi/create/infrastructure/debugInfo/ServerDebugInfoPacket.java new file mode 100644 index 000000000..3f3ff8cdd --- /dev/null +++ b/src/main/java/com/simibubi/create/infrastructure/debugInfo/ServerDebugInfoPacket.java @@ -0,0 +1,85 @@ +package com.simibubi.create.infrastructure.debugInfo; + +import java.util.List; +import java.util.Objects; + +import com.simibubi.create.foundation.networking.SimplePacketBase; + +import com.simibubi.create.foundation.utility.Components; +import com.simibubi.create.infrastructure.debugInfo.element.DebugInfoSection; + +import net.minecraft.ChatFormatting; +import net.minecraft.client.Minecraft; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.chat.Component; +import net.minecraft.world.entity.player.Player; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.api.distmarker.OnlyIn; +import net.minecraftforge.fml.DistExecutor; +import net.minecraftforge.network.NetworkEvent; + +public class ServerDebugInfoPacket extends SimplePacketBase { + public static final Component COPIED = Components.literal( + "Debug information has been copied to your clipboard." + ).withStyle(ChatFormatting.GREEN); + + private final List serverInfo; + private final Player player; + + public ServerDebugInfoPacket(Player player) { + this.serverInfo = DebugInformation.getServerInfo(); + this.player = player; + } + + public ServerDebugInfoPacket(FriendlyByteBuf buffer) { + this.serverInfo = buffer.readList(DebugInfoSection::readDirect); + this.player = null; + } + + @Override + public void write(FriendlyByteBuf buffer) { + buffer.writeCollection(this.serverInfo, (buf, section) -> section.write(player, buf)); + } + + @Override + public boolean handle(NetworkEvent.Context context) { + context.enqueueWork(() -> DistExecutor.unsafeRunWhenOn(Dist.CLIENT, () -> this::handleOnClient)); + return true; + } + + private void printInfo(String side, Player player, List sections, StringBuilder output) { + output.append("
"); + output.append('\n'); + output.append("").append(side).append(" Info").append(""); + output.append('\n').append('\n'); + output.append("```"); + output.append('\n'); + + for (int i = 0; i < sections.size(); i++) { + if (i != 0) { + output.append('\n'); + } + sections.get(i).print(player, line -> output.append(line).append('\n')); + } + + output.append("```"); + output.append('\n').append('\n'); + output.append("
"); + output.append('\n'); + } + + @OnlyIn(Dist.CLIENT) + private void handleOnClient() { + Player player = Objects.requireNonNull(Minecraft.getInstance().player); + StringBuilder output = new StringBuilder(); + List clientInfo = DebugInformation.getClientInfo(); + + printInfo("Client", player, clientInfo, output); + output.append("\n\n"); + printInfo("Server", player, serverInfo, output); + + String text = output.toString(); + Minecraft.getInstance().keyboardHandler.setClipboard(text); + player.displayClientMessage(COPIED, true); + } +} diff --git a/src/main/java/com/simibubi/create/infrastructure/debugInfo/element/DebugInfoSection.java b/src/main/java/com/simibubi/create/infrastructure/debugInfo/element/DebugInfoSection.java new file mode 100644 index 000000000..f3d7133e6 --- /dev/null +++ b/src/main/java/com/simibubi/create/infrastructure/debugInfo/element/DebugInfoSection.java @@ -0,0 +1,115 @@ +package com.simibubi.create.infrastructure.debugInfo.element; + +import com.google.common.collect.ImmutableList; + +import com.simibubi.create.infrastructure.debugInfo.DebugInformation; +import com.simibubi.create.infrastructure.debugInfo.InfoProvider; + +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.world.entity.player.Player; + +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.function.Consumer; +import java.util.function.Supplier; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * A section for organizing debug information. Can contain both information and other sections. + * To create one, use the {@link #builder(String) builder} method. + */ +public record DebugInfoSection(String name, ImmutableList elements) implements InfoElement { + @Override + public void write(Player player, FriendlyByteBuf buffer) { + buffer.writeBoolean(true); + buffer.writeUtf(name); + buffer.writeCollection(elements, (buf, element) -> element.write(player, buf)); + } + + public Builder builder() { + return builder(name).putAll(elements); + } + + @Override + public void print(int depth, @Nullable Player player, Consumer lineConsumer) { + String indent = DebugInformation.getIndent(depth); + lineConsumer.accept(indent + name + ":"); + elements.forEach(element -> element.print(depth + 1, player, lineConsumer)); + } + + public static DebugInfoSection read(FriendlyByteBuf buffer) { + String name = buffer.readUtf(); + ArrayList elements = buffer.readCollection(ArrayList::new, InfoElement::read); + return new DebugInfoSection(name, ImmutableList.copyOf(elements)); + } + + public static DebugInfoSection readDirect(FriendlyByteBuf buf) { + buf.readBoolean(); // discard type marker + return read(buf); + } + + public static Builder builder(String name) { + return new Builder(null, name); + } + + public static DebugInfoSection of(String name, Collection children) { + return builder(name).putAll(children).build(); + } + + public static class Builder { + private final Builder parent; + private final String name; + private final ImmutableList.Builder elements; + + public Builder(Builder parent, String name) { + this.parent = parent; + this.name = name; + this.elements = ImmutableList.builder(); + } + + public Builder put(InfoElement element) { + this.elements.add(element); + return this; + } + + public Builder put(String key, InfoProvider provider) { + return put(new InfoEntry(key, provider)); + } + + public Builder put(String key, Supplier value) { + return put(key, player -> value.get()); + } + + public Builder put(String key, String value) { + return put(key, player -> value); + } + + public Builder putAll(Collection elements) { + elements.forEach(this::put); + return this; + } + + public Builder section(String name) { + return new Builder(this, name); + } + + public Builder finishSection() { + if (parent == null) { + throw new IllegalStateException("Cannot finish the root section"); + } + parent.elements.add(this.build()); + return parent; + } + + public DebugInfoSection build() { + return new DebugInfoSection(name, elements.build()); + } + + public void buildTo(Consumer consumer) { + consumer.accept(this.build()); + } + } +} diff --git a/src/main/java/com/simibubi/create/infrastructure/debugInfo/element/InfoElement.java b/src/main/java/com/simibubi/create/infrastructure/debugInfo/element/InfoElement.java new file mode 100644 index 000000000..d5179af11 --- /dev/null +++ b/src/main/java/com/simibubi/create/infrastructure/debugInfo/element/InfoElement.java @@ -0,0 +1,27 @@ +package com.simibubi.create.infrastructure.debugInfo.element; + +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.world.entity.player.Player; + +import org.jetbrains.annotations.Nullable; + +import java.util.function.Consumer; + +public sealed interface InfoElement permits DebugInfoSection, InfoEntry { + void write(Player player, FriendlyByteBuf buffer); + + void print(int depth, @Nullable Player player, Consumer lineConsumer); + + default void print(@Nullable Player player, Consumer lineConsumer) { + print(0, player, lineConsumer); + } + + static InfoElement read(FriendlyByteBuf buffer) { + boolean section = buffer.readBoolean(); + if (section) { + return DebugInfoSection.read(buffer); + } else { + return InfoEntry.read(buffer); + } + } +} diff --git a/src/main/java/com/simibubi/create/infrastructure/debugInfo/element/InfoEntry.java b/src/main/java/com/simibubi/create/infrastructure/debugInfo/element/InfoEntry.java new file mode 100644 index 000000000..837875a8e --- /dev/null +++ b/src/main/java/com/simibubi/create/infrastructure/debugInfo/element/InfoEntry.java @@ -0,0 +1,52 @@ +package com.simibubi.create.infrastructure.debugInfo.element; + +import com.simibubi.create.infrastructure.debugInfo.DebugInformation; +import com.simibubi.create.infrastructure.debugInfo.InfoProvider; + +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.world.entity.player.Player; + +import org.jetbrains.annotations.Nullable; + +import java.util.function.Consumer; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public record InfoEntry(String name, InfoProvider provider) implements InfoElement { + public InfoEntry(String name, String info) { + this(name, player -> info); + } + + @Override + public void write(Player player, FriendlyByteBuf buffer) { + buffer.writeBoolean(false); + buffer.writeUtf(name); + buffer.writeUtf(provider.getInfoSafe(player)); + } + + @Override + public void print(int depth, @Nullable Player player, Consumer lineConsumer) { + String value = provider.getInfoSafe(player); + String indent = DebugInformation.getIndent(depth); + if (value.contains("\n")) { + String[] lines = value.split("\n"); + String firstLine = lines[0]; + String lineStart = name + ": "; + lineConsumer.accept(indent + lineStart + firstLine); + String extraIndent = Stream.generate(() -> " ").limit(lineStart.length()).collect(Collectors.joining()); + + for (int i = 1; i < lines.length; i++) { + lineConsumer.accept(indent + extraIndent + lines[i]); + } + } else { + lineConsumer.accept(indent + name + ": " + value); + } + + } + + public static InfoEntry read(FriendlyByteBuf buffer) { + String name = buffer.readUtf(); + String value = buffer.readUtf(); + return new InfoEntry(name, value); + } +} diff --git a/src/main/resources/create.mixins.json b/src/main/resources/create.mixins.json index 29193b8b3..63c44c55f 100644 --- a/src/main/resources/create.mixins.json +++ b/src/main/resources/create.mixins.json @@ -22,9 +22,14 @@ "accessor.GameTestHelperAccessor", "accessor.LivingEntityAccessor", "accessor.NbtAccounterAccessor", - "accessor.ServerLevelAccessor" + "accessor.ServerLevelAccessor", + "accessor.SystemReportAccessor" ], "client": [ + "accessor.AgeableListModelAccessor", + "accessor.GameRendererAccessor", + "accessor.HumanoidArmorLayerAccessor", + "accessor.ParticleEngineAccessor", "client.BlockDestructionProgressMixin", "client.CameraMixin", "client.EntityContraptionInteractionMixin", @@ -35,11 +40,7 @@ "client.MapRendererMapInstanceMixin", "client.ModelDataRefreshMixin", "client.PlayerRendererMixin", - "client.WindowResizeMixin", - "accessor.AgeableListModelAccessor", - "accessor.GameRendererAccessor", - "accessor.HumanoidArmorLayerAccessor", - "accessor.ParticleEngineAccessor" + "client.WindowResizeMixin" ], "injectors": { "defaultRequire": 1