/* * The MIT License * * Copyright 2015 Neil McAlister. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package dotaSoundEditor.Controls; import dotaSoundEditor.Helpers.SoundPlayer; import dotaSoundEditor.Helpers.ScriptParser; import dotaSoundEditor.Helpers.Utility; import dotaSoundEditor.*; import dotaSoundEditor.Helpers.CacheManager; import info.ata4.vpk.VPKArchive; import info.ata4.vpk.VPKEntry; import java.awt.event.ActionEvent; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.awt.event.MouseListener; import java.io.BufferedReader; import java.io.File; import java.io.IOException; import java.io.StringReader; import java.nio.ByteBuffer; import java.nio.channels.FileChannel; import java.nio.charset.Charset; import java.nio.file.*; import java.util.ArrayList; import java.util.Enumeration; import java.util.Scanner; import javax.sound.sampled.AudioInputStream; import javax.swing.JButton; import javax.swing.JComboBox; import javax.swing.JFileChooser; import javax.swing.JOptionPane; import javax.swing.JPanel; import javax.swing.JTree; import javax.swing.filechooser.FileNameExtensionFilter; import javax.swing.tree.*; import org.apache.commons.io.FileUtils; public abstract class EditorPanel extends JPanel { protected TreeModel currentTreeModel; protected SoundPlayer soundPlayer; protected JTree currentTree; protected JComboBox currentDropdown; protected String vpkPath; protected String installDir; protected CacheManager cacheManager; //TODO: Create some ActionListeners that pay attention to this value and react to it changing boolean inAdvancedMode = false; abstract void populateSoundList(); abstract void fillImageFrame(Object selectedItem) throws IOException; abstract void populateDropdownBox(); abstract String getCurrentScriptString(); abstract String getCustomSoundPathString(); abstract void updateCache(String scriptString, long internalCrc); protected File promptUserForNewFile(String wavePath) { JFileChooser chooser = new JFileChooser(new File(UserPrefs.getInstance().getWorkingDirectory())); FileNameExtensionFilter filter = new FileNameExtensionFilter("MP3s and WAVs", "mp3", "wav"); chooser.setAcceptAllFileFilterUsed((false)); chooser.setFileFilter(filter); chooser.setMultiSelectionEnabled(false); int chooserRetVal = chooser.showOpenDialog(chooser); if (chooserRetVal == JFileChooser.APPROVE_OPTION) { DefaultMutableTreeNode selectedFile = (DefaultMutableTreeNode) getTreeNodeFromWavePath(wavePath); Path chosenFile = Paths.get(chooser.getSelectedFile().getAbsolutePath()); //Strip caps and spaces out of filenames. The item sound parser seems to have trouble with them. String destFileName = chosenFile.getFileName().toString().toLowerCase().replace(" ", "_"); Path destPath = Paths.get(installDir, "/dota/sound/" + getCustomSoundPathString() + destFileName); UserPrefs.getInstance().setWorkingDirectory(chosenFile.getParent().toString()); try { new File(destPath.toString()).mkdirs(); Files.copy(chosenFile, destPath, StandardCopyOption.REPLACE_EXISTING); String waveString = selectedFile.getUserObject().toString(); int startIndex = -1; int endIndex = -1; if (waveString.contains("\"wave\"")) { startIndex = Utility.nthOccurrence(selectedFile.getUserObject().toString(), '\"', 2); endIndex = Utility.nthOccurrence(selectedFile.getUserObject().toString(), '\"', 3); } else //Some wavestrings don't have the "wave" at the beginning for some reason { startIndex = Utility.nthOccurrence(selectedFile.getUserObject().toString(), '\"', 0); endIndex = Utility.nthOccurrence(selectedFile.getUserObject().toString(), '\"', 1); } String waveStringFilePath = waveString.substring(startIndex, endIndex + 1); waveString = waveString.replace(waveStringFilePath, "\"" + getCustomSoundPathString() + destFileName + "\""); selectedFile.setUserObject(waveString); //Write out modified tree to scriptfile. ScriptParser parser = new ScriptParser(this.currentTreeModel); String scriptString = getCurrentScriptString(); Path scriptPath = Paths.get(scriptString); parser.writeModelToFile(scriptPath.toString()); //Update UI ((DefaultMutableTreeNode) currentTree.getLastSelectedPathComponent()).setUserObject(waveString); ((DefaultTreeModel) currentTree.getModel()).nodeChanged((DefaultMutableTreeNode) currentTree.getLastSelectedPathComponent()); JOptionPane.showMessageDialog(this, "Sound file successfully replaced."); } catch (IOException ex) { JOptionPane.showMessageDialog(null, "Unable to replace sound.\nDetails: " + ex.getMessage(), "Error", JOptionPane.ERROR_MESSAGE); } } return null; } //Overridden in VoicePanel, due to using a different VPK. protected void attachDoubleClickListenerToTree() { MouseListener ml = new MouseAdapter() { @Override public void mousePressed(MouseEvent e) { int selRow = currentTree.getRowForLocation(e.getX(), e.getY()); TreePath selPath = currentTree.getPathForLocation(e.getX(), e.getY()); if (selRow != -1 && ((DefaultMutableTreeNode) selPath.getLastPathComponent()).isLeaf()) { if (e.getClickCount() == 2) { playSelectedTreeSound(selPath, Paths.get(vpkPath)); } } } }; currentTree.addMouseListener(ml); } protected void playSelectedTreeSound(TreePath selPath, Path vpkToPlayFrom) { try { DefaultMutableTreeNode selectedFile = ((DefaultMutableTreeNode) selPath.getLastPathComponent()); String waveString = selectedFile.getUserObject().toString(); File soundFile = createSoundFileFromWaveString(waveString, vpkToPlayFrom); soundPlayer.loadSound(soundFile.getAbsolutePath()); soundPlayer.playSound(); } catch (Exception ex) { JOptionPane.showMessageDialog(null, "The selected node does not represent a valid sound file.", "Error", JOptionPane.ERROR_MESSAGE); } } protected ArrayList<String> getWavePathsAsList(TreeNode selectedFile) { ArrayList<String> wavePathsList = new ArrayList<>(); Enumeration e = selectedFile.children(); while (e.hasMoreElements()) { Object currentElement = e.nextElement(); //If a soundfile has multiple possible wavefiles if (currentElement.toString().contains("\"rndwave\"")) { Enumeration innerE = ((TreeNode) currentElement).children(); while (innerE.hasMoreElements()) { Object currentInnerElement = innerE.nextElement(); if ((currentInnerElement.toString().contains("\"wave\"") || currentInnerElement.toString().contains(".wav") || currentInnerElement.toString().contains(".mp3")) && !currentInnerElement.toString().trim().startsWith("//")) { //Maybe do some string massaging here before we just hand it back wavePathsList.add(((TreeNode) currentInnerElement).toString()); } } } //If it only has one else if (currentElement.toString().contains("\"wave\"")) { if (!currentElement.toString().trim().startsWith("//")) { wavePathsList.add(((TreeNode) currentElement).toString()); } } } return wavePathsList; } private File createSoundFileFromWaveString(String waveString, Path vpkToPlayFrom) { if (!(waveString.contains(".wav") || (waveString.contains(".mp3")))) { return null; } File file = new File(vpkToPlayFrom.toString()); VPKArchive vpk = new VPKArchive(); File entryFile = new File(""); String waveSubstring = ""; int startIndex = -1; int endIndex = -1; if (waveString.contains("\"wave\"")) { startIndex = Utility.nthOccurrence(waveString, '\"', 2); endIndex = Utility.nthOccurrence(waveString, '\"', 3); } //For weird special cases where the "wave" part of the string is missing, i.e. Treant's Overgrowth.Target spell else { startIndex = Utility.nthOccurrence(waveString, '\"', 0); endIndex = Utility.nthOccurrence(waveString, '\"', 1); } waveSubstring = waveString.substring(startIndex, endIndex + 1); waveSubstring = waveSubstring.replace(")", ""); waveSubstring = waveSubstring.replace("\"", ""); waveSubstring = waveSubstring.replace("\\", "/"); waveSubstring = waveSubstring.replace("#", ""); waveSubstring = waveSubstring.replace("*", ""); if (!waveString.contains("custom") && !waveString.contains("//Replaced")) { File localFile = new File(Paths.get(installDir + "/sound/" + waveSubstring).toString()); if (localFile.isFile()) { return localFile; } try { vpk.load(file); } catch (Exception ex) { System.err.println("Can't open archive: " + ex.getMessage()); } waveSubstring = "sound/" + waveSubstring; VPKEntry entry = vpk.getEntry(waveSubstring.toLowerCase()); entryFile = entry.getType().contains("wav") ? new File(Paths.get(System.getProperty("user.dir") + "/scratch/scratch.wav").toString()) : new File(Paths.get(System.getProperty("user.dir") + "/scratch/scratch.mp3").toString()); try (FileChannel fc = FileUtils.openOutputStream(entryFile).getChannel()) { fc.write(entry.getData()); } catch (IOException ex) { ex.printStackTrace(); } return entryFile; } else //If it's NOT stored in the VPK, it's on the local filesys { entryFile = new File(Paths.get(installDir, "/dota/sound/" + waveSubstring).toString()); return entryFile; } } protected TreeNode getTreeNodeFromWavePath(String wavePath) { TreeModel model = this.currentTreeModel; TreeNode root = (TreeNode) model.getRoot(); for (Enumeration e = ((DefaultMutableTreeNode) root).breadthFirstEnumeration(); e.hasMoreElements() && root != null;) { DefaultMutableTreeNode node = (DefaultMutableTreeNode) e.nextElement(); if (node.toString().contains(wavePath)) { return node; } } return null; } protected void deleteSoundFileByWaveString(String selectedWaveString) { int startIndex = -1; int endIndex = -1; if (selectedWaveString.contains("\"wave\"")) { startIndex = Utility.nthOccurrence(selectedWaveString, '\"', 2); endIndex = Utility.nthOccurrence(selectedWaveString, '\"', 3); } else { startIndex = Utility.nthOccurrence(selectedWaveString, '\"', 1); endIndex = Utility.nthOccurrence(selectedWaveString, '\"', 2); } String waveSubstring = selectedWaveString.substring(startIndex, endIndex + 1); waveSubstring = waveSubstring.replace(")", ""); waveSubstring = waveSubstring.replace("\"", ""); waveSubstring = waveSubstring.replace("*", ""); File soundFileToDelete = new File(Paths.get(installDir, "/dota/sound/" + waveSubstring).toString()); if (soundFileToDelete.isFile()) { soundFileToDelete.delete(); } else { System.err.println("Cannot find and delete custom sound file " + waveSubstring); } } protected ArrayList<String> getWavePathListFromString(String scriptString) { ArrayList<String> wavePathsList = new ArrayList<>(); BufferedReader buf = new BufferedReader(new StringReader(scriptString)); String line = null; try { while ((line = buf.readLine()) != null) { //Stop reading after we're done with the wave paths if (line.contains("soundentry")) { break; } if (line.contains("\"wave\"") || line.contains(".wav") || line.contains(".mp3")) { wavePathsList.add(line); } } return wavePathsList; } catch (IOException ex) { ex.printStackTrace(); return null; } } public boolean validateScriptFile(String scriptKey, String scriptPath) { if(cacheManager == null) { cacheManager = new CacheManager(); } cacheManager.putScriptPath(scriptKey, scriptPath); long crc = cacheManager.getSessionCrc(scriptKey); if (crc == 0) { return false; } return validateScriptFile(scriptKey, crc); } public boolean validateScriptFile(String scriptKey, long internalCrc) { if(cacheManager == null) { cacheManager = new CacheManager(); } if (!cacheManager.isUpToDate(scriptKey, internalCrc)) { return false; } else { return true; } } /** * * @param oldTree The tree that was previously in use. * @param scriptPath The full filepath to the script. * @return The merged tree. */ TreeModel mergeNewChanges(TreeModel oldTree, String scriptPath) { return mergeNewChanges(oldTree, new File(scriptPath)); } /** * @param oldTree The tree that was previously in use * @param scriptPath The full filepath to the script. * @return The merged tree. */ TreeModel mergeNewChanges(TreeModel oldTree, Path scriptPath) { return mergeNewChanges(oldTree, scriptPath.toFile()); } /** * @param oldTree The tree that was previously in use * @param scriptFilePath A File object pointing to or containing a script. * @return The merged tree. */ TreeModel mergeNewChanges(TreeModel oldTree, File scriptFilePath) { //Look for any modified wavestrings. Save their nodes, and note their indices. //Parse in updated script tree, replace nodes at indices with saved nodes. //Return merged tree System.out.println("Running a merge operation!"); TreeNode oldRoot = (TreeNode) oldTree.getRoot(); ArrayList<DefaultMutableTreeNode> savedNodeList = new ArrayList<>(); for (Enumeration e = ((DefaultMutableTreeNode) oldRoot).depthFirstEnumeration(); e.hasMoreElements() && oldRoot != null;) { DefaultMutableTreeNode node = (DefaultMutableTreeNode) e.nextElement(); if (node.getUserObject().toString().contains("custom\\") || node.getUserObject().toString().contains("custom/")) { savedNodeList.add(node); } } ScriptParser parser = new ScriptParser(scriptFilePath); TreeModel newTree = parser.getTreeModel(); TreeNode newRoot = (TreeNode) newTree.getRoot(); for (DefaultMutableTreeNode savedNode : savedNodeList) { int rndwaveIndex = -1; int childIndex = -1; int parentIndex = -1; DefaultMutableTreeNode parent = (DefaultMutableTreeNode) savedNode.getParent(); if (parent.getUserObject().toString().contains("rndwave")) { rndwaveIndex = parent.getParent().getIndex(parent); childIndex = parent.getIndex(savedNode); parent = (DefaultMutableTreeNode) parent.getParent(); parentIndex = parent.getParent().getIndex(parent); } else { parentIndex = parent.getParent().getIndex(parent); childIndex = parent.getIndex(savedNode); } TreeNode newParentNode = newRoot.getChildAt(parentIndex); TreeNode newChildNode; if (rndwaveIndex != -1) { newChildNode = newParentNode.getChildAt(rndwaveIndex); newChildNode = newChildNode.getChildAt(childIndex); } else { newChildNode = newParentNode.getChildAt(childIndex); } newChildNode = savedNode; } return newTree; } void advancedButtonActionPerformed(java.awt.event.ActionEvent evt, JButton advancedButton) { if (!getAdvancedMode()) { setAdvancedMode(true); String scriptPath = getCurrentScriptString(); ScriptParser parser = new ScriptParser(new File(Paths.get(scriptPath).toString())); TreeModel model = parser.getTreeModel(); currentTree.setModel(model); currentTree.setEditable(true); for (int i = 0; i < currentTree.getRowCount(); i++) { currentTree.expandRow(i); } //Change button and action to Basic-revert: advancedButton.setText("Basic <<"); advancedButton.setMnemonic('a'); } else if (getAdvancedMode()) { setAdvancedMode(false); this.populateSoundList(); advancedButton.setText("Advanced >>"); advancedButton.setMnemonic('a'); currentTree.setEditable(false); } } /** * @return Whether or not the panel is currently in advanced mode or not. */ public boolean getAdvancedMode() { return inAdvancedMode; } public void setAdvancedMode(boolean _newMode) { inAdvancedMode = _newMode; } protected TreeModel buildSoundListTree(TreeModel scriptTree) { TreeNode rootNode = (TreeNode) scriptTree.getRoot(); int childCount = rootNode.getChildCount(); TreeModel soundListTreeModel = new DefaultTreeModel(new DefaultMutableTreeNode("root")); ArrayList<String> wavePathsList = new ArrayList<>(); for (int i = 0; i < childCount; i++) { String nodeValue = scriptTree.getChild(rootNode, i).toString(); if (nodeValue.trim().startsWith("//")) { continue; } wavePathsList = getWavePathsAsList((TreeNode) scriptTree.getChild(rootNode, i)); DefaultMutableTreeNode newNode = new DefaultMutableTreeNode(nodeValue); for (String s : wavePathsList) { DefaultMutableTreeNode tempNode = new DefaultMutableTreeNode(s); newNode.add(tempNode); } ((DefaultMutableTreeNode) soundListTreeModel.getRoot()).add(newNode); } return soundListTreeModel; } protected void writeScriptFileToDisk(VPKEntry entryToWrite, boolean overwriteExisting) { File existsChecker = new File(Paths.get(installDir, entryToWrite.getPath()).toString()); boolean fileExistsLocally = existsChecker.exists(); if (fileExistsLocally && !overwriteExisting) { return; } File entryFile = new File(Paths.get(installDir, "/dota/").toFile(), entryToWrite.getPath()); File entryDir = entryFile.getParentFile(); if (entryDir != null && !entryDir.exists()) { entryDir.mkdirs(); } try (final FileChannel fc = FileUtils.openOutputStream(entryFile).getChannel()) { fc.write(entryToWrite.getData()); } catch (IOException ex) { JOptionPane.showMessageDialog(this, "Error: Unable to write script file to disk.\nDetails: " + ex.getMessage(), "Error writing script file", JOptionPane.ERROR_MESSAGE); } } protected void revertButtonActionPerformed(ActionEvent evt) { revertButtonActionPerformed(evt, Paths.get(vpkPath)); } protected void revertButtonActionPerformed(ActionEvent evt, Path vpkToRevert) { //TODO: See if we can abstract away some of this functionality if (currentTree.getSelectionRows().length != 0 && ((TreeNode) currentTree.getSelectionPath().getLastPathComponent()).isLeaf()) { DefaultMutableTreeNode selectedNode = (DefaultMutableTreeNode) currentTree.getSelectionPath().getLastPathComponent(); String selectedWaveString = ((DefaultMutableTreeNode) selectedNode).getUserObject().toString(); String selectedWaveParentString = ((DefaultMutableTreeNode) ((DefaultMutableTreeNode) selectedNode).getParent()).getUserObject().toString(); selectedNode = (DefaultMutableTreeNode) this.getTreeNodeFromWavePath(selectedWaveString); //First go in and delete the sound in customSounds deleteSoundFileByWaveString(selectedWaveString); //Get the relevant wavestring from the internal scriptfile VPKArchive vpk = new VPKArchive(); try { vpk.load(new File(vpkToRevert.toString())); } catch (IOException ex) { ex.printStackTrace(); } String scriptDir = getCurrentScriptString(); scriptDir = scriptDir.replace(Paths.get(installDir, "/dota/").toString(), ""); scriptDir = scriptDir.replace("\\", "/"); //Match internal forward slashes scriptDir = scriptDir.substring(1); //Cut off leading slash byte[] bytes = null; VPKEntry entry = vpk.getEntry(scriptDir); try { ByteBuffer scriptBuffer = entry.getData(); bytes = new byte[scriptBuffer.remaining()]; scriptBuffer.get(bytes); } catch (IOException ex) { ex.printStackTrace(); } String scriptFileString = new String(bytes, Charset.forName("UTF-8")); ArrayList<String> wavePathList = this.getWavePathsAsList(selectedNode.getParent()); int waveStringIndex = wavePathList.indexOf(selectedWaveString); //Cut off every part of the scriptFileString before we get to the entry describing the relevant hero action, so we don't accidentally get the wrong wavepaths StringBuilder scriptFileStringShortened = new StringBuilder(); Scanner scan = new Scanner(scriptFileString); boolean found = false; while (scan.hasNextLine()) { String curLine = scan.nextLine(); if (curLine.equals(selectedWaveParentString)) { found = true; } if (found == true) { scriptFileStringShortened.append(curLine).append(System.lineSeparator()); } } scriptFileString = scriptFileStringShortened.toString(); ArrayList<String> internalWavePathsList = getWavePathListFromString(scriptFileString); String replacementString = internalWavePathsList.get(waveStringIndex); selectedNode.setUserObject(replacementString); ScriptParser parser = new ScriptParser(this.currentTreeModel); parser.writeModelToFile(getCurrentScriptString()); //Modify the UI treeNode in addition to the backing TreeNode ((DefaultMutableTreeNode) currentTree.getLastSelectedPathComponent()).setUserObject(replacementString); ((DefaultTreeModel) currentTree.getModel()).nodeChanged((DefaultMutableTreeNode) currentTree.getLastSelectedPathComponent()); } } protected void playSoundButtonActionPerformed(ActionEvent evt) { playSoundButtonActionPerformed(evt, Paths.get(vpkPath)); } protected void playSoundButtonActionPerformed(ActionEvent evt, Path vpkToPlayFrom) { if (currentTree.getSelectionRows().length != 0 && ((TreeNode) currentTree.getSelectionPath().getLastPathComponent()).isLeaf()) { this.playSelectedTreeSound(currentTree.getSelectionPath(), vpkToPlayFrom); } } protected void replaceButtonActionPerformed(ActionEvent evt) { if (currentTree.getSelectionRows() != null && currentTree.getSelectionRows().length != 0 && ((TreeNode) currentTree.getSelectionPath().getLastPathComponent()).isLeaf()) { TreeNode selectedFile = (TreeNode) currentTree.getSelectionPath().getLastPathComponent(); promptUserForNewFile(selectedFile.toString()); } } protected void revertAllButtonActionPerformed(ActionEvent evt) { //Delete existing script file String scriptFilePath = getCurrentScriptString(); File scriptFileToDelete = new File(scriptFilePath); if (scriptFileToDelete.isFile()) { try { Files.delete(Paths.get(scriptFilePath)); } catch (NoSuchFileException | DirectoryNotEmptyException | SecurityException ex) { ex.printStackTrace(); } catch (IOException ex) { System.err.println("IOException in delete."); } } else { System.err.println("Unable to delete script file at " + scriptFileToDelete.getAbsolutePath()); } //Repopulate soundtree populateSoundList(); } }