Last active
November 4, 2024 14:58
-
-
Save Ensamisten/8c833138a6af44cd5efe31aee5562365 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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