com.technofovea.packbsp.AppModel.java Source code

Java tutorial

Introduction

Here is the source code for com.technofovea.packbsp.AppModel.java

Source

/**
 * Copyright (C) 2011 Darien Hager
 *
 * This code is part of the "PackBSP" project, and is licensed under
 * a Creative Commons Attribution-ShareAlike 3.0 Unported License. For
 * either a summary of conditions or the full legal text, please visit:
 *
 * http://creativecommons.org/licenses/by-sa/3.0/
 *
 * Permissions beyond the scope of this license may be available
 * at http://technofovea.com/ .
 */
package com.technofovea.packbsp;

import com.technofovea.hl2parse.vdf.SteamMetaReader;
import com.technofovea.hl2parse.ParseUtil;
import com.technofovea.hl2parse.fgd.DefaultLoader;
import com.technofovea.hl2parse.fgd.FgdSpec;
import com.technofovea.hl2parse.registry.BlobFolder;
import com.technofovea.hl2parse.registry.BlobParseFailure;
import com.technofovea.hl2parse.registry.CdrParser;
import com.technofovea.hl2parse.registry.ClientRegistry;
import com.technofovea.hl2parse.registry.RegParser;
import com.technofovea.hl2parse.vdf.GameInfoReader;
import com.technofovea.hl2parse.vdf.SloppyParser;
import com.technofovea.hl2parse.vdf.ValveTokenLexer;
import com.technofovea.hl2parse.vdf.VdfRoot;
import com.technofovea.hllib.HlLib;
import com.technofovea.hllib.methods.ManagedLibrary;
import com.technofovea.packbsp.assets.AssetHit;
import com.technofovea.packbsp.assets.AssetLocator;
import com.technofovea.packbsp.assets.AssetLocatorImpl;
import com.technofovea.packbsp.assets.AssetSource.Type;
import com.technofovea.packbsp.conf.IncludeItem;
import com.technofovea.packbsp.conf.MapIncludes;
import com.technofovea.packbsp.crawling.CachingLocatorWrap;
import com.technofovea.packbsp.crawling.CrawlListener;
import com.technofovea.packbsp.crawling.DependencyExpander;
import com.technofovea.packbsp.crawling.DependencyGraph;
import com.technofovea.packbsp.crawling.Edge;
import com.technofovea.packbsp.crawling.EmptyCrawlListener;
import com.technofovea.packbsp.crawling.Node;
import com.technofovea.packbsp.crawling.TraversalException;
import com.technofovea.packbsp.crawling.nodes.MapNode;
import com.technofovea.packbsp.devkits.Devkit;
import com.technofovea.packbsp.devkits.Game;
import com.technofovea.packbsp.devkits.SourceSDK;
import com.technofovea.packbsp.packaging.BspZipController;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileWriter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.xml.bind.JAXBException;
import org.antlr.runtime.ANTLRFileStream;
import org.antlr.runtime.ANTLRInputStream;
import org.antlr.runtime.CharStream;
import org.antlr.runtime.CommonTokenStream;
import org.antlr.runtime.RecognitionException;
import org.apache.commons.exec.ExecuteException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * This class is really just a wrapper around some reusable procedural code.
 * It is expected that the GUI and controller will be set up in such a way that
 * the various phases are called in order, or else an IllegalStateException--
 * which represents programmer error--may be thrown by any method.
 * 
 * @author Darien Hager
 */
public class AppModel {

    static final String BSLASH = "\\";
    final static String CR_BLOB = "clientregistry.blob";
    final static int SDK_APPID = 211;
    final static String STEAM_APPS_FOLDER = "steamapps";
    static final String ENGINE_BIN = "bin";
    static final String GAMEDATA_PATH = "bin" + BSLASH + "gameconfig.txt";
    static final String BSPZIP_FILENAME = "bspzip.exe";
    static final String STEAM_APP_DATA = "config/SteamAppData.vdf";

    public enum Phase {

