Newer
Older
sklauncher / build-tools / src / main / java / com / skcraft / launcher / buildtools / BuildTools.java
/*
 * SK's Minecraft Launcher
 * Copyright (C) 2010-2014 Albert Pham <http://www.sk89q.com> and contributors
 * Please see LICENSE.txt for license information.
 */

package com.skcraft.launcher.buildtools;

import com.beust.jcommander.JCommander;
import com.fasterxml.jackson.core.util.DefaultPrettyPrinter;
import com.fasterxml.jackson.core.util.DefaultPrettyPrinter.Lf2SpacesIndenter;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import com.skcraft.concurrency.ObservableFuture;
import com.skcraft.launcher.Instance;
import com.skcraft.launcher.InstanceList;
import com.skcraft.launcher.Launcher;
import com.skcraft.launcher.auth.OfflineSession;
import com.skcraft.launcher.auth.Session;
import com.skcraft.launcher.builder.BuilderConfig;
import com.skcraft.launcher.builder.BuilderOptions;
import com.skcraft.launcher.builder.FnPatternList;
import com.skcraft.launcher.buildtools.build.*;
import com.skcraft.launcher.buildtools.build.BuildDialog.BuildOptions;
import com.skcraft.launcher.buildtools.project.BuilderConfigDialog;
import com.skcraft.launcher.buildtools.http.LocalHttpServerBuilder;
import com.skcraft.launcher.buildtools.build.DeployServerDialog.DeployOptions;
import com.skcraft.launcher.dialog.ConfigurationDialog;
import com.skcraft.launcher.dialog.ConsoleFrame;
import com.skcraft.launcher.dialog.ProgressDialog;
import com.skcraft.launcher.launch.LaunchOptions;
import com.skcraft.launcher.launch.LaunchOptions.UpdatePolicy;
import com.skcraft.launcher.model.modpack.LaunchModifier;
import com.skcraft.launcher.persistence.Persistence;
import com.skcraft.launcher.swing.SwingHelper;
import lombok.Getter;
import lombok.extern.java.Log;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;

import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.File;
import java.io.IOException;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.List;
import java.util.logging.Level;
import java.util.regex.Pattern;

@Log
public class BuildTools {

    private static final DateFormat VERSION_DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss");
    private static final Pattern FILENAME_SANITIZE = Pattern.compile("[^a-z0-9_\\-\\.]+");
    private static final DefaultPrettyPrinter lf2ListPrettyPrinter;

    private final Launcher launcher;
    @Getter
    private int port;
    @Getter
    private final File inputDir;
    @Getter
    private final File srcDir;
    @Getter
    private final File wwwDir;
    @Getter
    private final File distDir;

    static {
        lf2ListPrettyPrinter = new DefaultPrettyPrinter();
        lf2ListPrettyPrinter.indentArraysWith(Lf2SpacesIndenter.instance);
    }

    @SuppressWarnings("ResultOfMethodCallIgnored")
    public BuildTools(File baseDir, int port) throws IOException {
        File launcherDir = new File(baseDir, "staging/launcher");
        inputDir = baseDir;
        srcDir = new File(baseDir, BuilderOptions.DEFAULT_SRC_DIRNAME);
        wwwDir = new File(baseDir, "staging/www");
        distDir = new File(baseDir, "upload");

        this.port = port;

        srcDir.mkdirs();
        new File(baseDir, BuilderOptions.DEFAULT_LOADERS_DIRNAME).mkdir();
        launcherDir.mkdirs();
        wwwDir.mkdirs();

        launcher = new Launcher(launcherDir);
        setPort(port);
    }

    private void setPort(int port) {
        this.port = port;
        launcher.getProperties().setProperty("newsUrl", "http://localhost:" + port + "/news.html");
        launcher.getProperties().setProperty("packageListUrl", "http://localhost:" + port + "/packages.json");
        launcher.getProperties().setProperty("selfUpdateUrl", "http://localhost:" + port + "/latest.json");
    }

    public File getConfigFile() {
        return new File(inputDir, BuilderOptions.DEFAULT_CONFIG_FILENAME);
    }

    public String generateManifestName() {
        File file = getConfigFile();
        if (file.exists()) {
            BuilderConfig config = Persistence.read(file, BuilderConfig.class, true);
            if (config != null) {
                String name = Strings.nullToEmpty(config.getName());
                name = name.toLowerCase();
                name = FILENAME_SANITIZE.matcher(name).replaceAll("-");
                name = name.trim();
                if (!name.isEmpty()) {
                    return name + ".json";
                }
            }
        }

        return "my_modpack.json";
    }

    public String getCurrentModpackName() {
        File file = getConfigFile();
        if (file.exists()) {
            BuilderConfig config = Persistence.read(file, BuilderConfig.class, true);
            if (config != null) {
                return config.getName();
            }
        }

        return null;
    }

    public Instance findCurrentInstance(List<Instance> instances) {
        String expected = getCurrentModpackName();

        for (Instance instance : instances) {
            if (instance.getName().equals(expected)) {
                return instance;
            }
        }

        return null;
    }

