installer.iss 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865
  1. ; VCMI Installer Script Instructions
  2. ; Steps to Add a New Translation to the Installer:
  3. ; 1. Download the ISL file for your language from the Inno Setup repository:
  4. ; https://github.com/jrsoftware/issrc/tree/main/Files/Languages
  5. ;
  6. ; 2. Add the required VCMI custom messages to the downloaded ISL file.
  7. ; - Refer to the English.isl file for examples of the necessary custom messages.
  8. ; - Ensure all custom messages, including WindowsVersionNotSupported, are properly translated and aligned with the English version.
  9. ; 3. Update the ConfirmUninstall message:
  10. ; - Custom Uninstall Wizard is used, ensure the ConfirmUninstall message is consistent with the English version and accurately reflects the intended functionality.
  11. ; 4. Update the WindowsVersionNotSupported message:
  12. ; - Ensure the WindowsVersionNotSupported message is consistent with the English version and accurately reflects the intended functionality.
  13. ; 5. Add the new language entry to the [Languages] section of the script:
  14. ; - Use the correct syntax to include the language and its corresponding ISL file in the installer configuration.
  15. ; Manual preprocessor definitions are provided using ISCC.exe parameters.
  16. ; #define AppVersion "1.7.0"
  17. ; #define AppBuild "1122334455A"
  18. ; #define InstallerArch "x64"
  19. ; #define AllowedArch "x64compatible"
  20. ; #define VCMIFolder "VCMI"
  21. ; #define InstallerName "VCMI-Windows"
  22. ; #define SourceFilesPath "C:\_VCMI_source\bin\Release"
  23. ; #define UCRTFilesPath "C:\Program Files (x86)\Windows Kits\10\Redist\10.0.22621.0\ucrt\DLLs"
  24. ; #define LangPath "C:\_VCMI_Source\CI\wininstaller\lang"
  25. ; #define LicenseFile "C:\_VCMI_Source\license.txt"
  26. ; #define IconFile "C:\_VCMI_Source\clientapp\icons\vcmi.ico"
  27. ; #define SmallLogo "C:\_VCMI_Source\CI\wininstaller\vcmismalllogo.bmp"
  28. ; #define WizardLogo "C:\_VCMI_Source\CI\wininstaller\vcmilogo.bmp"
  29. #define VCMIFilesFolder "My Games\vcmi"
  30. #define AppComment "VCMI is an open-source engine for Heroes III, offering new and extended possibilities."
  31. #define VCMITeam "VCMI Team"
  32. #define VCMICopyright "Copyright © VCMI Community. All rights reserved."
  33. #define VCMIHome "https://vcmi.eu/"
  34. #define VCMIContact "https://discord.gg/chBT42V"
  35. [Setup]
  36. AppId={#VCMIFolder}.{#InstallerArch}
  37. AppName={#VCMIFolder}
  38. AppVersion={#AppVersion}.{#AppBuild}
  39. AppVerName={#VCMIFolder}
  40. AppPublisher={#VCMITeam}
  41. AppPublisherURL={#VCMIHome}
  42. AppSupportURL={#VCMIContact}
  43. AppComments={#AppComment}
  44. DefaultDirName={code:GetDefaultDir}
  45. DefaultGroupName={#VCMIFolder}
  46. UninstallDisplayIcon={app}\VCMI_launcher.exe
  47. OutputBaseFilename={#InstallerName}
  48. PrivilegesRequiredOverridesAllowed=commandline dialog
  49. ShowLanguageDialog=yes
  50. DisableWelcomePage=no
  51. DisableProgramGroupPage=yes
  52. ChangesAssociations=yes
  53. UsePreviousLanguage=yes
  54. DirExistsWarning=no
  55. UsePreviousAppDir=yes
  56. UsePreviousTasks=yes
  57. UsePreviousGroup=yes
  58. DisableStartupPrompt=yes
  59. UsedUserAreasWarning=no
  60. WindowResizable=no
  61. CloseApplicationsFilter=*.exe
  62. CloseApplications=force
  63. Compression=lzma2/ultra64
  64. SolidCompression=yes
  65. ArchitecturesAllowed={#AllowedArch}
  66. LicenseFile={#LicenseFile}
  67. SetupIconFile={#IconFile}
  68. WizardSmallImageFile={#SmallLogo}
  69. WizardImageFile={#WizardLogo}
  70. ; Version informations
  71. MinVersion=6.1sp1
  72. VersionInfoCompany={#VCMITeam}
  73. VersionInfoDescription={#VCMIFolder} {#AppVersion} Setup (Build {#AppBuild})
  74. VersionInfoProductName={#VCMIFolder}
  75. VersionInfoCopyright={#VCMICopyright}
  76. VersionInfoVersion={#AppVersion}
  77. VersionInfoOriginalFileName={#InstallerName}.exe
  78. [Languages]
  79. Name: "english"; MessagesFile: "{#LangPath}\English.isl"
  80. Name: "czech"; MessagesFile: "{#LangPath}\Czech.isl"
  81. Name: "chinese"; MessagesFile: "{#LangPath}\ChineseSimplified.isl"
  82. Name: "finnish"; MessagesFile: "{#LangPath}\Finnish.isl"
  83. Name: "french"; MessagesFile: "{#LangPath}\French.isl"
  84. Name: "german"; MessagesFile: "{#LangPath}\German.isl"
  85. Name: "hungarian"; MessagesFile: "{#LangPath}\Hungarian.isl"
  86. Name: "italian"; MessagesFile: "{#LangPath}\Italian.isl"
  87. Name: "korean"; MessagesFile: "{#LangPath}\Korean.isl"
  88. Name: "polish"; MessagesFile: "{#LangPath}\Polish.isl"
  89. Name: "portuguese"; MessagesFile: "{#LangPath}\BrazilianPortuguese.isl"
  90. Name: "russian"; MessagesFile: "{#LangPath}\Russian.isl"
  91. Name: "spanish"; MessagesFile: "{#LangPath}\Spanish.isl"
  92. Name: "swedish"; MessagesFile: "{#LangPath}\Swedish.isl"
  93. Name: "turkish"; MessagesFile: "{#LangPath}\Turkish.isl"
  94. Name: "ukrainian"; MessagesFile: "{#LangPath}\Ukrainian.isl"
  95. Name: "vietnamese"; MessagesFile: "{#LangPath}\Vietnamese.isl"
  96. [Files]
  97. Source: "{#SourceFilesPath}\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs; Excludes: "*.pdb,*.lib,*.exp,*.ilk,*.obj,*.tlog,*.log,*.pch,*.idb,*.res,*.tmp,*.bak,*.sdf,*.ipch,*.vc.db,*.iobj,*.ipdb"; BeforeInstall: RunPreInstallTasks
  98. Source: "{#UCRTFilesPath}\{#InstallerArch}\*"; DestDir: "{app}"; Flags: ignoreversion; Check: IsUCRTNeeded
  99. [Icons]
  100. Name: "{group}\{cm:ShortcutLauncher}"; Filename: "{app}\VCMI_launcher.exe"; Comment: "{cm:ShortcutLauncherComment}"
  101. Name: "{group}\{cm:ShortcutMapEditor}"; Filename: "{app}\VCMI_mapeditor.exe"; Comment: "{cm:ShortcutMapEditorComment}"
  102. Name: "{group}\{cm:ShortcutWebPage}"; Filename: "{#VCMIHome}"; Comment: "{cm:ShortcutWebPageComment}"
  103. Name: "{group}\{cm:ShortcutDiscord}"; Filename: "{#VCMIContact}"; Comment: "{cm:ShortcutDiscordComment}"
  104. Name: "{code:GetUserDesktopFolder}\{cm:ShortcutLauncher}"; Filename: "{app}\VCMI_launcher.exe"; Tasks: desktop; Comment: "{cm:ShortcutLauncherComment}"
  105. [Tasks]
  106. Name: "desktop"; Description: "{cm:CreateDesktopShortcuts}"; GroupDescription: "{cm:SystemIntegration}"
  107. Name: "startmenu"; Description: "{cm:CreateStartMenuShortcuts}"; GroupDescription: "{cm:SystemIntegration}"
  108. Name: "fileassociation_h3m"; Description: "{cm:AssociateH3MFiles}"; GroupDescription: "{cm:SystemIntegration}"; Flags: unchecked
  109. Name: "fileassociation_vcmimap"; Description: "{cm:AssociateVCMIMapFiles}"; GroupDescription: "{cm:SystemIntegration}"
  110. Name: "firewallrules"; Description: "{cm:AddFirewallRules}"; GroupDescription: "{cm:VCMISettings}"; Check: IsAdminInstallMode
  111. Name: "h3copyfiles"; Description: "{cm:CopyH3Files}"; GroupDescription: "{cm:VCMISettings}"; Check: IsHeroes3Installed and IsCopyFilesNeeded
  112. [Registry]
  113. Root: HKCU; Subkey: "Software\{#VCMIFolder}"; ValueType: string; ValueName: "InstallPath"; ValueData: "{app}"; Flags: uninsdeletekey
  114. Root: HKCU; Subkey: "Software\Classes\.vmap"; ValueType: string; ValueName: ""; ValueData: "VCMI.vmap"; Flags: uninsdeletevalue; Tasks: fileassociation_vcmimap
  115. Root: HKCU; Subkey: "Software\Classes\VCMI.vmap"; ValueType: string; ValueName: ""; ValueData: "{cm:VMAPDescription}"; Flags: uninsdeletekey; Tasks: fileassociation_vcmimap
  116. Root: HKCU; Subkey: "Software\Classes\VCMI.vmap\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\VCMI_mapeditor.exe"" ""%1"""; Tasks: fileassociation_vcmimap
  117. Root: HKCU; Subkey: "Software\Classes\.vcmp"; ValueType: string; ValueName: ""; ValueData: "VCMI.vcmp"; Flags: uninsdeletevalue; Tasks: fileassociation_vcmimap
  118. Root: HKCU; Subkey: "Software\Classes\VCMI.vcmp"; ValueType: string; ValueName: ""; ValueData: "{cm:VCMPDescription}"; Flags: uninsdeletekey; Tasks: fileassociation_vcmimap
  119. Root: HKCU; Subkey: "Software\Classes\VCMI.vcmp\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\VCMI_mapeditor.exe"" ""%1"""; Tasks: fileassociation_vcmimap
  120. Root: HKCU; Subkey: "Software\Classes\.h3m"; ValueType: string; ValueName: ""; ValueData: "VCMI.h3m"; Flags: uninsdeletevalue; Tasks: fileassociation_h3m
  121. Root: HKCU; Subkey: "Software\Classes\VCMI.h3m"; ValueType: string; ValueName: ""; ValueData: "{cm:H3MDescription}"; Flags: uninsdeletekey; Tasks: fileassociation_h3m
  122. Root: HKCU; Subkey: "Software\Classes\VCMI.h3m\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\VCMI_mapeditor.exe"" ""%1"""; Tasks: fileassociation_h3m
  123. [Run]
  124. Filename: "netsh.exe"; Parameters: "advfirewall firewall add rule name=vcmi_server dir=in action=allow program=""{app}\vcmi_server.exe"" enable=yes profile=public,private"; Flags: runhidden; Tasks: firewallrules; Check: IsAdmin
  125. Filename: "netsh.exe"; Parameters: "advfirewall firewall add rule name=vcmi_client dir=in action=allow program=""{app}\vcmi_client.exe"" enable=yes profile=public,private"; Flags: runhidden; Tasks: firewallrules; Check: IsAdmin
  126. Filename: "{app}\VCMI_launcher.exe"; Description: "{cm:RunVCMILauncherAfterInstall}"; Flags: nowait postinstall; Check: ShouldRunLauncher
  127. [UninstallRun]
  128. ; Kill VCMI processes
  129. Filename: "taskkill.exe"; Parameters: "/F /IM VCMI_client.exe"; Flags: runhidden; RunOnceId: "KillVCMIClient"
  130. Filename: "taskkill.exe"; Parameters: "/F /IM VCMI_server.exe"; Flags: runhidden; RunOnceId: "KillVCMIServer"
  131. Filename: "taskkill.exe"; Parameters: "/F /IM VCMI_launcher.exe"; Flags: runhidden; RunOnceId: "KillVCMILauncher"
  132. Filename: "taskkill.exe"; Parameters: "/F /IM VCMI_mapeditor.exe"; Flags: runhidden; RunOnceId: "KillVCMIMapEditor"
  133. ; Remove firewall rules
  134. Filename: "netsh.exe"; Parameters: "advfirewall firewall delete rule name=vcmi_server"; Flags: runhidden; Check: IsAdmin; RunOnceId: "RemoveFirewallVCMIServer"
  135. Filename: "netsh.exe"; Parameters: "advfirewall firewall delete rule name=vcmi_client"; Flags: runhidden; Check: IsAdmin; RunOnceId: "RemoveFirewallVCMIClient"
  136. [Code]
  137. var
  138. InstallModePage: TInputOptionWizardPage;
  139. FooterLabel: TLabel;
  140. IsUpgrade: Boolean;
  141. Heroes3Path: String;
  142. GlobalUserName: String;
  143. GlobalUserDocsFolder: String;
  144. GlobalUserAppdataFolder: String;
  145. VCMIMapsFolder, VCMIDataFolder, VCMIMp3Folder: String;
  146. Heroes3MapsFolder, Heroes3DataFolder, Heroes3Mp3Folder: String;
  147. function RegistryQueryPath(Key, ValueName: String): String;
  148. begin
  149. if RegQueryStringValue(HKLM, Key, ValueName, Result) then
  150. Exit
  151. else
  152. Result := '';
  153. end;
  154. function ShouldRunLauncher(): Boolean;
  155. begin
  156. Result := True;
  157. if Pos('SILENT', UpperCase(GetCmdTail())) > 0 then
  158. Result := False;
  159. if Pos('LAUNCH', UpperCase(GetCmdTail())) > 0 then
  160. Result := True;
  161. end;
  162. function FolderSize(FolderPath: String): Int64;
  163. var
  164. FindRec: TFindRec;
  165. begin
  166. Result := 0;
  167. if FindFirst(FolderPath + '\*', FindRec) then
  168. begin
  169. try
  170. repeat
  171. if (FindRec.Attributes and FILE_ATTRIBUTE_DIRECTORY) = 0 then
  172. Result := Result + FindRec.SizeLow
  173. else if (FindRec.Name <> '.') and (FindRec.Name <> '..') then
  174. Result := Result + FolderSize(FolderPath + '\' + FindRec.Name);
  175. until not FindNext(FindRec);
  176. finally
  177. FindClose(FindRec);
  178. end;
  179. end;
  180. end;
  181. function IsFolderValid(FolderPath: String): Boolean;
  182. begin
  183. Result := DirExists(FolderPath) and (FolderSize(FolderPath) > 1024 * 1024);
  184. end;
  185. procedure CopyFolderContents(SourceDir, DestDir: String; Overwrite: Boolean);
  186. var
  187. FindRec: TFindRec;
  188. SourceFile, DestFile: String;
  189. begin
  190. // Ensure the destination directory exists
  191. if not DirExists(DestDir) then
  192. if not ForceDirectories(DestDir) then
  193. begin
  194. //MsgBox('Failed to create destination directory: ' + DestDir, mbError, MB_OK);
  195. Exit;
  196. end;
  197. // Start file copying
  198. if FindFirst(SourceDir + '\*.*', FindRec) then
  199. begin
  200. try
  201. repeat
  202. SourceFile := SourceDir + '\' + FindRec.Name;
  203. DestFile := DestDir + '\' + FindRec.Name;
  204. if (FindRec.Attributes and FILE_ATTRIBUTE_DIRECTORY) = 0 then
  205. begin
  206. if Overwrite or not FileExists(DestFile) then
  207. begin
  208. if not FileCopy(SourceFile, DestFile, False) then
  209. //MsgBox('Failed to copy file: ' + SourceFile + ' to ' + DestFile, mbError, MB_OK);
  210. end;
  211. end
  212. else if (FindRec.Name <> '.') and (FindRec.Name <> '..') then
  213. begin
  214. // Copy subdirectories recursively
  215. CopyFolderContents(SourceFile, DestFile, Overwrite);
  216. end;
  217. until not FindNext(FindRec);
  218. finally
  219. FindClose(FindRec);
  220. end;
  221. end
  222. //else
  223. // MsgBox('No files found in directory: ' + SourceDir, mbError, MB_OK);
  224. end;
  225. // A huge workaround to get non-admin profile name on elevated installer as admin
  226. function WTSQuerySessionInformation(hServer: THandle; SessionId: Cardinal; WTSInfoClass: Integer; var pBuffer: DWord; var BytesReturned: DWord): Boolean;
  227. external '[email protected] stdcall';
  228. procedure WTSFreeMemory(pMemory: DWord);
  229. external '[email protected] stdcall';
  230. procedure RtlMoveMemoryAsString(Dest: string; Source: DWord; Len: Integer);
  231. external '[email protected] stdcall';
  232. const
  233. WTS_CURRENT_SERVER_HANDLE = 0;
  234. WTS_CURRENT_SESSION = -1;
  235. WTSUserName = 5;
  236. function GetCurrentSessionUserName: string;
  237. var
  238. Buffer: DWord;
  239. BytesReturned: DWord;
  240. QueryResult: Boolean;
  241. begin
  242. // Initialize Result to an empty string
  243. Result := '';
  244. // Query the username for the current session
  245. QueryResult := WTSQuerySessionInformation(
  246. WTS_CURRENT_SERVER_HANDLE, WTS_CURRENT_SESSION, WTSUserName, Buffer, BytesReturned);
  247. if not QueryResult then
  248. begin
  249. // Error if the query fails
  250. Exit;
  251. end;
  252. try
  253. // Set the length of the result string (BytesReturned includes null terminator)
  254. SetLength(Result, (BytesReturned div 2) - 1); // Divide by 2 for Unicode and exclude null terminator
  255. // Copy the buffer contents into the result string
  256. RtlMoveMemoryAsString(Result, Buffer, BytesReturned - 2); // Exclude null terminator
  257. finally
  258. // Free the allocated memory
  259. WTSFreeMemory(Buffer);
  260. end;
  261. end;
  262. function GetCommonProgramFilesDir: String;
  263. begin
  264. if IsARM64 then
  265. begin
  266. if ExpandConstant('{#InstallerArch}') = 'x86' then
  267. // For 32-bit installer on ARM64, return the 32-bit Program Files directory
  268. Result := ExpandConstant('{commonpf32}')
  269. else
  270. // For AMR64 installer, return the Program Files directory
  271. Result := ExpandConstant('{commonpf}')
  272. end
  273. else if IsWin64 then
  274. begin
  275. if ExpandConstant('{#InstallerArch}') = 'x64' then
  276. // For 64-bit installer, return the 64-bit Program Files directory
  277. Result := ExpandConstant('{commonpf64}')
  278. else
  279. // For 32-bit installer on 64-bit system, return the 32-bit Program Files directory
  280. Result := ExpandConstant('{commonpf32}');
  281. end
  282. else
  283. // On 32-bit systems, always return the 32-bit Program Files directory
  284. Result := ExpandConstant('{commonpf32}');
  285. end;
  286. function GetDefaultDir(Default: String): String;
  287. begin
  288. if IsAdmin then
  289. // Default to Program Files for admins
  290. Result := GetCommonProgramFilesDir + '\{#VCMIFolder}'
  291. else
  292. // Default to User AppData for non-admin users
  293. Result := GlobalUserAppdataFolder + '\{#VCMIFolder}';
  294. end;
  295. function GetUserFolderPath(Constant: String): String;
  296. var
  297. FolderPath: String;
  298. OriginalUserName: String;
  299. CurrentSessionUserName: String;
  300. begin
  301. // Retrieve the current username from the session
  302. CurrentSessionUserName := '\' + GlobalUserName + '\';
  303. // Retrieve the original username
  304. OriginalUserName := '\' + GetUserNameString + '\';
  305. // Expand the specified constant
  306. FolderPath := ExpandConstant(Constant);
  307. // Replace the original username with the current session username in the path
  308. StringChangeEx(FolderPath, OriginalUserName, CurrentSessionUserName, True);
  309. // Return the modified folder path
  310. Result := FolderPath;
  311. end;
  312. procedure OnTaskCheck(Sender: TObject);
  313. var
  314. i: Integer;
  315. begin
  316. // Loop through all tasks in the tasks list
  317. for i := 0 to WizardForm.TasksList.Items.Count - 1 do
  318. begin
  319. // Check if the current task is "firewallrules"
  320. if WizardForm.TasksList.Items[i] = ExpandConstant('{cm:AddFirewallRules}') then
  321. begin
  322. // Check if the "firewallrules" task is unchecked
  323. if not WizardForm.TasksList.Checked[i] then
  324. begin
  325. // Show a custom warning message box
  326. MsgBox(ExpandConstant('{cm:Warning}') + '!' + #13#10 + #13#10 + ExpandConstant('{cm:InstallForMeOnly1}') + #13#10 + ExpandConstant('{cm:InstallForMeOnly2}'), mbError, MB_OK);
  327. end;
  328. Exit; // Task found, exit the loop
  329. end;
  330. end;
  331. end;
  332. // Specific functions for user folders
  333. function GetUserDocsFolder: String;
  334. begin
  335. Result := GetUserFolderPath('{userdocs}');
  336. end;
  337. function GetUserAppdataFolder: String;
  338. begin
  339. Result := GetUserFolderPath('{userappdata}');
  340. end;
  341. function GetUserDesktopFolder(Default: String): String;
  342. begin
  343. Result := GetUserFolderPath('{userdesktop}');
  344. end;
  345. function IsUCRTNeeded: Boolean;
  346. var
  347. FileName: String;
  348. begin
  349. Result := False; // Default to not copying files
  350. // Normalize and extract the file name from CurrentFileName
  351. FileName := ExtractFileName(ExpandConstant(CurrentFileName));
  352. // Check for file existence based on architecture
  353. if IsWin64 then
  354. begin
  355. if ExpandConstant('{#InstallerArch}') = 'x64' then
  356. // For 64-bit installer on 64-bit OS, check System32
  357. Result := not FileExists(ExpandConstant('{win}\System32\' + FileName))
  358. else
  359. // For 32-bit installer on 64-bit OS, check SysWOW64
  360. Result := not FileExists(ExpandConstant('{win}\SysWOW64\' + FileName));
  361. end
  362. else
  363. // For 32-bit OS, always check System32
  364. Result := not FileExists(ExpandConstant('{win}\System32\' + FileName));
  365. end;
  366. function IsHeroes3Installed(): Boolean;
  367. begin
  368. Result := False;
  369. if (Heroes3Path <> '') then
  370. Result := True;
  371. end;
  372. function IsCopyFilesNeeded(): Boolean;
  373. begin
  374. // Check if any of the required folders are not valid
  375. Result := not (IsFolderValid(VCMIDataFolder) and IsFolderValid(VCMIMapsFolder) and IsFolderValid(VCMIMp3Folder));
  376. end;
  377. function InitializeSetup(): Boolean;
  378. var
  379. InstallPath: String;
  380. begin
  381. // Check if the application is already installed
  382. IsUpgrade := RegQueryStringValue(HKCU, 'Software\{#VCMIFolder}', 'InstallPath', InstallPath);
  383. // Initialize the global variable during setup
  384. GlobalUserName := GetCurrentSessionUserName();
  385. GlobalUserDocsFolder := GetUserDocsFolder();
  386. GlobalUserAppdataFolder := GetUserAppdataFolder();
  387. // Define paths for VCMI
  388. VCMIMapsFolder := GlobalUserDocsFolder + '\' + '{#VCMIFilesFolder}' + '\Maps';
  389. VCMIDataFolder := GlobalUserDocsFolder + '\' + '{#VCMIFilesFolder}' + '\Data';
  390. VCMIMp3Folder := GlobalUserDocsFolder + '\' + '{#VCMIFilesFolder}' + '\Mp3';
  391. // Check for Heroes 3 installation paths
  392. Heroes3Path := RegistryQueryPath('SOFTWARE\GOG.com\Games\1207658787', 'path');
  393. if Heroes3Path = '' then
  394. Heroes3Path := RegistryQueryPath('SOFTWARE\WOW6432Node\GOG.com\Games\1207658787', 'path');
  395. if Heroes3Path = '' then
  396. Heroes3Path := RegistryQueryPath('SOFTWARE\New World Computing\Heroes of Might and Magic® III\1.0', 'AppPath');
  397. if Heroes3Path = '' then
  398. Heroes3Path := RegistryQueryPath('SOFTWARE\WOW6432Node\New World Computing\Heroes of Might and Magic® III\1.0', 'AppPath');
  399. if Heroes3Path = '' then
  400. Heroes3Path := RegistryQueryPath('SOFTWARE\New World Computing\Heroes of Might and Magic III\1.0', 'AppPath');
  401. if Heroes3Path = '' then
  402. Heroes3Path := RegistryQueryPath('SOFTWARE\WOW6432Node\New World Computing\Heroes of Might and Magic III\1.0', 'AppPath');
  403. if (Heroes3Path <> '') then
  404. begin
  405. Heroes3MapsFolder := Heroes3Path + '\Maps';
  406. Heroes3DataFolder := Heroes3Path + '\Data';
  407. Heroes3Mp3Folder := Heroes3Path + '\Mp3';
  408. end;
  409. Result := True;
  410. end;
  411. function InitializeUninstall(): Boolean;
  412. begin
  413. // Initialize the global variable during uninstall
  414. GlobalUserName := GetCurrentSessionUserName();
  415. GlobalUserDocsFolder := GetUserDocsFolder();
  416. GlobalUserAppdataFolder := GetUserAppdataFolder();
  417. Result := True;
  418. end;
  419. procedure InitializeWizard();
  420. begin
  421. // Check if the application is already installed
  422. if not IsUpgrade then
  423. begin
  424. // Create the install mode selection page only if it's not an upgrade
  425. InstallModePage := CreateInputOptionPage(
  426. wpWelcome,
  427. ExpandConstant('{cm:SelectSetupInstallModeTitle}'),
  428. ExpandConstant('{cm:SelectSetupInstallModeDesc}'),
  429. ExpandConstant('{cm:SelectSetupInstallModeSubTitle}'),
  430. True, False
  431. );
  432. // Option 0
  433. InstallModePage.Add(ExpandConstant(#13#10 + ' {cm:InstallForAllUsers}' + #13#10 + ' • {cm:InstallForAllUsers1}' + #13#10 + #13#10));
  434. // Option 1
  435. InstallModePage.Add(ExpandConstant(#13#10 + ' {cm:InstallForMeOnly}' + #13#10 + ' • {cm:InstallForMeOnly1}' + #13#10 + ' • {cm:InstallForMeOnly2}' + #13#10));
  436. if IsAdmin then
  437. begin
  438. // Default to "All Users"
  439. InstallModePage.SelectedValueIndex := 0;
  440. end
  441. else
  442. begin
  443. // Default to "Me Only"
  444. InstallModePage.SelectedValueIndex := 1;
  445. // Disable the first option ("Install for All Users") for non-admins
  446. InstallModePage.CheckListBox.ItemEnabled[0] := False;
  447. // Force a redraw of the CheckListBox to fix appearance
  448. InstallModePage.CheckListBox.Invalidate();
  449. end;
  450. end;
  451. // Attach an OnClick event handler to the tasks list
  452. WizardForm.TasksList.OnClickCheck := @OnTaskCheck;
  453. // Enable word wrap for the ReadyMemo
  454. WizardForm.ReadyMemo.ScrollBars := ssNone; // No scrollbars
  455. WizardForm.ReadyMemo.WordWrap := True;
  456. // Create a custom label for the footer message
  457. FooterLabel := TLabel.Create(WizardForm);
  458. FooterLabel.Parent := WizardForm;
  459. FooterLabel.Caption := '{#VCMIFolder} v' + '{#AppVersion}' + '.' + '{#AppBuild}';
  460. // Padding from the left edge
  461. FooterLabel.Left := 10;
  462. // Adjust to leave space for multiple lines
  463. FooterLabel.Top := WizardForm.ClientHeight - 30;
  464. // Adjust for padding
  465. FooterLabel.Width := WizardForm.ClientWidth - 20;
  466. // Adjust height to accommodate multiple lines
  467. FooterLabel.Height := 40;
  468. end;
  469. function ShouldSkipPage(PageID: Integer): Boolean;
  470. begin
  471. Result := False; // Default is not to skip the page
  472. if IsUpgrade then
  473. begin
  474. if (PageID = wpLicense) or (PageID = wpSelectTasks) or (PageID = wpReady) then
  475. begin
  476. Result := True; // Skip these pages during upgrade
  477. Exit;
  478. end;
  479. end;
  480. end;
  481. procedure CurPageChanged(CurPageID: Integer);
  482. begin
  483. // Ensure the footer message is visible on every page
  484. FooterLabel.Visible := True;
  485. end;
  486. function NextButtonClick(CurPageID: Integer): Boolean;
  487. begin
  488. // Skip the custom page on upgrade
  489. if IsUpgrade and Assigned(InstallModePage) and (CurPageID = InstallModePage.ID) then
  490. begin
  491. Result := True;
  492. Exit;
  493. end;
  494. // Handle logic for the custom page if it exists
  495. if Assigned(InstallModePage) and (CurPageID = InstallModePage.ID) then
  496. begin
  497. if (InstallModePage.SelectedValueIndex = 0) and not IsAdmin then
  498. begin
  499. Result := False;
  500. Exit;
  501. end;
  502. if InstallModePage.SelectedValueIndex = 0 then
  503. WizardForm.DirEdit.Text := GetCommonProgramFilesDir + '\{#VCMIFolder}'
  504. else
  505. WizardForm.DirEdit.Text := GlobalUserAppdataFolder + '\{#VCMIFolder}';
  506. end;
  507. Result := True;
  508. end;
  509. procedure RemoveLegacyInstaller();
  510. var
  511. AppFolder: String;
  512. ResultCode: Integer;
  513. begin
  514. AppFolder := ExpandConstant('{app}');
  515. // Silently remove old NSIS installation
  516. if FileExists(AppFolder + '\Uninstall.exe') then
  517. begin
  518. Exec(AppFolder + '\Uninstall.exe', '/S', '', SW_HIDE, ewWaitUntilTerminated, ResultCode);
  519. // Attempt to remove leftovers from target folder to ensure clean install
  520. if DirExists(AppFolder) then
  521. DelTree(AppFolder, True, True, False);
  522. end;
  523. end;
  524. procedure PerformHeroes3FileCopy();
  525. var
  526. i: Integer;
  527. begin
  528. // Loop through all tasks to find the "h3copyfiles" task
  529. for i := 0 to WizardForm.TasksList.Items.Count - 1 do
  530. begin
  531. // Check if the current task is "h3copyfiles"
  532. if WizardForm.TasksList.Items[i] = ExpandConstant('{cm:CopyH3Files}') then
  533. begin
  534. // Check if the "h3copyfiles" task is checked
  535. if WizardForm.TasksList.Checked[i] then
  536. begin
  537. if IsCopyFilesNeeded then
  538. begin
  539. // Copy folders if conditions are met
  540. if (IsFolderValid(Heroes3MapsFolder) and not IsFolderValid(VCMIMapsFolder)) then
  541. CopyFolderContents(Heroes3MapsFolder, VCMIMapsFolder, True);
  542. if (IsFolderValid(Heroes3DataFolder) and not IsFolderValid(VCMIDataFolder)) then
  543. CopyFolderContents(Heroes3DataFolder, VCMIDataFolder, True);
  544. if (IsFolderValid(Heroes3Mp3Folder) and not IsFolderValid(VCMIMp3Folder)) then
  545. CopyFolderContents(Heroes3Mp3Folder, VCMIMp3Folder, True);
  546. end;
  547. end;
  548. Exit; // Task found, exit the loop
  549. end;
  550. end;
  551. end;
  552. procedure RunPreInstallTasks();
  553. begin
  554. // Remove Legacy installer when needed
  555. RemoveLegacyInstaller();
  556. // Copy H3 files when needed
  557. PerformHeroes3FileCopy();
  558. end;
  559. /// Uninstall ///////////////////////////////////////////////////////////////////////////////////////////////////////////////
  560. var
  561. DeleteUserDataCheckbox: TNewCheckBox;
  562. DeleteUserDataLabel: TLabel;
  563. function DeleteFolderContents(const FolderPath: String): Boolean;
  564. var
  565. FindResult: TFindRec;
  566. SubPath: String;
  567. begin
  568. Result := True;
  569. if FindFirst(FolderPath + '\*', FindResult) then
  570. begin
  571. try
  572. repeat
  573. if (FindResult.Name <> '.') and (FindResult.Name <> '..') then
  574. begin
  575. SubPath := FolderPath + '\' + FindResult.Name;
  576. if (FindResult.Attributes and FILE_ATTRIBUTE_DIRECTORY) <> 0 then
  577. begin
  578. if not DeleteFolderContents(SubPath) then
  579. begin
  580. Result := False;
  581. Exit;
  582. end;
  583. if not RemoveDir(SubPath) then
  584. begin
  585. Result := False;
  586. Exit;
  587. end;
  588. end
  589. else
  590. begin
  591. if not DeleteFile(SubPath) then
  592. begin
  593. Result := False;
  594. Exit;
  595. end;
  596. end;
  597. end;
  598. until not FindNext(FindResult);
  599. finally
  600. FindClose(FindResult);
  601. end;
  602. end;
  603. end;
  604. procedure PerformFileDeletion;
  605. var
  606. UserDataFolder: String;
  607. begin
  608. if (DeleteUserDataCheckbox <> nil) and DeleteUserDataCheckbox.Checked then
  609. begin
  610. UserDataFolder := GlobalUserDocsFolder + '\' + '{#VCMIFilesFolder}';
  611. if DirExists(UserDataFolder) then
  612. begin
  613. if DeleteFolderContents(UserDataFolder) then
  614. begin
  615. if not RemoveDir(UserDataFolder) then
  616. begin
  617. // Log or handle failed root directory removal if necessary
  618. end;
  619. end;
  620. end;
  621. end;
  622. end;
  623. procedure CurUninstallStepChanged(CurUninstallStep: TUninstallStep);
  624. begin
  625. if CurUninstallStep = usUninstall then
  626. PerformFileDeletion;
  627. // Repeat delete process after uninstall due logs from killed processes during uninstall
  628. if CurUninstallStep = usPostUninstall then
  629. PerformFileDeletion;
  630. end;
  631. procedure UninsNextButtonOnClick(Sender: TObject);
  632. begin
  633. with UninstallProgressForm.InnerNotebook do
  634. begin
  635. ActivePage := Pages[ActivePage.PageIndex + 1];
  636. if ActivePage.PageIndex = PageCount - 1 then
  637. begin
  638. TButton(Sender).Hide;
  639. UninstallProgressForm.Close;
  640. end;
  641. end;
  642. end;
  643. procedure UninsCancelButtonOnClick(Sender: TObject);
  644. begin
  645. // Optionally handle user cancellation
  646. end;
  647. procedure InitializeUninstallProgressForm();
  648. var
  649. Page: TNewNotebookPage;
  650. UninsNextButton: TButton;
  651. begin
  652. with UninstallProgressForm do
  653. begin
  654. // -- Create the "Uninstall" button
  655. UninsNextButton := TButton.Create(UninstallProgressForm);
  656. with UninsNextButton do
  657. begin
  658. Parent := UninstallProgressForm;
  659. Top := CancelButton.Top;
  660. Width := CancelButton.Width;
  661. Height := CancelButton.Height;
  662. Left := CancelButton.Left - Width - ScaleX(10);
  663. Caption := ExpandConstant('{cm:Uninstall}');
  664. OnClick := @UninsNextButtonOnClick;
  665. TabOrder := 1; // Ensure this button is first in the tab order
  666. Default := True; // Make it the default button (triggered by Enter key)
  667. end;
  668. // -- Configure the Cancel button so it aborts the form
  669. CancelButton.Enabled := True;
  670. CancelButton.ModalResult := mrAbort;
  671. CancelButton.OnClick := @UninsCancelButtonOnClick;
  672. // -- Create a custom page (as the first page in the notebook)
  673. Page := TNewNotebookPage.Create(InnerNotebook);
  674. with Page do
  675. begin
  676. Parent := InnerNotebook;
  677. Notebook := InnerNotebook;
  678. PageIndex := 0; // first page
  679. end;
  680. // -- Create our "Delete user data" checkbox on that custom page
  681. DeleteUserDataCheckbox := TNewCheckBox.Create(UninstallProgressForm);
  682. with DeleteUserDataCheckbox do
  683. begin
  684. Parent := Page;
  685. Top := ScaleX(20);
  686. Left := ScaleX(20);
  687. Width := ScaleX(400);
  688. Checked := False;
  689. Caption := ExpandConstant('{cm:DeleteUserData}');
  690. TabOrder := 0; // Tab focus goes to this control after the Uninstall button
  691. end;
  692. // -- Add a label for the additional text
  693. DeleteUserDataLabel := TLabel.Create(UninstallProgressForm);
  694. with DeleteUserDataLabel do
  695. begin
  696. Parent := Page;
  697. Top := DeleteUserDataCheckbox.Top + ScaleY(20); // Position below the checkbox
  698. Left := DeleteUserDataCheckbox.Left + ScaleX(20); // Indent slightly to align with the text
  699. Width := ScaleX(400);
  700. Caption := GlobalUserDocsFolder + '\' + '{#VCMIFilesFolder}';
  701. end;
  702. // -- Activate the first page
  703. InnerNotebook.ActivePage := Page;
  704. // -- Make InstallingPage the last page
  705. InstallingPage.PageIndex := InnerNotebook.PageCount - 1;
  706. // -- Show the form modally; if user clicks Cancel, ShowModal = mrAbort -> Abort uninstallation
  707. if ShowModal = mrAbort then
  708. Abort;
  709. end;
  710. end;