        /**
         * Done: Nothing.
         * Require: Steam directory path
         */
        STEAM,
        /**
         * Done: Parsed steam settings, found current user and SDK dir, parsed gameconfig
         * Require: Engine and Game choice
         */
        GAME,
        /**
         * Done: Loaded FGD data and game-specific paths
         * Require: Source BSP
         */
        SOURCE,
        /**
         * Done: Set up for pre-crawling phase
         * Require: Callback for crawling effort
         */
        CRAWL,
        /**
         * Done: Done: Crawling and dependency detection, packing listt finalized
         * Require: Destination BSP or list output
         */
        PACK,
        /**
         * Done: Packed new BSP or exported list
         * Require: Exit or return to either GAME or SOURCE step
         */
        FINAL,;

        private boolean isAfter(Phase phase) {
            //TODO make this not dependent on declaration order
            return (this.ordinal() > phase.ordinal());
        }
    }

    private static final Logger logger = LoggerFactory.getLogger(AppModel.class);
    Phase currentPhase = Phase.STEAM;
    File _steamDirectory;
    CdrParser _cdr;
    MapIncludes _includeConf;
    Set<IncludeItem> _includes;
    ClientRegistry _reg;
    File _sourceBsp;
    File _sourceCopy;
    List<Devkit> _kits;
    Game _chosenGame;
    GameInfoReader _gameInfoData;
    DependencyGraph _graph;
    Map<String, DependencyItem> _deps;

    public Phase getCurrentPhase() {
        return currentPhase;
    }

    protected void assertPhase(Phase val) {
        if (currentPhase != val) {
            throw new IllegalStateException(
                    "Model was in phase " + currentPhase + " but the function called is only valid for " + val);
        }
    }

    protected void assertPhaseAfter(Phase val) {
        if (!currentPhase.isAfter(val)) {
            throw new IllegalStateException("Model was in phase " + currentPhase
                    + " but the function called is only valid for states following " + val);
        }
    }

    protected void assertPhaseSameOrAfter(Phase val) {
        if ((currentPhase != val) && (!currentPhase.isAfter(val))) {
            throw new IllegalStateException("Model was in phase " + currentPhase
                    + " but the function called is only valid for states including or following " + val);
        }
    }

    /**
     *
     * @param dir
     * @throws IllegalArgumentException If arguments are incorrect
     * @throws IllegalStateException If called during the wrong phase
     * @throws PackbspException If another error occurs. May or may no be related to bad input.
     */
    public void acceptSteamDirectory(final File steamDir)
            throws IllegalArgumentException, IllegalStateException, PackbspException {
        assertPhaseSameOrAfter(Phase.STEAM);
        if (!steamDir.isDirectory()) {
            throw new IllegalArgumentException("The given directory does not exist");
        }
        final File originalBlob = new File(steamDir, CR_BLOB);
        if (!originalBlob.isFile()) {
            throw new IllegalArgumentException("Required file " + CR_BLOB + " could not be found.");
        }

        logger.info("Creating temporary copy of registry blob to avoid read/write conflicts.");
        final File blobfile;
        try {
            blobfile = PackbspUtil.createTempCopy(originalBlob);
        } catch (IOException ex) {
            throw new PackbspException(
                    "Could not create temporary copy of client registry blob. Pause any game-downloads or wait for Steam to finish updating and try again.",
                    ex);
        }

        logger.info("Attempting to parse client registry blob at: {}", blobfile);
        final ClientRegistry reg;
        try {
            BlobFolder bf = RegParser.parseClientRegistry(ParseUtil.mapFile(blobfile));
            reg = new ClientRegistry(bf);
        } catch (IOException ex) {
            throw new PackbspException("Could not access client registry blob (" + ex.getMessage() + ")", ex);
        } catch (BlobParseFailure ex) {
            throw new PackbspException("Could not parse the data within client registry blob", ex);
        }

        final CdrParser cdr = reg.getContentDescriptionRecord();
        File steamAppData = new File(steamDir, STEAM_APP_DATA);

        final String currentUser;
        try {
            CharStream ais = new ANTLRFileStream(steamAppData.getAbsolutePath());
            ValveTokenLexer lexer = new ValveTokenLexer(ais);
            SloppyParser parser = new SloppyParser(new CommonTokenStream(lexer));
            VdfRoot metaData = parser.main();
            SteamMetaReader smr = new SteamMetaReader(metaData);
            currentUser = smr.getAutoLogon();
            if ("".equals(currentUser)) {
                throw new PackbspException("Is Steam running? Could not read current Steam username from: "
                        + steamAppData.getAbsolutePath());
            }
            logger.info("Username detected as: {}", currentUser);
        } catch (IOException ex) {
            throw new PackbspException("Is Steam running? Could not read current Steam username from: "
                    + steamAppData.getAbsolutePath(), ex);
        } catch (RecognitionException ex) {
            throw new PackbspException("Is Steam running? Could not determine current Steam username from: "
                    + steamAppData.getAbsolutePath(), ex);
        }

        /**
         * Init supported development kits
         */
        List<Devkit> kits = new ArrayList<Devkit>();
        try {
            Devkit basic = SourceSDK.createKit(steamDir, cdr, currentUser);
            kits.add(basic);
        } catch (BlobParseFailure ex) {
            logger.warn("A problem occurred checking for the Source SDK", ex);
        }

        currentPhase = Phase.GAME;
        // On success, save everything
        _kits = kits;
        _reg = reg;
        _cdr = cdr;
        _steamDirectory = steamDir;
    }