    private void addDefaultConfig(BuilderConfig config) {
        config.setName("My Modpack");
        config.setTitle("My Modpack");
        config.setGameVersion("1.8");

        LaunchModifier launchModifier = new LaunchModifier();
        launchModifier.setFlags(ImmutableList.of("-Dfml.ignoreInvalidMinecraftCertificates=true"));
        config.setLaunchModifier(launchModifier);

        FnPatternList userFiles = new FnPatternList();
        userFiles.setInclude(Lists.newArrayList("options.txt", "optionsshaders.txt", "mods/VoxelMods/*"));
        userFiles.setExclude(Lists.<String>newArrayList());
        config.setUserFiles(userFiles);
    }

    public Server startHttpServer() throws Exception {
        LocalHttpServerBuilder builder = new LocalHttpServerBuilder();
        builder.setBaseDir(wwwDir);
        builder.setPort(port);

        Server server = builder.build();
        server.start();
        setPort(((ServerConnector) server.getConnectors()[0]).getLocalPort());
        return server;
    }

    private void showMainWindow() {
        final ToolsFrame frame = new ToolsFrame();

        frame.getEditConfigButton().addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent event) {
                File file = getConfigFile();
                boolean existed = file.exists();

                BuilderConfig config = Persistence.read(file, BuilderConfig.class);
                if (!existed) {
                    addDefaultConfig(config);
                }

                if (BuilderConfigDialog.showEditor(frame, config)) {
                    try {
                        Persistence.write(file, config, lf2ListPrettyPrinter);
                    } catch (IOException e) {
                        SwingHelper.showErrorDialog(frame, "Couldn't write modpack.json to disk due to an error", "Write Error", e);
                    }
                }
            }
        });

        frame.getOpenFolderButton().addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                SwingHelper.browseDir(getInputDir(), frame);
            }
        });

        frame.getCheckProblemsButton().addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                ProblemChecker runnable = new ProblemChecker(BuildTools.this);
                ObservableFuture<List<Problem>> future = new ObservableFuture<List<Problem>>(launcher.getExecutor().submit(runnable), runnable);
                ProgressDialog.showProgress(frame, future, "Checking for problems...", "Checking for problems...");

                Futures.addCallback(future, new FutureCallback<List<Problem>>() {
                    @Override
                    public void onSuccess(List<Problem> problems) {
                        if (problems.isEmpty()) {
                            SwingHelper.showMessageDialog(frame, "No potential problems found!", "Success", null, JOptionPane.INFORMATION_MESSAGE);
                        } else {
                            ProblemViewer viewer = new ProblemViewer(frame, problems);
                            viewer.setVisible(true);
                        }
                    }

                    @Override
                    public void onFailure(Throwable t) {
                    }
                });

                SwingHelper.addErrorDialogCallback(frame, future);
            }
        });

        frame.getBuildButton().addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                final BuildOptions options = BuildDialog.showBuildDialog(frame, generateVersionFromDate(), generateManifestName(), distDir);
                if (options != null) {
                    ConsoleFrame.showMessages();

                    options.getDestDir().mkdirs();
                    ModpackBuilder runnable = new ModpackBuilder(inputDir, options.getDestDir(), options.getVersion(), options.getManifestFilename(), options.isClean());
                    ObservableFuture<ModpackBuilder> future = new ObservableFuture<ModpackBuilder>(launcher.getExecutor().submit(runnable), runnable);
                    ProgressDialog.showProgress(frame, future, "Building modpack...", "Building modpack for release...");

                    Futures.addCallback(future, new FutureCallback<ModpackBuilder>() {
                        @Override
                        public void onSuccess(ModpackBuilder result) {
                            SwingHelper.browseDir(options.getDestDir(), frame);
                        }

                        @Override
                        public void onFailure(Throwable t) {
                        }
                    });

                    SwingHelper.addErrorDialogCallback(frame, future);
                }
            }
        });

        frame.getDeployServerButton().addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                final DeployOptions options = DeployServerDialog.showDeployDialog(frame);
                if (options != null) {
                    ConsoleFrame.showMessages();

                    distDir.mkdirs();
                    ServerDeployer runnable = new ServerDeployer(srcDir, options);
                    ObservableFuture<ServerDeployer> future = new ObservableFuture<ServerDeployer>(launcher.getExecutor().submit(runnable), runnable);
                    ProgressDialog.showProgress(frame, future, "Deploying files...", "Deploying server files...");

                    Futures.addCallback(future, new FutureCallback<ServerDeployer>() {
                        @Override
                        public void onSuccess(ServerDeployer result) {
                            SwingHelper.showMessageDialog(frame, "Server deployment complete!", "Success", null, JOptionPane.INFORMATION_MESSAGE);
                        }

                        @Override
                        public void onFailure(Throwable t) {
                        }
                    });

                    SwingHelper.addErrorDialogCallback(frame, future);
                }
            }
        });

        frame.getTestButton().addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                ConsoleFrame.showMessages();

                ModpackBuilder runnable = new ModpackBuilder(inputDir, wwwDir, generateVersionFromDate(), "staging.json", false);
                ObservableFuture<ModpackBuilder> future = new ObservableFuture<ModpackBuilder>(launcher.getExecutor().submit(runnable), runnable);
                ProgressDialog.showProgress(frame, future, "Preparing files...", "Preparing files for launch...");
                SwingHelper.addErrorDialogCallback(frame, future);

                Futures.addCallback(future, new FutureCallback<ModpackBuilder>() {
                    @Override
                    public void onSuccess(ModpackBuilder result) {
                        launchInstance(frame);
                        ConsoleFrame.hideMessages();
                    }

                    @Override
                    public void onFailure(Throwable t) {
                    }
                });
            }
        });

        frame.getOptionsButton().addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                ConfigurationDialog configDialog = new ConfigurationDialog(frame, launcher);
                configDialog.setVisible(true);
            }
        });

        frame.getClearInstanceButton().addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                DirectoryRemover remover = new DirectoryRemover(launcher.getInstancesDir());
                ObservableFuture<DirectoryRemover> future = new ObservableFuture<DirectoryRemover>(launcher.getExecutor().submit(remover), remover);
                ProgressDialog.showProgress(frame, future, "Removing files...", "Removing files...");
                SwingHelper.addErrorDialogCallback(frame, future);
            }
        });

        frame.getClearWebRootButton().addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                DirectoryRemover remover = new DirectoryRemover(wwwDir);
                ObservableFuture<DirectoryRemover> future = new ObservableFuture<DirectoryRemover>(launcher.getExecutor().submit(remover), remover);
                ProgressDialog.showProgress(frame, future, "Removing files...", "Removing files...");
                SwingHelper.addErrorDialogCallback(frame, future);
            }
        });

        frame.getOpenConsoleButton().addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                ConsoleFrame.showMessages();
            }
        });

        frame.getDocsButton().addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                SwingHelper.openURL("https://github.com/SKCraft/Launcher/wiki", frame);
            }
        });

        frame.getQuitButton().addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                frame.dispose();
                System.exit(0);
            }
        });

        frame.setVisible(true);
    }

    private void launchInstance(final Window window) {
        final InstanceList instanceList = launcher.getInstances();
        InstanceList.Enumerator loader = instanceList.createEnumerator();
        ObservableFuture<InstanceList> future = new ObservableFuture<InstanceList>(launcher.getExecutor().submit(loader), loader);
        ProgressDialog.showProgress(window, future, "Loading modpacks...", "Loading modpacks...");
        SwingHelper.addErrorDialogCallback(window, future);

        Futures.addCallback(future, new FutureCallback<InstanceList>() {
            @Override
            public void onSuccess(InstanceList result) {
                Session session = new OfflineSession("Player");

                Instance instance = findCurrentInstance(instanceList.getInstances());

                if (instance != null) {
                    LaunchOptions options = new LaunchOptions.Builder()
                            .setInstance(instance)
                            .setUpdatePolicy(UpdatePolicy.ALWAYS_UPDATE)
                            .setWindow(window)
                            .setSession(session)
                            .build();

                    launcher.getLaunchSupervisor().launch(options);
                } else {
                    SwingHelper.showErrorDialog(window,
                            "After generating the necessary files, it appears the modpack can't be found in the " +
                                    "launcher. Did you change modpack.json while the launcher was launching?", "Launch Error");
                }
            }

            @Override
            public void onFailure(Throwable t) {
            }
        });
    }

    public static void main(String[] args) throws Exception {
        Launcher.setupLogger();
        System.setProperty("skcraftLauncher.killWithoutConfirm", "true");

        ToolArguments options = new ToolArguments();
        new JCommander(options, args);

        SwingUtilities.invokeAndWait(new Runnable() {
            @Override
            public void run() {
                UIManager.getDefaults().put("SplitPane.border", BorderFactory.createEmptyBorder());
                try {
                    UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
                } catch (Exception ignored) {
                }
            }
        });

        File inputDir = new ProjectDirectoryChooser(options.getDir()).choose();

        if (inputDir == null) {
            System.exit(100);
            return;
        }

        final BuildTools main = new BuildTools(inputDir, options.getPort());

        try {
            main.startHttpServer();
        } catch (Throwable t) {
            log.log(Level.WARNING, "Web server start failure", t);
            SwingHelper.showErrorDialog(null, "Couldn't start the local web server on a free TCP port! " +
                    "The web server is required to temporarily host the modpack files for the launcher.", "Build Tools Error", t);
            System.exit(1);
            return;
        }

        SwingUtilities.invokeLater(new Runnable() {
            @Override
            public void run() {
                try {
                    main.showMainWindow();
                } catch (Throwable t) {
                    log.log(Level.WARNING, "Load failure", t);
                    SwingHelper.showErrorDialog(null, "Failed to launch build tools!", "Build Tools Error", t);
                }
            }
        });
    }

    public static String generateVersionFromDate() {
        Date today = Calendar.getInstance().getTime();
        return VERSION_DATE_FORMAT.format(today);
    }

}