diff --git a/src/main/java/net/vhati/modmanager/FTLModManager.java b/src/main/java/net/vhati/modmanager/FTLModManager.java index cfdb0c1..0c1cb89 100644 --- a/src/main/java/net/vhati/modmanager/FTLModManager.java +++ b/src/main/java/net/vhati/modmanager/FTLModManager.java @@ -6,6 +6,7 @@ import java.io.InputStream; import java.io.InputStreamReader; import java.io.IOException; import java.util.Properties; +import javax.swing.JFileChooser; import javax.swing.JOptionPane; import javax.swing.LookAndFeel; import javax.swing.SwingUtilities; @@ -69,6 +70,7 @@ public class FTLModManager { Properties props = new Properties(); props.setProperty( SlipstreamConfig.ALLOW_ZIP, "false" ); props.setProperty( SlipstreamConfig.FTL_DATS_PATH, "" ); + props.setProperty( SlipstreamConfig.STEAM_EXE_PATH, "" ); props.setProperty( SlipstreamConfig.RUN_STEAM_FTL, "false" ); props.setProperty( SlipstreamConfig.NEVER_RUN_FTL, "false" ); props.setProperty( SlipstreamConfig.USE_DEFAULT_UI, "false" ); @@ -185,21 +187,90 @@ public class FTLModManager { throw new ExitException(); } + // Ask about Steam. + if ( appConfig.getProperty( SlipstreamConfig.STEAM_EXE_PATH, "" ).length() == 0 ) { + + int steamBasedResponse = JOptionPane.showConfirmDialog( null, "Was FTL installed via Steam?", "Confirm", JOptionPane.YES_NO_OPTION, JOptionPane.QUESTION_MESSAGE ); + if ( steamBasedResponse == JOptionPane.YES_OPTION ) { + File steamExeFile = FTLUtilities.findSteamExe(); + + if ( steamExeFile == null && System.getProperty( "os.name" ).startsWith( "Windows" ) ) { + try { + String registryExePath = FTLUtilities.queryRegistryKey( "HKCU\\Software\\Valve\\Steam", "SteamExe", "REG_SZ" ); + if ( registryExePath != null && !(steamExeFile=new File( registryExePath )).exists() ) { + steamExeFile = null; + } + } + catch( IOException e ) { + log.error( "Error while querying registry for Steam's path", e ); + } + } + + if ( steamExeFile != null ) { + int response = JOptionPane.showConfirmDialog( null, "Steam was found at:\n"+ steamExeFile.getPath() +"\nIs this correct?", "Confirm", JOptionPane.YES_NO_OPTION, JOptionPane.QUESTION_MESSAGE ); + if ( response == JOptionPane.NO_OPTION ) steamExeFile = null; + } + + if ( steamExeFile == null ) { + log.debug( "Steam was not located automatically. Prompting user for location" ); + + StringBuilder steamBuf = new StringBuilder(); + steamBuf.append( "You will be prompted to locate Steam's executable.\n" ); + steamBuf.append( "- Windows: Steam.exe\n" ); + steamBuf.append( "- Linux: steam\n" ); + steamBuf.append( "- OSX: Steam.app\n" ); + steamBuf.append( "\n" ); + steamBuf.append( "If you can't find it, you can cancel and set it later." ); + JOptionPane.showMessageDialog( null, steamBuf.toString(), "Find Steam", JOptionPane.INFORMATION_MESSAGE ); + + JFileChooser steamExeChooser = new JFileChooser(); + steamExeChooser.setDialogTitle( "Find Steam.exe or steam or Steam.app" ); + steamExeChooser.setFileHidingEnabled( false ); + steamExeChooser.setMultiSelectionEnabled( false ); + + if ( steamExeChooser.showOpenDialog( null ) == JFileChooser.APPROVE_OPTION ) { + steamExeFile = steamExeChooser.getSelectedFile(); + if ( !steamExeFile.exists() ) steamExeFile = null; + } + } + + if ( steamExeFile != null ) { + appConfig.setProperty( SlipstreamConfig.STEAM_EXE_PATH, steamExeFile.getAbsolutePath() ); + writeConfig = true; + log.info( "Steam located at: "+ steamExeFile.getAbsolutePath() ); + } + + if ( appConfig.getProperty( SlipstreamConfig.STEAM_EXE_PATH, "" ).length() > 0 ) { + + String[] launchOptions = new String[] {"Directly", "Steam"}; + int launchResponse = JOptionPane.showOptionDialog( null, "Would you prefer to launch FTL directly, or via Steam?", "How to Launch?", JOptionPane.DEFAULT_OPTION, JOptionPane.PLAIN_MESSAGE, null, launchOptions, launchOptions[1] ); + if ( launchResponse == 0 ) { + appConfig.setProperty( SlipstreamConfig.RUN_STEAM_FTL, "false" ); + writeConfig = true; + } + else if ( launchResponse == 1 ) { + appConfig.setProperty( SlipstreamConfig.RUN_STEAM_FTL, "true" ); + writeConfig = true; + } + } + } + else { + appConfig.setProperty( SlipstreamConfig.RUN_STEAM_FTL, "false" ); + writeConfig = true; + } + } + // Prompt if update_catalog is invalid or hasn't been set. boolean askAboutUpdates = false; - String catalogUpdateInterval = appConfig.getProperty( SlipstreamConfig.UPDATE_CATALOG ); - String appUpdateInterval = appConfig.getProperty( SlipstreamConfig.UPDATE_APP ); - - if ( catalogUpdateInterval == null || !catalogUpdateInterval.matches( "^\\d+$" ) ) + if ( !appConfig.getProperty( SlipstreamConfig.UPDATE_CATALOG, "" ).matches( "^\\d+$" ) ) askAboutUpdates = true; - if ( appUpdateInterval == null || !appUpdateInterval.matches( "^\\d+$" ) ) + if ( !appConfig.getProperty( SlipstreamConfig.UPDATE_APP, "" ).matches( "^\\d+$" ) ) askAboutUpdates = true; if ( askAboutUpdates ) { String message = ""; - message += "Would you like Slipstream to periodically\n"; - message += "check for updates and download descriptions\n"; - message += "for the latest mods?\n\n"; + message += "Would you like Slipstream to periodically check for updates?\n"; + message += "\n"; message += "You can change this later in modman.cfg."; int response = JOptionPane.showConfirmDialog( null, message, "Updates", JOptionPane.YES_NO_OPTION, JOptionPane.QUESTION_MESSAGE ); diff --git a/src/main/java/net/vhati/modmanager/core/FTLUtilities.java b/src/main/java/net/vhati/modmanager/core/FTLUtilities.java index 015921b..5e27655 100644 --- a/src/main/java/net/vhati/modmanager/core/FTLUtilities.java +++ b/src/main/java/net/vhati/modmanager/core/FTLUtilities.java @@ -1,10 +1,15 @@ package net.vhati.modmanager.core; import java.awt.Component; +import java.io.BufferedReader; import java.io.File; +import java.io.InputStream; +import java.io.InputStreamReader; import java.io.IOException; import java.util.ArrayList; import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import javax.swing.JFileChooser; import javax.swing.JOptionPane; import javax.swing.filechooser.FileFilter; @@ -89,7 +94,7 @@ public class FTLUtilities { candidates.add( new File( xdgDataHome +"/Steam/steamapps/common/FTL Faster Than Light/data/resources" ) ); candidates.add( new File( xdgDataHome +"/Steam/SteamApps/common/FTL Faster Than Light/data/resources" ) ); } - if ( home != null ) { + if ( home != null ) { // I think .steam/ contains symlinks to the paths above. candidates.add( new File( home +"/.steam/steam/steamapps/common/FTL Faster Than Light/data" ) ); candidates.add( new File( home +"/.steam/steam/SteamApps/common/FTL Faster Than Light/data" ) ); @@ -150,7 +155,7 @@ public class FTLUtilities { message += "Or select 'FTL.app', if you're on OSX."; JOptionPane.showMessageDialog( parentComponent, message, "Find FTL", JOptionPane.INFORMATION_MESSAGE ); - final JFileChooser fc = new JFileChooser(); + JFileChooser fc = new JFileChooser(); fc.setDialogTitle( "Find ftl.dat or data.dat or FTL.app" ); fc.setFileHidingEnabled( false ); fc.addChoosableFileFilter(new FileFilter() { @@ -264,7 +269,12 @@ public class FTLUtilities { * On Linux, "steam" is a script. ( http://moritzmolch.com/815 ) * On OSX, "Steam.app" is a bundle. * + * The definitive Windows registry will not be checked. + * Key,Name,Type: "HKCU\\Software\\Valve\\Steam", "SteamExe", "REG_SZ". + * * The args to launch FTL are: ["-applaunch", STEAM_APPID_FTL] + * + * @see #queryRegistryKey(String, String, String) */ public static File findSteamExe() { String programFiles86 = System.getenv( "ProgramFiles(x86)" ); @@ -301,6 +311,27 @@ public class FTLUtilities { return result; } + /** + * Tells Steam to "verify game cache". + * + * This will spawn a process to notify Steam and exit immediately. + * + * Steam will start, if not already running, and a popup with progress bar + * will appear. + * + * For FTL, this method amounts to running: + * Steam.exe "steam://validate/212680" + * + * Steam registers itself with the OS as a custom URI handler. The URI gets + * passed as an argument when a "steam://" address is visited. + */ + public static Process verifySteamGameCache( File exeFile, String appId ) throws IOException { + if ( appId == null || appId.length() == 0 ) throw new IllegalArgumentException( "No Steam APP_ID was provided" ); + + String[] exeArgs = new String[] {"steam://validate/"+ appId}; + return launchExe( exeFile, exeArgs ); + } + /** * Launches an executable. * @@ -382,4 +413,63 @@ public class FTLUtilities { return result; } + + /** + * Returns a value from the Windows registry, by scraping reg.exe, or null. + * + * This is equivalent to: reg.exe query {key} /v {valueName} /t {valueType} + * + * This view will not be jailed in Wow6432Node, even if Java is? + * Characters outside windows-1252 are unsupported (results will be mangled). + * + * Bad unicode example: "HKLM\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Console\\TrueTypeFont", "932", "REG_SZ". + * + * @param key a backslash path starting with HKLM, HKCU, HKCR, HKU, HKCC + * @param valueName a value name, or "" for the "(Default)" value + * @param valueType REG_SZ ("Abc"), REG_DWORD ("0x1"), REG_BINARY ("44E09C"), etc + */ + public static String queryRegistryKey( String key, String valueName, String valueType ) throws IOException { + if ( !System.getProperty( "os.name" ).startsWith( "Windows" ) ) return null; + if ( key == null || valueType == null || key.length() * valueType.length() == 0 ) { + throw new IllegalArgumentException( "key and valueType cannot be null or empty" ); + } + + BufferedReader r = null; + try { + String regExePath = "reg.exe"; + String winDir = System.getenv( "windir" ); + + if ( winDir != null && winDir.length() > 0 ) { + // When Java's in Wow64 redirection jail, sysnative is a virtual dir with the 64bit commands. + // I don't know if this will ever happen to Java. + File unWowRegExeFile = new File( winDir, "sysnative\\reg.exe" ); + if ( unWowRegExeFile.exists() ) regExePath = unWowRegExeFile.getAbsolutePath(); + } + + String[] steamRegArgs = new String[] {regExePath, "query", key, "/v", valueName, "/t", valueType}; + Pattern regPtn = Pattern.compile( Pattern.quote( (( valueName != null ) ? valueName : "(Default)") ) +"\\s+"+ Pattern.quote( valueType ) +"\\s+(.*)" ); + + Process p = new ProcessBuilder( steamRegArgs ).start(); + p.waitFor(); + if ( p.exitValue() == 0 ) { + r = new BufferedReader( new InputStreamReader( p.getInputStream(), "windows-1252" ) ); + Matcher m; + String line; + while ( (line=r.readLine()) != null ) { + if ( (m=regPtn.matcher( line )).find() ) { + return m.group( 1 ); + } + } + } + } + catch ( InterruptedException e ) { // *shrug* + Thread.currentThread().interrupt(); // Set interrupt flag. + } + finally { + try {if ( r != null ) r.close();} + catch ( IOException e ) {} + } + + return null; + } } diff --git a/src/main/java/net/vhati/modmanager/core/SlipstreamConfig.java b/src/main/java/net/vhati/modmanager/core/SlipstreamConfig.java index 72d0008..4230cf6 100644 --- a/src/main/java/net/vhati/modmanager/core/SlipstreamConfig.java +++ b/src/main/java/net/vhati/modmanager/core/SlipstreamConfig.java @@ -16,6 +16,7 @@ public class SlipstreamConfig { public static final String ALLOW_ZIP = "allow_zip"; public static final String FTL_DATS_PATH = "ftl_dats_path"; + public static final String STEAM_EXE_PATH = "steam_exe_path"; public static final String RUN_STEAM_FTL = "run_steam_ftl"; public static final String NEVER_RUN_FTL = "never_run_ftl"; public static final String UPDATE_CATALOG = "update_catalog"; @@ -80,6 +81,7 @@ public class SlipstreamConfig { userFieldsMap.put( ALLOW_ZIP, "Sets whether to treat .zip files as .ftl files. Default: false." ); userFieldsMap.put( FTL_DATS_PATH, "The path to FTL's resources folder. If invalid, you'll be prompted." ); + userFieldsMap.put( STEAM_EXE_PATH, "The path to Steam's executable, if FTL was installed via Steam." ); userFieldsMap.put( RUN_STEAM_FTL, "If true, SMM will use Steam to launch FTL, if possible. Default: false." ); userFieldsMap.put( NEVER_RUN_FTL, "If true, there will be no offer to run FTL after patching. Default: false." ); userFieldsMap.put( UPDATE_CATALOG, "If a number greater than 0, check for new mod descriptions every N days." ); diff --git a/src/main/java/net/vhati/modmanager/ui/ManagerFrame.java b/src/main/java/net/vhati/modmanager/ui/ManagerFrame.java index 46527d3..87f644f 100644 --- a/src/main/java/net/vhati/modmanager/ui/ManagerFrame.java +++ b/src/main/java/net/vhati/modmanager/ui/ManagerFrame.java @@ -193,7 +193,7 @@ public class ManagerFrame extends JFrame implements ActionListener, ModsScanObse modsFolderBtn = new JButton( "Open mods/" ); modsFolderBtn.setMargin( actionInsets ); - modsFolderBtn.addMouseListener( new StatusbarMouseListener( this, "Open the mods/ folder." ) ); + modsFolderBtn.addMouseListener( new StatusbarMouseListener( this, String.format( "Open the %s/ folder.", modsDir.getName() ) ) ); modsFolderBtn.addActionListener( this ); modsFolderBtn.setEnabled( Desktop.isDesktopSupported() ); modActionsPanel.add( modsFolderBtn ); @@ -258,7 +258,6 @@ public class ManagerFrame extends JFrame implements ActionListener, ModsScanObse @Override public void windowClosed( WindowEvent e ) { // dispose() was called. - ListState tableState = getCurrentModsTableState(); saveModsTableState( tableState ); @@ -667,25 +666,38 @@ public class ManagerFrame extends JFrame implements ActionListener, ModsScanObse ModPatchDialog patchDlg = new ModPatchDialog( this, true ); - String neverRunFtl = appConfig.getProperty( SlipstreamConfig.NEVER_RUN_FTL, "false" ); - if ( !neverRunFtl.equals( "true" ) ) { + // Offer to run FTL. + if ( !"true".equals( appConfig.getProperty( SlipstreamConfig.NEVER_RUN_FTL, "false" ) ) ) { File exeFile = null; String[] exeArgs = null; - if ( appConfig.getProperty( SlipstreamConfig.RUN_STEAM_FTL, "false" ).equals( "true" ) ) { - exeFile = FTLUtilities.findSteamExe(); - exeArgs = new String[] {"-applaunch", FTLUtilities.STEAM_APPID_FTL}; + // Try to run via Steam. + if ( "true".equals( appConfig.getProperty( SlipstreamConfig.RUN_STEAM_FTL, "false" ) ) ) { + + String steamPath = appConfig.getProperty( SlipstreamConfig.STEAM_EXE_PATH ); + if ( steamPath.length() > 0 ) { + exeFile = new File( steamPath ); + + if ( exeFile.exists() ) { + exeArgs = new String[] {"-applaunch", FTLUtilities.STEAM_APPID_FTL}; + } + else { + log.warn( String.format( "%s does not exist: %s", SlipstreamConfig.STEAM_EXE_PATH, exeFile.getAbsolutePath() ) ); + exeFile = null; + } + } if ( exeFile == null ) { - log.warn( "Steam executable could not be found; FTL will be launched directly" ); + log.warn( "Steam executable could not be found, so FTL will be launched directly" ); } } // Try to run directly. if ( exeFile == null ) { exeFile = FTLUtilities.findGameExe( datsDir ); - exeArgs = new String[0]; - if ( exeFile == null ) { + if ( exeFile != null ) { + exeArgs = new String[0]; + } else { log.warn( "FTL executable could not be found" ); } } @@ -743,7 +755,7 @@ public class ManagerFrame extends JFrame implements ActionListener, ModsScanObse if ( Desktop.isDesktopSupported() ) { Desktop.getDesktop().open( modsDir.getCanonicalFile() ); } else { - log.error( "Opening the mods/ folder is not possible on your OS" ); + log.error( String.format( "Java cannot open the %s/ folder for you on this OS", modsDir.getName() ) ); } } catch ( IOException f ) { diff --git a/src/main/java/net/vhati/modmanager/ui/SlipstreamConfigDialog.java b/src/main/java/net/vhati/modmanager/ui/SlipstreamConfigDialog.java index 228b1a4..4c6f7b5 100644 --- a/src/main/java/net/vhati/modmanager/ui/SlipstreamConfigDialog.java +++ b/src/main/java/net/vhati/modmanager/ui/SlipstreamConfigDialog.java @@ -11,6 +11,7 @@ import javax.swing.Box; import javax.swing.BoxLayout; import javax.swing.JButton; import javax.swing.JCheckBox; +import javax.swing.JFileChooser; import javax.swing.JFrame; import javax.swing.JLabel; import javax.swing.JPanel; @@ -36,6 +37,7 @@ public class SlipstreamConfigDialog extends JFrame implements ActionListener { protected static final String UPDATE_CATALOG = SlipstreamConfig.UPDATE_CATALOG; protected static final String UPDATE_APP = SlipstreamConfig.UPDATE_APP; protected static final String FTL_DATS_PATH = SlipstreamConfig.FTL_DATS_PATH; + protected static final String STEAM_EXE_PATH = SlipstreamConfig.STEAM_EXE_PATH; protected SlipstreamConfig appConfig; @@ -76,16 +78,19 @@ public class SlipstreamConfigDialog extends JFrame implements ActionListener { editorPanel.addRow( FTL_DATS_PATH, ContentType.CHOOSER ); editorPanel.addTextRow( "Path to FTL's resources folder." ); editorPanel.addSeparatorRow(); + editorPanel.addRow( STEAM_EXE_PATH, ContentType.CHOOSER ); + editorPanel.addTextRow( "Path to Steam's executable." ); + editorPanel.addSeparatorRow(); editorPanel.addBlankRow(); editorPanel.addTextRow( "Note: Some changes may have no immediate effect." ); editorPanel.addBlankRow(); editorPanel.addFillRow(); - editorPanel.getBoolean( ALLOW_ZIP ).setSelected( appConfig.getProperty( SlipstreamConfig.ALLOW_ZIP, "false" ).equals( "true" ) ); - editorPanel.getBoolean( RUN_STEAM_FTL ).setSelected( appConfig.getProperty( SlipstreamConfig.RUN_STEAM_FTL, "false" ).equals( "true" ) ); - editorPanel.getBoolean( NEVER_RUN_FTL ).setSelected( appConfig.getProperty( SlipstreamConfig.NEVER_RUN_FTL, "false" ).equals( "true" ) ); - editorPanel.getBoolean( USE_DEFAULT_UI ).setSelected( appConfig.getProperty( SlipstreamConfig.USE_DEFAULT_UI, "false" ).equals( "true" ) ); - editorPanel.getBoolean( REMEMBER_GEOMETRY ).setSelected( appConfig.getProperty( SlipstreamConfig.REMEMBER_GEOMETRY, "true" ).equals( "true" ) ); + editorPanel.getBoolean( ALLOW_ZIP ).setSelected( "true".equals( appConfig.getProperty( SlipstreamConfig.ALLOW_ZIP, "false" ) ) ); + editorPanel.getBoolean( RUN_STEAM_FTL ).setSelected( "true".equals( appConfig.getProperty( SlipstreamConfig.RUN_STEAM_FTL, "false" ) ) ); + editorPanel.getBoolean( NEVER_RUN_FTL ).setSelected( "true".equals( appConfig.getProperty( SlipstreamConfig.NEVER_RUN_FTL, "false" ) ) ); + editorPanel.getBoolean( USE_DEFAULT_UI ).setSelected( "true".equals( appConfig.getProperty( SlipstreamConfig.USE_DEFAULT_UI, "false" ) ) ); + editorPanel.getBoolean( REMEMBER_GEOMETRY ).setSelected( "true".equals( appConfig.getProperty( SlipstreamConfig.REMEMBER_GEOMETRY, "true" ) ) ); editorPanel.getInt( UPDATE_CATALOG ).setText( Integer.toString( appConfig.getPropertyAsInt( SlipstreamConfig.UPDATE_CATALOG, 0 ) ) ); editorPanel.getInt( UPDATE_APP ).setText( Integer.toString( appConfig.getPropertyAsInt( SlipstreamConfig.UPDATE_APP, 0 ) ) ); @@ -94,6 +99,11 @@ public class SlipstreamConfigDialog extends JFrame implements ActionListener { ftlDatsPathField.setPreferredSize( new Dimension( 150, ftlDatsPathField.getPreferredSize().height ) ); editorPanel.getChooser( FTL_DATS_PATH ).getButton().addActionListener( this ); + JTextField steamExePathField = editorPanel.getChooser( STEAM_EXE_PATH ).getTextField(); + steamExePathField.setText( appConfig.getProperty( SlipstreamConfig.STEAM_EXE_PATH, "" ) ); + steamExePathField.setPreferredSize( new Dimension( 150, steamExePathField.getPreferredSize().height ) ); + editorPanel.getChooser( STEAM_EXE_PATH ).getButton().addActionListener( this ); + JPanel ctrlPanel = new JPanel(); ctrlPanel.setLayout( new BoxLayout( ctrlPanel, BoxLayout.X_AXIS ) ); ctrlPanel.setBorder( BorderFactory.createEmptyBorder( 10, 0, 10, 0 ) ); @@ -165,6 +175,11 @@ public class SlipstreamConfigDialog extends JFrame implements ActionListener { appConfig.setProperty( SlipstreamConfig.FTL_DATS_PATH, tmp ); } + tmp = editorPanel.getChooser( STEAM_EXE_PATH ).getTextField().getText(); + if ( tmp.length() > 0 && new File( tmp ).exists() ) { + appConfig.setProperty( SlipstreamConfig.STEAM_EXE_PATH, tmp ); + } + this.setVisible( false ); this.dispose(); } @@ -174,5 +189,23 @@ public class SlipstreamConfigDialog extends JFrame implements ActionListener { editorPanel.getChooser( FTL_DATS_PATH ).getTextField().setText( datsDir.getAbsolutePath() ); } } + else if ( source == editorPanel.getChooser( STEAM_EXE_PATH ).getButton() ) { + String currentPath = editorPanel.getChooser( STEAM_EXE_PATH ).getTextField().getText(); + + JFileChooser steamExeChooser = new JFileChooser(); + steamExeChooser.setDialogTitle( "Find Steam.exe or steam or Steam.app" ); + steamExeChooser.setFileHidingEnabled( false ); + steamExeChooser.setMultiSelectionEnabled( false ); + if ( currentPath.length() > 0 ) { + steamExeChooser.setCurrentDirectory( new File( currentPath ) ); + } + + if ( steamExeChooser.showOpenDialog( null ) == JFileChooser.APPROVE_OPTION ) { + File steamExeFile = steamExeChooser.getSelectedFile(); + if ( steamExeFile.exists() ) { + editorPanel.getChooser( STEAM_EXE_PATH ).getTextField().setText( steamExeFile.getAbsolutePath() ); + } + } + } } }