// Near Infinity - An Infinity Engine Browser and Editor // Copyright (C) 2001 - 2005 Jon Olav Hauglid // See LICENSE.txt for license information package org.infinity.check; import java.awt.BorderLayout; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.io.BufferedWriter; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Set; import java.util.concurrent.ThreadPoolExecutor; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.swing.BorderFactory; import javax.swing.JFileChooser; import javax.swing.JLabel; import javax.swing.JMenu; import javax.swing.JMenuBar; import javax.swing.JMenuItem; import javax.swing.JOptionPane; import javax.swing.JPanel; import javax.swing.JScrollPane; import javax.swing.JTextArea; import javax.swing.ProgressMonitor; import javax.swing.event.ListSelectionEvent; import javax.swing.event.ListSelectionListener; import org.infinity.NearInfinity; import org.infinity.datatype.StringRef; import org.infinity.gui.BrowserMenuBar; import org.infinity.gui.Center; import org.infinity.gui.ChildFrame; import org.infinity.gui.SortableTable; import org.infinity.gui.TableItem; import org.infinity.gui.WindowBlocker; import org.infinity.icon.Icons; import org.infinity.resource.AbstractStruct; import org.infinity.resource.Profile; import org.infinity.resource.Resource; import org.infinity.resource.ResourceFactory; import org.infinity.resource.StructEntry; import org.infinity.resource.bcs.BcsResource; import org.infinity.resource.bcs.Compiler; import org.infinity.resource.bcs.Decompiler; import org.infinity.resource.dlg.AbstractCode; import org.infinity.resource.dlg.Action; import org.infinity.resource.dlg.DlgResource; import org.infinity.resource.key.ResourceEntry; import org.infinity.resource.text.PlainTextResource; import org.infinity.search.SearchClient; import org.infinity.search.SearchMaster; import org.infinity.util.Debugging; import org.infinity.util.Misc; import org.infinity.util.StringResource; public final class StringUseChecker implements Runnable, ListSelectionListener, SearchClient, ActionListener { private static final String FMT_PROGRESS = "Checking %ss..."; private static final Pattern NUMBERPATTERN = Pattern.compile("\\d+", Pattern.DOTALL); private static final String[] FILETYPES = {"2DA", "ARE", "BCS", "BS", "CHR", "CHU", "CRE", "DLG", "EFF", "INI", "ITM", "SPL", "SRC", "STO", "WMP"}; private ChildFrame resultFrame; private JTextArea textArea; private SortableTable table; private boolean[] strUsed; private JMenuItem save; private List<ResourceEntry> files; private ProgressMonitor progress; private int progressIndex; public StringUseChecker() { new Thread(this).start(); } // --------------------- Begin Interface ListSelectionListener --------------------- @Override public void valueChanged(ListSelectionEvent event) { if (table.getSelectedRow() == -1) textArea.setText(null); else { TableItem item = table.getTableItemAt(table.getSelectedRow()); textArea.setText(item.toString()); } textArea.setCaretPosition(0); } // --------------------- End Interface ListSelectionListener --------------------- // --------------------- Begin Interface Runnable --------------------- @Override public void run() { WindowBlocker blocker = new WindowBlocker(NearInfinity.getInstance()); blocker.setBlocked(true); try { ThreadPoolExecutor executor = Misc.createThreadPool(); files = new ArrayList<ResourceEntry>(); for (final String fileType : FILETYPES) files.addAll(ResourceFactory.getResources(fileType)); String type = "WWWW"; progressIndex = 0; progress = new ProgressMonitor(NearInfinity.getInstance(), "Searching...", String.format(FMT_PROGRESS, type), 0, files.size()); List<Class<? extends Object>> colClasses = new ArrayList<Class<? extends Object>>(2); colClasses.add(Object.class); colClasses.add(Integer.class); table = new SortableTable(Arrays.asList(new String[]{"String", "StrRef"}), colClasses, Arrays.asList(new Integer[]{450, 20})); StringResource.getStringRef(0); strUsed = new boolean[StringResource.getMaxIndex() + 1]; boolean isCancelled = false; Debugging.timerReset(); for (int i = 0; i < files.size(); i++) { ResourceEntry entry = files.get(i); if (i % 10 == 0) { String ext = entry.getExtension(); if (ext != null && !type.equalsIgnoreCase(ext)) { type = ext; progress.setNote(String.format(FMT_PROGRESS, type)); } } Misc.isQueueReady(executor, true, -1); executor.execute(new Worker(entry)); if (progress.isCanceled()) { isCancelled = true; break; } } // enforcing thread termination if process has been cancelled if (isCancelled) { executor.shutdownNow(); } else { executor.shutdown(); } // waiting for pending threads to terminate while (!executor.isTerminated()) { if (!isCancelled && progress.isCanceled()) { executor.shutdownNow(); isCancelled = true; } try { Thread.sleep(1); } catch (InterruptedException e) {} } if (isCancelled) { JOptionPane.showMessageDialog(NearInfinity.getInstance(), "Operation cancelled", "Info", JOptionPane.INFORMATION_MESSAGE); return; } for (int i = 0; i < strUsed.length; i++) { if (!strUsed[i]) { table.addTableItem(new UnusedStringTableItem(new Integer(i))); } } if (table.getRowCount() == 0) { resultFrame.close(); JOptionPane.showMessageDialog(NearInfinity.getInstance(), "No unused strings found", "Info", JOptionPane.INFORMATION_MESSAGE); } else { table.tableComplete(1); textArea = new JTextArea(10, 40); textArea.setEditable(false); textArea.setWrapStyleWord(true); textArea.setLineWrap(true); JScrollPane scrollText = new JScrollPane(textArea); resultFrame = new ChildFrame("Result", true); save = new JMenuItem("Save"); save.addActionListener(this); JMenu fileMenu = new JMenu("File"); fileMenu.add(save); JMenuBar menuBar = new JMenuBar(); menuBar.add(fileMenu); resultFrame.setJMenuBar(menuBar); resultFrame.setIconImage(Icons.getIcon(Icons.ICON_FIND_16).getImage()); JLabel count = new JLabel(table.getRowCount() + " unused string(s) found", JLabel.CENTER); count.setFont(count.getFont().deriveFont((float)count.getFont().getSize() + 2.0f)); JScrollPane scrollTable = new JScrollPane(table); scrollTable.getViewport().setBackground(table.getBackground()); JPanel pane = (JPanel)resultFrame.getContentPane(); pane.setLayout(new BorderLayout(0, 3)); pane.add(count, BorderLayout.NORTH); pane.add(scrollTable, BorderLayout.CENTER); JPanel bottomPanel = new JPanel(new BorderLayout()); JPanel searchPanel = SearchMaster.createAsPanel(this, resultFrame); bottomPanel.add(scrollText, BorderLayout.CENTER); bottomPanel.add(searchPanel, BorderLayout.EAST); pane.add(bottomPanel, BorderLayout.SOUTH); table.setFont(BrowserMenuBar.getInstance().getScriptFont()); pane.setBorder(BorderFactory.createEmptyBorder(3, 3, 3, 3)); table.getSelectionModel().addListSelectionListener(this); resultFrame.pack(); Center.center(resultFrame, NearInfinity.getInstance().getBounds()); resultFrame.setVisible(true); } } finally { advanceProgress(true); blocker.setBlocked(false); if (files != null) { files.clear(); files = null; } } Debugging.timerShow("Check completed", Debugging.TimeFormat.MILLISECONDS); } // --------------------- End Interface Runnable --------------------- // --------------------- Begin Interface ActionListener --------------------- @Override public void actionPerformed(ActionEvent e) { if (e.getSource() == save) { JFileChooser c = new JFileChooser(Profile.getGameRoot().toFile()); c.setDialogTitle("Save result"); if (c.showSaveDialog(resultFrame) == JFileChooser.APPROVE_OPTION) { Path output = c.getSelectedFile().toPath(); if (Files.exists(output)) { String[] options = {"Overwrite", "Cancel"}; if (JOptionPane.showOptionDialog(resultFrame, output + "exists. Overwrite?", "Save result",JOptionPane.YES_NO_OPTION, JOptionPane.WARNING_MESSAGE, null, options, options[0]) != 0) return; } try (BufferedWriter bw = Files.newBufferedWriter(output)) { bw.write("Searched for unused strings"); bw.newLine(); bw.write("Number of hits: " + table.getRowCount()); bw.newLine(); bw.newLine(); for (int i = 0; i < table.getRowCount(); i++) { bw.write("StringRef: " + table.getTableItemAt(i).getObjectAt(1) + " /* " + table.getTableItemAt(i).toString().replaceAll("\r\n", Misc.LINE_SEPARATOR) + " */"); bw.newLine(); } JOptionPane.showMessageDialog(resultFrame, "Result saved to " + output, "Save complete", JOptionPane.INFORMATION_MESSAGE); } catch (IOException ex) { JOptionPane.showMessageDialog(resultFrame, "Error while saving " + output, "Error", JOptionPane.ERROR_MESSAGE); ex.printStackTrace(); } } } } // --------------------- End Interface ActionListener --------------------- // --------------------- Begin Interface SearchClient --------------------- @Override public String getText(int nr) { if (nr < 0 || nr >= table.getRowCount()) return null; return table.getTableItemAt(nr).toString(); } @Override public void hitFound(int nr) { table.getSelectionModel().addSelectionInterval(nr, nr); table.scrollRectToVisible(table.getCellRect(table.getSelectionModel().getMinSelectionIndex(), 0, true)); } // --------------------- End Interface SearchClient --------------------- private void checkDialog(DlgResource dialog) { List<StructEntry> flatList = dialog.getFlatList(); for (int i = 0; i < flatList.size(); i++) { if (flatList.get(i) instanceof StringRef) { StringRef ref = (StringRef)flatList.get(i); if (ref.getValue() >= 0 && ref.getValue() < strUsed.length) { synchronized (strUsed) { strUsed[ref.getValue()] = true; } } } else if (flatList.get(i) instanceof AbstractCode) { AbstractCode code = (AbstractCode)flatList.get(i); try { Compiler compiler = new Compiler(code.toString(), (code instanceof Action) ? Compiler.ScriptType.ACTION : Compiler.ScriptType.TRIGGER); String compiled = compiler.getCode(); Decompiler decompiler = new Decompiler(compiled, true); if (code instanceof Action) { decompiler.setScriptType(Decompiler.ScriptType.ACTION); } else { decompiler.setScriptType(Decompiler.ScriptType.TRIGGER); } decompiler.decompile(); Set<Integer> used = decompiler.getStringRefsUsed(); for (final Integer stringRef : used) { int u = stringRef.intValue(); if (u >= 0 && u < strUsed.length) { synchronized (strUsed) { strUsed[u] = true; } } } } catch (Exception e) { e.printStackTrace(); } } } } private void checkScript(BcsResource script) { Decompiler decompiler = new Decompiler(script.getCode(), true); decompiler.decompile(); Set<Integer> used = decompiler.getStringRefsUsed(); for (final Integer stringRef : used) { int u = stringRef.intValue(); if (u >= 0 && u < strUsed.length) { synchronized (strUsed) { strUsed[u] = true; } } } } private void checkStruct(AbstractStruct struct) { List<StructEntry> flatList = struct.getFlatList(); for (int i = 0, size = flatList.size(); i < size; i++) { if (flatList.get(i) instanceof StringRef) { StringRef ref = (StringRef)flatList.get(i); if (ref.getValue() >= 0 && ref.getValue() < strUsed.length) { synchronized (strUsed) { strUsed[ref.getValue()] = true; } } } } } private void checkTextfile(PlainTextResource text) { Matcher m = NUMBERPATTERN.matcher(text.getText()); while (m.find()) { long nr = Long.parseLong(text.getText().substring(m.start(), m.end())); if (nr >= 0 && nr < strUsed.length) { synchronized (strUsed) { strUsed[(int)nr] = true; } } } } private synchronized void advanceProgress(boolean finished) { if (progress != null) { if (finished) { progressIndex = 0; progress.close(); progress = null; } else { progressIndex++; progress.setProgress(progressIndex); } } } // -------------------------- INNER CLASSES -------------------------- private static final class UnusedStringTableItem implements TableItem { private final Integer strRef; private final String string; private UnusedStringTableItem(Integer strRef) { this.strRef = strRef; string = StringResource.getStringRef(strRef.intValue()); } @Override public Object getObjectAt(int columnIndex) { if (columnIndex == 1) return strRef; return string; } @Override public String toString() { return string; } } private class Worker implements Runnable { private final ResourceEntry entry; public Worker(ResourceEntry entry) { this.entry = entry; } @Override public void run() { if (entry != null) { Resource resource = ResourceFactory.getResource(entry); if (resource instanceof DlgResource) { checkDialog((DlgResource)resource); } else if (resource instanceof BcsResource) { checkScript((BcsResource)resource); } else if (resource instanceof PlainTextResource) { checkTextfile((PlainTextResource)resource); } else if (resource != null) { checkStruct((AbstractStruct)resource); } } advanceProgress(false); } } }