    public List<Devkit> getKits() {
        List<Devkit> copy = new ArrayList<Devkit>();
        copy.addAll(_kits);
        return copy;
    }

    public void acceptGame(final Game chosen) throws PackbspException, IllegalArgumentException {
        assertPhaseSameOrAfter(Phase.GAME);

        if (chosen == null) {
            throw new IllegalArgumentException("No game was selected");
        } else if (!chosen.isPresent()) {
            throw new PackbspException(
                    "The chosen game doesn't appear to be present or has a configuration problem.");
        }

        // Find and load gameinfo file
        final File gameInfoPath = new File(chosen.getGameDir(), GameInfoReader.DEFAULT_FILENAME);
        logger.info("Gameinfo file at: {}", gameInfoPath.getAbsolutePath());

        if (!gameInfoPath.isFile()) {
            throw new PackbspException("Game info not found at: " + gameInfoPath.getAbsolutePath());
        }

        final GameInfoReader gameInfoData;
        try {
            ANTLRInputStream ais = new ANTLRInputStream(new FileInputStream(gameInfoPath));
            ValveTokenLexer lexer = new ValveTokenLexer(ais);
            SloppyParser parser = new SloppyParser(new CommonTokenStream(lexer));
            VdfRoot n = parser.main();
            gameInfoData = new GameInfoReader(n, gameInfoPath);
        } catch (RecognitionException ex) {
            throw new PackbspException("Could not parse Gameinfo file", ex);
        } catch (FileNotFoundException ex) {
            throw new PackbspException("Could not read Gameinfo file", ex);
        } catch (IOException ex) {
            throw new PackbspException("Could not read Gameinfo file", ex);
        }

        _includeConf = null;
        _includes = new HashSet<IncludeItem>();
        try {
            _includeConf = MapIncludes.fromXml(new File("conf/map_includes.xml"));
            _includes = _includeConf.getItems(chosen);
        } catch (JAXBException ex) {
            throw new PackbspException("Could not read map-includes configuration file", ex);
        } catch (FileNotFoundException ex) {
            throw new PackbspException("Could not read map-includes configuration file", ex);
        }

        currentPhase = Phase.SOURCE;
        _chosenGame = chosen;
        _gameInfoData = gameInfoData;
    }

    public File getSourceDir() {
        assertPhaseAfter(Phase.GAME);
        return _chosenGame.getVmfDir();
    }

    public File getBspDir() {
        assertPhaseAfter(Phase.GAME);
        return _chosenGame.getBspDir();
    }

    public File getGameDir() {
        assertPhaseAfter(Phase.GAME);
        return _chosenGame.getGameDir();

    }

