/*
* Copyright (C) 2023-2024 NotEnoughUpdates contributors
*
* This file is part of NotEnoughUpdates.
*
* NotEnoughUpdates is free software: you can redistribute it
* and/or modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation, either
* version 3 of the License, or (at your option) any later version.
*
* NotEnoughUpdates is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with NotEnoughUpdates. If not, see .
*/
package io.github.moulberry.notenoughupdates.commands.dev
import com.mojang.brigadier.context.CommandContext
import com.sun.management.OperatingSystemMXBean
import com.sun.management.UnixOperatingSystemMXBean
import io.github.moulberry.notenoughupdates.NotEnoughUpdates
import io.github.moulberry.notenoughupdates.autosubscribe.NEUAutoSubscribe
import io.github.moulberry.notenoughupdates.events.RegisterBrigadierCommandEvent
import io.github.moulberry.notenoughupdates.util.*
import io.github.moulberry.notenoughupdates.util.brigadier.reply
import io.github.moulberry.notenoughupdates.util.brigadier.thenExecute
import io.github.moulberry.notenoughupdates.util.brigadier.thenLiteralExecute
import io.github.moulberry.notenoughupdates.util.brigadier.withHelp
import net.minecraft.client.Minecraft
import net.minecraft.client.renderer.OpenGlHelper
import net.minecraft.command.ICommandSender
import net.minecraft.util.EnumChatFormatting.DARK_RED
import net.minecraft.util.EnumChatFormatting.GREEN
import net.minecraftforge.common.ForgeVersion
import net.minecraftforge.fml.client.FMLClientHandler
import net.minecraftforge.fml.common.Loader
import net.minecraftforge.fml.common.eventhandler.SubscribeEvent
import org.lwjgl.opengl.Display
import org.lwjgl.opengl.GL11
import java.awt.datatransfer.StringSelection
import java.lang.management.ManagementFactory
import javax.management.JMX
import javax.management.ObjectName
@NEUAutoSubscribe
class NEUStatsCommand {
@SubscribeEvent
fun onCommands(event: RegisterBrigadierCommandEvent) {
event.command( "neustats") {
thenLiteralExecute("modlist") {
clipboardAndSendMessage(
DiscordMarkdownBuilder()
.also(::appendModList)
.toString()
)
}.withHelp("Copy the mod list to your clipboard")
thenLiteralExecute("tablist") {
clipboardAndSendMessage("```\n${TabListUtils.getTabList()}\n```")
}.withHelp("Copy the current tab list to your clipboard")
thenLiteralExecute("repo") {
clipboardAndSendMessage(
DiscordMarkdownBuilder()
.also(::appendRepoStats)
.also(::appendAdvancedRepoStats)
.toString()
)
}.withHelp("Copy the repo stats to your clipboard")
thenLiteralExecute("full") {
clipboardAndSendMessage(
DiscordMarkdownBuilder()
.also(::appendStats)
.also(::appendAdvancedRepoStats)
.also(::appendModList)
.toString()
)
}.withHelp("Copy the full list of all NEU stats and your mod list to your clipboard")
thenLiteralExecute("dump") {
reply("${GREEN}This will copy a summary of the loaded Java classes to your clipboard. Please paste this into your discord support ticket by pressing CTRL+V.")
generateDataUsage().copyToClipboard()
}.withHelp("Dump all loaded classes and their memory usage and copy that to your clipboard.")
thenExecute {
clipboardAndSendMessage(
DiscordMarkdownBuilder()
.also(::appendStats)
.also {
if (Loader.instance().activeModList.size <= 15) appendModList(it)
}
.toString()
)
}
}.withHelp("Copy a list of NEU relevant stats to your clipboard for debugging purposes")
}
interface DiagnosticCommandMXBean {
fun gcClassHistogram(array: Array): String
}
private fun generateDataUsage(): String {
val server = ManagementFactory.getPlatformMBeanServer()
val objectName = ObjectName.getInstance("com.sun.management:type=DiagnosticCommand")
val proxy = JMX.newMXBeanProxy(
server,
objectName,
DiagnosticCommandMXBean::class.java
)
return proxy.gcClassHistogram(emptyArray()).replace("[", "[]")
}
private fun getMemorySize(): Long {
try {
return (ManagementFactory.getOperatingSystemMXBean() as OperatingSystemMXBean).totalPhysicalMemorySize
} catch (e: java.lang.Exception) {
try {
return (ManagementFactory.getOperatingSystemMXBean() as UnixOperatingSystemMXBean).totalPhysicalMemorySize
} catch (ignored: java.lang.Exception) { /*IGNORE*/
}
}
return -1
}
val ONE_MB = 1024L * 1024L
private fun appendStats(builder: DiscordMarkdownBuilder) {
val maxMemory = Runtime.getRuntime().maxMemory()
val totalMemory = Runtime.getRuntime().totalMemory()
val freeMemory = Runtime.getRuntime().freeMemory()
val currentMemory = totalMemory - freeMemory
builder.category("System Stats")
builder.append("OS", System.getProperty("os.name"))
builder.append("CPU", OpenGlHelper.getCpu())
builder.append(
"Display",
String.format("%dx%d (%s)", Display.getWidth(), Display.getHeight(), GL11.glGetString(GL11.GL_VENDOR))
)
builder.append("GPU", GL11.glGetString(GL11.GL_RENDERER))
builder.append("GPU Driver", GL11.glGetString(GL11.GL_VERSION))
if (getMemorySize() > 0)
builder.append(
"Maximum Memory",
"${getMemorySize() / ONE_MB}MB"
)
builder.append("Shaders", ("" + OpenGlHelper.isFramebufferEnabled()).uppercase())
builder.category("Java Stats")
builder.append(
"Java",
"${System.getProperty("java.version")} ${if (Minecraft.getMinecraft().isJava64bit) 64 else 32}bit",
)
builder.append(
"Memory", String.format(
"% 2d%% %03d/%03dMB",
currentMemory * 100L / maxMemory,
currentMemory / ONE_MB,
maxMemory / ONE_MB
)
)
builder.append(
"Memory Allocated",
String.format("% 2d%% %03dMB", totalMemory * 100L / maxMemory, totalMemory / ONE_MB)
)
builder.category("Game Stats")
builder.append("FPS", Minecraft.getDebugFPS().toString())
builder.append("Loaded Mods", Loader.instance().activeModList.size)
builder.append("Forge", ForgeVersion.getVersion())
builder.append("Optifine", if (FMLClientHandler.instance().hasOptifine()) "TRUE" else "FALSE")
builder.category("Neu Settings")
builder.append("On SkyBlock", if (NotEnoughUpdates.INSTANCE.hasSkyblockScoreboard()) "TRUE" else "FALSE")
builder.append(
"Mod Version",
Loader.instance().indexedModList[NotEnoughUpdates.MODID]!!.displayVersion
)
builder.append(
"Version Id",
NotEnoughUpdates.VERSION_ID
)
builder.append("SB Profile", SBInfo.getInstance().currentProfile)
.also(::appendRepoStats)
}
private fun appendModList(builder: DiscordMarkdownBuilder): DiscordMarkdownBuilder {
builder.category("Mods Loaded")
Loader.instance().activeModList.forEach {
builder.append(it.name, "${it.source.name} (${it.displayVersion})")
}
return builder
}
private fun appendRepoStats(builder: DiscordMarkdownBuilder): DiscordMarkdownBuilder {
val apiData = NotEnoughUpdates.INSTANCE.config.apiData
if (apiData.repoUser.isEmpty() || apiData.repoName.isEmpty() || apiData.repoBranch.isEmpty()) {
apiData.repoUser = "NotEnoughUpdates"
apiData.repoName = "NotEnoughUpdates-REPO"
apiData.repoBranch = "master"
builder.category("Reset Repository location")
} else {
builder.category("Repo Stats")
builder.append("Last Commit", NotEnoughUpdates.INSTANCE.manager.latestRepoCommit)
builder.append("Repo Location", "https://github.com/${apiData.repoUser}/${apiData.repoName}/tree/${apiData.repoBranch}")
}
builder.append("Using Backup", NotEnoughUpdates.INSTANCE.manager.onBackupRepo)
builder.append("Loaded Items", NotEnoughUpdates.INSTANCE.manager.itemInformation.size.toString())
if (apiData.moulberryCodesApi.isEmpty()) {
apiData.moulberryCodesApi = "moulberry.codes"
builder.category("Reset API location")
} else {
builder.append("Lowest Bin API Location", apiData.moulberryCodesApi)
}
return builder
}
private fun appendAdvancedRepoStats(builder: DiscordMarkdownBuilder): DiscordMarkdownBuilder {
if (NotEnoughUpdates.INSTANCE.manager.repoLocation.isDirectory) {
val files = NotEnoughUpdates.INSTANCE.manager.repoLocation.listFiles()
builder.category("Repo Files")
files?.forEach { file ->
if (file.isDirectory) {
builder.append(file.name, file.listFiles()?.size)
} else if (file.isFile) {
builder.append("", file.name)
}
}
} else {
builder.category("Repo folder not found!")
}
return builder
}
fun CommandContext.clipboardAndSendMessage(data: String?) {
if (data == null) {
reply("${DARK_RED}Error occurred trying to perform command.")
return
}
try {
val clipboard = StringSelection(data)
Utils.copyToClipboard(clipboard, null)
reply("${GREEN}Dev info copied to clipboard.")
} catch (ignored: Exception) {
reply("${DARK_RED}Could not copy to clipboard.")
}
}
}