package com.patchworkmc; import java.awt.BorderLayout; import java.awt.Component; import java.awt.Dimension; import java.awt.Font; import java.awt.Toolkit; import java.io.File; import java.io.FileInputStream; import java.io.FileWriter; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.PrintStream; import java.net.URL; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.security.Permission; import java.util.Collections; import java.util.List; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.function.Supplier; import java.util.zip.ZipEntry; import java.util.zip.ZipInputStream; import javax.swing.BoxLayout; import javax.swing.JButton; import javax.swing.JCheckBox; import javax.swing.JComboBox; import javax.swing.JFileChooser; import javax.swing.JFrame; import javax.swing.JLabel; import javax.swing.JPanel; import javax.swing.JScrollPane; import javax.swing.JTextField; import javax.swing.JTextPane; import javax.swing.SizeRequirements; import javax.swing.SwingUtilities; import javax.swing.UIManager; import javax.swing.border.EmptyBorder; import javax.swing.text.Element; import javax.swing.text.ParagraphView; import javax.swing.text.View; import javax.swing.text.ViewFactory; import javax.swing.text.html.HTMLEditorKit; import javax.swing.text.html.InlineView; import com.google.gson.Gson; import com.google.gson.GsonBuilder; import com.google.gson.JsonArray; import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.google.gson.reflect.TypeToken; import org.apache.commons.io.FileUtils; import org.apache.commons.io.IOUtils; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import net.fabricmc.tinyremapper.IMappingProvider; import net.fabricmc.tinyremapper.TinyUtils; import com.patchworkmc.mapping.BridgedMappings; import com.patchworkmc.mapping.RawMapping; import com.patchworkmc.mapping.TinyWriter; import com.patchworkmc.mapping.Tsrg; import com.patchworkmc.mapping.TsrgClass; import com.patchworkmc.mapping.TsrgMappings; public class PatchworkUI { private static final String[] SUPPORTED_VERSIONS = {"1.14.4"}; public static final Logger LOGGER = LogManager.getFormatterLogger("Patchwork/UI"); private static Supplier<JTextPane> area = () -> null; private static JComboBox<String> versions; private static JTextField modsFolder; private static JTextField outputFolder; private static JCheckBox generateMCPTiny; private static JCheckBox generateDevJar; private static JCheckBox ignoreSidedAnnotations; private static JComboBox<YarnBuild> yarnVersions; private static File root = new File(System.getProperty("user.dir")); private static ExecutorService service = Executors.newScheduledThreadPool(4); private static PrintStream oldOut; private static PrintStream oldErr; public static void main(String[] args) throws Exception { new File(root, "input").mkdirs(); new File(root, "output").mkdirs(); UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); JFrame frame = new JFrame("Patchwork Patcher"); frame.setIconImage(Toolkit.getDefaultToolkit().getImage(PatchworkUI.class.getResource("/patchwork.png"))); JPanel overallPane = new JPanel(); frame.setContentPane(overallPane); overallPane.setLayout(new BorderLayout()); ColorPane area = new ColorPane(); PatchworkUI.area = () -> area; UIAppender.setPane(area); area.setEditable(false); area.setEditorKit(new HTMLEditorKit() { // Prevent serializable warning. private static final long serialVersionUID = -828745134521267417L; @Override public ViewFactory getViewFactory() { return new HTMLFactory() { @Override public View create(Element e) { View v = super.create(e); if (v instanceof InlineView) { return new InlineView(e) { @Override public int getBreakWeight(int axis, float pos, float len) { return GoodBreakWeight; } @Override public View breakView(int axis, int p0, float pos, float len) { if (axis == View.X_AXIS) { checkPainter(); int p1 = getGlyphPainter().getBoundedPosition(this, p0, pos, len); if (p0 == getStartOffset() && p1 == getEndOffset()) { return this; } return createFragment(p0, p1); } return this; } }; } else if (v instanceof ParagraphView) { return new ParagraphView(e) { @Override protected SizeRequirements calculateMinorAxisRequirements(int axis, SizeRequirements r) { if (r == null) { r = new SizeRequirements(); } float pref = layoutPool.getPreferredSpan(axis); float min = layoutPool.getMinimumSpan(axis); r.minimum = (int) min; r.preferred = Math.max(r.minimum, (int) pref); r.maximum = Integer.MAX_VALUE; r.alignment = 0.5f; return r; } }; } return v; } }; } }); area.setFont(area.getFont().deriveFont(14f)); JScrollPane scrollPane = new JScrollPane(area, JScrollPane.VERTICAL_SCROLLBAR_ALWAYS, JScrollPane.HORIZONTAL_SCROLLBAR_NEVER); overallPane.add(scrollPane, BorderLayout.CENTER); { JPanel pane = new JPanel(); pane.setLayout(new BoxLayout(pane, BoxLayout.Y_AXIS)); { JLabel title = new JLabel("Patchwork Patcher"); title.setAlignmentX(Component.CENTER_ALIGNMENT); title.setBorder(new EmptyBorder(10, 10, 10, 10)); title.setFont(title.getFont().deriveFont(Font.BOLD, 16f)); pane.add(title); } { PatchworkUI.versions = new JComboBox<>(SUPPORTED_VERSIONS); JPanel versionsPane = new JPanel(new BorderLayout()); versionsPane.add(new JLabel("Minecraft Version: "), BorderLayout.WEST); versionsPane.add(versions, BorderLayout.CENTER); versions.addItemListener(e -> service.submit(() -> { try { updateYarnVersions(); } catch (Exception ex) { ex.printStackTrace(); } })); versionsPane.setBorder(new EmptyBorder(0, 0, 10, 0)); pane.add(versionsPane); } { PatchworkUI.modsFolder = new JTextField(new File(root, "input").getAbsolutePath(), 20); JButton button = new JButton("Browse"); button.addActionListener(e -> { JFileChooser chooser = new JFileChooser(); File file = null; try { file = new File(modsFolder.getName()); } catch (Exception ignored) { // ignored } chooser.setCurrentDirectory(file != null && file.exists() ? file : new File(root, "input")); chooser.setDialogTitle("Browse Input Mods Folder"); chooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY); chooser.setAcceptAllFileFilterUsed(false); if (chooser.showOpenDialog(frame) == JFileChooser.APPROVE_OPTION) { if (chooser.getSelectedFile() != null) { modsFolder.setText(chooser.getSelectedFile().getAbsolutePath()); } else if (chooser.getCurrentDirectory() != null) { modsFolder.setText(chooser.getCurrentDirectory().getAbsolutePath()); } modsFolder.requestFocus(); modsFolder.setCaretPosition(modsFolder.getDocument().getLength()); } }); JPanel modsPane = new JPanel(new BorderLayout()); modsPane.add(new JLabel("Input Mods: "), BorderLayout.WEST); modsPane.add(modsFolder, BorderLayout.CENTER); modsPane.add(button, BorderLayout.EAST); modsPane.setBorder(new EmptyBorder(0, 0, 5, 0)); pane.add(modsPane); } { PatchworkUI.outputFolder = new JTextField(new File(root, "output").getAbsolutePath(), 20); JButton button = new JButton("Browse"); button.addActionListener(e -> { JFileChooser chooser = new JFileChooser(); File file = null; try { file = new File(outputFolder.getName()); } catch (Exception ignored) { // ignored } chooser.setCurrentDirectory(file != null && file.exists() ? file : new File(root, "output")); chooser.setDialogTitle("Browse Output Folder"); chooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY); chooser.setAcceptAllFileFilterUsed(false); if (chooser.showOpenDialog(frame) == JFileChooser.APPROVE_OPTION) { if (chooser.getSelectedFile() != null) { outputFolder.setText(chooser.getSelectedFile().getAbsolutePath()); } else if (chooser.getCurrentDirectory() != null) { outputFolder.setText(chooser.getCurrentDirectory().getAbsolutePath()); } outputFolder.requestFocus(); outputFolder.setCaretPosition(outputFolder.getDocument().getLength()); } }); JPanel outputPane = new JPanel(new BorderLayout()); outputPane.add(new JLabel("Output Folder: "), BorderLayout.WEST); outputPane.add(outputFolder, BorderLayout.CENTER); outputPane.add(button, BorderLayout.EAST); outputPane.setBorder(new EmptyBorder(0, 0, 10, 0)); pane.add(outputPane); } { generateMCPTiny = new JCheckBox("Generate Tiny MCP", false); ignoreSidedAnnotations = new JCheckBox("Ignore Sided Events", System.getProperty("patchwork:ignore_sided_annotations", "false").equals("true")); JPanel checkboxPanel = new JPanel(new BorderLayout()); checkboxPanel.add(generateMCPTiny, BorderLayout.WEST); checkboxPanel.add(ignoreSidedAnnotations, BorderLayout.CENTER); checkboxPanel.setBorder(new EmptyBorder(0, 0, 10, 0)); pane.add(checkboxPanel); } { generateDevJar = new JCheckBox("Generate Development Jar", false); JPanel checkboxPanel = new JPanel(new BorderLayout()); checkboxPanel.add(generateDevJar, BorderLayout.WEST); generateDevJar.addActionListener(e -> yarnVersions.setEnabled(generateDevJar.isSelected())); checkboxPanel.setBorder(new EmptyBorder(0, 0, 5, 0)); pane.add(checkboxPanel); } { yarnVersions = new JComboBox<>(); JPanel yarnPanel = new JPanel(new BorderLayout()); yarnVersions.setEnabled(generateDevJar.isSelected()); yarnPanel.add(new JLabel("Yarn Version: "), BorderLayout.WEST); yarnPanel.add(yarnVersions, BorderLayout.CENTER); yarnPanel.setBorder(new EmptyBorder(0, 0, 10, 0)); pane.add(yarnPanel); } JPanel jPanel = new JPanel(new BorderLayout()); { JButton clearCache = new JButton("Clear Cached Data"); clearCache.addActionListener(e -> { jPanel.setVisible(false); service.submit(() -> { try { clearCache(); } catch (Throwable throwable) { throwable.printStackTrace(); } SwingUtilities.invokeLater(() -> jPanel.setVisible(true)); }); }); JPanel clearCachePanel = new JPanel(new BorderLayout()); clearCachePanel.add(clearCache, BorderLayout.WEST); clearCachePanel.setBorder(new EmptyBorder(0, 0, 10, 0)); pane.add(clearCachePanel); } JPanel jPanel1 = new JPanel(); jPanel1.add(pane); jPanel.add(jPanel1, BorderLayout.CENTER); JButton patchButton = new JButton("Patch"); patchButton.addActionListener(e -> { jPanel.setVisible(false); service.submit(() -> { try { runWithNoExitCall(() -> { try { startPatching(); } catch (Throwable throwable) { throwable.printStackTrace(); } }); } catch (Throwable throwable) { throwable.printStackTrace(); } SwingUtilities.invokeLater(() -> jPanel.setVisible(true)); }); }); jPanel.add(patchButton, BorderLayout.SOUTH); overallPane.add(jPanel, BorderLayout.WEST); } frame.setMinimumSize(new Dimension(800, 300)); frame.setSize(new Dimension(800, 500)); frame.setLocationRelativeTo(null); frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); frame.setVisible(true); service.submit(() -> { try { updateYarnVersions(); } catch (Exception e) { e.printStackTrace(); } }); LOGGER.info("Welcome to Patchwork Patcher!"); LOGGER.info("Patchwork is still an early project, things might not work as expected! Let us know the issues on GitHub!"); } private static void updateYarnVersions() throws IOException { Gson gson = new GsonBuilder().disableHtmlEscaping().create(); List<YarnBuild> builds = gson.fromJson(new InputStreamReader(new URL("https://meta.fabricmc.net/v2/versions/yarn").openStream()), new TypeToken<List<YarnBuild>>() { }.getType()); SwingUtilities.invokeLater(() -> { yarnVersions.removeAllItems(); for (YarnBuild build : builds) { if (build.gameVersion.equals(versions.getSelectedItem())) { yarnVersions.addItem(build); } } if (yarnVersions.getItemCount() > 0) { yarnVersions.setSelectedIndex(0); } else { yarnVersions.setSelectedIndex(-1); } }); } private static void runWithNoExitCall(Runnable runnable) { forbidSystemExitCall(); runnable.run(); enableSystemExitCall(); } private static void forbidSystemExitCall() { final SecurityManager securityManager = new SecurityManager() { @Override public void checkPermission(Permission perm) { if (perm.getName().contains("exitVM")) { throw new ExitTrappedException(); } } }; System.setSecurityManager(securityManager); } private static void enableSystemExitCall() { System.setSecurityManager(null); } private static void clearCache() throws IOException { LOGGER.info("Clearing cache."); FileUtils.deleteDirectory(new File(root, "data")); FileUtils.deleteDirectory(new File(root, "temp")); LOGGER.info("Cleared cache."); } private static void startPatching() throws IOException { System.setProperty("patchwork:ignore_sided_annotations", ignoreSidedAnnotations.isSelected() + ""); Path rootPath = root.toPath(); String version = (String) versions.getSelectedItem(); YarnBuild yarnBuild = PatchworkUI.generateDevJar.isSelected() ? (YarnBuild) yarnVersions.getSelectedItem() : null; LOGGER.info("Checking whether intermediary for %s exists...", version); loadOrDownloadIntermediary(version, new File(root, "data/mappings")); LOGGER.info("Checking whether MCPConfig for %s exists...", version); File voldemapTiny = new File(root, "data/mappings/voldemap-" + version + ".tiny"); List<TsrgClass<RawMapping>> classes = Tsrg.readMappings(loadOrDownloadMCPConfig(version, new File(root, "data/mappings"))); System.out.println("Creating tiny mappings provider..."); IMappingProvider intermediary = TinyUtils.createTinyMappingProvider(rootPath.resolve("data/mappings/intermediary-" + version + ".tiny"), "official", "intermediary"); System.out.println("Creating tsrg mappings..."); TsrgMappings mappings = new TsrgMappings(classes, intermediary); File voldemapBridged = new File(root, "data/mappings/voldemap-bridged-" + version + ".tiny"); IMappingProvider bridged; IMappingProvider bridgedInverted; if (!voldemapBridged.exists()) { System.out.println("Generating bridged (srg -> intermediary) tiny mappings..."); TinyWriter tinyWriter = new TinyWriter("srg", "intermediary"); bridged = new BridgedMappings(mappings, intermediary); bridged.load(tinyWriter); Files.write(voldemapBridged.toPath(), tinyWriter.toString().getBytes(StandardCharsets.UTF_8)); System.out.println("Using generated bridged (srg -> intermediary) tiny mappings"); } else { System.out.println("Using cached bridged (srg -> intermediary) tiny mappings"); bridged = TinyUtils.createTinyMappingProvider(voldemapBridged.toPath(), "srg", "intermediary"); } bridgedInverted = TinyUtils.createTinyMappingProvider(voldemapBridged.toPath(), "intermediary", "srg"); if (yarnBuild != null) { LOGGER.info("Checking whether yarn for %s exists...", yarnBuild.toString()); downloadYarn(yarnBuild, new File(root, "data/mappings")); } if (generateMCPTiny.isSelected()) { LOGGER.info("Generating tiny MCP."); if (voldemapTiny.exists()) { LOGGER.info("Tiny MCP already exists. deleting existing tiny file."); Files.delete(voldemapTiny.toPath()); } LOGGER.info("Generating tiny MCP from tsrg data."); TinyWriter tinyWriter = new TinyWriter("official", "srg"); mappings.load(tinyWriter); String tiny = tinyWriter.toString(); Files.write(voldemapTiny.toPath(), tiny.getBytes(StandardCharsets.UTF_8)); LOGGER.info("Generated tiny MCP."); } Files.createDirectories(rootPath.resolve("input")); Files.createDirectories(rootPath.resolve("temp")); Files.createDirectories(rootPath.resolve("output")); Path officialJar = rootPath.resolve("data/" + version + "-client+official.jar"); Path srgJar = rootPath.resolve("data/" + version + "-client+srg.jar"); IMappingProvider[] yarnMappings = {null}; { if (!officialJar.toFile().exists()) { LOGGER.info("Trying to download Minecraft " + version + " client jar."); Gson gson = new GsonBuilder().disableHtmlEscaping().create(); JsonArray versions = gson.fromJson(new InputStreamReader(new URL("https://launchermeta.mojang.com/mc/game/version_manifest.json").openStream()), JsonObject.class).get("versions").getAsJsonArray(); Files.deleteIfExists(srgJar); for (JsonElement jsonElement : versions) { if (jsonElement.isJsonObject()) { JsonObject object = jsonElement.getAsJsonObject(); String id = object.get("id").getAsJsonPrimitive().getAsString(); if (id.equals(version)) { String versionUrl = object.get("url").getAsJsonPrimitive().getAsString(); JsonObject versionMeta = gson.fromJson(new InputStreamReader(new URL(versionUrl).openStream()), JsonObject.class); String versionJarUrl = versionMeta.get("downloads").getAsJsonObject().get("client").getAsJsonObject().get("url").getAsJsonPrimitive().getAsString(); LOGGER.info("Downloading Minecraft client " + version + "."); FileUtils.copyURLToFile(new URL(versionJarUrl), officialJar.toFile()); LOGGER.info("Downloaded Minecraft client " + version + "."); break; } } } if (!officialJar.toFile().exists()) { throw new IllegalStateException("Failed to find Minecraft version " + version); } } else { LOGGER.info("Minecraft jar already exists for Minecraft " + version + "."); } if (!srgJar.toFile().exists()) { LOGGER.info("Remapping Minecraft (official -> srg)"); Patchwork.remap(mappings, officialJar, srgJar); } if (yarnBuild != null) { Path intermediaryJar = rootPath.resolve("data/" + version + "-client+intermediary.jar"); yarnMappings[0] = TinyUtils.createTinyMappingProvider(rootPath.resolve("data/mappings/yarn-" + yarnBuild.version + "-v2.tiny"), "intermediary", "named"); if (!intermediaryJar.toFile().exists()) { LOGGER.info("Remapping Minecraft (official -> intermediary)"); Patchwork.remap(intermediary, officialJar, intermediaryJar); } } } LOGGER.info("Preparation Complete!\n"); Path inputFolder = new File(modsFolder.getText()).toPath(); Path outputFolder = new File(PatchworkUI.outputFolder.getText()).toPath(); Path dataFolder = rootPath.resolve("data"); Path tempFolder = Files.createTempDirectory(new File(System.getProperty("java.io.tmpdir")).toPath(), "patchwork-patcher-ui"); List<IMappingProvider> devMappings = generateDevJar.isSelected() ? Collections.singletonList(yarnMappings[0]) : Collections.emptyList(); Patchwork patchwork = new Patchwork(inputFolder, outputFolder, dataFolder, tempFolder, bridged, bridgedInverted, devMappings); int patched = patchwork.patchAndFinish(); LOGGER.info("Successfully patched " + patched + " mod(s)!"); } private static void downloadYarn(YarnBuild yarnBuild, File parent) throws IOException { parent.mkdirs(); File file = new File(parent, "yarn-" + yarnBuild.version + "-v2.tiny"); if (!file.exists()) { LOGGER.info("Downloading Yarn for " + yarnBuild.version + "."); InputStream stream = new URL("https://maven.fabricmc.net/" + yarnBuild.maven.replace(yarnBuild.version, "").replace('.', '/').replace(':', '/') + yarnBuild.version + "/" + "yarn-" + yarnBuild.version + "-v2.jar").openStream(); ZipInputStream zipInputStream = new ZipInputStream(stream); while (true) { ZipEntry nextEntry = zipInputStream.getNextEntry(); if (nextEntry == null) { break; } if (!nextEntry.isDirectory() && nextEntry.getName().endsWith("/mappings.tiny")) { FileWriter writer = new FileWriter(file, false); IOUtils.copy(zipInputStream, writer, Charset.defaultCharset()); writer.close(); LOGGER.info("Downloaded Yarn for " + yarnBuild.version + "."); break; } } zipInputStream.close(); } else { LOGGER.info("Yarn for " + yarnBuild.version + " already exists, using downloaded data."); } } public static InputStream loadOrDownloadMCPConfig(String version, File parent) throws IOException { parent.mkdirs(); File file = new File(parent, "voldemap-" + version + ".tsrg"); if (!file.exists()) { LOGGER.info("Downloading MCPConfig for " + version + "."); InputStream stream = new URL("http://files.minecraftforge.net/maven/de/oceanlabs/mcp/mcp_config/" + version + "/mcp_config-" + version + ".zip").openStream(); ZipInputStream zipInputStream = new ZipInputStream(stream); while (true) { ZipEntry nextEntry = zipInputStream.getNextEntry(); if (nextEntry == null) { break; } if (!nextEntry.isDirectory() && nextEntry.getName().endsWith("/joined.tsrg")) { FileWriter writer = new FileWriter(file, false); IOUtils.copy(zipInputStream, writer, Charset.defaultCharset()); writer.close(); LOGGER.info("Downloaded MCPConfig for " + version + "."); break; } } } else { LOGGER.info("MCPConfig for " + version + " already exists, using downloaded data."); } return new FileInputStream(file); } public static InputStream loadOrDownloadIntermediary(String version, File parent) throws IOException { parent.mkdirs(); File file = new File(parent, "intermediary-" + version + ".tiny"); if (!file.exists()) { LOGGER.info("Downloading Intermediary for " + version + "."); InputStream stream = new URL("https://maven.fabricmc.net/net/fabricmc/intermediary/" + version + "/intermediary-" + version + ".jar").openStream(); ZipInputStream zipInputStream = new ZipInputStream(stream); while (true) { ZipEntry nextEntry = zipInputStream.getNextEntry(); if (nextEntry == null) { break; } if (!nextEntry.isDirectory() && nextEntry.getName().endsWith("/mappings.tiny")) { FileWriter writer = new FileWriter(file, false); IOUtils.copy(zipInputStream, writer, Charset.defaultCharset()); writer.close(); LOGGER.info("Downloaded intermediary for " + version + "."); break; } } } else { LOGGER.info("Intermediary for " + version + " already exists, using downloaded data."); } return new FileInputStream(file); } @SuppressWarnings("unused") private static class YarnBuild { String gameVersion; String separator; int build; String maven; String version; boolean stable; @Override public String toString() { return version; } } private static class ExitTrappedException extends SecurityException { // Prevent serializable warning. private static final long serialVersionUID = -8774888159798495064L; } }