Add create debuginfo command (#5531)

This commit is contained in:
TropheusJ 2023-10-22 05:03:28 -04:00 committed by GitHub
parent 2e1bcdb619
commit 7eea02854e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 545 additions and 8 deletions

View file

@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists 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 networkTimeout=10000
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists

View file

@ -93,6 +93,8 @@ import com.simibubi.create.foundation.utility.ServerSpeedProvider;
import com.simibubi.create.infrastructure.command.HighlightPacket; import com.simibubi.create.infrastructure.command.HighlightPacket;
import com.simibubi.create.infrastructure.command.SConfigureConfigPacket; import com.simibubi.create.infrastructure.command.SConfigureConfigPacket;
import com.simibubi.create.infrastructure.debugInfo.ServerDebugInfoPacket;
import net.minecraft.core.BlockPos; import net.minecraft.core.BlockPos;
import net.minecraft.network.FriendlyByteBuf; import net.minecraft.network.FriendlyByteBuf;
import net.minecraft.resources.ResourceLocation; import net.minecraft.resources.ResourceLocation;
@ -203,7 +205,7 @@ public enum AllPackets {
CONTRAPTION_ACTOR_TOGGLE(ContraptionDisableActorPacket.class, ContraptionDisableActorPacket::new, PLAY_TO_CLIENT), CONTRAPTION_ACTOR_TOGGLE(ContraptionDisableActorPacket.class, ContraptionDisableActorPacket::new, PLAY_TO_CLIENT),
CONTRAPTION_COLLIDER_LOCK(ContraptionColliderLockPacket.class, ContraptionColliderLockPacket::new, PLAY_TO_CLIENT), CONTRAPTION_COLLIDER_LOCK(ContraptionColliderLockPacket.class, ContraptionColliderLockPacket::new, PLAY_TO_CLIENT),
ATTACHED_COMPUTER(AttachedComputerPacket.class, AttachedComputerPacket::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"); public static final ResourceLocation CHANNEL_NAME = Create.asResource("main");

View file

@ -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<String, String> getEntries();
}

View file

@ -30,6 +30,7 @@ public class AllCommands {
.then(OverlayConfigCommand.register()) .then(OverlayConfigCommand.register())
.then(DumpRailwaysCommand.register()) .then(DumpRailwaysCommand.register())
.then(FixLightingCommand.register()) .then(FixLightingCommand.register())
.then(DebugInfoCommand.register())
.then(HighlightCommand.register()) .then(HighlightCommand.register())
.then(KillTrainCommand.register()) .then(KillTrainCommand.register())
.then(PassengerCommand.register()) .then(PassengerCommand.register())
@ -39,6 +40,7 @@ public class AllCommands {
.then(CloneCommand.register()) .then(CloneCommand.register())
.then(GlueCommand.register()) .then(GlueCommand.register())
// utility // utility
.then(util); .then(util);

View file

@ -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<CommandSourceStack, ?> 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;
});
}
}

View file

@ -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<DebugInfoSection> client = new ArrayList<>();
private static final List<DebugInfoSection> server = new ArrayList<>();
private static final ImmutableMap<String, String> 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<DebugInfoSection> getClientInfo() {
return client;
}
public static List<DebugInfoSection> 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<InfoElement> listAllOtherMods() {
List<InfoElement> 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<InfoElement> listAllGraphicsCards() {
List<InfoElement> 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();
}
}

View file

@ -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();
}
}
}

View file

@ -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<DebugInfoSection> 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<DebugInfoSection> sections, StringBuilder output) {
output.append("<details>");
output.append('\n');
output.append("<summary>").append(side).append(" Info").append("</summary>");
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("</details>");
output.append('\n');
}
@OnlyIn(Dist.CLIENT)
private void handleOnClient() {
Player player = Objects.requireNonNull(Minecraft.getInstance().player);
StringBuilder output = new StringBuilder();
List<DebugInfoSection> 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);
}
}

View file

@ -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<InfoElement> 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<String> 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<InfoElement> 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<DebugInfoSection> children) {
return builder(name).putAll(children).build();
}
public static class Builder {
private final Builder parent;
private final String name;
private final ImmutableList.Builder<InfoElement> 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<String> value) {
return put(key, player -> value.get());
}
public Builder put(String key, String value) {
return put(key, player -> value);
}
public Builder putAll(Collection<? extends InfoElement> 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<DebugInfoSection> consumer) {
consumer.accept(this.build());
}
}
}

View file

@ -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<String> lineConsumer);
default void print(@Nullable Player player, Consumer<String> 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);
}
}
}

View file

@ -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<String> 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);
}
}

View file

@ -22,9 +22,14 @@
"accessor.GameTestHelperAccessor", "accessor.GameTestHelperAccessor",
"accessor.LivingEntityAccessor", "accessor.LivingEntityAccessor",
"accessor.NbtAccounterAccessor", "accessor.NbtAccounterAccessor",
"accessor.ServerLevelAccessor" "accessor.ServerLevelAccessor",
"accessor.SystemReportAccessor"
], ],
"client": [ "client": [
"accessor.AgeableListModelAccessor",
"accessor.GameRendererAccessor",
"accessor.HumanoidArmorLayerAccessor",
"accessor.ParticleEngineAccessor",
"client.BlockDestructionProgressMixin", "client.BlockDestructionProgressMixin",
"client.CameraMixin", "client.CameraMixin",
"client.EntityContraptionInteractionMixin", "client.EntityContraptionInteractionMixin",
@ -35,11 +40,7 @@
"client.MapRendererMapInstanceMixin", "client.MapRendererMapInstanceMixin",
"client.ModelDataRefreshMixin", "client.ModelDataRefreshMixin",
"client.PlayerRendererMixin", "client.PlayerRendererMixin",
"client.WindowResizeMixin", "client.WindowResizeMixin"
"accessor.AgeableListModelAccessor",
"accessor.GameRendererAccessor",
"accessor.HumanoidArmorLayerAccessor",
"accessor.ParticleEngineAccessor"
], ],
"injectors": { "injectors": {
"defaultRequire": 1 "defaultRequire": 1