Skip to content

Instantly share code, notes, and snippets.

@Ensamisten
Last active November 4, 2024 14:58
Show Gist options
  • Save Ensamisten/8c833138a6af44cd5efe31aee5562365 to your computer and use it in GitHub Desktop.
Save Ensamisten/8c833138a6af44cd5efe31aee5562365 to your computer and use it in GitHub Desktop.
package io.github.ensamisten.util.mcfiles;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import io.github.ensamisten.AllyshipClient;
import net.fabricmc.loader.api.FabricLoader;
import net.minecraft.GameVersion;
import net.minecraft.SharedConstants;
import net.minecraft.client.MinecraftClient;
import net.minecraft.client.font.TextRenderer;
import net.minecraft.client.gui.DrawContext;
import net.minecraft.client.gui.screen.Screen;
import net.minecraft.client.gui.screen.narration.NarrationMessageBuilder;
import net.minecraft.client.gui.widget.EntryListWidget;
import net.minecraft.client.render.RenderLayer;
import net.minecraft.client.texture.NativeImage;
import net.minecraft.client.texture.NativeImageBackedTexture;
import net.minecraft.text.Text;
import net.minecraft.util.Identifier;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
public class McFilesScreen extends Screen {
private static final Logger LOGGER = LoggerFactory.getLogger(McFilesScreen.class);
private final List<ModInfo> modInfos;
private ModListWidget modListWidget;
// For notifications
private final List<Text> notifications = new ArrayList<>();
private final List<Long> notificationTimestamps = new ArrayList<>();
private static final long NOTIFICATION_DURATION_MS = 5000; // 5 seconds
private final Path modsFolderPath;
public McFilesScreen() {
super(Text.literal("McFiles"));
this.modInfos = new ArrayList<>();
this.modsFolderPath = FabricLoader.getInstance().getGameDir().resolve("mods");
loadModInfos();
}
private void loadModInfos() {
// Ensure the mods directory exists
try {
if (!Files.exists(modsFolderPath)) {
Files.createDirectories(modsFolderPath);
}
} catch (IOException e) {
LOGGER.error("Failed to create mods directory.", e);
return;
}
List<Path> modPaths;
// Search mods directory recursively
try (Stream<Path> modStream = Files.walk(modsFolderPath)) {
modPaths = new ArrayList<>(modStream
.filter(path -> Files.isRegularFile(path) && path.toString().endsWith(".jar"))
.collect(Collectors.toList()));
} catch (IOException e) {
LOGGER.error("Error while walking mods directory.", e);
return;
}
// Process each mod file
for (Path modPath : modPaths) {
processModFile(modPath);
}
}
private void processModFile(Path modPath) {
File file = modPath.toFile();
try (ZipFile zipFile = new ZipFile(file)) {
ZipEntry entry = zipFile.getEntry("fabric.mod.json");
if (entry != null) {
try (InputStream inputStream = zipFile.getInputStream(entry);
InputStreamReader reader = new InputStreamReader(inputStream)) {
JsonObject modJson = JsonParser.parseReader(reader).getAsJsonObject();
String modId = modJson.get("id").getAsString();
String modName = modJson.has("name") ? modJson.get("name").getAsString() : modId;
String modVersion = modJson.get("version").getAsString();
// Fetch the fabric loader version
String fabricLoaderVersion = getDependencyVersion(modJson, "fabricloader");
if (fabricLoaderVersion == null) {
fabricLoaderVersion = "unknown_version";
}
// Load the mod metadata
ModMetadata modMetadata = new ModMetadata(
modId,
modVersion,
modName,
getDependencyVersion(modJson, "minecraft"),
fabricLoaderVersion
);
// Load the icon
Identifier iconIdentifier;
NativeImageBackedTexture iconTexture = null;
// Extract the icon path from "icon" field in fabric.mod.json
String iconPath = modJson.has("icon") ? modJson.get("icon").getAsString() : null;
if (iconPath != null) {
ZipEntry iconEntry = zipFile.getEntry(iconPath);
if (iconEntry != null) {
try (InputStream iconStream = zipFile.getInputStream(iconEntry)) {
NativeImage nativeImage = NativeImage.read(iconStream);
iconTexture = new NativeImageBackedTexture(nativeImage);
// Generate a unique identifier for the texture
iconIdentifier = Identifier.of(AllyshipClient.MOD_ID, "icons/" + modId);
// Register the texture
MinecraftClient.getInstance().getTextureManager().registerTexture(iconIdentifier, iconTexture);
LOGGER.info("Loaded icon for mod: {} from {}", modId, iconPath);
} catch (IOException e) {
LOGGER.error("Failed to read icon for mod: {}", modId, e);
// Use default icon
iconIdentifier = Identifier.ofVanilla("textures/block/grass_block_side.png");
}
} else {
LOGGER.warn("Icon file not found for mod: {} at {}", modId, iconPath);
// Use default icon
iconIdentifier = Identifier.ofVanilla("textures/block/grass_block_side.png");
}
} else {
LOGGER.warn("Icon path not specified in fabric.mod.json for mod: {}", modId);
// Use default icon
iconIdentifier = Identifier.ofVanilla("textures/block/grass_block_side.png");
}
// Determine if the mod is enabled (not in a "disabled" subdirectory)
boolean isEnabled = modPath.getParent().equals(modsFolderPath);
// Add the mod info to the list
modInfos.add(new ModInfo(file.getName(), modMetadata, iconIdentifier, iconTexture, modPath, isEnabled));
}
} else {
LOGGER.warn("fabric.mod.json not found in {}", file.getName());
}
} catch (IOException e) {
LOGGER.error("Failed to read mod file: {}", file.getName(), e);
}
}
private String getDependencyVersion(JsonObject modJson, String modId) {
if (modJson.has("depends")) {
JsonObject dependencies = modJson.getAsJsonObject("depends");
if (dependencies.has(modId)) {
// Handle if the dependency is specified as a string
if (dependencies.get(modId).isJsonPrimitive()) {
return dependencies.get(modId).getAsString();
}
// Handle other cases as needed (e.g., object or array)
}
}
return null;
}
@Override
public void tick() {
super.tick();
// Clean up old notifications
cleanUpNotifications(System.currentTimeMillis());
}
private void cleanUpNotifications(long currentTime) {
Iterator<Long> timestampIterator = notificationTimestamps.iterator();
Iterator<Text> notificationIterator = notifications.iterator();
while (timestampIterator.hasNext() && notificationIterator.hasNext()) {
Long timestamp = timestampIterator.next();
notificationIterator.next();
if (currentTime - timestamp > NOTIFICATION_DURATION_MS) {
timestampIterator.remove();
notificationIterator.remove();
}
}
}
@Override
protected void init() {
super.init();
int top = 30;
int bottom = this.height - 30;
int iconSize = 64; // Match the icon size
int padding = 10; // Padding between icons and rows
int listWidth = this.width - 40; // Adjusted to have margins on both sides
int listX = 20; // Left margin
modListWidget = new ModListWidget(this.client, listWidth, this.height, top, bottom, iconSize + 30, padding);
this.addSelectableChild(modListWidget);
// Populate the list
populateList();
}
private void populateList() {
LOGGER.info("Populating mod list with {} entries", modInfos.size());
int iconsPerRow = 4;
List<ModInfo> currentRow = new ArrayList<>();
for (int i = 0; i < modInfos.size(); i++) {
currentRow.add(modInfos.get(i));
if (currentRow.size() == iconsPerRow || i == modInfos.size() - 1) {
ModListWidget.Entry entry = modListWidget.new Entry(new ArrayList<>(currentRow));
modListWidget.addEntry(entry);
LOGGER.info("Added mod row with {} mods", currentRow.size());
currentRow.clear();
}
}
LOGGER.info("Total entries in widget: {}", modListWidget.children().size());
}
@Override
public void render(DrawContext drawContext, int mouseX, int mouseY, float delta) {
this.renderBackground(drawContext, mouseX, mouseY, delta);
TextRenderer textRenderer = this.client.textRenderer;
// Display the Minecraft version in the top-right corner
GameVersion minecraftVersion = SharedConstants.getGameVersion();
String versionText = "Minecraft Version: " + minecraftVersion.getName();
int versionWidth = textRenderer.getWidth(versionText);
drawContext.drawText(textRenderer, Text.literal(versionText), this.width - versionWidth - 10, 10, 0xFFFFFF, false);
// Render the ListWidget
modListWidget.render(drawContext, mouseX, mouseY, delta);
// Render notifications
int notificationY = this.height - 30;
for (int i = 0; i < notifications.size(); i++) {
Text notification = notifications.get(i);
drawContext.drawText(textRenderer, notification, 10, notificationY, 0xFFFF00, false);
notificationY -= 10; // Adjust spacing as needed
}
super.render(drawContext, mouseX, mouseY, delta);
// Render tooltips above everything else
if (modListWidget.getHoveredEntry() != null) {
modListWidget.getHoveredEntry().renderTooltip(drawContext, mouseX, mouseY);
}
}
@Override
public void removed() {
super.removed();
// Dispose of dynamic textures
for (ModInfo modInfo : modInfos) {
if (modInfo.iconTexture != null) {
modInfo.iconTexture.close();
// Also unregister the texture
this.client.getTextureManager().destroyTexture(modInfo.iconIdentifier);
}
}
}
// ModInfo class definition
private static class ModInfo {
public final String fileName;
public final ModMetadata metadata;
public final Identifier iconIdentifier;
public final NativeImageBackedTexture iconTexture;
public Path modPath;
public boolean isEnabled;
public ModInfo(String fileName, ModMetadata metadata, Identifier iconIdentifier, NativeImageBackedTexture iconTexture, Path modPath, boolean isEnabled) {
this.fileName = fileName;
this.metadata = metadata;
this.iconIdentifier = iconIdentifier;
this.iconTexture = iconTexture;
this.modPath = modPath;
this.isEnabled = isEnabled;
}
}
// ModMetadata class definition
private static class ModMetadata {
public final String id;
public final String version;
public final String name;
public final String minecraftVersion;
public final String fabricLoaderVersion;
public ModMetadata(String id, String version, String name, String minecraftVersion, String fabricLoaderVersion) {
this.id = id;
this.version = version;
this.name = name;
this.minecraftVersion = minecraftVersion;
this.fabricLoaderVersion = fabricLoaderVersion;
}
}
private class ModListWidget extends EntryListWidget<ModListWidget.Entry> {
private final int padding;
public ModListWidget(MinecraftClient client, int width, int height, int top, int bottom, int itemHeight, int padding) {
super(client, width, height, top, itemHeight);
this.padding = padding;
}
@Override
public int getRowWidth() {
return this.width;
}
@Override
protected int getScrollbarX() {
return this.width - 6;
}
@Override
protected void appendClickableNarrations(NarrationMessageBuilder builder) {
// Implement if needed
}
// New Entry class representing a row of mods
public class Entry extends EntryListWidget.Entry<ModListWidget.Entry> {
private final List<ModInfo> modsInRow;
private final int iconsPerRow = 4;
private final int padding = ModListWidget.this.padding;
private final int iconSize = 64;
// Variables to store the last rendered positions
private int lastRenderX;
private int lastRenderY;
// Variables to store hover state
private ModInfo hoveredMod = null;
private int hoveredModIndex = -1;
public Entry(List<ModInfo> mods) {
this.modsInRow = mods;
}
@Override
public void render(DrawContext drawContext, int index, int y, int x, int entryWidth, int entryHeight, int mouseX, int mouseY, boolean hovered, float delta) {
// Store the current x and y positions
this.lastRenderX = x;
this.lastRenderY = y;
TextRenderer textRenderer = client.textRenderer;
int startX = x + padding; // Starting X position for the first icon in the row
hoveredMod = null;
hoveredModIndex = -1;
for (int i = 0; i < modsInRow.size(); i++) {
ModInfo modInfo = modsInRow.get(i);
int currentX = startX + i * (iconSize + padding);
int currentY = y;
// Draw mod name centered above the icon
String modName = modInfo.metadata.name;
int textWidth = textRenderer.getWidth(modName);
int textX = currentX + (iconSize - textWidth) / 2;
int textY = currentY + 5; // Top padding
drawContext.drawText(textRenderer, Text.literal(modName), textX, textY, 0xFFFFFF, false);
// Draw icon centered below the mod name
int iconY = textY + textRenderer.fontHeight + 5; // Spacing between text and icon
if (modInfo.iconIdentifier != null) {
// Draw the texture
Function<Identifier, RenderLayer> renderLayerFunction = RenderLayer::getGuiTextured;
drawContext.drawTexture(renderLayerFunction, modInfo.iconIdentifier, currentX, iconY, 0, 0, iconSize, iconSize, iconSize, iconSize);
}
// Render checkbox in the bottom right corner of the icon
int checkboxSize = 10;
int checkboxX = currentX + iconSize - checkboxSize - padding;
int checkboxY = iconY + iconSize - checkboxSize - padding;
int color = modInfo.isEnabled ? 0xFF00FF00 : 0xFFFF0000; // Green if enabled, red if not
drawContext.fill(checkboxX, checkboxY, checkboxX + checkboxSize, checkboxY + checkboxSize, color);
drawContext.drawBorder(checkboxX, checkboxY, checkboxSize, checkboxSize, 0xFFFFFFFF);
// Detect if the mouse is hovering over the icon for tooltip
if (mouseX >= currentX && mouseX <= currentX + iconSize &&
mouseY >= iconY && mouseY <= iconY + iconSize) {
hoveredMod = modInfo;
hoveredModIndex = i;
// Optionally, you can add a highlight or effect here
}
}
}
@Override
public boolean mouseClicked(double mouseX, double mouseY, int button) {
if (button != 0) { // Only handle left-clicks
return super.mouseClicked(mouseX, mouseY, button);
}
// Determine which mod in the row was clicked
TextRenderer textRenderer = client.textRenderer;
int startX = lastRenderX + padding;
for (int i = 0; i < modsInRow.size(); i++) {
ModInfo modInfo = modsInRow.get(i);
int currentX = startX + i * (iconSize + padding);
int currentY = lastRenderY;
// Calculate positions as in the render method
int textY = currentY + 5;
int iconY = textY + textRenderer.fontHeight + 5;
int checkboxSize = 10;
int checkboxX = currentX + iconSize - checkboxSize - padding;
int checkboxY = iconY + iconSize - checkboxSize - padding;
// Check if the mouse click is within the checkbox bounds
if (mouseX >= checkboxX && mouseX <= checkboxX + checkboxSize &&
mouseY >= checkboxY && mouseY <= checkboxY + checkboxSize) {
handleSelectionChange(modInfo);
return true; // Indicate that the click was handled
}
}
return super.mouseClicked(mouseX, mouseY, button);
}
private void handleSelectionChange(ModInfo modInfo) {
Path modsDir = FabricLoader.getInstance().getGameDir().resolve("mods");
try {
LOGGER.info("Attempting to toggle mod: {}", modInfo.metadata.name);
LOGGER.info("Current mod path: {}", modInfo.modPath);
String loaderVersion = modInfo.metadata.fabricLoaderVersion != null ? modInfo.metadata.fabricLoaderVersion : "unknown_version";
if (modInfo.isEnabled) {
// Create a subdirectory based on the loader version for disabled mods
Path disabledDir = modsDir.resolve(loaderVersion);
Files.createDirectories(disabledDir);
Path targetPath = disabledDir.resolve(modInfo.modPath.getFileName());
LOGGER.info("Copying mod to disabled directory: {}", targetPath);
Files.copy(modInfo.modPath, targetPath, StandardCopyOption.REPLACE_EXISTING);
Files.delete(modInfo.modPath);
LOGGER.info("Mod copied to disabled directory and original deleted.");
modInfo.modPath = targetPath;
modInfo.isEnabled = false;
notifications.add(Text.literal("Disabled mod: " + modInfo.metadata.name));
} else {
// Enable the mod by moving it back to the main mods folder
Path targetPath = modsDir.resolve(modInfo.modPath.getFileName());
LOGGER.info("Copying mod back to mods directory: {}", targetPath);
Files.copy(modInfo.modPath, targetPath, StandardCopyOption.REPLACE_EXISTING);
Files.delete(modInfo.modPath);
LOGGER.info("Mod copied back to mods directory and original deleted.");
modInfo.modPath = targetPath;
modInfo.isEnabled = true;
notifications.add(Text.literal("Enabled mod: " + modInfo.metadata.name));
}
notificationTimestamps.add(System.currentTimeMillis());
// Refresh the mod list
modInfos.clear();
loadModInfos();
modListWidget.clearEntries();
populateList();
} catch (IOException e) {
LOGGER.error("Failed to toggle mod {} from {} to {}", modInfo.fileName, modInfo.modPath, modsDir, e);
notifications.add(Text.literal("Error toggling mod: " + modInfo.metadata.name));
notificationTimestamps.add(System.currentTimeMillis());
}
}
public void renderTooltip(DrawContext drawContext, int mouseX, int mouseY) {
if (hoveredMod != null) {
List<Text> tooltip = new ArrayList<>();
tooltip.add(Text.literal("Mod Name: " + hoveredMod.metadata.name));
tooltip.add(Text.literal("Version: " + hoveredMod.metadata.version));
if (hoveredMod.metadata.minecraftVersion != null) {
tooltip.add(Text.literal("Minecraft Version: " + hoveredMod.metadata.minecraftVersion));
}
if (hoveredMod.metadata.fabricLoaderVersion != null) {
tooltip.add(Text.literal("Fabric Loader Version: " + hoveredMod.metadata.fabricLoaderVersion));
}
tooltip.add(Text.literal("File: " + hoveredMod.fileName));
drawContext.drawTooltip(client.textRenderer, tooltip, mouseX, mouseY);
}
}
@Override
public boolean mouseDragged(double mouseX, double mouseY, int button, double deltaX, double deltaY) {
return super.mouseDragged(mouseX, mouseY, button, deltaX, deltaY);
}
@Override
public boolean keyPressed(int keyCode, int scanCode, int modifiers) {
return super.keyPressed(keyCode, scanCode, modifiers);
}
}
}
@Override
public void filesDragged(List<Path> paths) {
for (Path path : paths) {
File droppedFile = path.toFile();
if (droppedFile.exists() && droppedFile.getName().endsWith(".jar")) {
handleFileDrop(droppedFile);
}
}
super.filesDragged(paths);
}
private void handleFileDrop(File file) {
// Extract fabricloader version from the mod file
String fabricLoaderVersion = getFabricLoaderVersionFromModFile(file);
if (fabricLoaderVersion == null) {
fabricLoaderVersion = "unknown_version";
}
// Define the target directory where mods are stored
Path modsDir = FabricLoader.getInstance().getGameDir().resolve("mods");
Path targetPath = modsDir.resolve(file.getName());
try {
// Create directories if needed
Files.createDirectories(targetPath.getParent());
// Move the JAR file to the mods directory
Files.move(file.toPath(), targetPath, StandardCopyOption.REPLACE_EXISTING);
LOGGER.info("Successfully added mod: {}", file.getName());
// Refresh the mod list to include the new mod
modInfos.clear();
loadModInfos();
modListWidget.clearEntries();
populateList();
// Provide user feedback
notifications.add(Text.literal("Added mod: " + file.getName()));
notificationTimestamps.add(System.currentTimeMillis());
} catch (IOException e) {
LOGGER.error("Failed to add mod: {}", file.getName(), e);
notifications.add(Text.literal("Failed to add mod: " + file.getName()));
notificationTimestamps.add(System.currentTimeMillis());
}
}
private String getFabricLoaderVersionFromModFile(File file) {
try (ZipFile zipFile = new ZipFile(file)) {
ZipEntry entry = zipFile.getEntry("fabric.mod.json");
if (entry != null) {
try (InputStream inputStream = zipFile.getInputStream(entry);
InputStreamReader reader = new InputStreamReader(inputStream)) {
JsonObject modJson = JsonParser.parseReader(reader).getAsJsonObject();
return getDependencyVersion(modJson, "fabricloader");
}
}
} catch (IOException e) {
LOGGER.error("Failed to read fabric.mod.json from mod file: {}", file.getName(), e);
}
return null;
}
public void renderBackground(DrawContext context, int mouseX, int mouseY, float delta) {
if (this.client.world == null) {
this.renderPanoramaBackground(context, delta);
}
this.renderDarkening(context);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment