├── Icons ├── Icon.icns ├── Icon.ico └── Icon Readme.txt ├── IndieLauncher ├── Ionic.Zip.dll ├── EmbeddedGame.zip ├── Resources │ └── Languages │ │ ├── debug.lang │ │ └── en.lang ├── Launcher │ ├── Util │ │ ├── ICancellable.cs │ │ ├── IOCancelledException.cs │ │ ├── ProgramArguments.cs │ │ ├── EmbeddedAssembly.cs │ │ ├── ProgressStream.cs │ │ ├── Language.cs │ │ └── KeyValuePairs.cs │ ├── Main │ │ ├── Platform.cs │ │ ├── GameUpdatePrompt.cs │ │ ├── GameUpdateStage.cs │ │ ├── Logger.cs │ │ ├── GameLauncher.cs │ │ ├── Program.cs │ │ ├── Installer.cs │ │ └── GameUpdater.cs │ ├── RSS │ │ ├── RSSEntry.cs │ │ ├── RSSChannel.cs │ │ └── RSSFile.cs │ └── Interface │ │ ├── GTK │ │ ├── GameListWindow.cs │ │ ├── GTKInterface.cs │ │ ├── CredentialsDialog.cs │ │ └── UpdateWindow.cs │ │ └── WinForms │ │ ├── WinFormsInterface.cs │ │ ├── CredentialsForm.cs │ │ └── UpdateForm.cs ├── EmbeddedGame.txt ├── Properties │ └── AssemblyInfo.cs └── IndieLauncher.csproj ├── Screenshots ├── Screenshot1.png ├── Screenshot2.png └── Screenshot3.png ├── .gitmodules ├── UpdateLoc.sh ├── LICENSE ├── IndieLauncher.sln ├── Deploy.sh ├── .gitignore └── README.md /Icons/Icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dan200/IndieLauncher/HEAD/Icons/Icon.icns -------------------------------------------------------------------------------- /Icons/Icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dan200/IndieLauncher/HEAD/Icons/Icon.ico -------------------------------------------------------------------------------- /IndieLauncher/Ionic.Zip.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dan200/IndieLauncher/HEAD/IndieLauncher/Ionic.Zip.dll -------------------------------------------------------------------------------- /Screenshots/Screenshot1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dan200/IndieLauncher/HEAD/Screenshots/Screenshot1.png -------------------------------------------------------------------------------- /Screenshots/Screenshot2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dan200/IndieLauncher/HEAD/Screenshots/Screenshot2.png -------------------------------------------------------------------------------- /Screenshots/Screenshot3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dan200/IndieLauncher/HEAD/Screenshots/Screenshot3.png -------------------------------------------------------------------------------- /IndieLauncher/EmbeddedGame.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dan200/IndieLauncher/HEAD/IndieLauncher/EmbeddedGame.zip -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "LanguageExport"] 2 | path = LanguageExport 3 | url = https://github.com/dan200/LanguageExport 4 | -------------------------------------------------------------------------------- /IndieLauncher/Resources/Languages/debug.lang: -------------------------------------------------------------------------------- 1 | // debug translations (intentionally left empty) 2 | meta.english_language_name=Debug 3 | meta.native_language_name=Debug 4 | -------------------------------------------------------------------------------- /Icons/Icon Readme.txt: -------------------------------------------------------------------------------- 1 | These placeholder icons provided by icons8.com. See icons8.com/license for details. 2 | It is recommended to replace these icons with ones better representing your game when creating your launcher! 3 | -------------------------------------------------------------------------------- /IndieLauncher/Launcher/Util/ICancellable.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Dan200.Launcher.Util 4 | { 5 | public interface ICancellable 6 | { 7 | bool Cancelled { get; } 8 | void Cancel(); 9 | } 10 | } 11 | 12 | -------------------------------------------------------------------------------- /IndieLauncher/Launcher/Main/Platform.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Dan200.Launcher.Main 4 | { 5 | public enum Platform 6 | { 7 | Windows, 8 | OSX, 9 | Linux, 10 | Unknown, 11 | } 12 | } 13 | 14 | -------------------------------------------------------------------------------- /UpdateLoc.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | mono \ 4 | LanguageExport/Precompiled/LanguageExport.exe \ 5 | https://docs.google.com/spreadsheets/d/18FjL2zScJZNa1Eqgl3jAjQh_4YB6PZI0JzvbKV4DHUc/edit?usp=sharing \ 6 | IndieLauncher/Resources/Languages \ 7 | en \ 8 | > /dev/null 9 | -------------------------------------------------------------------------------- /IndieLauncher/Launcher/Util/IOCancelledException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | 4 | namespace Dan200.Launcher.Util 5 | { 6 | public class IOCancelledException : IOException 7 | { 8 | public IOCancelledException() 9 | { 10 | } 11 | } 12 | } 13 | 14 | -------------------------------------------------------------------------------- /IndieLauncher/Launcher/RSS/RSSEntry.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Dan200.Launcher.RSS 4 | { 5 | public class RSSEntry 6 | { 7 | public string Title; 8 | public string Description; 9 | public string Link; 10 | 11 | public RSSEntry() 12 | { 13 | } 14 | } 15 | } 16 | 17 | -------------------------------------------------------------------------------- /IndieLauncher/Launcher/RSS/RSSChannel.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace Dan200.Launcher.RSS 5 | { 6 | public class RSSChannel 7 | { 8 | public string Title; 9 | public string Description; 10 | public string Link; 11 | public readonly IList Entries; 12 | 13 | public RSSChannel() 14 | { 15 | Entries = new List(); 16 | } 17 | } 18 | } 19 | 20 | -------------------------------------------------------------------------------- /IndieLauncher/Launcher/Util/ProgramArguments.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace Dan200.Launcher.Util 5 | { 6 | public class ProgramArguments : KeyValuePairs 7 | { 8 | public ProgramArguments( string[] args ) 9 | { 10 | string lastOption = null; 11 | foreach( string arg in args ) 12 | { 13 | if( arg.StartsWith( "-" ) ) 14 | { 15 | if( lastOption != null ) 16 | { 17 | Set( lastOption, true ); 18 | } 19 | lastOption = arg.Substring( 1 ); 20 | } 21 | else if( lastOption != null ) 22 | { 23 | Set( lastOption, arg ); 24 | lastOption = null; 25 | } 26 | } 27 | if( lastOption != null ) 28 | { 29 | Set( lastOption, true ); 30 | } 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /IndieLauncher/Resources/Languages/en.lang: -------------------------------------------------------------------------------- 1 | // en translations 2 | meta.english_language_name=English 3 | meta.native_language_name=English 4 | meta.translator_name=Daniel Ratcliffe 5 | meta.translator_twitter=@DanTwoHundred 6 | meta.parent_language=debug 7 | window.title={0} Launcher 8 | status.checking=Checking for Updates 9 | status.extracting=Extracting Update 10 | status.downloading=Downloading Update 11 | status.installing=Installing Update 12 | status.launching=Launching Game 13 | status.complete=Update Complete 14 | status.cancelled=Update Cancelled 15 | status.failed=Update Failed 16 | status.unknown=Please Wait 17 | prompt.download_new_version=A new version of {0} is available, would you like to download it? 18 | prompt.launch_old_version={0} failed to update, would you like to run the previous version? 19 | button.yes=Yes 20 | button.no=No 21 | button.ok=OK 22 | button.cancel=Cancel 23 | label.username=User Name: 24 | label.password=Password: 25 | prompt.credentials=Login Required 26 | -------------------------------------------------------------------------------- /IndieLauncher/EmbeddedGame.txt: -------------------------------------------------------------------------------- 1 | // Details of the embedded game to install or download when running this launcher. 2 | // Field explanations: 3 | // game: Required. The name of the game to launch, this should not contain spaces. 4 | // version: Optional. The version of the game embedded as EmbeddedGame.zip, omit this if no game is embedded. 5 | // url: Optional. The URL of the feed to check for updates, omit this if you've embedded a game and don't want to provide updates to it. 6 | game=ExampleGame 7 | version=1.0 8 | url=https://raw.githubusercontent.com/dan200/IndieLauncherExampleGame/master/ExampleGame.xml 9 | 10 | // Authentication fields: 11 | // If the web server hosting the game downloads returns HTTP error 401 (Unauthorized), IndieLauncher 12 | // will prompt the user to enter a username and password to connect with. 13 | // The fields here allow you to hardcode one or both of these values, so the user will only have to enter 14 | // the value not specified. If both fields are specified, these will be used every time and the user will never be 15 | // prompted. Uncomment these fields to use them. 16 | //username=guest 17 | //password=example -------------------------------------------------------------------------------- /IndieLauncher/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | 4 | // Information about this assembly is defined by the following attributes. 5 | // Change them to the values specific to your project. 6 | 7 | [assembly: AssemblyTitle ("IndieLauncher")] 8 | [assembly: AssemblyDescription ("")] 9 | [assembly: AssemblyConfiguration ("")] 10 | [assembly: AssemblyCompany ("")] 11 | [assembly: AssemblyProduct ("")] 12 | [assembly: AssemblyCopyright ("Daniel Ratcliffe")] 13 | [assembly: AssemblyTrademark ("")] 14 | [assembly: AssemblyCulture ("")] 15 | 16 | // The assembly version has the format "{Major}.{Minor}.{Build}.{Revision}". 17 | // The form "{Major}.{Minor}.*" will automatically update the build and revision, 18 | // and "{Major}.{Minor}.{Build}.*" will update just the revision. 19 | 20 | [assembly: AssemblyVersion ("1.0.*")] 21 | 22 | // The following attributes are used to specify the signing key for the assembly, 23 | // if desired. See the Mono documentation for more information about signing. 24 | 25 | //[assembly: AssemblyDelaySign(false)] 26 | //[assembly: AssemblyKeyFile("")] 27 | 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Daniel Ratcliffe 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /IndieLauncher/Launcher/Main/GameUpdatePrompt.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Dan200.Launcher.Util; 3 | 4 | namespace Dan200.Launcher.Main 5 | { 6 | public enum GameUpdatePrompt 7 | { 8 | None, 9 | DownloadNewVersion, 10 | LaunchOldVersion, 11 | Username, 12 | Password, 13 | UsernameAndPassword, 14 | CustomMessage, 15 | } 16 | 17 | public static class GameUpdatePromptExtensions 18 | { 19 | public static string GetQuestion( this GameUpdatePrompt prompt, Language lang, string gameTitle ) 20 | { 21 | switch( prompt ) 22 | { 23 | case GameUpdatePrompt.None: 24 | { 25 | return ""; 26 | } 27 | case GameUpdatePrompt.DownloadNewVersion: 28 | { 29 | return lang.Translate( "prompt.download_new_version", gameTitle ); 30 | } 31 | case GameUpdatePrompt.LaunchOldVersion: 32 | { 33 | return lang.Translate( "prompt.launch_old_version", gameTitle ); 34 | } 35 | default: 36 | { 37 | return ""; 38 | } 39 | } 40 | } 41 | } 42 | } 43 | 44 | -------------------------------------------------------------------------------- /IndieLauncher/Launcher/Util/EmbeddedAssembly.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | using System.IO; 5 | using System.Reflection; 6 | using System.Security.Cryptography; 7 | 8 | namespace Dan200.Launcher.Util 9 | { 10 | public class EmbeddedAssembly 11 | { 12 | static Dictionary s_assemblies = new Dictionary(); 13 | 14 | public static void Load( string resourceName ) 15 | { 16 | var currentAssembly = Assembly.GetExecutingAssembly(); 17 | using( var stream = currentAssembly.GetManifestResourceStream( resourceName ) ) 18 | { 19 | // Read the resource into bytes 20 | byte[] bytes = new byte[ (int)stream.Length ]; 21 | int pos = 0; 22 | while( pos < bytes.Length ) 23 | { 24 | pos += stream.Read( bytes, pos, bytes.Length - pos ); 25 | } 26 | 27 | // Load the assembly from the bytes 28 | var assembly = Assembly.Load( bytes ); 29 | s_assemblies.Add( assembly.FullName, assembly ); 30 | } 31 | } 32 | 33 | public static Assembly Get( string assemblyFullName ) 34 | { 35 | if( s_assemblies.ContainsKey( assemblyFullName ) ) 36 | { 37 | return s_assemblies[ assemblyFullName ]; 38 | } 39 | return null; 40 | } 41 | } 42 | } -------------------------------------------------------------------------------- /IndieLauncher.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 2012 4 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "IndieLauncher", "IndieLauncher\IndieLauncher.csproj", "{2DC8EA72-2DD1-4AC2-ACC3-9E6F28313594}" 5 | EndProject 6 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ExampleGame", "ExampleGame", "{7508A77D-A9B8-472C-A31A-A54EBA731953}" 7 | ProjectSection(SolutionItems) = preProject 8 | ExampleGame\ExampleGame.xml = ExampleGame\ExampleGame.xml 9 | EndProjectSection 10 | EndProject 11 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{56B7CE84-DCA3-4028-A00C-FC752047AEDC}" 12 | ProjectSection(SolutionItems) = preProject 13 | Deploy.sh = Deploy.sh 14 | UpdateLoc.sh = UpdateLoc.sh 15 | EndProjectSection 16 | EndProject 17 | Global 18 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 19 | Debug|x86 = Debug|x86 20 | Release|x86 = Release|x86 21 | EndGlobalSection 22 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 23 | {2DC8EA72-2DD1-4AC2-ACC3-9E6F28313594}.Debug|x86.ActiveCfg = Debug|x86 24 | {2DC8EA72-2DD1-4AC2-ACC3-9E6F28313594}.Debug|x86.Build.0 = Debug|x86 25 | {2DC8EA72-2DD1-4AC2-ACC3-9E6F28313594}.Release|x86.ActiveCfg = Release|x86 26 | {2DC8EA72-2DD1-4AC2-ACC3-9E6F28313594}.Release|x86.Build.0 = Release|x86 27 | EndGlobalSection 28 | GlobalSection(NestedProjects) = preSolution 29 | EndGlobalSection 30 | GlobalSection(MonoDevelopProperties) = preSolution 31 | StartupItem = IndieLauncher\IndieLauncher.csproj 32 | Policies = $0 33 | $0.VersionControlPolicy = $1 34 | $1.inheritsSet = Mono 35 | EndGlobalSection 36 | EndGlobal 37 | -------------------------------------------------------------------------------- /IndieLauncher/Launcher/Interface/GTK/GameListWindow.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Gtk; 3 | using Dan200.Launcher.Main; 4 | 5 | namespace Dan200.Launcher.Interface.GTK 6 | { 7 | public class GameListWindow : Window 8 | { 9 | public GameListWindow() : base( WindowType.Toplevel ) 10 | { 11 | Build(); 12 | } 13 | 14 | private void OnShown( object sender, EventArgs e ) 15 | { 16 | } 17 | 18 | private void OnDeleteEvent( object sender, DeleteEventArgs a ) 19 | { 20 | Application.Quit(); 21 | } 22 | 23 | private void Build() 24 | { 25 | this.Title = "Select Game"; 26 | this.BorderWidth = 6; 27 | this.WindowPosition = WindowPosition.Center; 28 | this.TypeHint = Gdk.WindowTypeHint.Dialog; 29 | 30 | var vbox = new VBox( false, 4 ); 31 | 32 | //var label = new Label (); 33 | //label.Text = "Checking for updates..."; 34 | //vbox.PackStart (label, false, false, 0 ); 35 | 36 | var combo = new ComboBox( Installer.GetInstalledGames() ); 37 | combo.Active = 0; 38 | vbox.PackStart( combo, false, false, 0 ); 39 | 40 | var button = new Button (); 41 | button.Label = "Launch"; 42 | button.Clicked += delegate( object sender, EventArgs args ) 43 | { 44 | this.HideAll(); 45 | 46 | var updateWindow = new UpdateWindow( combo.ActiveText, null, null ); 47 | updateWindow.ShowAll(); 48 | }; 49 | vbox.PackStart( button, false, false, 0 ); 50 | 51 | this.Add( vbox ); 52 | 53 | this.SetDefaultSize( 300, 100 ); 54 | this.SetSizeRequest( 300, -1 ); 55 | this.Resizable = false; 56 | 57 | this.Shown += OnShown; 58 | this.DeleteEvent += OnDeleteEvent; 59 | } 60 | } 61 | } 62 | 63 | -------------------------------------------------------------------------------- /IndieLauncher/Launcher/Main/GameUpdateStage.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Dan200.Launcher.Util; 3 | 4 | namespace Dan200.Launcher.Main 5 | { 6 | public enum GameUpdateStage 7 | { 8 | NotStarted, 9 | CheckingForUpdate, 10 | ExtractingUpdate, 11 | DownloadingUpdate, 12 | InstallingUpdate, 13 | LaunchingGame, 14 | Finished, 15 | Cancelled, 16 | Failed 17 | } 18 | 19 | public static class GameUpdateStageExtensions 20 | { 21 | public static string GetStatus( this GameUpdateStage stage, Language lang ) 22 | { 23 | switch( stage ) 24 | { 25 | case GameUpdateStage.NotStarted: 26 | { 27 | return ""; 28 | } 29 | case GameUpdateStage.CheckingForUpdate: 30 | { 31 | return lang.Translate( "status.checking" ); 32 | } 33 | case GameUpdateStage.ExtractingUpdate: 34 | { 35 | return lang.Translate( "status.extracting" ); 36 | } 37 | case GameUpdateStage.DownloadingUpdate: 38 | { 39 | return lang.Translate( "status.downloading" ); 40 | } 41 | case GameUpdateStage.InstallingUpdate: 42 | { 43 | return lang.Translate( "status.installing" ); 44 | } 45 | case GameUpdateStage.LaunchingGame: 46 | { 47 | return lang.Translate( "status.launching" ); 48 | } 49 | case GameUpdateStage.Finished: 50 | { 51 | return lang.Translate( "status.complete" ); 52 | } 53 | case GameUpdateStage.Cancelled: 54 | { 55 | return lang.Translate( "status.cancelled" ); 56 | } 57 | case GameUpdateStage.Failed: 58 | { 59 | return lang.Translate( "status.failed" ); 60 | } 61 | default: 62 | { 63 | return lang.Translate( "status.unknown" ); 64 | } 65 | } 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /IndieLauncher/Launcher/Interface/WinForms/WinFormsInterface.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Windows.Forms; 3 | using Dan200.Launcher.Main; 4 | 5 | namespace Dan200.Launcher.Interface.WinForms 6 | { 7 | public class WinFormsInterface 8 | { 9 | public static void Run() 10 | { 11 | // Init 12 | Application.EnableVisualStyles(); 13 | 14 | // Determine which game and which version to run 15 | string gameTitle, gameVersion, updateURL; 16 | string embeddedGameTitle, embeddedGameVersion, embeddedGameURL, embeddedUsername, embeddedPassword; 17 | if( Installer.GetEmbeddedGameInfo( out embeddedGameTitle, out embeddedGameVersion, out embeddedGameURL, out embeddedUsername, out embeddedPassword ) ) 18 | { 19 | // Run game from embedded game info 20 | gameTitle = embeddedGameTitle; 21 | gameVersion = Program.Arguments.GetString( "version" ); 22 | updateURL = embeddedGameURL; 23 | } 24 | else 25 | { 26 | // Run game from command line 27 | gameTitle = Program.Arguments.GetString( "game" ); 28 | gameVersion = Program.Arguments.GetString( "version" ); 29 | updateURL = null; 30 | } 31 | 32 | // Show the appropriate window 33 | if( gameTitle != null ) 34 | { 35 | ShowUpdateWindow( gameTitle, gameVersion, updateURL ); 36 | } 37 | else 38 | { 39 | ShowErrorWindow( "No game specified. Exiting." ); 40 | } 41 | } 42 | 43 | 44 | public static void ShowUpdateWindow( string gameTitle, string optionalGameVersion, string optionalUpdateURL ) 45 | { 46 | var form = new UpdateForm( gameTitle, optionalGameVersion, optionalUpdateURL ); 47 | form.ShowDialog(); 48 | } 49 | 50 | public static void ShowErrorWindow( string errorMessage, Form parentForm=null ) 51 | { 52 | MessageBox.Show( 53 | parentForm, 54 | errorMessage, 55 | "IndieLauncher", 56 | MessageBoxButtons.OK, 57 | MessageBoxIcon.Error 58 | ); 59 | } 60 | } 61 | } 62 | 63 | -------------------------------------------------------------------------------- /Deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Settings 4 | GAME_TITLE=ExampleGame 5 | GAME_LONG_TITLE="Example Game" 6 | 7 | # Common 8 | rm -rf Deploy 9 | mkdir Deploy 10 | mkdir Deploy/Common 11 | if [ -f IndieLauncher/bin/Release/IndieLauncher.exe ] 12 | then 13 | cp IndieLauncher/bin/Release/IndieLauncher.exe Deploy/Common/${GAME_TITLE}.exe 14 | else 15 | echo "IndieLauncher.exe not found. Build the solution in Release mode first." 16 | exit 17 | fi 18 | 19 | ########### 20 | # WINDOWS # 21 | ########### 22 | echo "Creating Windows Deployment" 23 | rm -rf Deploy/Windows 24 | rm -rf Deploy/${GAME_TITLE}_Windows.zip 25 | mkdir -p Deploy/Windows 26 | cd Deploy/Windows 27 | cp ../Common/${GAME_TITLE}.exe "${GAME_LONG_TITLE} Launcher.exe" 28 | zip -rq ../${GAME_TITLE}_Windows.zip . 29 | cd ../.. 30 | 31 | ####### 32 | # OSX # 33 | ####### 34 | echo "Creating OSX Deployment" 35 | rm -rf Deploy/OSX 36 | rm -rf Deploy/${GAME_TITLE}_OSX.zip 37 | mkdir -p Deploy/OSX 38 | cd Deploy/OSX 39 | macpack -n "${GAME_TITLE}Launcher" -i ../../Icons/Icon.icns -m cocoa ../Common/${GAME_TITLE}.exe 40 | cat "${GAME_TITLE}Launcher.app/Contents/MacOS/${GAME_TITLE}Launcher" | head -n 9 > temp 41 | echo "# Make GTK# work" >> temp 42 | echo "export DYLD_FALLBACK_LIBRARY_PATH=\"/Library/Frameworks/Mono.framework/Versions/Current/lib:\${DYLD_FALLBACK_LIBRARY_PATH}:/usr/lib\"" >> temp 43 | echo "" >> temp 44 | cat "${GAME_TITLE}Launcher.app/Contents/MacOS/${GAME_TITLE}Launcher" | tail -n +10 >> temp 45 | cat temp > "${GAME_TITLE}Launcher.app/Contents/MacOS/${GAME_TITLE}Launcher" 46 | rm -f temp 47 | mv "${GAME_TITLE}Launcher.app" "${GAME_LONG_TITLE} Launcher.app" 48 | zip -rq ../${GAME_TITLE}_OSX.zip . 49 | cd ../.. 50 | 51 | ######### 52 | # LINUX # 53 | ######### 54 | echo "Creating Linux Deployment" 55 | rm -rf Deploy/Linux 56 | rm -rf Deploy/${GAME_TITLE}_Linux.zip 57 | mkdir -p Deploy/Linux 58 | 59 | # Add header 60 | cat > Deploy/Linux/${GAME_TITLE}Launcher.sh << END 61 | #!/bin/sh 62 | if [ ! -f "${GAME_TITLE}Launcher.exe" ] 63 | then 64 | echo "Unpacking ${GAME_LONG_TITLE} Launcher..." 65 | tail -n +20 \$0 | uudecode 66 | fi 67 | if command -v mono > /dev/null 68 | then 69 | if [ \`uname\`=="Darwin" ] 70 | then 71 | echo "Fixing Mono library paths..." 72 | export DYLD_FALLBACK_LIBRARY_PATH="/Library/Frameworks/Mono.framework/Versions/Current/lib:\${DYLD_FALLBACK_LIBRARY_PATH}:/usr/lib" 73 | fi 74 | echo "Starting ${GAME_LONG_TITLE} Launcher..." 75 | mono ${GAME_TITLE}Launcher.exe 76 | else 77 | echo "${GAME_LONG_TITLE} requires Mono. Get it at http://www.mono-project.org" 78 | fi 79 | exit 80 | END 81 | 82 | # Add payload 83 | uuencode -m Deploy/Common/${GAME_TITLE}.exe ${GAME_TITLE}Launcher.exe >> Deploy/Linux/${GAME_TITLE}Launcher.sh 84 | 85 | # Zip 86 | cd Deploy/Linux 87 | zip -rq ../${GAME_TITLE}_Linux.zip . 88 | cd ../.. 89 | 90 | -------------------------------------------------------------------------------- /IndieLauncher/Launcher/Interface/GTK/GTKInterface.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Gtk; 3 | using Dan200.Launcher.Main; 4 | 5 | namespace Dan200.Launcher.Interface.GTK 6 | { 7 | public class GTKInterface 8 | { 9 | public static void Run() 10 | { 11 | // Init framework 12 | Application.Init(); 13 | 14 | // Determine which game and which version to run 15 | string gameTitle, gameVersion, updateURL; 16 | string embeddedGameTitle, embeddedGameVersion, embeddedGameURL, embeddedUsername, embeddedPassword; 17 | if( Installer.GetEmbeddedGameInfo( out embeddedGameTitle, out embeddedGameVersion, out embeddedGameURL, out embeddedUsername, out embeddedPassword ) ) 18 | { 19 | // Run game from embedded game info 20 | gameTitle = embeddedGameTitle; 21 | gameVersion = Program.Arguments.GetString( "version" ); 22 | updateURL = embeddedGameURL; 23 | } 24 | else 25 | { 26 | // Run game from command line 27 | gameTitle = Program.Arguments.GetString( "game" ); 28 | gameVersion = Program.Arguments.GetString( "version" ); 29 | updateURL = null; 30 | } 31 | 32 | // Show the appropriate window 33 | if( gameTitle != null ) 34 | { 35 | ShowUpdateWindow( gameTitle, gameVersion, updateURL ); 36 | } 37 | else if( Installer.GetInstalledGames().Length > 0 ) 38 | { 39 | ShowGameListWindow(); 40 | } 41 | else 42 | { 43 | ShowErrorWindow( "No games installed. Exiting." ); 44 | } 45 | } 46 | 47 | public static void ShowUpdateWindow( string gameTitle, string optionalGameVersion, string optionalUpdateURL ) 48 | { 49 | var updateWindow = new UpdateWindow( gameTitle, optionalGameVersion, optionalUpdateURL ); 50 | updateWindow.ShowAll(); 51 | Application.Run(); 52 | } 53 | 54 | public static void ShowGameListWindow() 55 | { 56 | var gameListWindow = new GameListWindow(); 57 | gameListWindow.ShowAll(); 58 | Application.Run(); 59 | } 60 | 61 | public static void ShowErrorWindow( string errorMessage, Window parentWindow=null ) 62 | { 63 | var dialog = new MessageDialog( 64 | parentWindow, 65 | DialogFlags.Modal, 66 | MessageType.Error, 67 | ButtonsType.Ok, 68 | errorMessage 69 | ); 70 | dialog.WindowPosition = WindowPosition.Center; 71 | dialog.Show(); 72 | dialog.Run(); 73 | dialog.Hide(); 74 | } 75 | } 76 | } 77 | 78 | -------------------------------------------------------------------------------- /IndieLauncher/Launcher/Interface/GTK/CredentialsDialog.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Gtk; 3 | using Dan200.Launcher.Main; 4 | 5 | namespace Dan200.Launcher.Interface.GTK 6 | { 7 | public class CredentialsDialog : Dialog 8 | { 9 | private Entry m_usernameEntry; 10 | private Entry m_passwordEntry; 11 | 12 | public string Username 13 | { 14 | get 15 | { 16 | if( m_usernameEntry != null ) 17 | { 18 | return m_usernameEntry.Text; 19 | } 20 | return ""; 21 | } 22 | } 23 | 24 | public string Password 25 | { 26 | get 27 | { 28 | if( m_passwordEntry != null ) 29 | { 30 | return m_passwordEntry.Text; 31 | } 32 | return ""; 33 | } 34 | } 35 | 36 | public CredentialsDialog( Window parent, string username, string password ) : base( 37 | Program.Language.Translate( "prompt.credentials" ), 38 | parent, DialogFlags.Modal, 39 | Program.Language.Translate( "button.cancel" ), ResponseType.Cancel, 40 | Program.Language.Translate( "button.ok" ), ResponseType.Ok ) 41 | { 42 | this.DefaultResponse = ResponseType.Ok; 43 | Build( username, password ); 44 | } 45 | 46 | private void Build( string username, string password ) 47 | { 48 | //this.Title = ""; 49 | //this.BorderWidth = 6; 50 | this.WindowPosition = WindowPosition.Center; 51 | //this.TypeHint = Gdk.WindowTypeHint.Dialog; 52 | 53 | var vbox = new VBox( true, 4 ); 54 | vbox.BorderWidth = 4; 55 | 56 | if( username != null ) 57 | { 58 | var hbox = new HBox( false, 6 ); 59 | 60 | var label = new Label(); 61 | label.Text = Program.Language.Translate( "label.username" ); 62 | label.Xalign = 0.0f; 63 | label.WidthRequest = 70; 64 | hbox.PackStart( label, false, false, 0 ); 65 | 66 | m_usernameEntry = new Entry(); 67 | m_usernameEntry.Text = username; 68 | hbox.PackStart( m_usernameEntry, true, true, 0 ); 69 | 70 | vbox.PackStart( hbox, false, false, 0 ); 71 | } 72 | 73 | if( password != null ) 74 | { 75 | var hbox = new HBox( false, 6 ); 76 | 77 | var label = new Label(); 78 | label.Text = Program.Language.Translate( "label.password" ); 79 | label.Xalign = 0.0f; 80 | label.WidthRequest = 70; 81 | hbox.PackStart( label, false, false, 0 ); 82 | 83 | m_passwordEntry = new Entry(); 84 | m_passwordEntry.Text = password; 85 | m_passwordEntry.Visibility = false; 86 | hbox.PackStart( m_passwordEntry, true, true, 0 ); 87 | 88 | vbox.PackStart( hbox, false, false, 0 ); 89 | } 90 | 91 | this.VBox.PackStart( vbox ); 92 | 93 | this.SetDefaultSize( 275, 50 ); 94 | this.SetSizeRequest( 275, -1 ); 95 | this.Resizable = false; 96 | } 97 | } 98 | } 99 | 100 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # User-specific files 5 | *.suo 6 | *.user 7 | *.sln.docstates 8 | *.userprefs 9 | 10 | # Build results 11 | [Dd]ebug/ 12 | [Dd]ebugPublic/ 13 | [Rr]elease/ 14 | x64/ 15 | build/ 16 | bld/ 17 | [Bb]in/ 18 | [Oo]bj/ 19 | Deploy/ 20 | 21 | # MSTest test Results 22 | [Tt]est[Rr]esult*/ 23 | [Bb]uild[Ll]og.* 24 | 25 | #NUNIT 26 | *.VisualState.xml 27 | TestResult.xml 28 | 29 | # Build Results of an ATL Project 30 | [Dd]ebugPS/ 31 | [Rr]eleasePS/ 32 | dlldata.c 33 | 34 | *_i.c 35 | *_p.c 36 | *_i.h 37 | *.ilk 38 | *.meta 39 | *.obj 40 | *.pch 41 | *.pdb 42 | *.pgc 43 | *.pgd 44 | *.rsp 45 | *.sbr 46 | *.tlb 47 | *.tli 48 | *.tlh 49 | *.tmp 50 | *.tmp_proj 51 | *.log 52 | *.vspscc 53 | *.vssscc 54 | .builds 55 | *.pidb 56 | *.svclog 57 | *.scc 58 | 59 | # Chutzpah Test files 60 | _Chutzpah* 61 | 62 | # Visual C++ cache files 63 | ipch/ 64 | *.aps 65 | *.ncb 66 | *.opensdf 67 | *.sdf 68 | *.cachefile 69 | 70 | # Visual Studio profiler 71 | *.psess 72 | *.vsp 73 | *.vspx 74 | 75 | # TFS 2012 Local Workspace 76 | $tf/ 77 | 78 | # Guidance Automation Toolkit 79 | *.gpState 80 | 81 | # ReSharper is a .NET coding add-in 82 | _ReSharper*/ 83 | *.[Rr]e[Ss]harper 84 | *.DotSettings.user 85 | 86 | # JustCode is a .NET coding addin-in 87 | .JustCode 88 | 89 | # TeamCity is a build add-in 90 | _TeamCity* 91 | 92 | # DotCover is a Code Coverage Tool 93 | *.dotCover 94 | 95 | # NCrunch 96 | *.ncrunch* 97 | _NCrunch_* 98 | .*crunch*.local.xml 99 | 100 | # MightyMoose 101 | *.mm.* 102 | AutoTest.Net/ 103 | 104 | # Web workbench (sass) 105 | .sass-cache/ 106 | 107 | # Installshield output folder 108 | [Ee]xpress/ 109 | 110 | # DocProject is a documentation generator add-in 111 | DocProject/buildhelp/ 112 | DocProject/Help/*.HxT 113 | DocProject/Help/*.HxC 114 | DocProject/Help/*.hhc 115 | DocProject/Help/*.hhk 116 | DocProject/Help/*.hhp 117 | DocProject/Help/Html2 118 | DocProject/Help/html 119 | 120 | # Click-Once directory 121 | publish/ 122 | 123 | # Publish Web Output 124 | *.[Pp]ublish.xml 125 | *.azurePubxml 126 | 127 | # NuGet Packages Directory 128 | packages/ 129 | ## TODO: If the tool you use requires repositories.config uncomment the next line 130 | #!packages/repositories.config 131 | 132 | # Enable "build/" folder in the NuGet Packages folder since NuGet packages use it for MSBuild targets 133 | # This line needs to be after the ignore of the build folder (and the packages folder if the line above has been uncommented) 134 | !packages/build/ 135 | 136 | # Windows Azure Build Output 137 | csx/ 138 | *.build.csdef 139 | 140 | # Windows Store app package directory 141 | AppPackages/ 142 | 143 | # Others 144 | sql/ 145 | *.Cache 146 | ClientBin/ 147 | [Ss]tyle[Cc]op.* 148 | ~$* 149 | *~ 150 | *.dbmdl 151 | *.dbproj.schemaview 152 | *.pfx 153 | *.publishsettings 154 | node_modules/ 155 | 156 | # RIA/Silverlight projects 157 | Generated_Code/ 158 | 159 | # Backup & report files from converting an old project file to a newer 160 | # Visual Studio version. Backup files are not needed, because we have git ;-) 161 | _UpgradeReport_Files/ 162 | Backup*/ 163 | UpgradeLog*.XML 164 | UpgradeLog*.htm 165 | 166 | # SQL Server files 167 | *.mdf 168 | *.ldf 169 | 170 | # Business Intelligence projects 171 | *.rdl.data 172 | *.bim.layout 173 | *.bim_*.settings 174 | 175 | # Microsoft Fakes 176 | FakesAssemblies/ 177 | 178 | # OSX 179 | *.DS_store 180 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | IndieLauncher 2 | ============= 3 | 4 | IndieLauncher is a customisable, unobtrusive Launcher application that you can use to transparently provide automatic updates for your Indie Game. Updates are published by editing an RSS file on your website, and are automatically detected by the launcher when users try to run your game. 5 | 6 | There's no system tray apps, background processes or login screens, just an small program that checks for updates to your game, downloads them, then launches your game and gets out of the way. 7 | 8 | Screenshots 9 | =========== 10 | 11 | ![Screenshot 1](Screenshots/Screenshot1.png) 12 | ![Screenshot 2](Screenshots/Screenshot2.png) 13 | ![Screenshot 3](Screenshots/Screenshot3.png) 14 | 15 | Creating a Launcher for your game 16 | ================================= 17 | 18 | The code in this repository provides a launcher that downloads the example game hosted [here](https://github.com/dan200/IndieLauncherExampleGame). To customise it into a launcher that will update and launch *your* game, you'll need to make some small changes: 19 | 20 | 1. Take a look at the [Example Game RSS file](https://github.com/dan200/IndieLauncherExampleGame/blob/master/ExampleGame.xml), customise it with the download links for your game, and host it somewhere online. Each entry in the RSS file represents a version of the game, and newer versions go at the top. 21 | 22 | 2. Download this repository, and take a look at [IndieLauncher/EmbeddedGame.txt](https://github.com/dan200/IndieLauncher/blob/master/IndieLauncher/EmbeddedGame.txt). Change the "game" field to the channel title from your RSS feed, and the "url" feed to where you have your RSS file hosted. 23 | 24 | 3. Optionally, you can embed a version of the game within the launcher itself, so that users without an active internet connection will always have something to play when they first run your launcher. Overwite [IndieLauncher/EmbeddedGame.zip](https://github.com/dan200/IndieLauncher/blob/master/IndieLauncher/EmbeddedGame.zip) with this file, and set the "version" field in EmbeddedGame.txt with the version of the game it represents. If you skip this step, remove this line and delete EmbeddedGame.zip. 25 | 26 | 4. Customise the Assembly Name and Icon to whatever you want your users to see, and build the soluton. The resultant .exe is standalone, and will update and run your game when it gets launched. Distribute it to your players however you like, and they'll always be up-to-date when you publish new versions of your game! 27 | 28 | Securing your Launcher 29 | ====================== 30 | 31 | If your game is free, or you don't mind people who pirate your game getting automatic updates, your work here is done. Otherwise, you can use HTTP Authentication to ensure that only users who have a valid username and/or password can download updates to your game: 32 | 33 | 1. Configure your web server to return HTTP 401 Unauthorized for file downloads without a valid username and password. This will cause the user to be prompted for these details. Most webhost frontends will have options to set this up automatically for simple username/password combinations, or you can implement your own checks. Take a look at [this example](https://github.com/dan200/IndieLauncherExampleGame/tree/master/SecureExample) for a best practice guide and an example implementation in PHP. 34 | 35 | 2. If the username or password is the same for all your users, you can customise the "username" or "password" fields in EmbeddedGame.txt to save your users some typing. This allows you to have a launcher guarded by a single password or username only. 36 | -------------------------------------------------------------------------------- /IndieLauncher/Launcher/Main/Logger.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text; 3 | using System.Diagnostics; 4 | using System.IO; 5 | using System.Linq; 6 | 7 | namespace Dan200.Launcher.Main 8 | { 9 | public static class Logger 10 | { 11 | #if DEBUG 12 | public static bool DebugBuild = true; 13 | #else 14 | public static bool DebugBuild = false; 15 | #endif 16 | 17 | private static StringBuilder s_log = new StringBuilder(); 18 | 19 | public static void Log( string text ) 20 | { 21 | lock( s_log ) 22 | { 23 | s_log.AppendLine( text ); 24 | if( DebugBuild ) 25 | { 26 | Debug.WriteLine( text ); 27 | } 28 | else 29 | { 30 | Console.WriteLine( text ); 31 | } 32 | } 33 | } 34 | 35 | public static void Log( string text, params object[] args ) 36 | { 37 | Log( string.Format( text, args ) ); 38 | } 39 | 40 | public static void DebugLog( string text ) 41 | { 42 | if( DebugBuild ) 43 | { 44 | Log( text ); 45 | } 46 | } 47 | 48 | public static void DebugLog( string text, params object[] args ) 49 | { 50 | if( DebugBuild ) 51 | { 52 | Log( string.Format( text, args ) ); 53 | } 54 | } 55 | 56 | public static void Save() 57 | { 58 | string logPath; 59 | try 60 | { 61 | // Build the path 62 | logPath = Installer.GetBasePath(); 63 | logPath = Path.Combine( logPath, "Logs" ); 64 | logPath = Path.Combine( logPath, DateTime.Now.ToString( "s" ).Replace( ":", "-" ) + ".txt" ); 65 | 66 | // Prepare the directory 67 | var logDirectory = Path.GetDirectoryName( logPath ); 68 | if( !Directory.Exists( logDirectory ) ) 69 | { 70 | // Create the log file directory 71 | Directory.CreateDirectory( logDirectory ); 72 | } 73 | else 74 | { 75 | // Delete old log files from the directory 76 | var directoryInfo = new DirectoryInfo( logDirectory ); 77 | var oldFiles = directoryInfo.EnumerateFiles() 78 | .Where( file => file.Extension == ".txt" ) 79 | .OrderByDescending( file => file.CreationTime ) 80 | .Skip( 4 ); 81 | foreach( var file in oldFiles.ToList() ) 82 | { 83 | file.Delete(); 84 | } 85 | } 86 | } 87 | catch( Exception ) 88 | { 89 | logPath = "Log.txt"; 90 | } 91 | 92 | // Write the log 93 | try 94 | { 95 | Log( "Writing log file to {0}", logPath ); 96 | lock( s_log ) 97 | { 98 | File.WriteAllText( logPath, s_log.ToString() ); 99 | } 100 | } 101 | catch( Exception ) 102 | { 103 | Log( "Failed to write log file to {0}", logPath ); 104 | } 105 | } 106 | } 107 | } 108 | 109 | -------------------------------------------------------------------------------- /IndieLauncher/Launcher/Main/GameLauncher.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Diagnostics; 4 | 5 | namespace Dan200.Launcher.Main 6 | { 7 | public static class GameLauncher 8 | { 9 | public static bool LaunchGame( string gameTitle, string gameVersion ) 10 | { 11 | if( Installer.IsGameInstalled( gameTitle, gameVersion ) ) 12 | { 13 | Logger.Log( "Determining path to game exe ({0} {1})", gameTitle, gameVersion ); 14 | 15 | // Search for a suitable exe to run 16 | string gamePath = Installer.GetInstallPath( gameTitle, gameVersion ); 17 | string launchPath = null; 18 | switch( Program.Platform ) 19 | { 20 | case Platform.Windows: 21 | { 22 | string exePath = Path.Combine( gamePath, gameTitle + ".exe" ); 23 | if( File.Exists( exePath ) ) 24 | { 25 | launchPath = exePath; 26 | break; 27 | } 28 | string batPath = Path.Combine( gamePath, gameTitle + ".bat" ); 29 | if( File.Exists( batPath ) ) 30 | { 31 | launchPath = batPath; 32 | break; 33 | } 34 | break; 35 | } 36 | case Platform.OSX: 37 | { 38 | string appPath = Path.Combine( gamePath, gameTitle + ".app" ); 39 | if( Directory.Exists( appPath ) ) 40 | { 41 | launchPath = appPath; 42 | break; 43 | } 44 | string shPath = Path.Combine( gamePath, gameTitle + ".sh" ); 45 | if( File.Exists( shPath ) ) 46 | { 47 | launchPath = shPath; 48 | break; 49 | } 50 | break; 51 | } 52 | case Platform.Linux: 53 | default: 54 | { 55 | string shPath = Path.Combine( gamePath, gameTitle + ".sh" ); 56 | if( File.Exists( shPath ) ) 57 | { 58 | launchPath = shPath; 59 | break; 60 | } 61 | break; 62 | } 63 | } 64 | 65 | if( launchPath != null ) 66 | { 67 | // Run the exe 68 | Logger.Log( "Launching {0}", launchPath ); 69 | if( Path.GetExtension( launchPath ) == ".sh" ) 70 | { 71 | var startInfo = new ProcessStartInfo(); 72 | startInfo.FileName = "/bin/sh"; 73 | startInfo.WorkingDirectory = gamePath; 74 | startInfo.Arguments = Path.GetFileName( launchPath ); 75 | Process.Start( startInfo ); 76 | return true; 77 | } 78 | else 79 | { 80 | var startInfo = new ProcessStartInfo(); 81 | startInfo.FileName = launchPath; 82 | startInfo.WorkingDirectory = gamePath; 83 | Process.Start( startInfo ); 84 | return true; 85 | } 86 | } 87 | else 88 | { 89 | // If no exe was found, just open the folder 90 | Logger.Log( "Opening {0}", gamePath ); 91 | Process.Start( gamePath ); 92 | return true; 93 | } 94 | } 95 | return false; 96 | } 97 | } 98 | } 99 | 100 | -------------------------------------------------------------------------------- /IndieLauncher/Launcher/Util/ProgressStream.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using Dan200.Launcher.Main; 4 | 5 | namespace Dan200.Launcher.Util 6 | { 7 | public delegate void ProgressDelegate( int percentage ); 8 | 9 | public class ProgressStream : Stream 10 | { 11 | private Stream m_innerStream; 12 | private ProgressDelegate m_listener; 13 | private ICancellable m_cancelObject; 14 | 15 | private long m_length; 16 | private long m_position; 17 | private int m_lastProgress; 18 | 19 | public override bool CanRead 20 | { 21 | get 22 | { 23 | return m_innerStream.CanRead; 24 | } 25 | } 26 | 27 | public override bool CanSeek 28 | { 29 | get 30 | { 31 | return m_innerStream.CanSeek; 32 | } 33 | } 34 | 35 | public override bool CanWrite 36 | { 37 | get 38 | { 39 | return false; 40 | } 41 | } 42 | 43 | public override long Length 44 | { 45 | get 46 | { 47 | return m_innerStream.Length; 48 | } 49 | } 50 | 51 | public override long Position 52 | { 53 | get 54 | { 55 | return m_position; 56 | } 57 | set 58 | { 59 | m_innerStream.Position = value; 60 | m_position = value; 61 | EmitProgress(); 62 | } 63 | } 64 | 65 | public ProgressStream( Stream innerStream, long lengthHint, ProgressDelegate listener, ICancellable cancelObject ) 66 | { 67 | m_innerStream = innerStream; 68 | m_listener = listener; 69 | m_cancelObject = cancelObject; 70 | 71 | m_length = lengthHint; 72 | m_position = 0; 73 | m_lastProgress = -1; 74 | EmitProgress(); 75 | } 76 | 77 | protected override void Dispose( bool disposing ) 78 | { 79 | if( disposing ) 80 | { 81 | m_innerStream.Dispose(); 82 | } 83 | } 84 | 85 | public override void Close() 86 | { 87 | m_innerStream.Close(); 88 | } 89 | 90 | public override void Flush() 91 | { 92 | m_innerStream.Flush(); 93 | } 94 | 95 | public override int ReadByte() 96 | { 97 | CheckCancel(); 98 | var result = m_innerStream.ReadByte(); 99 | m_position++; 100 | EmitProgress(); 101 | return result; 102 | } 103 | 104 | public override int Read( byte[] buffer, int offset, int count ) 105 | { 106 | CheckCancel(); 107 | var result = m_innerStream.Read( buffer, offset, count ); 108 | m_position += result; 109 | EmitProgress(); 110 | return result; 111 | } 112 | 113 | public override long Seek( long offset, SeekOrigin origin ) 114 | { 115 | CheckCancel(); 116 | var result = m_innerStream.Seek( offset, origin ); 117 | m_position = result; 118 | EmitProgress(); 119 | return result; 120 | } 121 | 122 | public override void SetLength( long value ) 123 | { 124 | throw new InvalidOperationException(); 125 | } 126 | 127 | public override void Write( byte[] buffer, int offset, int count ) 128 | { 129 | throw new InvalidOperationException(); 130 | } 131 | 132 | private void EmitProgress() 133 | { 134 | long length = m_length; 135 | if( length < 0 && m_innerStream.CanSeek ) 136 | { 137 | length = m_innerStream.Length; 138 | } 139 | 140 | if( length > 0 ) 141 | { 142 | int percentage = Math.Min( (int)((m_position * 100) / length), 100 ); 143 | if( percentage != m_lastProgress ) 144 | { 145 | m_listener.Invoke( percentage ); 146 | m_lastProgress = percentage; 147 | } 148 | } 149 | } 150 | 151 | private void CheckCancel() 152 | { 153 | if( m_cancelObject.Cancelled ) 154 | { 155 | throw new IOCancelledException(); 156 | } 157 | } 158 | } 159 | } 160 | 161 | -------------------------------------------------------------------------------- /IndieLauncher/Launcher/Interface/WinForms/CredentialsForm.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Windows.Forms; 3 | using System.Drawing; 4 | using Dan200.Launcher.Main; 5 | 6 | namespace Dan200.Launcher.Interface.WinForms 7 | { 8 | public class CredentialsForm : Form 9 | { 10 | private TextBox m_usernameBox; 11 | private TextBox m_passwordBox; 12 | 13 | public string Username 14 | { 15 | get 16 | { 17 | if( m_usernameBox != null ) 18 | { 19 | return m_usernameBox.Text; 20 | } 21 | return ""; 22 | } 23 | } 24 | 25 | public string Password 26 | { 27 | get 28 | { 29 | if( m_passwordBox != null ) 30 | { 31 | return m_passwordBox.Text; 32 | } 33 | return ""; 34 | } 35 | } 36 | 37 | public CredentialsForm( string username, string password ) 38 | { 39 | Build( username, password ); 40 | } 41 | 42 | private void OnOKButtonPressed( object sender, EventArgs args ) 43 | { 44 | this.DialogResult = DialogResult.OK; 45 | this.Close(); 46 | } 47 | 48 | private void OnCancelButtonPressed( object sender, EventArgs args ) 49 | { 50 | this.DialogResult = DialogResult.Cancel; 51 | this.Close(); 52 | } 53 | 54 | private void Build( string username, string password ) 55 | { 56 | this.Text = Program.Language.Translate( "prompt.credentials" ); 57 | this.StartPosition = FormStartPosition.CenterScreen; 58 | this.ClientSize = new Size( 275, 100 ); 59 | this.FormBorderStyle = FormBorderStyle.FixedDialog; 60 | this.MaximizeBox = false; 61 | this.SuspendLayout(); 62 | 63 | int yPos = 6; 64 | if( username != null ) 65 | { 66 | m_usernameBox = new TextBox(); 67 | m_usernameBox.Text = username; 68 | m_usernameBox.Location = new Point( 6 + 75 + 6, yPos ); 69 | m_usernameBox.Width = this.ClientSize.Width - (12 + 75 + 6); 70 | this.Controls.Add( m_usernameBox ); 71 | 72 | var label = new Label(); 73 | label.Text = Program.Language.Translate( "label.username" ); 74 | label.TextAlign = ContentAlignment.MiddleLeft; 75 | label.Width = 75; 76 | label.Height = m_usernameBox.Height; 77 | label.Location = new Point( 6, yPos ); 78 | this.Controls.Add( label ); 79 | 80 | yPos += m_usernameBox.Height + 6; 81 | } 82 | if( password != null ) 83 | { 84 | m_passwordBox = new TextBox(); 85 | m_passwordBox.Text = password; 86 | m_passwordBox.Location = new Point( 6 + 75 + 6, yPos ); 87 | m_passwordBox.Width = this.ClientSize.Width - (12 + 75 + 6); 88 | m_passwordBox.UseSystemPasswordChar = true; 89 | this.Controls.Add( m_passwordBox ); 90 | 91 | var label = new Label(); 92 | label.Text = Program.Language.Translate( "label.password" ); 93 | label.TextAlign = ContentAlignment.MiddleLeft; 94 | label.Width = 75; 95 | label.Height = m_passwordBox.Height; 96 | label.Location = new Point( 6, yPos ); 97 | this.Controls.Add( label ); 98 | 99 | yPos += m_passwordBox.Height + 6; 100 | } 101 | 102 | var okButton = new Button(); 103 | okButton.Location = new Point( this.ClientSize.Width - 6 - 80 - 6 - 80, yPos ); 104 | okButton.Width = 80; 105 | okButton.TextAlign = ContentAlignment.MiddleCenter; 106 | okButton.Text = Program.Language.Translate( "button.ok" ); 107 | okButton.Click += OnOKButtonPressed; 108 | this.Controls.Add( okButton ); 109 | 110 | var cancelButton = new Button(); 111 | cancelButton.Location = new Point( this.ClientSize.Width - 6 - 80, yPos ); 112 | cancelButton.Width = 80; 113 | cancelButton.TextAlign = ContentAlignment.MiddleCenter; 114 | cancelButton.Text = Program.Language.Translate( "button.cancel" ); 115 | cancelButton.Click += OnCancelButtonPressed; 116 | this.Controls.Add( cancelButton ); 117 | 118 | this.AcceptButton = okButton; 119 | this.ClientSize = new Size( 275, yPos + okButton.Height + 6 ); 120 | this.ResumeLayout(); 121 | } 122 | } 123 | } 124 | 125 | -------------------------------------------------------------------------------- /IndieLauncher/Launcher/RSS/RSSFile.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Xml; 3 | using System.IO; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using System.Net; 7 | using Dan200.Launcher.Util; 8 | using Dan200.Launcher.Main; 9 | 10 | namespace Dan200.Launcher.RSS 11 | { 12 | public class RSSFile 13 | { 14 | public readonly IList Channels; 15 | 16 | public static RSSFile Download( string url, ProgressDelegate listener, ICancellable cancelObject ) 17 | { 18 | try 19 | { 20 | Logger.Log( "Downloading RSS file from {0}", url ); 21 | var request = HttpWebRequest.Create( url ); 22 | request.Timeout = 15000; 23 | using( var response = request.GetResponse() ) 24 | { 25 | using( var stream = new ProgressStream( response.GetResponseStream(), response.ContentLength, listener, cancelObject ) ) 26 | { 27 | try 28 | { 29 | return new RSSFile( stream ); 30 | } 31 | finally 32 | { 33 | stream.Close(); 34 | } 35 | } 36 | } 37 | } 38 | catch( Exception e ) 39 | { 40 | Logger.Log( "Caught exception: {0}", e.ToString() ); 41 | return null; 42 | } 43 | } 44 | 45 | public RSSFile() 46 | { 47 | Channels = new List(); 48 | } 49 | 50 | public RSSFile( Stream stream ) : this() 51 | { 52 | // Read document 53 | Logger.Log( "Parsing RSS file" ); 54 | var document = new XmlDocument(); 55 | try 56 | { 57 | document.Load( stream ); 58 | } 59 | catch( Exception e ) 60 | { 61 | Logger.Log( "Caught exception: {0}", e.ToString() ); 62 | return; 63 | } 64 | finally 65 | { 66 | stream.Close(); 67 | } 68 | 69 | // Parse document 70 | var root = document.DocumentElement; 71 | if( root.Name == "rss" ) 72 | { 73 | var rss = root; 74 | var channelItems = rss.GetElementsByTagName( "channel" ).OfType(); 75 | foreach( var channelItem in channelItems ) 76 | { 77 | var channel = new RSSChannel(); 78 | 79 | // Parse channel info 80 | { 81 | var title = channelItem[ "title" ]; 82 | if( title != null ) 83 | { 84 | channel.Title = title.InnerText; 85 | } 86 | var description = channelItem[ "description" ]; 87 | if( description != null ) 88 | { 89 | channel.Description = description.InnerText; 90 | } 91 | var link = channelItem[ "link" ]; 92 | if( link != null ) 93 | { 94 | channel.Link = link.InnerText; 95 | } 96 | } 97 | 98 | // Parse entries 99 | { 100 | var entryItems = channelItem.GetElementsByTagName( "item" ).OfType(); 101 | foreach( var entryItem in entryItems ) 102 | { 103 | var entry = new RSSEntry(); 104 | 105 | // Parse entry info 106 | var title = entryItem[ "title" ]; 107 | if( title != null ) 108 | { 109 | entry.Title = title.InnerText; 110 | } 111 | var description = entryItem[ "description" ]; 112 | if( description != null ) 113 | { 114 | entry.Description = description.InnerText; 115 | } 116 | var link = entryItem[ "link" ]; 117 | if( link != null ) 118 | { 119 | entry.Link = link.InnerText; 120 | } 121 | 122 | // Store entry 123 | channel.Entries.Add( entry ); 124 | } 125 | } 126 | 127 | // Store channel 128 | Channels.Add( channel ); 129 | } 130 | } 131 | } 132 | } 133 | } 134 | 135 | -------------------------------------------------------------------------------- /IndieLauncher/Launcher/Main/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Dan200.Launcher.RSS; 3 | using System.Net; 4 | using System.IO; 5 | using System.Diagnostics; 6 | using Ionic.Zip; 7 | using System.Runtime.InteropServices; 8 | using Dan200.Launcher.Util; 9 | using System.Reflection; 10 | using System.Globalization; 11 | using Dan200.Launcher.Interface.GTK; 12 | using Dan200.Launcher.Interface.WinForms; 13 | 14 | namespace Dan200.Launcher.Main 15 | { 16 | public class Program 17 | { 18 | [DllImport( "libc" )] 19 | private static extern int uname( IntPtr buf ); 20 | 21 | public static Platform Platform 22 | { 23 | get; 24 | private set; 25 | } 26 | 27 | public static ProgramArguments Arguments 28 | { 29 | get; 30 | private set; 31 | } 32 | 33 | public static Language Language 34 | { 35 | get; 36 | private set; 37 | } 38 | 39 | private static Platform DeterminePlatform() 40 | { 41 | switch( Environment.OSVersion.Platform ) 42 | { 43 | case PlatformID.Win32NT: 44 | case PlatformID.Win32S: 45 | case PlatformID.Win32Windows: 46 | case PlatformID.WinCE: 47 | { 48 | return Platform.Windows; 49 | } 50 | case PlatformID.MacOSX: 51 | { 52 | return Platform.OSX; 53 | } 54 | case PlatformID.Unix: 55 | { 56 | IntPtr buffer = IntPtr.Zero; 57 | try 58 | { 59 | buffer = Marshal.AllocHGlobal( 8192 ); 60 | if( uname( buffer ) == 0 ) 61 | { 62 | string os = Marshal.PtrToStringAnsi( buffer ); 63 | if( os == "Darwin" ) 64 | { 65 | return Platform.OSX; 66 | } 67 | else if( os == "Linux" ) 68 | { 69 | return Platform.Linux; 70 | } 71 | } 72 | return Platform.Unknown; 73 | } 74 | catch( Exception ) 75 | { 76 | return Platform.Unknown; 77 | } 78 | finally 79 | { 80 | if( buffer != IntPtr.Zero ) 81 | { 82 | Marshal.FreeHGlobal( buffer ); 83 | } 84 | } 85 | } 86 | default: 87 | { 88 | return Platform.Unknown; 89 | } 90 | } 91 | } 92 | 93 | private static void SetupEmbeddedAssemblies() 94 | { 95 | EmbeddedAssembly.Load( "Ionic.Zip.dll" ); 96 | AppDomain.CurrentDomain.AssemblyResolve += delegate(object sender, ResolveEventArgs args ) 97 | { 98 | return EmbeddedAssembly.Get( args.Name ); 99 | }; 100 | } 101 | 102 | public static Language DetermineLanguage() 103 | { 104 | Dan200.Launcher.Util.Language.LoadAll(); 105 | string targetLanguage = Arguments.GetString( "lang" ); 106 | if( targetLanguage == null ) 107 | { 108 | targetLanguage = CultureInfo.CurrentUICulture.Name.Replace( '-', '_' ); 109 | } 110 | return Dan200.Launcher.Util.Language.GetMostSimilarTo( targetLanguage ); 111 | } 112 | 113 | public static void Main( string[] args ) 114 | { 115 | // Init 116 | Logger.Log( "IndieLauncher" ); 117 | Platform = DeterminePlatform(); 118 | Logger.Log( "Platform: {0}", Platform ); 119 | 120 | SetupEmbeddedAssemblies(); 121 | Arguments = new ProgramArguments( args ); 122 | Language = DetermineLanguage(); 123 | Logger.Log( "Language: {0}", Language.Code ); 124 | 125 | // Determine UI to run 126 | string gui = Arguments.GetString( "gui" ); 127 | if( gui == null ) 128 | { 129 | if( Platform == Platform.Windows ) 130 | { 131 | gui = "winforms"; 132 | } 133 | else 134 | { 135 | gui = "gtk"; 136 | } 137 | } 138 | Logger.Log( "GUI: {0}", gui ); 139 | 140 | // Run UI 141 | if( gui == "winforms" ) 142 | { 143 | WinFormsInterface.Run(); 144 | } 145 | else if( gui == "gtk" ) 146 | { 147 | GTKInterface.Run(); 148 | } 149 | 150 | // Save log 151 | Logger.Save(); 152 | } 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /IndieLauncher/Launcher/Util/Language.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Reflection; 5 | 6 | namespace Dan200.Launcher.Util 7 | { 8 | public class Language 9 | { 10 | private static IDictionary s_languages = new Dictionary(); 11 | 12 | public static void LoadAll() 13 | { 14 | var currentAssembly = Assembly.GetExecutingAssembly(); 15 | foreach( string resource in currentAssembly.GetManifestResourceNames() ) 16 | { 17 | if( resource.StartsWith( "Resources.Languages." ) ) 18 | { 19 | string fileName = resource.Substring( "Resources.Languages.".Length ); 20 | if( fileName.EndsWith( ".lang" ) ) 21 | { 22 | var kvp = new KeyValuePairs(); 23 | kvp.Load( currentAssembly.GetManifestResourceStream( resource ) ); 24 | 25 | string code = fileName.Substring( 0, fileName.Length - 5 ); 26 | s_languages.Add( code, new Language( code, kvp ) ); 27 | } 28 | } 29 | } 30 | } 31 | 32 | public static Language Get( string code ) 33 | { 34 | if( s_languages.ContainsKey( code ) ) 35 | { 36 | return s_languages[ code ]; 37 | } 38 | return null; 39 | } 40 | 41 | public static Language GetMostSimilarTo( string code ) 42 | { 43 | // Look for an exact match 44 | Language exactMatch = Language.Get( code ); 45 | if( exactMatch != null ) 46 | { 47 | return exactMatch; 48 | } 49 | 50 | int underscoreIndex = code.IndexOf( '_' ); 51 | if( underscoreIndex != 0 ) 52 | { 53 | // Look for a root match on the language part (ie: en_GB -> en) 54 | string langPart = (underscoreIndex > 0) ? code.Substring( 0, underscoreIndex ) : code; 55 | Language langPartMatch = Get( langPart ); 56 | if( langPartMatch != null ) 57 | { 58 | return langPartMatch; 59 | } 60 | 61 | // Look for a similar match on the language part (ie: en_GB -> en_US) 62 | foreach( string otherCode in s_languages.Keys ) 63 | { 64 | if( otherCode.StartsWith( langPart ) ) 65 | { 66 | return Language.Get( otherCode ); 67 | } 68 | } 69 | } 70 | 71 | // If there was nothing simular, use english 72 | return Language.Get( "en" ); 73 | } 74 | 75 | private string m_code; 76 | private KeyValuePairs m_translations; 77 | 78 | public string Code 79 | { 80 | get 81 | { 82 | return m_code; 83 | } 84 | } 85 | 86 | public bool IsEnglish 87 | { 88 | get 89 | { 90 | return Code.StartsWith( "en" ); 91 | } 92 | } 93 | 94 | public bool IsDebug 95 | { 96 | get 97 | { 98 | return Code == "debug"; 99 | } 100 | } 101 | 102 | public Language Parent 103 | { 104 | get 105 | { 106 | if( m_translations.ContainsKey( "meta.parent_language" ) ) 107 | { 108 | return Language.Get( m_translations.GetString( "meta.parent_language" ) ); 109 | } 110 | return null; 111 | } 112 | } 113 | 114 | public string Name 115 | { 116 | get 117 | { 118 | return Translate( "meta.native_language_name" ); 119 | } 120 | } 121 | 122 | public string EnglishName 123 | { 124 | get 125 | { 126 | return Translate( "meta.english_language_name" ); 127 | } 128 | } 129 | 130 | public IEnumerable Translators 131 | { 132 | get 133 | { 134 | if( m_translations.ContainsKey( "meta.translator_name" ) ) 135 | { 136 | var names = m_translations.GetString( "meta.translator_name" ).Split( ',' ); 137 | for( int i = 0; i < names.Length; ++i ) 138 | { 139 | var name = names[ i ].Trim(); 140 | if( name.Length > 0 ) 141 | { 142 | yield return name; 143 | } 144 | } 145 | } 146 | } 147 | } 148 | 149 | private Language( string code, KeyValuePairs translations ) 150 | { 151 | m_code = code; 152 | m_translations = translations; 153 | } 154 | 155 | public bool HasTranslation( string symbol ) 156 | { 157 | if( m_translations.ContainsKey( symbol ) ) 158 | { 159 | return true; 160 | } 161 | if( Parent != null ) 162 | { 163 | return Parent.HasTranslation( symbol ); 164 | } 165 | return false; 166 | } 167 | 168 | public string Translate( string symbol ) 169 | { 170 | if( m_translations.ContainsKey( symbol ) ) 171 | { 172 | return m_translations.GetString( symbol ); 173 | } 174 | if( Parent != null ) 175 | { 176 | return Parent.Translate( symbol ); 177 | } 178 | return symbol; 179 | } 180 | 181 | public string TranslateCount( string baseSymbol, long number ) 182 | { 183 | if( number == 1 ) 184 | { 185 | return Translate( baseSymbol + ".singular", number ); 186 | } 187 | else 188 | { 189 | return Translate( baseSymbol + ".plural", number ); 190 | } 191 | } 192 | 193 | public string Translate( string symbol, object arg1 ) 194 | { 195 | return string.Format( Translate( symbol ), arg1 ); 196 | } 197 | 198 | public string Translate( string symbol, object arg1, object arg2 ) 199 | { 200 | return string.Format( Translate( symbol ), arg1, arg2 ); 201 | } 202 | 203 | public string Translate( string symbol, object arg1, object arg2, object arg3 ) 204 | { 205 | return string.Format( Translate( symbol ), arg1, arg2, arg3 ); 206 | } 207 | 208 | public string Translate( string symbol, params object[] args ) 209 | { 210 | return string.Format( Translate( symbol ), args ); 211 | } 212 | } 213 | } 214 | 215 | -------------------------------------------------------------------------------- /IndieLauncher/IndieLauncher.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Debug 5 | x86 6 | {2DC8EA72-2DD1-4AC2-ACC3-9E6F28313594} 7 | WinExe 8 | IndieLauncher 9 | Dan200.Launcher.Main.Program 10 | ..\Icons\Icon.ico 11 | 12 | 13 | true 14 | full 15 | false 16 | bin\Debug 17 | DEBUG; 18 | prompt 19 | 4 20 | x86 21 | false 22 | 23 | 24 | full 25 | true 26 | bin\Release 27 | prompt 28 | 4 29 | x86 30 | false 31 | 32 | 33 | 34 | 35 | 36 | Ionic.Zip.dll 37 | False 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | -------------------------------------------------------------------------------- /IndieLauncher/Launcher/Interface/WinForms/UpdateForm.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Windows.Forms; 3 | using Dan200.Launcher.Main; 4 | using System.ComponentModel; 5 | using System.Drawing; 6 | 7 | namespace Dan200.Launcher.Interface.WinForms 8 | { 9 | public class UpdateForm : Form 10 | { 11 | private GameUpdater m_updater; 12 | private ProgressBar m_progressBar; 13 | 14 | public UpdateForm( string gameTitle, string optionalGameVersion, string optionalUpdateURL ) 15 | { 16 | m_updater = new GameUpdater( gameTitle, optionalGameVersion, optionalUpdateURL ); 17 | m_updater.StageChanged += OnStageChanged; 18 | m_updater.StageChanged += OnProgressChanged; 19 | m_updater.ProgressChanged += OnProgressChanged; 20 | m_updater.PromptChanged += OnPromptChanged; 21 | Build(); 22 | } 23 | 24 | protected override void OnLoad( EventArgs e ) 25 | { 26 | base.OnLoad( e ); 27 | m_updater.Start(); 28 | } 29 | 30 | private void OnStageChanged( object sender, EventArgs e ) 31 | { 32 | var stage = m_updater.Stage; 33 | this.BeginInvoke( (Action)delegate 34 | { 35 | string status = stage.GetStatus( Program.Language ); 36 | //Logger.Log( "Status changed to: {0}", status ); 37 | if( stage == GameUpdateStage.Finished || 38 | stage == GameUpdateStage.Cancelled ) 39 | { 40 | this.Close(); 41 | } 42 | } ); 43 | } 44 | 45 | private void OnProgressChanged( object sender, EventArgs e ) 46 | { 47 | var stage = m_updater.Stage; 48 | var progress = m_updater.StageProgress; 49 | this.BeginInvoke( (Action)delegate 50 | { 51 | int percentage = (int)(progress * 100.0); 52 | if( stage == GameUpdateStage.NotStarted || 53 | stage == GameUpdateStage.Finished || 54 | stage == GameUpdateStage.Cancelled || 55 | stage == GameUpdateStage.Failed ) 56 | { 57 | this.Text = stage.GetStatus( Program.Language ); 58 | } 59 | else 60 | { 61 | this.Text = stage.GetStatus( Program.Language ) + " (" + percentage + "%)"; 62 | } 63 | m_progressBar.Value = percentage; 64 | } ); 65 | } 66 | 67 | private void OnPromptChanged( object sender, EventArgs e ) 68 | { 69 | var prompt = m_updater.CurrentPrompt; 70 | var customMessage = m_updater.CustomMessage; 71 | var description = m_updater.GameDescription; 72 | var previousUsername = m_updater.PreviouslyEnteredUsername; 73 | var previousPassword = m_updater.PreviouslyEnteredPassword; 74 | this.BeginInvoke( (Action)delegate 75 | { 76 | if( prompt == GameUpdatePrompt.Username || 77 | prompt == GameUpdatePrompt.Password || 78 | prompt == GameUpdatePrompt.UsernameAndPassword ) 79 | { 80 | // Show credentials dialog 81 | var dialog = new CredentialsForm( 82 | (prompt != GameUpdatePrompt.Password) ? 83 | ((previousUsername != null) ? previousUsername : "") : 84 | null, 85 | (prompt != GameUpdatePrompt.Username) ? 86 | ((previousPassword != null) ? previousPassword : "") : 87 | null 88 | ); 89 | var result = dialog.ShowDialog( this ); 90 | 91 | // Inform the updater 92 | if( result == DialogResult.OK ) 93 | { 94 | m_updater.AnswerPrompt( true, dialog.Username, dialog.Password ); 95 | } 96 | else 97 | { 98 | m_updater.AnswerPrompt( false ); 99 | } 100 | } 101 | else if( prompt == GameUpdatePrompt.CustomMessage ) 102 | { 103 | // Show message dialog 104 | MessageBox.Show( 105 | this, 106 | customMessage, 107 | Program.Language.Translate( "window.title", description ), 108 | MessageBoxButtons.OK, 109 | MessageBoxIcon.Information 110 | ); 111 | 112 | // Inform the updater 113 | m_updater.AnswerPrompt( true ); 114 | } 115 | else 116 | { 117 | // Show question dialog 118 | var result = MessageBox.Show( 119 | this, 120 | prompt.GetQuestion( Program.Language, description ), 121 | Program.Language.Translate( "window.title", description ), 122 | MessageBoxButtons.YesNo, 123 | MessageBoxIcon.Question 124 | ); 125 | 126 | // Inform the updater 127 | if( result == DialogResult.Cancel ) 128 | { 129 | m_updater.Cancel(); 130 | m_updater.AnswerPrompt( false ); 131 | } 132 | else 133 | { 134 | m_updater.AnswerPrompt( result == DialogResult.Yes ); 135 | } 136 | } 137 | } ); 138 | } 139 | 140 | protected override void OnClosing( CancelEventArgs e ) 141 | { 142 | base.OnClosing( e ); 143 | var stage = m_updater.Stage; 144 | if( stage == GameUpdateStage.NotStarted || 145 | stage == GameUpdateStage.Finished || 146 | stage == GameUpdateStage.Cancelled || 147 | stage == GameUpdateStage.Failed ) 148 | { 149 | return; 150 | } 151 | else 152 | { 153 | m_updater.Cancel(); 154 | e.Cancel = true; 155 | } 156 | } 157 | 158 | private void Build() 159 | { 160 | this.Text = ""; 161 | this.StartPosition = FormStartPosition.CenterScreen; 162 | this.ClientSize = new Size( 350, 36 ); 163 | this.FormBorderStyle = FormBorderStyle.FixedDialog; 164 | this.MaximizeBox = false; 165 | this.SuspendLayout(); 166 | 167 | m_progressBar = new ProgressBar(); 168 | m_progressBar.Minimum = 0; 169 | m_progressBar.Maximum = 100; 170 | m_progressBar.Value = 0; 171 | m_progressBar.Location = new Point( 6, 6 ); 172 | m_progressBar.Size = new Size( this.ClientSize.Width - 12, this.ClientSize.Height - 12 ); 173 | this.Controls.Add( m_progressBar ); 174 | 175 | this.ResumeLayout(); 176 | } 177 | } 178 | } 179 | 180 | -------------------------------------------------------------------------------- /IndieLauncher/Launcher/Interface/GTK/UpdateWindow.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Gtk; 3 | using System.Threading.Tasks; 4 | using System.Threading; 5 | using Dan200.Launcher.Main; 6 | 7 | namespace Dan200.Launcher.Interface.GTK 8 | { 9 | public class UpdateWindow : Window 10 | { 11 | private GameUpdater m_updater; 12 | private ProgressBar m_progressBar; 13 | 14 | public UpdateWindow( string gameTitle, string optionalGameVersion, string optionalUpdateURL ) : base( WindowType.Toplevel ) 15 | { 16 | m_updater = new GameUpdater( gameTitle, optionalGameVersion, optionalUpdateURL ); 17 | m_updater.StageChanged += OnStageChanged; 18 | m_updater.ProgressChanged += OnProgressChanged; 19 | m_updater.PromptChanged += OnPromptChanged; 20 | Build(); 21 | } 22 | 23 | private void OnShown( object sender, EventArgs e ) 24 | { 25 | m_updater.Start(); 26 | } 27 | 28 | private void OnStageChanged( object sender, EventArgs e ) 29 | { 30 | var stage = m_updater.Stage; 31 | Application.Invoke( delegate 32 | { 33 | string status = stage.GetStatus( Program.Language ); 34 | //Logger.Log( "Status changed to: {0}", status ); 35 | this.Title = status; 36 | if( stage == GameUpdateStage.Finished || 37 | stage == GameUpdateStage.Cancelled ) 38 | { 39 | Application.Quit(); 40 | } 41 | } ); 42 | } 43 | 44 | private void OnProgressChanged( object sender, EventArgs e ) 45 | { 46 | var progress = m_updater.StageProgress; 47 | Application.Invoke( delegate 48 | { 49 | int percentage = (int)(progress * 100.0); 50 | m_progressBar.Text = string.Format( "{0}%", percentage ); 51 | m_progressBar.Fraction = progress; 52 | } ); 53 | } 54 | 55 | private void OnPromptChanged( object sender, EventArgs e ) 56 | { 57 | var prompt = m_updater.CurrentPrompt; 58 | var description = m_updater.GameDescription; 59 | var customMessage = m_updater.CustomMessage; 60 | var previousUsername = m_updater.PreviouslyEnteredUsername; 61 | var previousPassword = m_updater.PreviouslyEnteredPassword; 62 | Application.Invoke( delegate 63 | { 64 | if( prompt == GameUpdatePrompt.Username || 65 | prompt == GameUpdatePrompt.Password || 66 | prompt == GameUpdatePrompt.UsernameAndPassword ) 67 | { 68 | // Show credentials dialog 69 | var dialog = new CredentialsDialog( 70 | this, 71 | (prompt != GameUpdatePrompt.Password) ? 72 | ((previousUsername != null) ? previousUsername : "") : 73 | null, 74 | (prompt != GameUpdatePrompt.Username) ? 75 | ((previousPassword != null) ? previousPassword : "") : 76 | null 77 | ); 78 | dialog.ShowAll(); 79 | int response = dialog.Run(); 80 | string username = dialog.Username; 81 | string password = dialog.Password; 82 | dialog.Destroy(); 83 | 84 | // Inform the updater 85 | if( response == (int)ResponseType.Close || 86 | response == (int)ResponseType.DeleteEvent ) 87 | { 88 | m_updater.Cancel(); 89 | m_updater.AnswerPrompt( false ); 90 | } 91 | else 92 | { 93 | m_updater.AnswerPrompt( 94 | response == (int)ResponseType.Ok, 95 | username, 96 | password 97 | ); 98 | } 99 | } 100 | else if( prompt == GameUpdatePrompt.CustomMessage ) 101 | { 102 | // Show message dialog 103 | Console.WriteLine( customMessage ); 104 | var dialog = new MessageDialog( 105 | this, 106 | DialogFlags.Modal, 107 | MessageType.Info, 108 | ButtonsType.Ok, 109 | customMessage 110 | ); 111 | dialog.ShowAll(); 112 | int response = dialog.Run(); 113 | dialog.Destroy(); 114 | 115 | // Inform the updater 116 | m_updater.AnswerPrompt( true ); 117 | } 118 | else 119 | { 120 | // Show question dialog 121 | var dialog = new MessageDialog( 122 | this, 123 | DialogFlags.Modal, 124 | MessageType.Question, 125 | ButtonsType.YesNo, 126 | prompt.GetQuestion( Program.Language, description ) 127 | ); 128 | dialog.ShowAll(); 129 | int response = dialog.Run(); 130 | dialog.Destroy(); 131 | 132 | // Inform the updater 133 | if( response == (int)ResponseType.Close || 134 | response == (int)ResponseType.DeleteEvent ) 135 | { 136 | m_updater.Cancel(); 137 | m_updater.AnswerPrompt( false ); 138 | } 139 | else 140 | { 141 | m_updater.AnswerPrompt( response == (int)ResponseType.Yes ); 142 | } 143 | } 144 | } ); 145 | } 146 | 147 | private void OnDeleteEvent( object sender, DeleteEventArgs a ) 148 | { 149 | var stage = m_updater.Stage; 150 | if( stage == GameUpdateStage.NotStarted || 151 | stage == GameUpdateStage.Finished || 152 | stage == GameUpdateStage.Cancelled || 153 | stage == GameUpdateStage.Failed ) 154 | { 155 | Application.Quit(); 156 | } 157 | else 158 | { 159 | m_updater.Cancel(); 160 | a.RetVal = true; 161 | } 162 | } 163 | 164 | private void Build() 165 | { 166 | this.Title = ""; 167 | this.BorderWidth = 6; 168 | this.WindowPosition = WindowPosition.Center; 169 | this.TypeHint = Gdk.WindowTypeHint.Dialog; 170 | 171 | var vbox = new VBox( false, 4 ); 172 | 173 | m_progressBar = new ProgressBar(); 174 | m_progressBar.Fraction = 0.0f; 175 | m_progressBar.Text = "0%"; 176 | vbox.PackStart( m_progressBar, false, false, 0 ); 177 | 178 | this.Add( vbox ); 179 | 180 | this.SetDefaultSize( 300, 100 ); 181 | this.SetSizeRequest( 300, -1 ); 182 | this.Resizable = false; 183 | 184 | this.Shown += OnShown; 185 | this.DeleteEvent += OnDeleteEvent; 186 | } 187 | } 188 | } 189 | -------------------------------------------------------------------------------- /IndieLauncher/Launcher/Util/KeyValuePairs.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | using System.Collections.Generic; 4 | using System.IO; 5 | using System.Text; 6 | 7 | namespace Dan200.Launcher.Util 8 | { 9 | public class KeyValuePairs 10 | { 11 | private string m_comment; 12 | private IDictionary m_pairs; 13 | private bool m_modified; 14 | 15 | public string Comment 16 | { 17 | get 18 | { 19 | return m_comment; 20 | } 21 | set 22 | { 23 | if( m_comment != value ) 24 | { 25 | m_comment = value; 26 | m_modified = true; 27 | } 28 | } 29 | } 30 | 31 | public IEnumerable Keys 32 | { 33 | get 34 | { 35 | return m_pairs.Keys; 36 | } 37 | } 38 | 39 | public bool Modified 40 | { 41 | get 42 | { 43 | return m_modified; 44 | } 45 | set 46 | { 47 | m_modified = value; 48 | } 49 | } 50 | 51 | public KeyValuePairs() 52 | { 53 | m_comment = null; 54 | m_pairs = new SortedDictionary(); 55 | m_modified = false; 56 | } 57 | 58 | public void Load( Stream stream ) 59 | { 60 | using( var reader = new StreamReader( stream, Encoding.UTF8 ) ) 61 | { 62 | Load( reader ); 63 | } 64 | } 65 | 66 | public void Load( TextReader reader ) 67 | { 68 | string line = null; 69 | while( (line = reader.ReadLine()) != null ) 70 | { 71 | int commentIndex; 72 | if( line.StartsWith( "//" ) ) 73 | { 74 | commentIndex = 0; 75 | } 76 | else 77 | { 78 | commentIndex = line.IndexOf( " //" ); 79 | commentIndex = (commentIndex >= 0) ? (commentIndex + 1) : -1; 80 | } 81 | if( commentIndex >= 0 ) 82 | { 83 | if( m_pairs.Count == 0 && Comment == null ) 84 | { 85 | Comment = line.Substring( commentIndex + 2 ).Trim(); 86 | } 87 | line = line.Substring( 0, commentIndex ); 88 | } 89 | 90 | int equalsIndex = line.IndexOf( '=' ); 91 | if( equalsIndex >= 0 ) 92 | { 93 | string key = line.Substring( 0, equalsIndex ).Trim(); 94 | string value = line.Substring( equalsIndex + 1 ).Trim(); 95 | if( value.Length > 0 ) 96 | { 97 | Set( key, value ); 98 | } 99 | } 100 | } 101 | reader.Close(); 102 | } 103 | 104 | public bool ContainsKey( string key ) 105 | { 106 | return m_pairs.ContainsKey( key ); 107 | } 108 | 109 | public void Remove( string key ) 110 | { 111 | if( m_pairs.ContainsKey( key ) ) 112 | { 113 | m_pairs.Remove( key ); 114 | m_modified = true; 115 | } 116 | } 117 | 118 | public void Clear() 119 | { 120 | if( m_pairs.Count > 0 ) 121 | { 122 | m_pairs.Clear(); 123 | m_modified = true; 124 | } 125 | } 126 | 127 | public string GetString( string key ) 128 | { 129 | return GetString( key, null ); 130 | } 131 | 132 | public string GetString( string key, string _default ) 133 | { 134 | if( m_pairs.ContainsKey( key ) ) 135 | { 136 | return m_pairs[ key ]; 137 | } 138 | return _default; 139 | } 140 | 141 | public int GetInteger( string key ) 142 | { 143 | return GetInteger( key, 0 ); 144 | } 145 | 146 | public int GetInteger( string key, int _default ) 147 | { 148 | if( m_pairs.ContainsKey( key ) ) 149 | { 150 | int result; 151 | if( int.TryParse( m_pairs[ key ], out result ) ) 152 | { 153 | return result; 154 | } 155 | } 156 | return _default; 157 | } 158 | 159 | public float GetFloat( string key ) 160 | { 161 | return GetFloat( key, 0.0f ); 162 | } 163 | 164 | public float GetFloat( string key, float _default ) 165 | { 166 | if( m_pairs.ContainsKey( key ) ) 167 | { 168 | float result; 169 | if( float.TryParse( m_pairs[ key ], out result ) ) 170 | { 171 | return result; 172 | } 173 | } 174 | return _default; 175 | } 176 | 177 | public bool GetBool( string key ) 178 | { 179 | return GetBool( key, false ); 180 | } 181 | 182 | public bool GetBool( string key, bool _default ) 183 | { 184 | if( m_pairs.ContainsKey( key ) ) 185 | { 186 | string value = m_pairs[ key ]; 187 | if( value == "true" ) 188 | { 189 | return true; 190 | } 191 | if( value == "false" ) 192 | { 193 | return false; 194 | } 195 | } 196 | return _default; 197 | } 198 | 199 | public void Set( string key, string value ) 200 | { 201 | if( m_pairs.ContainsKey( key ) ) 202 | { 203 | if( value != null ) 204 | { 205 | if( m_pairs[ key ] != value ) 206 | { 207 | m_pairs[ key ] = value; 208 | m_modified = true; 209 | } 210 | } 211 | else 212 | { 213 | m_pairs.Remove( key ); 214 | m_modified = true; 215 | } 216 | } 217 | else if( value != null ) 218 | { 219 | m_pairs.Add( key, value ); 220 | m_modified = true; 221 | } 222 | } 223 | 224 | public void Set( string key, int value ) 225 | { 226 | Set( key, value.ToString() ); 227 | } 228 | 229 | public void Set( string key, float value ) 230 | { 231 | Set( key, value.ToString() ); 232 | } 233 | 234 | public void Set( string key, bool value ) 235 | { 236 | Set( key, value ? "true" : "false" ); 237 | } 238 | 239 | public void Ensure( string key, string _default ) 240 | { 241 | Set( key, GetString( key, _default ) ); 242 | } 243 | 244 | public void Ensure( string key, int _default ) 245 | { 246 | Set( key, GetInteger( key, _default ) ); 247 | } 248 | 249 | public void Ensure( string key, float _default ) 250 | { 251 | Set( key, GetFloat( key, _default ) ); 252 | } 253 | 254 | public void Ensure( string key, bool _default ) 255 | { 256 | Set( key, GetBool( key, _default ) ); 257 | } 258 | } 259 | } 260 | 261 | -------------------------------------------------------------------------------- /IndieLauncher/Launcher/Main/Installer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Linq; 4 | using Ionic.Zip; 5 | using System.Net; 6 | using System.Reflection; 7 | using Dan200.Launcher.Util; 8 | using Dan200.Launcher.RSS; 9 | 10 | namespace Dan200.Launcher.Main 11 | { 12 | public static class Installer 13 | { 14 | public static string GetBasePath() 15 | { 16 | string basePath; 17 | if( Program.Platform == Platform.OSX ) 18 | { 19 | basePath = Path.Combine( 20 | Environment.GetFolderPath( Environment.SpecialFolder.Personal ), 21 | "Library/Application Support" 22 | ); 23 | } 24 | else 25 | { 26 | basePath = Environment.GetFolderPath( Environment.SpecialFolder.ApplicationData ); 27 | } 28 | basePath = Path.Combine( basePath, "IndieLauncher" ); 29 | return basePath; 30 | } 31 | 32 | public static string GetBasePath( string gameTitle ) 33 | { 34 | string basePath = GetBasePath(); 35 | basePath = Path.Combine( basePath, "Games" ); 36 | basePath = Path.Combine( basePath, gameTitle ); 37 | return basePath; 38 | } 39 | 40 | public static string GetDownloadPath( string gameTitle, string gameVersion ) 41 | { 42 | var downloadPath = GetBasePath( gameTitle ); 43 | downloadPath = Path.Combine( downloadPath, "Downloads" ); 44 | downloadPath = Path.Combine( downloadPath, gameVersion + ".zip" ); 45 | return downloadPath; 46 | } 47 | 48 | public static string GetInstallPath( string gameTitle, string gameVersion ) 49 | { 50 | var installPath = GetBasePath( gameTitle ); 51 | installPath = Path.Combine( installPath, "Versions" ); 52 | installPath = Path.Combine( installPath, gameVersion ); 53 | return installPath; 54 | } 55 | 56 | public static bool IsGameDownloaded( string gameTitle, string gameVersion ) 57 | { 58 | var downloadPath = GetDownloadPath( gameTitle, gameVersion ); 59 | return File.Exists( downloadPath ); 60 | } 61 | 62 | public static string GetLatestInstalledVersion( string gameTitle ) 63 | { 64 | var versionPath = Installer.GetBasePath( gameTitle ); 65 | versionPath = Path.Combine( versionPath, "Versions" ); 66 | versionPath = Path.Combine( versionPath, "Latest.txt" ); 67 | if( File.Exists( versionPath ) ) 68 | { 69 | string gameVersion = File.ReadAllText( versionPath ).Trim(); 70 | if( IsGameInstalled( gameTitle, gameVersion ) ) 71 | { 72 | return gameVersion; 73 | } 74 | } 75 | return null; 76 | } 77 | 78 | public static string RecordLatestInstalledVersion( string gameTitle, string gameVersion, bool overwrite ) 79 | { 80 | var versionPath = Installer.GetBasePath( gameTitle ); 81 | versionPath = Path.Combine( versionPath, "Versions" ); 82 | versionPath = Path.Combine( versionPath, "Latest.txt" ); 83 | if( overwrite || !File.Exists( versionPath ) ) 84 | { 85 | Logger.Log( "Recording game version {0} to Latest.txt", gameVersion ); 86 | File.WriteAllText( versionPath, gameVersion ); 87 | } 88 | return null; 89 | } 90 | 91 | public static bool GetLatestVersionInfo( RSSFile rssFile, string gameTitle, out string o_gameVersion, out string o_gameDescription, out string o_versionDownloadURL, out string o_versionDescription, out bool o_versionIsNewest ) 92 | { 93 | // Find a matching title 94 | foreach( var channel in rssFile.Channels ) 95 | { 96 | if( channel.Title == gameTitle ) 97 | { 98 | // Find a matching version 99 | for( int i=0; i Path.GetFileName(p) ).ToArray(); 395 | } 396 | 397 | private static void MakeFileExecutable( string filePath ) 398 | { 399 | Mono.Unix.Native.Syscall.chmod( 400 | filePath, 401 | Mono.Unix.Native.FilePermissions.ACCESSPERMS 402 | ); 403 | } 404 | 405 | public static bool InstallGame( string gameTitle, string gameVersion, ProgressDelegate listener, ICancellable cancelObject ) 406 | { 407 | var downloadPath = GetDownloadPath( gameTitle, gameVersion ); 408 | var installPath = GetInstallPath( gameTitle, gameVersion ); 409 | if( File.Exists( downloadPath ) ) 410 | { 411 | Logger.Log( "Installing game ({0} {1})", gameTitle, gameVersion ); 412 | try 413 | { 414 | using( var zipFile = new ZipFile( downloadPath ) ) 415 | { 416 | // Delete old install 417 | if( Directory.Exists( installPath ) ) 418 | { 419 | Directory.Delete( installPath, true ); 420 | } 421 | Directory.CreateDirectory( installPath ); 422 | 423 | // Extract new install 424 | int totalFiles = zipFile.Entries.Count; 425 | int filesInstalled = 0; 426 | int lastProgress = 0; 427 | listener.Invoke( 0 ); 428 | foreach( var entry in zipFile.Entries ) 429 | { 430 | // Extract the file 431 | var entryInstallPath = Path.Combine( installPath, entry.FileName ); 432 | if( entry.IsDirectory ) 433 | { 434 | Directory.CreateDirectory( entryInstallPath ); 435 | } 436 | else 437 | { 438 | Directory.CreateDirectory( Path.GetDirectoryName( entryInstallPath ) ); 439 | using( var file = File.OpenWrite( entryInstallPath ) ) 440 | { 441 | try 442 | { 443 | using( var reader = new ProgressStream( entry.OpenReader(), -1, delegate { 444 | // TODO: Emit progress during installation of large individual files? 445 | }, cancelObject ) ) 446 | { 447 | try 448 | { 449 | reader.CopyTo( file ); 450 | try 451 | { 452 | if( Program.Platform == Platform.Linux || 453 | Program.Platform == Platform.OSX ) 454 | { 455 | MakeFileExecutable( entryInstallPath ); 456 | } 457 | } 458 | catch( Exception e ) 459 | { 460 | Logger.Log( "Caught Exception: {0}", e.ToString() ); 461 | Logger.Log( "Failed to set file permissions on {0}", entryInstallPath ); 462 | } 463 | } 464 | finally 465 | { 466 | reader.Close(); 467 | } 468 | } 469 | } 470 | finally 471 | { 472 | file.Close(); 473 | } 474 | } 475 | } 476 | 477 | // Check for cancellation 478 | if( cancelObject.Cancelled ) 479 | { 480 | throw new IOCancelledException(); 481 | } 482 | 483 | // Notify the progress listener 484 | filesInstalled++; 485 | int progress = (filesInstalled * 100) / totalFiles; 486 | if( progress != lastProgress ) 487 | { 488 | listener.Invoke( progress ); 489 | lastProgress = progress; 490 | } 491 | } 492 | } 493 | return true; 494 | } 495 | catch( Exception e ) 496 | { 497 | Logger.Log( "Caught Exception: {0}", e.ToString() ); 498 | if( Directory.Exists( installPath ) ) 499 | { 500 | Directory.Delete( installPath, true ); 501 | } 502 | return false; 503 | } 504 | } 505 | return false; 506 | } 507 | } 508 | } 509 | 510 | -------------------------------------------------------------------------------- /IndieLauncher/Launcher/Main/GameUpdater.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | using Dan200.Launcher.RSS; 5 | using Dan200.Launcher.Util; 6 | 7 | namespace Dan200.Launcher.Main 8 | { 9 | public class GameUpdater : ICancellable 10 | { 11 | private string m_gameTitle; 12 | private string m_gameDescription; 13 | private string m_optionalGameVersion; 14 | private string m_optionalUpdateURL; 15 | 16 | private GameUpdateStage m_stage; 17 | private double m_progress; 18 | private bool m_cancelled; 19 | 20 | private GameUpdatePrompt m_currentPrompt; 21 | private string m_customMessage; 22 | private bool m_promptResponse; 23 | private string m_promptUsername; 24 | private string m_promptPassword; 25 | private AutoResetEvent m_promptWaitHandle; 26 | 27 | public GameUpdateStage Stage 28 | { 29 | get 30 | { 31 | lock( this ) 32 | { 33 | return m_stage; 34 | } 35 | } 36 | private set 37 | { 38 | lock( this ) 39 | { 40 | m_stage = value; 41 | if( value == GameUpdateStage.Finished || 42 | value == GameUpdateStage.Cancelled || 43 | value == GameUpdateStage.Failed ) 44 | { 45 | m_progress = 1.0f; 46 | } 47 | else 48 | { 49 | m_progress = 0.0f; 50 | } 51 | 52 | var stageChanged = StageChanged; 53 | if( stageChanged != null ) 54 | { 55 | stageChanged.Invoke( this, EventArgs.Empty ); 56 | } 57 | var progressChanged = ProgressChanged; 58 | if( progressChanged != null ) 59 | { 60 | progressChanged.Invoke( this, EventArgs.Empty ); 61 | } 62 | } 63 | } 64 | } 65 | 66 | public double StageProgress 67 | { 68 | get 69 | { 70 | lock( this ) 71 | { 72 | return m_progress; 73 | } 74 | } 75 | private set 76 | { 77 | lock( this ) 78 | { 79 | m_progress = value; 80 | var progressChanged = ProgressChanged; 81 | if( progressChanged != null ) 82 | { 83 | progressChanged.Invoke( this, EventArgs.Empty ); 84 | } 85 | } 86 | } 87 | } 88 | 89 | public GameUpdatePrompt CurrentPrompt 90 | { 91 | get 92 | { 93 | lock( this ) 94 | { 95 | return m_currentPrompt; 96 | } 97 | } 98 | } 99 | 100 | public string CustomMessage 101 | { 102 | get 103 | { 104 | lock( this ) 105 | { 106 | return m_customMessage; 107 | } 108 | } 109 | } 110 | 111 | public bool Cancelled 112 | { 113 | get 114 | { 115 | lock( this ) 116 | { 117 | return m_cancelled; 118 | } 119 | } 120 | } 121 | 122 | public string GameDescription 123 | { 124 | get 125 | { 126 | lock( this ) 127 | { 128 | return m_gameDescription; 129 | } 130 | } 131 | private set 132 | { 133 | lock( this ) 134 | { 135 | m_gameDescription = value; 136 | } 137 | } 138 | } 139 | 140 | public string PreviouslyEnteredUsername 141 | { 142 | get 143 | { 144 | lock( this ) 145 | { 146 | return m_promptUsername; 147 | } 148 | } 149 | } 150 | 151 | public string PreviouslyEnteredPassword 152 | { 153 | get 154 | { 155 | lock( this ) 156 | { 157 | return m_promptPassword; 158 | } 159 | } 160 | } 161 | 162 | public event EventHandler StageChanged; 163 | public event EventHandler ProgressChanged; 164 | public event EventHandler PromptChanged; 165 | 166 | public GameUpdater( string gameTitle, string optionalGameVersion, string optionalUpdateURL ) 167 | { 168 | m_gameTitle = gameTitle; 169 | m_gameDescription = gameTitle; 170 | m_optionalGameVersion = optionalGameVersion; 171 | m_optionalUpdateURL = optionalUpdateURL; 172 | 173 | m_stage = GameUpdateStage.NotStarted; 174 | m_progress = 0.0f; 175 | m_cancelled = false; 176 | 177 | m_currentPrompt = GameUpdatePrompt.None; 178 | m_promptWaitHandle = new AutoResetEvent( false ); 179 | } 180 | 181 | private bool ShowPrompt( GameUpdatePrompt prompt ) 182 | { 183 | lock( this ) 184 | { 185 | m_currentPrompt = prompt; 186 | var promptChanged = PromptChanged; 187 | if( promptChanged != null ) 188 | { 189 | promptChanged.Invoke( this, EventArgs.Empty ); 190 | } 191 | } 192 | m_promptWaitHandle.WaitOne(); 193 | return m_promptResponse; 194 | } 195 | 196 | private bool ShowUsernamePrompt( ref string o_username ) 197 | { 198 | if( ShowPrompt( GameUpdatePrompt.Username ) ) 199 | { 200 | o_username = m_promptUsername; 201 | return true; 202 | } 203 | return false; 204 | } 205 | 206 | private bool ShowPasswordPrompt( ref string o_password ) 207 | { 208 | if( ShowPrompt( GameUpdatePrompt.Password ) ) 209 | { 210 | o_password = m_promptPassword; 211 | return true; 212 | } 213 | return false; 214 | } 215 | 216 | private bool ShowUsernameAndPasswordPrompt( ref string o_username, ref string o_password ) 217 | { 218 | if( ShowPrompt( GameUpdatePrompt.UsernameAndPassword ) ) 219 | { 220 | o_username = m_promptUsername; 221 | o_password = m_promptPassword; 222 | return true; 223 | } 224 | return false; 225 | } 226 | 227 | private bool ShowCustomMessagePrompt( string customMessage ) 228 | { 229 | m_customMessage = customMessage; 230 | return ShowPrompt( GameUpdatePrompt.CustomMessage ); 231 | } 232 | 233 | private bool Extract() 234 | { 235 | string embeddedGameVersion = Installer.GetEmbeddedGameVersion( m_gameTitle ); 236 | if( !Installer.IsGameDownloaded( m_gameTitle, embeddedGameVersion ) ) 237 | { 238 | Stage = GameUpdateStage.ExtractingUpdate; 239 | if( !Installer.ExtractEmbeddedGame( delegate( int progress ) { 240 | StageProgress = (double)progress / 100.0; 241 | }, this ) ) 242 | { 243 | return false; 244 | } 245 | if( Cancelled ) 246 | { 247 | return false; 248 | } 249 | StageProgress = 1.0; 250 | } 251 | return true; 252 | } 253 | 254 | private bool Download( string gameVersion, string downloadURL, string username=null, string password=null ) 255 | { 256 | if( !Installer.IsGameDownloaded( m_gameTitle, gameVersion ) ) 257 | { 258 | Stage = GameUpdateStage.DownloadingUpdate; 259 | 260 | string embeddedGameTitle, embeddedGameVersion, embeddedGameURL, embeddedUsername, embeddedPassword; 261 | if( Installer.GetEmbeddedGameInfo( out embeddedGameTitle, out embeddedGameVersion, out embeddedGameURL, out embeddedUsername, out embeddedPassword ) ) 262 | { 263 | if( username == null ) 264 | { 265 | username = embeddedUsername; 266 | } 267 | if( password == null ) 268 | { 269 | password = embeddedPassword; 270 | } 271 | } 272 | 273 | bool authFailure; 274 | string customMessage; 275 | if( !Installer.DownloadGame( 276 | m_gameTitle, gameVersion, 277 | downloadURL, 278 | username, 279 | password, 280 | delegate( int progress ) { 281 | StageProgress = (double)progress / 100.0; 282 | }, 283 | this, 284 | out authFailure, 285 | out customMessage 286 | ) ) 287 | { 288 | if( Cancelled ) 289 | { 290 | return false; 291 | } 292 | if( customMessage != null && !ShowCustomMessagePrompt( customMessage ) ) 293 | { 294 | return false; 295 | } 296 | if( authFailure ) 297 | { 298 | if( embeddedUsername == null && embeddedPassword == null ) 299 | { 300 | if( ShowUsernameAndPasswordPrompt( ref username, ref password ) ) 301 | { 302 | return Download( gameVersion, downloadURL, username, password ); 303 | } 304 | } 305 | else if( embeddedUsername == null ) 306 | { 307 | if( ShowUsernamePrompt( ref username ) ) 308 | { 309 | return Download( gameVersion, downloadURL, username, embeddedPassword ); 310 | } 311 | } 312 | else if( embeddedPassword == null ) 313 | { 314 | if( ShowPasswordPrompt( ref password ) ) 315 | { 316 | return Download( gameVersion, downloadURL, embeddedUsername, password ); 317 | } 318 | } 319 | } 320 | return false; 321 | } 322 | if( Cancelled ) 323 | { 324 | return false; 325 | } 326 | if( customMessage != null && !ShowCustomMessagePrompt( customMessage ) ) 327 | { 328 | return false; 329 | } 330 | StageProgress = 1.0; 331 | } 332 | return true; 333 | } 334 | 335 | private bool Install( string gameVersion ) 336 | { 337 | if( !Installer.IsGameInstalled( m_gameTitle, gameVersion ) ) 338 | { 339 | Stage = GameUpdateStage.InstallingUpdate; 340 | if( !Installer.InstallGame( m_gameTitle, gameVersion, delegate( int progress ) { 341 | StageProgress = (double)progress / 100.0; 342 | }, this ) ) 343 | { 344 | return false; 345 | } 346 | if( Cancelled ) 347 | { 348 | return false; 349 | } 350 | StageProgress = 1.0; 351 | } 352 | return true; 353 | } 354 | 355 | private bool DownloadAndInstall( string gameVersion, string downloadURL, bool isLatest ) 356 | { 357 | if( !Installer.IsGameInstalled( m_gameTitle, gameVersion ) ) 358 | { 359 | if( !Download( gameVersion, downloadURL ) ) 360 | { 361 | return false; 362 | } 363 | if( !Install( gameVersion ) ) 364 | { 365 | return false; 366 | } 367 | } 368 | Installer.RecordLatestInstalledVersion( m_gameTitle, gameVersion, isLatest ); 369 | return true; 370 | } 371 | 372 | private bool ExtractAndInstall( bool isLatest ) 373 | { 374 | string embeddedGameVersion = Installer.GetEmbeddedGameVersion( m_gameTitle ); 375 | if( !Installer.IsGameInstalled( m_gameTitle, embeddedGameVersion ) ) 376 | { 377 | if( !Extract() ) 378 | { 379 | return false; 380 | } 381 | if( !Install( embeddedGameVersion ) ) 382 | { 383 | return false; 384 | } 385 | } 386 | Installer.RecordLatestInstalledVersion( m_gameTitle, embeddedGameVersion, isLatest ); 387 | return true; 388 | } 389 | 390 | private bool Launch( string gameVersion ) 391 | { 392 | Stage = GameUpdateStage.LaunchingGame; 393 | if( Cancelled ) 394 | { 395 | return false; 396 | } 397 | if( !GameLauncher.LaunchGame( m_gameTitle, gameVersion ) ) 398 | { 399 | return false; 400 | } 401 | StageProgress = 1.0; 402 | return true; 403 | } 404 | 405 | private RSSFile DownloadRSSFile( string updateURL ) 406 | { 407 | // Download RSS file 408 | Stage = GameUpdateStage.CheckingForUpdate; 409 | var rssFile = RSSFile.Download( m_optionalUpdateURL, delegate(int percentage) { 410 | StageProgress = (double)percentage / 100.0; 411 | }, this ); 412 | if( rssFile == null ) 413 | { 414 | return null; 415 | } 416 | if( Cancelled ) 417 | { 418 | return null; 419 | } 420 | StageProgress = 1.0; 421 | return rssFile; 422 | } 423 | 424 | private bool GetSpecificDownloadURL( string updateURL, string gameVersion, out string o_downloadURL, out bool o_isNewest ) 425 | { 426 | // Get the RSS file 427 | var rssFile = DownloadRSSFile( updateURL ); 428 | if( rssFile == null ) 429 | { 430 | o_downloadURL = null; 431 | o_isNewest = false; 432 | return false; 433 | } 434 | 435 | // Inspect RSS file for version download info 436 | string gameDescription; 437 | string updateDescription; 438 | if( !Installer.GetSpecificVersionInfo( rssFile, m_gameTitle, gameVersion, out gameDescription, out o_downloadURL, out updateDescription, out o_isNewest ) ) 439 | { 440 | o_downloadURL = null; 441 | o_isNewest = false; 442 | return false; 443 | } 444 | GameDescription = gameDescription; 445 | return true; 446 | } 447 | 448 | private bool GetLatestDownloadURL( string updateURL, out string o_gameVersion, out string o_downloadURL ) 449 | { 450 | // Get the RSS file 451 | var rssFile = DownloadRSSFile( updateURL ); 452 | if( rssFile == null ) 453 | { 454 | o_gameVersion = null; 455 | o_downloadURL = null; 456 | return false; 457 | } 458 | 459 | // Inspect RSS file for version download info 460 | string gameDescription; 461 | string updateDescription; 462 | bool gameVersionIsNewest; 463 | if( !Installer.GetLatestVersionInfo( rssFile, m_gameTitle, out o_gameVersion, out gameDescription, out o_downloadURL, out updateDescription, out gameVersionIsNewest ) ) 464 | { 465 | o_gameVersion = null; 466 | o_downloadURL = null; 467 | return false; 468 | } 469 | GameDescription = gameDescription; 470 | return true; 471 | } 472 | 473 | private void Fail() 474 | { 475 | Stage = GameUpdateStage.Failed; 476 | Logger.Log( "Update failed" ); 477 | } 478 | 479 | private void Finish() 480 | { 481 | Stage = GameUpdateStage.Finished; 482 | Logger.Log( "Update finished" ); 483 | } 484 | 485 | private void FailOrCancel() 486 | { 487 | if( Cancelled ) 488 | { 489 | Stage = GameUpdateStage.Cancelled; 490 | Logger.Log( "Update cancelled" ); 491 | } 492 | else 493 | { 494 | Fail(); 495 | } 496 | } 497 | 498 | private bool TryCancel() 499 | { 500 | if( Cancelled ) 501 | { 502 | Stage = GameUpdateStage.Cancelled; 503 | Logger.Log( "Update cancelled" ); 504 | return true; 505 | } 506 | return false; 507 | } 508 | 509 | public void Start() 510 | { 511 | if( Stage == GameUpdateStage.NotStarted ) 512 | { 513 | Task.Factory.StartNew( delegate() 514 | { 515 | try 516 | { 517 | string latestInstalledVersion = Installer.GetLatestInstalledVersion( m_gameTitle ); 518 | string embeddedGameVersion = Installer.GetEmbeddedGameVersion( m_gameTitle ); 519 | if( m_optionalGameVersion != null ) 520 | { 521 | // A specific version has been requested 522 | // Try to locate it: 523 | if( Installer.IsGameInstalled( m_gameTitle, m_optionalGameVersion ) ) 524 | { 525 | if( m_optionalGameVersion == embeddedGameVersion ) 526 | { 527 | // Try to extract it 528 | if( !ExtractAndInstall( false ) ) 529 | { 530 | FailOrCancel(); 531 | return; 532 | } 533 | } 534 | else if( m_optionalUpdateURL != null ) 535 | { 536 | // Try to download it 537 | string downloadURL; 538 | bool isNewest; 539 | if( !GetSpecificDownloadURL( m_optionalUpdateURL, m_optionalGameVersion, out downloadURL, out isNewest ) ) 540 | { 541 | FailOrCancel(); 542 | return; 543 | } 544 | if( !DownloadAndInstall( m_optionalGameVersion, downloadURL, isNewest ) ) 545 | { 546 | FailOrCancel(); 547 | return; 548 | } 549 | } 550 | else 551 | { 552 | // Give up 553 | Fail(); 554 | return; 555 | } 556 | } 557 | 558 | // Try to run it 559 | if( !Launch( m_optionalGameVersion ) ) 560 | { 561 | FailOrCancel(); 562 | return; 563 | } 564 | 565 | // Finish 566 | Finish(); 567 | } 568 | else 569 | { 570 | // The "latest" version has been requested 571 | // Try to determine what it is: 572 | string latestVersion = null; 573 | string latestVersionDownloadURL = null; 574 | if( m_optionalUpdateURL != null ) 575 | { 576 | if( !GetLatestDownloadURL( m_optionalUpdateURL, out latestVersion, out latestVersionDownloadURL ) ) 577 | { 578 | if( TryCancel() ) 579 | { 580 | return; 581 | } 582 | } 583 | } 584 | 585 | string launchVersion = null; 586 | if( latestVersion != null ) 587 | { 588 | Logger.Log( "Latest version is {0}", latestVersion ); 589 | if( Installer.IsGameInstalled( m_gameTitle, latestVersion ) ) 590 | { 591 | // If we already have it, there's nothing to do 592 | launchVersion = latestVersion; 593 | Installer.RecordLatestInstalledVersion( m_gameTitle, latestVersion, true ); 594 | } 595 | else if( latestVersion == embeddedGameVersion ) 596 | { 597 | // If the latest version is the embedded version, just extract it 598 | if( ExtractAndInstall( true ) ) 599 | { 600 | launchVersion = embeddedGameVersion; 601 | } 602 | else 603 | { 604 | FailOrCancel(); 605 | return; 606 | } 607 | } 608 | else 609 | { 610 | // Try to download it (with the users consent) 611 | if( latestVersionDownloadURL != null ) 612 | { 613 | bool fallbackAvailable = (latestInstalledVersion != null) || (embeddedGameVersion != null); 614 | bool userPromptResult = false; 615 | if( fallbackAvailable ) 616 | { 617 | userPromptResult = ShowPrompt( GameUpdatePrompt.DownloadNewVersion ); 618 | if( TryCancel() ) 619 | { 620 | return; 621 | } 622 | } 623 | if( !fallbackAvailable || userPromptResult ) 624 | { 625 | if( TryCancel() ) 626 | { 627 | return; 628 | } 629 | if( DownloadAndInstall( latestVersion, latestVersionDownloadURL, true ) ) 630 | { 631 | launchVersion = latestVersion; 632 | } 633 | else 634 | { 635 | if( TryCancel() ) 636 | { 637 | return; 638 | } 639 | if( !fallbackAvailable ) 640 | { 641 | Fail(); 642 | return; 643 | } 644 | else if( !ShowPrompt( GameUpdatePrompt.LaunchOldVersion ) ) 645 | { 646 | Fail(); 647 | return; 648 | } 649 | } 650 | } 651 | } 652 | } 653 | } 654 | 655 | // Try one of the fallback methods 656 | if( launchVersion == null ) 657 | { 658 | if( latestInstalledVersion != null ) 659 | { 660 | launchVersion = latestInstalledVersion; 661 | } 662 | else if( embeddedGameVersion != null ) 663 | { 664 | if( ExtractAndInstall( false ) ) 665 | { 666 | launchVersion = embeddedGameVersion; 667 | } 668 | else 669 | { 670 | FailOrCancel(); 671 | return; 672 | } 673 | } 674 | else 675 | { 676 | Fail(); 677 | return; 678 | } 679 | } 680 | 681 | // Try to run it 682 | if( !Launch( launchVersion ) ) 683 | { 684 | FailOrCancel(); 685 | return; 686 | } 687 | 688 | // Finish 689 | Finish(); 690 | } 691 | } 692 | catch( Exception e ) 693 | { 694 | Logger.Log( "Caught exception: {0}", e.ToString() ); 695 | Logger.Log( e.StackTrace ); 696 | Fail(); 697 | } 698 | } ); 699 | } 700 | } 701 | 702 | public void AnswerPrompt( bool response, string username=null, string password=null ) 703 | { 704 | lock( this ) 705 | { 706 | if( m_currentPrompt != GameUpdatePrompt.None ) 707 | { 708 | Logger.Log( "Prompt answered: {0} {1} {2}", response, username, password ); 709 | m_currentPrompt = GameUpdatePrompt.None; 710 | m_promptResponse = response; 711 | m_promptUsername = username; 712 | m_promptPassword = password; 713 | m_promptWaitHandle.Set(); 714 | } 715 | } 716 | } 717 | 718 | public void Cancel() 719 | { 720 | lock( this ) 721 | { 722 | Logger.Log( "Cancelling update" ); 723 | m_cancelled = true; 724 | } 725 | } 726 | } 727 | } 728 | 729 | --------------------------------------------------------------------------------