    public void acceptSourceFile(final File source) throws PackbspException {
        assertPhaseSameOrAfter(Phase.SOURCE);
        if (!source.isFile()) {
            throw new IllegalArgumentException("Specified source file does not exist");
        }
        logger.info("Source BSP specified as: {}", source.getAbsolutePath());

        File tempCopy;
        try {
            tempCopy = PackbspUtil.createTempCopy(source);
        } catch (IOException ex) {
            throw new PackbspException("Unable to create a temporary copy of the source file for secure reading",
                    ex);
        }

        _sourceBsp = source;
        _sourceCopy = tempCopy;
        currentPhase = Phase.CRAWL;
    }

    public File getSourceFile() {
        assertPhaseAfter(Phase.SOURCE);
        return _sourceBsp;
    }

    public void acceptCrawling(final CrawlListener crawlingListener, final GraphListener graphListener)
            throws PackbspException {
        assertPhaseSameOrAfter(Phase.CRAWL);

        final ManagedLibrary hllib;
        final AssetLocator locator;
        final DependencyGraph graph;
        final FgdSpec spec;
        final MapNode startMapNode;
        final DependencyExpander expander;
        final DependencyManager depManager = new DependencyManager();
        // We keep this mapping because we need to transfer "required or optional"
        // status from the GraphContext--which has no concept of paths--to the
        // Dependencies, which have no concept of nodes.
        final Map<String, Node> nodeMapping = new HashMap<String, Node>();

        // Load HlLib DLL
        try {
            hllib = HlLib.getLibrary();
        } catch (UnsatisfiedLinkError ule) {
            throw new PackbspException("Unable to load HlLib.dll", ule);
        }

        // Make locator
        AssetLocator realLocator = new AssetLocatorImpl(_gameInfoData, new File(_steamDirectory, "steamapps"), _reg,
                hllib, _sourceCopy);
        locator = new CachingLocatorWrap(realLocator);

        // Make FGD spec
        //TODO determine if gamedata0 overrides gamedata1 or vice-versa in gameconfig. Assuming later entries override former

        spec = new FgdSpec();
        try {
            for (File fgd : _chosenGame.getFgdFiles()) {
                logger.info("Parsing FGD at: {}", fgd.getAbsolutePath());
                DefaultLoader.fillSpec(fgd, spec);
            }
        } catch (IOException ex) {
            throw new PackbspException("Could not access FGD data", ex);
        } catch (RecognitionException ex) {
            throw new PackbspException("Could not interpret FGD data", ex);
        }

        // This object will listen to crawling activity and handle the storage
        // of which items are optional/required/present/missing/skipped
        final EmptyCrawlListener mainListener = new EmptyCrawlListener() {

            @Override
            public void nodeContentMissing(Edge edge, Node node, String filePath) {
                depManager.setMissing(filePath);
                nodeMapping.put(filePath, node);
            }

            @Override
            public void nodeContentSkipped(Edge edge, Node node, String filePath) {
                //Query locator and figure out if it's in the BSP or just stock
                boolean inBsp = false;
                List<AssetHit> hits = locator.locate(filePath);
                for (AssetHit h : hits) {
                    if (Type.BSP.equals(h.getSource().getType())) {
                        inBsp = true;
                        break;
                    }
                }
                if (inBsp) {
                    depManager.setPrePacked(filePath);
                } else {
                    depManager.setStock(filePath);
                }
                nodeMapping.put(filePath, node);

            }

            @Override
            public void nodePackingFound(Edge edge, Node node, String filePath, File dataSource) {
                depManager.setFound(filePath, dataSource);
                nodeMapping.put(filePath, node);
            }
        };

        graph = new DependencyGraph();
        startMapNode = new MapNode(_sourceCopy, _sourceBsp.getName(), spec, _includes);
        graph.addVertex(startMapNode);

        if (graphListener != null) {
            graphListener.graphCreated(graph);
        }
        expander = new DependencyExpander(graph, startMapNode, locator);
        expander.addListener(mainListener);
        if (crawlingListener != null) {
            expander.addListener(crawlingListener);
        }
        logger.info("Beginning dependency graph traversal");
        while (!expander.isDone()) {
            try {
                expander.step();
            } catch (TraversalException ex) {
                if (ex.isRecoverable()) {
                    logger.warn("Recoverable traversal error: " + ex.getMessage(), ex);
                } else {
                    throw new PackbspException("An error occurred while exploring the dependency graph", ex);
                }
            }
        }

        logger.info("Dependency graph traversal complete");

        // Determine required-vs-optional items and set status appropriately
        assert (expander.isDone());
        Collection<String> paths = depManager.getPaths();
        for (String path : paths) {
            Node n = nodeMapping.get(path);
            if ((n != null) && expander.wasRequired(n)) {
                depManager.setRequired(path, true);
            } else {
                depManager.setRequired(path, false);
            }
        }

        currentPhase = Phase.PACK;
        // On success, save everything
        _graph = graph;
        _deps = depManager.items;
    }

