/* * HalfNES by Andrew Hoffman * Licensed under the GNU GPL Version 3. See LICENSE file */ package com.grapeshot.halfnes.ui; import com.grapeshot.halfnes.FileUtils; import com.grapeshot.halfnes.NES; import com.grapeshot.halfnes.PrefsSingleton; import com.grapeshot.halfnes.video.RGBRenderer; import com.grapeshot.halfnes.cheats.ActionReplay; import com.grapeshot.halfnes.cheats.ActionReplayGui; import com.grapeshot.halfnes.video.NTSCRenderer; import com.grapeshot.halfnes.video.Renderer; import java.awt.*; import java.awt.datatransfer.DataFlavor; import java.awt.datatransfer.Transferable; import java.awt.datatransfer.UnsupportedFlavorException; import java.awt.event.*; import java.awt.image.BufferStrategy; import java.awt.image.BufferedImage; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.util.ArrayList; import java.util.Enumeration; import java.util.List; import java.util.zip.ZipEntry; import java.util.zip.ZipFile; import java.util.zip.ZipInputStream; import javax.swing.*; public class SwingUI extends JFrame implements GUIInterface { private Canvas canvas; private BufferStrategy buffer; private NES nes; private static final long serialVersionUID = 6411494245530679723L; private final AL listener = new AL(); private int screenScaleFactor; private final long[] frametimes = new long[60]; private int frametimeptr = 0; private boolean smoothScale, inFullScreen = false; private GraphicsDevice gd; private int NES_HEIGHT, NES_WIDTH; private Renderer renderer; private final ControllerImpl padController1, padController2; public SwingUI(String[] args) { nes = new NES(this); screenScaleFactor = PrefsSingleton.get().getInt("screenScaling", 2); padController1 = new ControllerImpl(this, 0); padController2 = new ControllerImpl(this, 1); nes.setControllers(padController1, padController2); padController1.startEventQueue(); padController2.startEventQueue(); if (args == null || args.length < 1 || args[0] == null) { nes.run(); } else { nes.run(args[0]); } } @Override public NES getNes() { return nes; } @Override public void setNES(NES nes) { this.nes = nes; } public synchronized void setRenderOptions() { if (canvas != null) { this.remove(canvas); } screenScaleFactor = PrefsSingleton.get().getInt("screenScaling", 2); smoothScale = PrefsSingleton.get().getBoolean("smoothScaling", false); if (PrefsSingleton.get().getBoolean("TVEmulation", false)) { renderer = new NTSCRenderer(); NES_WIDTH = 302; } else { renderer = new RGBRenderer(); NES_WIDTH = 256; } if (PrefsSingleton.get().getInt("region", 0) > 1) { NES_HEIGHT = 240; renderer.setClip(0); } else { NES_HEIGHT = 224; renderer.setClip(8); } // Create canvas for painting canvas = new Canvas(); canvas.setSize(NES_WIDTH * screenScaleFactor, NES_HEIGHT * screenScaleFactor); canvas.setEnabled(false); //otherwise it steals input events. // Add canvas to game window this.add(canvas); this.pack(); canvas.createBufferStrategy(2); buffer = canvas.getBufferStrategy(); } @Override public synchronized void run() { //construct window this.setTitle("HalfNES " + NES.VERSION); this.setResizable(false); buildMenus(); setRenderOptions(); this.getRootPane().registerKeyboardAction(listener, "Escape", KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), JComponent.WHEN_IN_FOCUSED_WINDOW); this.getRootPane().registerKeyboardAction(listener, "Toggle Fullscreen", KeyStroke.getKeyStroke(KeyEvent.VK_F11, 0), JComponent.WHEN_IN_FOCUSED_WINDOW); this.getRootPane().registerKeyboardAction(listener, "Quit", KeyStroke.getKeyStroke(KeyEvent.VK_F4, KeyEvent.ALT_DOWN_MASK), JComponent.WHEN_IN_FOCUSED_WINDOW); this.setLocation(PrefsSingleton.get().getInt("windowX", 0), PrefsSingleton.get().getInt("windowY", 0)); this.addWindowListener(listener); this.setDefaultCloseOperation(DO_NOTHING_ON_CLOSE); this.setVisible(true); // Create BackBuffer //now add the drag and drop handler. TransferHandler handler = new TransferHandler() { @Override public boolean canImport(final TransferHandler.TransferSupport support) { if (!support.isDataFlavorSupported(DataFlavor.javaFileListFlavor)) { return false; } return true; } @Override public boolean importData(final TransferHandler.TransferSupport support) { if (!canImport(support)) { return false; } Transferable t = support.getTransferable(); try { //holy typecasting batman (this interface predates generics) File toload = (File) ((java.util.List) t.getTransferData(DataFlavor.javaFileListFlavor)).get(0); loadROM(toload.getCanonicalPath()); } catch (UnsupportedFlavorException e) { return false; } catch (IOException e) { return false; } return true; } }; this.setTransferHandler(handler); } public void buildMenus() { JMenuBar menus = new JMenuBar(); JMenu file = new JMenu("File"); JMenuItem item; file.add(item = new JMenuItem("Open ROM")); item.addActionListener(listener); item.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_O, Toolkit.getDefaultToolkit().getMenuShortcutKeyMask())); file.addSeparator(); file.add(item = new JMenuItem("Preferences")); item.addActionListener(listener); item.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_P, Toolkit.getDefaultToolkit().getMenuShortcutKeyMask())); file.addSeparator(); file.add(item = new JMenuItem("Toggle Fullscreen")); item.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_F11, 0)); item.addActionListener(listener); menus.add(file); file.add(item = new JMenuItem("Quit")); item.addActionListener(listener); menus.add(file); JMenu nesmenu = new JMenu("NES"); nesmenu.add(item = new JMenuItem("Reset")); item.addActionListener(listener); item.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_R, Toolkit.getDefaultToolkit().getMenuShortcutKeyMask())); nesmenu.add(item = new JMenuItem("Hard Reset")); item.addActionListener(listener); item.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_T, Toolkit.getDefaultToolkit().getMenuShortcutKeyMask())); nesmenu.add(item = new JMenuItem("Pause")); item.addActionListener(listener); item.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_F7, 0)); nesmenu.add(item = new JMenuItem("Resume")); item.addActionListener(listener); item.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_F8, 0)); nesmenu.add(item = new JMenuItem("Fast Forward")); item.addActionListener(listener); item.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_SPACE, Toolkit.getDefaultToolkit().getMenuShortcutKeyMask())); nesmenu.add(item = new JMenuItem("Frame Advance")); item.addActionListener(listener); item.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_PERIOD, Toolkit.getDefaultToolkit().getMenuShortcutKeyMask())); nesmenu.addSeparator(); nesmenu.add(item = new JMenuItem("Controller Settings")); item.addActionListener(listener); item.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_S, Toolkit.getDefaultToolkit().getMenuShortcutKeyMask())); nesmenu.add(item = new JMenuItem("Cheat Codes")); item.addActionListener(listener); item.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_F10, Toolkit.getDefaultToolkit().getMenuShortcutKeyMask())); nesmenu.addSeparator(); nesmenu.add(item = new JMenuItem("ROM Info")); item.addActionListener(listener); item.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_I, Toolkit.getDefaultToolkit().getMenuShortcutKeyMask())); menus.add(nesmenu); JMenu help = new JMenu("Help"); help.add(item = new JMenuItem("About")); item.addActionListener(listener); item.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_F1, 0)); menus.add(help); this.setJMenuBar(menus); } public void loadROM() { FileDialog fileDialog = new FileDialog(this); fileDialog.setMode(FileDialog.LOAD); fileDialog.setTitle("Select a ROM to load"); //should open last folder used, and if that doesn't exist, the folder it's running in final String path = PrefsSingleton.get().get("filePath", System.getProperty("user.dir", "")); final File startDirectory = new File(path); if (startDirectory.isDirectory()) { fileDialog.setDirectory(path); } //and if the last path used doesn't exist don't set the directory at all //and hopefully the jFileChooser will open somewhere usable //on Windows it does - on Mac probably not. fileDialog.setFilenameFilter(new NESFileFilter()); boolean wasInFullScreen = false; if (inFullScreen) { wasInFullScreen = true; //load dialog won't show if we are in full screen, so this fixes for now. toggleFullScreen(); } fileDialog.setVisible(true); if (fileDialog.getFile() != null) { PrefsSingleton.get().put("filePath", fileDialog.getDirectory()); loadROM(fileDialog.getDirectory() + fileDialog.getFile()); } if (wasInFullScreen) { toggleFullScreen(); } } private void loadROM(String path) { if (path.endsWith(".zip") || path.endsWith(".ZIP")) { try { loadRomFromZip(path); } catch (IOException ex) { this.messageBox("Could not load file:\nFile does not exist or is not a valid NES game.\n" + ex.getMessage()); } } else { nes.loadROM(path); } } private void loadRomFromZip(String zipName) throws IOException { final String romName = selectRomInZip(listRomsInZip(zipName)); if (romName != null) { final File extractedFile = extractRomFromZip(zipName, romName); if (extractedFile != null) { extractedFile.deleteOnExit(); nes.loadROM(extractedFile.getCanonicalPath()); } } } private List<String> listRomsInZip(String zipName) throws IOException { final ZipFile zipFile = new ZipFile(zipName); final Enumeration<? extends ZipEntry> zipEntries = zipFile.entries(); final List<String> romNames = new ArrayList<>(); while (zipEntries.hasMoreElements()) { final ZipEntry entry = zipEntries.nextElement(); if (!entry.isDirectory() && (entry.getName().endsWith(".nes") || entry.getName().endsWith(".fds") || entry.getName().endsWith(".nsf"))) { romNames.add(entry.getName()); } } zipFile.close(); if (romNames.isEmpty()) { throw new IOException("No NES games found in ZIP file."); } return romNames; } private String selectRomInZip(List<String> romNames) { if (romNames.size() > 1) { return (String) JOptionPane.showInputDialog(this, "Select ROM to load", "Select ROM to load", JOptionPane.PLAIN_MESSAGE, null, romNames.toArray(), romNames.get(0)); } else if (romNames.size() == 1) { return romNames.get(0); } return null; } private File extractRomFromZip(String zipName, String romName) throws IOException { final ZipInputStream zipStream = new ZipInputStream(new FileInputStream(zipName)); ZipEntry entry; do { entry = zipStream.getNextEntry(); } while ((entry != null) && (!entry.getName().equals(romName))); if (entry == null) { zipStream.close(); throw new IOException("Cannot find file " + romName + " inside archive " + zipName); } //name temp. extracted file after parent zip and file inside //note: here's the bug, when it saves the temp file if it's in a folder //in the zip it's trying to put it in the same folder outside the zip final File outputFile = new File(new File(zipName).getParent() + File.separator + FileUtils.stripExtension(new File(zipName).getName()) + " - " + romName); if (outputFile.exists()) { this.messageBox("Cannot extract file. File " + outputFile.getCanonicalPath() + " already exists."); zipStream.close(); return null; } final byte[] buf = new byte[4096]; final FileOutputStream fos = new FileOutputStream(outputFile); int numBytes; while ((numBytes = zipStream.read(buf, 0, buf.length)) != -1) { fos.write(buf, 0, numBytes); } zipStream.close(); fos.close(); return outputFile; } public synchronized void toggleFullScreen() { if (inFullScreen) { this.dispose(); gd.setFullScreenWindow(null); canvas.setSize(NES_HEIGHT * screenScaleFactor, NES_WIDTH * screenScaleFactor); this.setUndecorated(false); this.setVisible(true); inFullScreen = false; buildMenus(); // nes.resume(); } else { setJMenuBar(null); gd = getGraphicsConfiguration().getDevice(); if (!gd.isFullScreenSupported()) { //then fullscreen will give a window the size of the screen instead messageBox("Fullscreen is not supported by your OS or version of Java."); } this.dispose(); this.setUndecorated(true); gd.setFullScreenWindow(this); this.setVisible(true); inFullScreen = true; } } @Override public void messageBox(final String message) { JOptionPane.showMessageDialog(this, message); } int bgcolor; BufferedImage frame; double fps; int frameskip = 0; @Override public final synchronized void setFrame(final int[] nextframe, final int[] bgcolors, boolean dotcrawl) { //todo: stop running video filters while paused! //also move video filters into a worker thread because they //don't really depend on emulation state at all. Yes this is going to //cause more lag but it will hopefully get back up to playable speed with NTSC filter frametimes[frametimeptr] = nes.getFrameTime(); ++frametimeptr; frametimeptr %= frametimes.length; if (frametimeptr == 0) { long averageframes = 0; for (long l : frametimes) { averageframes += l; } averageframes /= frametimes.length; fps = 1E9 / averageframes; this.setTitle(String.format("HalfNES %s - %s, %2.2f fps" + ((frameskip > 0) ? " frameskip " + frameskip : ""), NES.VERSION, nes.getCurrentRomName(), fps)); } if (nes.framecount % (frameskip + 1) == 0) { frame = renderer.render(nextframe, bgcolors, dotcrawl); render(); } } @Override public final synchronized void render() { final Graphics graphics = buffer.getDrawGraphics(); if (smoothScale) { ((Graphics2D) graphics).setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR); } if (inFullScreen) { graphics.setColor(Color.BLACK); DisplayMode dm = gd.getDisplayMode(); int scrnheight = dm.getHeight(); int scrnwidth = dm.getWidth(); //don't ask why this needs to be done every frame, //but it does b/c the canvas keeps resizing itself canvas.setSize(scrnwidth, scrnheight); graphics.fillRect(0, 0, scrnwidth, scrnheight); if (PrefsSingleton.get().getBoolean("maintainAspect", true)) { double scalefactor = getmaxscale(scrnwidth, scrnheight); int height = (int) (NES_HEIGHT * scalefactor); int width = (int) (256 * scalefactor * 1.1666667); graphics.drawImage(frame, ((scrnwidth / 2) - (width / 2)), ((scrnheight / 2) - (height / 2)), width, height, null); } else { graphics.drawImage(frame, 0, 0, scrnwidth, scrnheight, null); } graphics.setColor(Color.DARK_GRAY); graphics.drawString(this.getTitle(), 16, 16); } else { graphics.drawImage(frame, 0, 0, NES_WIDTH * screenScaleFactor, NES_HEIGHT * screenScaleFactor, null); } graphics.dispose(); buffer.show(); } private void showOptions() { final PreferencesDialog dialog = new PreferencesDialog(this); dialog.setVisible(true); if (dialog.okClicked()) { setRenderOptions(); nes.setParameters(); } } private void showControlsDialog() { final ControlsDialog dialog = new ControlsDialog(this); dialog.setVisible(true); if (dialog.okClicked()) { padController1.setButtons(); padController2.setButtons(); } } private void showActionReplayDialog() { nes.pause(); final ActionReplay actionReplay = nes.getActionReplay(); if (actionReplay != null) { final ActionReplayGui dialog = new ActionReplayGui(this, false, actionReplay); dialog.setVisible(true); } else { JOptionPane.showMessageDialog(this, "You have to load a game first.", "No ROM", JOptionPane.ERROR_MESSAGE); } nes.resume(); } public void savewindowposition() { PrefsSingleton.get().putInt("windowX", this.getX()); PrefsSingleton.get().putInt("windowY", this.getY()); } private double getmaxscale(final int width, final int height) { return Math.min(height / (double) NES_HEIGHT, width / (double) NES_WIDTH); } @Override public void loadROMs(String path) { throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates. } public class AL implements ActionListener, WindowListener { @Override public void actionPerformed(final ActionEvent arg0) { // placeholder for more robust handler if (arg0.getActionCommand().equals("Quit")) { close(); } else if (arg0.getActionCommand().equals("Reset")) { nes.reset(); } else if (arg0.getActionCommand().equals("Hard Reset")) { nes.reloadROM(); } else if (arg0.getActionCommand().equals("Pause")) { nes.pause(); } else if (arg0.getActionCommand().equals("Resume")) { nes.resume(); } else if (arg0.getActionCommand().equals("Preferences")) { showOptions(); } else if (arg0.getActionCommand().equals("Fast Forward")) { nes.toggleFrameLimiter(); } else if (arg0.getActionCommand().equals("About")) { messageBox("HalfNES " + NES.VERSION + "\n" + "Get the latest version and report any bugs at https://github.com/andrew-hoffman/halfnes \n" + "\n" + "This program is free software licensed under the GPL version 3, and comes with \n" + "NO WARRANTY of any kind. (but if something's broken, please report it). \n" + "See the license.txt file for details."); } else if (arg0.getActionCommand().equals("ROM Info")) { String info = nes.getrominfo(); if (info != null) { messageBox(info); } } else if (arg0.getActionCommand().equals("Open ROM")) { loadROM(); } else if (arg0.getActionCommand().equals("Toggle Fullscreen")) { toggleFullScreen(); } else if (arg0.getActionCommand().equals("Frame Advance")) { nes.frameAdvance(); } else if (arg0.getActionCommand().equals("Escape")) { if (inFullScreen) { toggleFullScreen(); } else { close(); } } else if (arg0.getActionCommand().equals("Controller Settings")) { showControlsDialog(); } else if (arg0.getActionCommand().equals("Cheat Codes")) { showActionReplayDialog(); } } @Override public void windowOpened(WindowEvent e) { } @Override public void windowClosing(WindowEvent e) { close(); } private void close() { dispose(); savewindowposition(); padController1.stopEventQueue(); padController2.stopEventQueue(); nes.quit(); } @Override public void windowClosed(WindowEvent e) { //we don't care about these events } @Override public void windowIconified(WindowEvent e) { //but java wants us to implement something for all of them } @Override public void windowDeiconified(WindowEvent e) { //so we can use the interface. } @Override public void windowActivated(WindowEvent e) { } @Override public void windowDeactivated(WindowEvent e) { } } }