    public DependencyGraph getGraph() {
        assertPhaseAfter(Phase.CRAWL);
        return _graph;
    }

    public Map<String, DependencyItem> getDependencies() {
        assertPhaseAfter(Phase.CRAWL);
        return _deps;
    }

    public static void savePackList(final Map<String, File> items, final File packListFile) throws IOException {

        logger.info("Packing list will be written to: {}", packListFile.getAbsolutePath());
        FileWriter fw = new FileWriter(packListFile, false);
        BspZipController.writePackingList(items, fw);
        fw.close();

    }

    public void acceptPacking(final File packListFile, final File destination) throws PackbspException {
        assertPhaseSameOrAfter(Phase.PACK);
        logger.info("Packing list file at: {}", packListFile.getAbsolutePath());
        logger.info("Final destination path: {}", destination.getAbsolutePath());
        boolean alreadyExists;

        if (destination.isDirectory()) {
            throw new IllegalArgumentException("Destination file is a directory");
        }
        try {
            alreadyExists = !destination.createNewFile();
        } catch (IOException ex) {
            throw new PackbspException("Unable to create destination BSP file", ex);
        }
        logger.info("Does destination file already exist: {}", alreadyExists);

        // If it already exists, move it to a name with a backup
        if (alreadyExists) {

            // If foo.bak exists, try foo.bak1, if foo.bak1 exists, try foo.bak2, etc.
            int n = 1;
            String baseBakName = destination.getAbsolutePath();
            File bakCandidate = new File(baseBakName + ".bak");
            while (bakCandidate.exists()) {
                n += 1;
                bakCandidate = new File(baseBakName + ".bak" + n);
                assert (n < 1000000); // Should never happen in normal use, but quickly hit on logic error
            }

            logger.info("Renaming existing destination file to a backup: {}", bakCandidate.getAbsolutePath());
            boolean success = destination.renameTo(bakCandidate);
            if (!success) {
                throw new PackbspException("Could not rename target file to a backup.");
            }
        }

        final File sdkToolsDir = _chosenGame.getParent().getBinDir();
        final File bspzipExe = new File(sdkToolsDir, "bin/" + BSPZIP_FILENAME);
        final BspZipController bspzip = new BspZipController(bspzipExe);
        ByteArrayOutputStream resultStream = new ByteArrayOutputStream();
        try {
            logger.info("Running bspzip at:", bspzipExe.getAbsolutePath());
            int exitval = bspzip.packAssets(packListFile, _sourceCopy, destination, getGameDir(), resultStream);

            String text = resultStream.toString();
            text = "\t" + text.replace("\n", "\n\t");
            logger.debug("BSPZIP output:\n{}", text);
            if (!text.contains("Writing new bsp file")) {
                if (text.toLowerCase().contains("local steam service is not running")) {
                    throw new PackbspException("BspZip cannot execute: Steam is not running.");
                }
                throw new PackbspException("BspZip does not have expected success text:\n" + text);
            }
        } catch (ExecuteException ex) {
            logger.error("BSPZIP failed with exception.", ex);
            String text = resultStream.toString();
            text = "\t" + text.replace("\n", "\n\t");
            if (text.trim().length() > 0) {
                logger.error("BSPZIP failed with text:\n{}", text);
            }
            throw new PackbspException(
                    "An error occured while creating a copy of the map with packed content. Please see logs for more details.",
                    ex);
        }
        logger.info("Packing completed");

        currentPhase = Phase.FINAL;

    }
}