From f908236492b391428a22d0b31bc9f88cad47991c Mon Sep 17 00:00:00 2001 From: Marc-Aurel Zent Date: Mon, 16 Jan 2023 23:21:18 +0100 Subject: [PATCH] initial xlcommon2 commit --- .../Compatibility/CompatibilityTools.cs | 281 +++++++ .../Compatibility/Dxvk.cs | 79 ++ .../Compatibility/DxvkSettings.cs | 131 +++ .../Compatibility/GameFixes/GameFix.cs | 28 + .../Compatibility/GameFixes/GameFixApply.cs | 33 + .../GameFixes/Implementations/MacVideoFix.cs | 58 ++ .../Compatibility/WineSettings.cs | 37 + .../UnixDalamudCompatibilityCheck.cs | 33 + .../UnixDalamudRunner.cs | 116 +++ .../UnixGameRunner.cs | 34 + src/XIVLauncher2.Common.Unix/UnixSteam.cs | 81 ++ .../XIVLauncher2.Common.Unix.csproj | 50 ++ .../libsteam_api64.dylib | Bin 0 -> 609584 bytes .../libsteam_api64.so | Bin 0 -> 416413 bytes .../NativeAclFix.cs | 537 ++++++++++++ .../WindowsDalamudCompatibilityCheck.cs | 124 +++ .../WindowsDalamudRunner.cs | 201 +++++ .../WindowsGameRunner.cs | 45 + .../WindowsRestartManager.cs | 270 ++++++ .../WindowsSteam.cs | 175 ++++ .../XIVLauncher2.Common.Windows.csproj | 44 + src/XIVLauncher2.Common.Windows/steam_api.dll | Bin 0 -> 263080 bytes .../steam_api64.dll | Bin 0 -> 295336 bytes src/XIVLauncher2.Common/Addon/AddonEntry.cs | 10 + src/XIVLauncher2.Common/Addon/AddonManager.cs | 70 ++ src/XIVLauncher2.Common/Addon/IAddon.cs | 9 + .../Addon/INotifyAddonAfterClose.cs | 7 + .../Addon/IPersistentAddon.cs | 7 + .../Addon/IRunnableAddon.cs | 7 + .../Addon/Implementations/GenericAddon.cs | 208 +++++ src/XIVLauncher2.Common/Class1.cs | 6 - src/XIVLauncher2.Common/ClientLanguage.cs | 39 + .../CommonUniqueIdCache.cs | 109 +++ src/XIVLauncher2.Common/Constants.cs | 32 + .../Dalamud/AssetManager.cs | 203 +++++ .../Dalamud/DalamudConsoleOutput.cs | 13 + .../Dalamud/DalamudInjectorArgs.cs | 22 + .../Dalamud/DalamudLauncher.cs | 170 ++++ .../Dalamud/DalamudLoadMethod.cs | 14 + .../Dalamud/DalamudRunnerException.cs | 11 + .../Dalamud/DalamudSettings.cs | 34 + .../Dalamud/DalamudStartInfo.cs | 20 + .../Dalamud/DalamudUpdater.cs | 501 +++++++++++ .../Dalamud/DalamudVersionInfo.cs | 30 + src/XIVLauncher2.Common/DpiAwareness.cs | 8 + .../Encryption/ArgumentBuilder.cs | 139 +++ .../Encryption/BlockCipher/Blowfish.cs | 93 ++ .../Encryption/BlockCipher/BlowfishState.cs | 369 ++++++++ .../Encryption/BlockCipher/Ecb.cs | 72 ++ .../Encryption/BlockCipher/IBlockCipher.cs | 54 ++ .../Encryption/BlockCipher/IBlockMode.cs | 18 + src/XIVLauncher2.Common/Encryption/CrtRand.cs | 17 + .../Encryption/LegacyBlowfish.cs | 316 +++++++ src/XIVLauncher2.Common/Encryption/Ticket.cs | 122 +++ .../EnvironmentSettings.cs | 14 + src/XIVLauncher2.Common/ExistingProcess.cs | 28 + .../Exceptions/BinaryNotPresentException.cs | 14 + .../Game/Exceptions/GameExitedException.cs | 11 + .../Exceptions/InvalidResponseException.cs | 14 + .../InvalidVersionFilesException.cs | 11 + .../Exceptions/NoVersionReferenceException.cs | 11 + .../Game/Exceptions/OauthLoginException.cs | 33 + .../Game/Exceptions/SteamException.cs | 11 + .../Exceptions/SteamLinkNeededException.cs | 11 + .../Exceptions/SteamWrongAccountException.cs | 11 + src/XIVLauncher2.Common/Game/GateStatus.cs | 16 + src/XIVLauncher2.Common/Game/Headlines.cs | 67 ++ .../Game/IntegrityCheck.cs | 152 ++++ src/XIVLauncher2.Common/Game/Launcher.cs | 702 ++++++++++++++++ .../Patch/Acquisition/AcquisitionMethod.cs | 17 + .../Patch/Acquisition/AcquisitionProgress.cs | 8 + .../Patch/Acquisition/AcquisitionResult.cs | 9 + .../Aria/AriaHttpPatchAcquisition.cs | 187 +++++ .../Patch/Acquisition/Aria/AriaManager.cs | 201 +++++ .../Acquisition/Aria/Attributes/AriaFile.cs | 31 + .../Aria/Attributes/AriaGlobalStatus.cs | 27 + .../Acquisition/Aria/Attributes/AriaOption.cs | 339 ++++++++ .../Acquisition/Aria/Attributes/AriaServer.cs | 32 + .../Aria/Attributes/AriaSession.cs | 15 + .../Acquisition/Aria/Attributes/AriaStatus.cs | 53 ++ .../Aria/Attributes/AriaTorrent.cs | 39 + .../Acquisition/Aria/Attributes/AriaUri.cs | 18 + .../Aria/Attributes/AriaVersionInfo.cs | 19 + .../Aria/JsonRpc/JsonRpcHttpClient.cs | 44 + .../Aria/JsonRpc/JsonRpcResponse.cs | 16 + .../NetDownloaderPatchAcquisition.cs | 92 ++ .../Patch/Acquisition/PatchAcquisition.cs | 26 + .../Acquisition/TorrentPatchAcquisition.cs | 104 +++ .../Game/Patch/GamePatchType.cs | 8 + .../Game/Patch/NotEnoughSpaceException.cs | 26 + .../Game/Patch/PatchInstaller.cs | 167 ++++ .../Game/Patch/PatchInstallerException.cs | 11 + .../Game/Patch/PatchList/PatchListEntry.cs | 33 + .../PatchList/PatchListParseException.cs | 14 + .../Game/Patch/PatchList/PatchListParser.cs | 44 + .../Game/Patch/PatchManager.cs | 524 ++++++++++++ .../Game/Patch/PatchVerifier.cs | 666 +++++++++++++++ src/XIVLauncher2.Common/Http/HttpServer.cs | 90 ++ src/XIVLauncher2.Common/Http/OtpListener.cs | 46 + .../IIndexedZiPatchIndexInstaller.cs | 53 ++ .../IndexedZiPatch/IndexedZiPatchIndex.cs | 325 +++++++ .../IndexedZiPatchIndexLocalInstaller.cs | 156 ++++ .../IndexedZiPatchIndexRemoteInstaller.cs | 701 ++++++++++++++++ .../IndexedZiPatch/IndexedZiPatchInstaller.cs | 793 ++++++++++++++++++ .../IndexedZiPatchOperations.cs | 174 ++++ .../IndexedZiPatchPartLocator.cs | 349 ++++++++ .../IndexedZiPatchTargetFile.cs | 232 +++++ .../IndexedZiPatchTargetViewStream.cs | 80 ++ .../Patching/RemotePatchInstaller.cs | 215 +++++ src/XIVLauncher2.Common/Patching/Rpc/IRpc.cs | 11 + .../Rpc/Implementations/InProcessRpc.cs | 52 ++ .../Rpc/Implementations/SharedMemoryRpc.cs | 41 + .../Patching/Rpc/IpcHelpers.cs | 16 + .../Patching/Rpc/PatcherIpcEnvelope.cs | 22 + .../Patching/Rpc/PatcherIpcOpCode.cs | 13 + .../Patching/Rpc/PatcherIpcStartInstall.cs | 29 + .../Patching/Util/BinaryReaderHelpers.cs | 126 +++ .../Patching/Util/ChecksumBinaryReader.cs | 53 ++ .../Patching/Util/CircularMemoryStream.cs | 268 ++++++ .../Patching/Util/Crc32.cs | 67 ++ .../Patching/Util/FullDeflateStreamReader.cs | 22 + .../Patching/Util/MultipartResponseHandler.cs | 376 +++++++++ .../Util/ReusableByteBufferManager.cs | 106 +++ .../ZiPatch/Chunk/AddDirectoryChunk.cs | 36 + .../ZiPatch/Chunk/ApplyFreeSpaceChunk.cs | 31 + .../ZiPatch/Chunk/ApplyOptionChunk.cs | 61 ++ .../ZiPatch/Chunk/DeleteDirectoryChunk.cs | 45 + .../Patching/ZiPatch/Chunk/EndOfFileChunk.cs | 23 + .../Patching/ZiPatch/Chunk/FileHeaderChunk.cs | 62 ++ .../Patching/ZiPatch/Chunk/SqpkChunk.cs | 66 ++ .../ZiPatch/Chunk/SqpkCommand/SqpkAddData.cs | 58 ++ .../Chunk/SqpkCommand/SqpkDeleteData.cs | 51 ++ .../Chunk/SqpkCommand/SqpkExpandData.cs | 51 ++ .../ZiPatch/Chunk/SqpkCommand/SqpkFile.cs | 111 +++ .../ZiPatch/Chunk/SqpkCommand/SqpkHeader.cs | 69 ++ .../ZiPatch/Chunk/SqpkCommand/SqpkIndex.cs | 53 ++ .../Chunk/SqpkCommand/SqpkPatchInfo.cs | 35 + .../Chunk/SqpkCommand/SqpkTargetInfo.cs | 55 ++ .../Patching/ZiPatch/Chunk/XXXXChunk.cs | 25 + .../Patching/ZiPatch/Chunk/ZiPatchChunk.cs | 113 +++ .../Patching/ZiPatch/Util/SqexFile.cs | 55 ++ .../Patching/ZiPatch/Util/SqexFileStream.cs | 55 ++ .../ZiPatch/Util/SqexFileStreamStore.cs | 31 + .../Patching/ZiPatch/Util/SqpackDatFile.cs | 37 + .../Patching/ZiPatch/Util/SqpackFile.cs | 38 + .../Patching/ZiPatch/Util/SqpackIndexFile.cs | 13 + .../ZiPatch/Util/SqpkCompressedBlock.cs | 45 + .../Patching/ZiPatch/ZiPatchConfig.cs | 27 + .../Patching/ZiPatch/ZiPatchException.cs | 11 + .../Patching/ZiPatch/ZiPatchFile.cs | 61 ++ src/XIVLauncher2.Common/Paths.cs | 22 + src/XIVLauncher2.Common/Platform.cs | 10 + .../IDalamudCompatibilityCheck.cs | 20 + .../IDalamudLoadingOverlay.cs | 21 + .../PlatformAbstractions/IDalamudRunner.cs | 11 + .../PlatformAbstractions/IGameRunner.cs | 9 + .../PlatformAbstractions/ISettings.cs | 16 + .../PlatformAbstractions/ISteam.cs | 32 + .../PlatformAbstractions/IUniqueIdCache.cs | 19 + src/XIVLauncher2.Common/Repository.cs | 113 +++ src/XIVLauncher2.Common/SeVersion.cs | 88 ++ src/XIVLauncher2.Common/SettingsAnnotation.cs | 17 + src/XIVLauncher2.Common/Storage.cs | 49 ++ src/XIVLauncher2.Common/Support/LogInit.cs | 68 ++ .../Support/SerilogEventSink.cs | 50 ++ src/XIVLauncher2.Common/Util/ApiHelpers.cs | 80 ++ src/XIVLauncher2.Common/Util/DebugHelpers.cs | 73 ++ src/XIVLauncher2.Common/Util/GameHelpers.cs | 105 +++ .../Util/HttpClientWithProgress.cs | 90 ++ .../Util/PlatformHelpers.cs | 148 ++++ .../XIVLauncher2.Common.csproj | 58 +- src/XIVLauncher2.sln | 22 +- 172 files changed, 15803 insertions(+), 15 deletions(-) create mode 100644 src/XIVLauncher2.Common.Unix/Compatibility/CompatibilityTools.cs create mode 100644 src/XIVLauncher2.Common.Unix/Compatibility/Dxvk.cs create mode 100644 src/XIVLauncher2.Common.Unix/Compatibility/DxvkSettings.cs create mode 100644 src/XIVLauncher2.Common.Unix/Compatibility/GameFixes/GameFix.cs create mode 100644 src/XIVLauncher2.Common.Unix/Compatibility/GameFixes/GameFixApply.cs create mode 100644 src/XIVLauncher2.Common.Unix/Compatibility/GameFixes/Implementations/MacVideoFix.cs create mode 100644 src/XIVLauncher2.Common.Unix/Compatibility/WineSettings.cs create mode 100644 src/XIVLauncher2.Common.Unix/UnixDalamudCompatibilityCheck.cs create mode 100644 src/XIVLauncher2.Common.Unix/UnixDalamudRunner.cs create mode 100644 src/XIVLauncher2.Common.Unix/UnixGameRunner.cs create mode 100644 src/XIVLauncher2.Common.Unix/UnixSteam.cs create mode 100644 src/XIVLauncher2.Common.Unix/XIVLauncher2.Common.Unix.csproj create mode 100644 src/XIVLauncher2.Common.Unix/libsteam_api64.dylib create mode 100644 src/XIVLauncher2.Common.Unix/libsteam_api64.so create mode 100644 src/XIVLauncher2.Common.Windows/NativeAclFix.cs create mode 100644 src/XIVLauncher2.Common.Windows/WindowsDalamudCompatibilityCheck.cs create mode 100644 src/XIVLauncher2.Common.Windows/WindowsDalamudRunner.cs create mode 100644 src/XIVLauncher2.Common.Windows/WindowsGameRunner.cs create mode 100644 src/XIVLauncher2.Common.Windows/WindowsRestartManager.cs create mode 100644 src/XIVLauncher2.Common.Windows/WindowsSteam.cs create mode 100644 src/XIVLauncher2.Common.Windows/XIVLauncher2.Common.Windows.csproj create mode 100644 src/XIVLauncher2.Common.Windows/steam_api.dll create mode 100644 src/XIVLauncher2.Common.Windows/steam_api64.dll create mode 100644 src/XIVLauncher2.Common/Addon/AddonEntry.cs create mode 100644 src/XIVLauncher2.Common/Addon/AddonManager.cs create mode 100644 src/XIVLauncher2.Common/Addon/IAddon.cs create mode 100644 src/XIVLauncher2.Common/Addon/INotifyAddonAfterClose.cs create mode 100644 src/XIVLauncher2.Common/Addon/IPersistentAddon.cs create mode 100644 src/XIVLauncher2.Common/Addon/IRunnableAddon.cs create mode 100644 src/XIVLauncher2.Common/Addon/Implementations/GenericAddon.cs delete mode 100644 src/XIVLauncher2.Common/Class1.cs create mode 100644 src/XIVLauncher2.Common/ClientLanguage.cs create mode 100644 src/XIVLauncher2.Common/CommonUniqueIdCache.cs create mode 100644 src/XIVLauncher2.Common/Constants.cs create mode 100644 src/XIVLauncher2.Common/Dalamud/AssetManager.cs create mode 100644 src/XIVLauncher2.Common/Dalamud/DalamudConsoleOutput.cs create mode 100644 src/XIVLauncher2.Common/Dalamud/DalamudInjectorArgs.cs create mode 100644 src/XIVLauncher2.Common/Dalamud/DalamudLauncher.cs create mode 100644 src/XIVLauncher2.Common/Dalamud/DalamudLoadMethod.cs create mode 100644 src/XIVLauncher2.Common/Dalamud/DalamudRunnerException.cs create mode 100644 src/XIVLauncher2.Common/Dalamud/DalamudSettings.cs create mode 100644 src/XIVLauncher2.Common/Dalamud/DalamudStartInfo.cs create mode 100644 src/XIVLauncher2.Common/Dalamud/DalamudUpdater.cs create mode 100644 src/XIVLauncher2.Common/Dalamud/DalamudVersionInfo.cs create mode 100644 src/XIVLauncher2.Common/DpiAwareness.cs create mode 100644 src/XIVLauncher2.Common/Encryption/ArgumentBuilder.cs create mode 100644 src/XIVLauncher2.Common/Encryption/BlockCipher/Blowfish.cs create mode 100644 src/XIVLauncher2.Common/Encryption/BlockCipher/BlowfishState.cs create mode 100644 src/XIVLauncher2.Common/Encryption/BlockCipher/Ecb.cs create mode 100644 src/XIVLauncher2.Common/Encryption/BlockCipher/IBlockCipher.cs create mode 100644 src/XIVLauncher2.Common/Encryption/BlockCipher/IBlockMode.cs create mode 100644 src/XIVLauncher2.Common/Encryption/CrtRand.cs create mode 100644 src/XIVLauncher2.Common/Encryption/LegacyBlowfish.cs create mode 100644 src/XIVLauncher2.Common/Encryption/Ticket.cs create mode 100644 src/XIVLauncher2.Common/EnvironmentSettings.cs create mode 100644 src/XIVLauncher2.Common/ExistingProcess.cs create mode 100644 src/XIVLauncher2.Common/Game/Exceptions/BinaryNotPresentException.cs create mode 100644 src/XIVLauncher2.Common/Game/Exceptions/GameExitedException.cs create mode 100644 src/XIVLauncher2.Common/Game/Exceptions/InvalidResponseException.cs create mode 100644 src/XIVLauncher2.Common/Game/Exceptions/InvalidVersionFilesException.cs create mode 100644 src/XIVLauncher2.Common/Game/Exceptions/NoVersionReferenceException.cs create mode 100644 src/XIVLauncher2.Common/Game/Exceptions/OauthLoginException.cs create mode 100644 src/XIVLauncher2.Common/Game/Exceptions/SteamException.cs create mode 100644 src/XIVLauncher2.Common/Game/Exceptions/SteamLinkNeededException.cs create mode 100644 src/XIVLauncher2.Common/Game/Exceptions/SteamWrongAccountException.cs create mode 100644 src/XIVLauncher2.Common/Game/GateStatus.cs create mode 100644 src/XIVLauncher2.Common/Game/Headlines.cs create mode 100644 src/XIVLauncher2.Common/Game/IntegrityCheck.cs create mode 100644 src/XIVLauncher2.Common/Game/Launcher.cs create mode 100644 src/XIVLauncher2.Common/Game/Patch/Acquisition/AcquisitionMethod.cs create mode 100644 src/XIVLauncher2.Common/Game/Patch/Acquisition/AcquisitionProgress.cs create mode 100644 src/XIVLauncher2.Common/Game/Patch/Acquisition/AcquisitionResult.cs create mode 100644 src/XIVLauncher2.Common/Game/Patch/Acquisition/Aria/AriaHttpPatchAcquisition.cs create mode 100644 src/XIVLauncher2.Common/Game/Patch/Acquisition/Aria/AriaManager.cs create mode 100644 src/XIVLauncher2.Common/Game/Patch/Acquisition/Aria/Attributes/AriaFile.cs create mode 100644 src/XIVLauncher2.Common/Game/Patch/Acquisition/Aria/Attributes/AriaGlobalStatus.cs create mode 100644 src/XIVLauncher2.Common/Game/Patch/Acquisition/Aria/Attributes/AriaOption.cs create mode 100644 src/XIVLauncher2.Common/Game/Patch/Acquisition/Aria/Attributes/AriaServer.cs create mode 100644 src/XIVLauncher2.Common/Game/Patch/Acquisition/Aria/Attributes/AriaSession.cs create mode 100644 src/XIVLauncher2.Common/Game/Patch/Acquisition/Aria/Attributes/AriaStatus.cs create mode 100644 src/XIVLauncher2.Common/Game/Patch/Acquisition/Aria/Attributes/AriaTorrent.cs create mode 100644 src/XIVLauncher2.Common/Game/Patch/Acquisition/Aria/Attributes/AriaUri.cs create mode 100644 src/XIVLauncher2.Common/Game/Patch/Acquisition/Aria/Attributes/AriaVersionInfo.cs create mode 100644 src/XIVLauncher2.Common/Game/Patch/Acquisition/Aria/JsonRpc/JsonRpcHttpClient.cs create mode 100644 src/XIVLauncher2.Common/Game/Patch/Acquisition/Aria/JsonRpc/JsonRpcResponse.cs create mode 100644 src/XIVLauncher2.Common/Game/Patch/Acquisition/NetDownloaderPatchAcquisition.cs create mode 100644 src/XIVLauncher2.Common/Game/Patch/Acquisition/PatchAcquisition.cs create mode 100644 src/XIVLauncher2.Common/Game/Patch/Acquisition/TorrentPatchAcquisition.cs create mode 100644 src/XIVLauncher2.Common/Game/Patch/GamePatchType.cs create mode 100644 src/XIVLauncher2.Common/Game/Patch/NotEnoughSpaceException.cs create mode 100644 src/XIVLauncher2.Common/Game/Patch/PatchInstaller.cs create mode 100644 src/XIVLauncher2.Common/Game/Patch/PatchInstallerException.cs create mode 100644 src/XIVLauncher2.Common/Game/Patch/PatchList/PatchListEntry.cs create mode 100644 src/XIVLauncher2.Common/Game/Patch/PatchList/PatchListParseException.cs create mode 100644 src/XIVLauncher2.Common/Game/Patch/PatchList/PatchListParser.cs create mode 100644 src/XIVLauncher2.Common/Game/Patch/PatchManager.cs create mode 100644 src/XIVLauncher2.Common/Game/Patch/PatchVerifier.cs create mode 100644 src/XIVLauncher2.Common/Http/HttpServer.cs create mode 100644 src/XIVLauncher2.Common/Http/OtpListener.cs create mode 100644 src/XIVLauncher2.Common/Patching/IndexedZiPatch/IIndexedZiPatchIndexInstaller.cs create mode 100644 src/XIVLauncher2.Common/Patching/IndexedZiPatch/IndexedZiPatchIndex.cs create mode 100644 src/XIVLauncher2.Common/Patching/IndexedZiPatch/IndexedZiPatchIndexLocalInstaller.cs create mode 100644 src/XIVLauncher2.Common/Patching/IndexedZiPatch/IndexedZiPatchIndexRemoteInstaller.cs create mode 100644 src/XIVLauncher2.Common/Patching/IndexedZiPatch/IndexedZiPatchInstaller.cs create mode 100644 src/XIVLauncher2.Common/Patching/IndexedZiPatch/IndexedZiPatchOperations.cs create mode 100644 src/XIVLauncher2.Common/Patching/IndexedZiPatch/IndexedZiPatchPartLocator.cs create mode 100644 src/XIVLauncher2.Common/Patching/IndexedZiPatch/IndexedZiPatchTargetFile.cs create mode 100644 src/XIVLauncher2.Common/Patching/IndexedZiPatch/IndexedZiPatchTargetViewStream.cs create mode 100644 src/XIVLauncher2.Common/Patching/RemotePatchInstaller.cs create mode 100644 src/XIVLauncher2.Common/Patching/Rpc/IRpc.cs create mode 100644 src/XIVLauncher2.Common/Patching/Rpc/Implementations/InProcessRpc.cs create mode 100644 src/XIVLauncher2.Common/Patching/Rpc/Implementations/SharedMemoryRpc.cs create mode 100644 src/XIVLauncher2.Common/Patching/Rpc/IpcHelpers.cs create mode 100644 src/XIVLauncher2.Common/Patching/Rpc/PatcherIpcEnvelope.cs create mode 100644 src/XIVLauncher2.Common/Patching/Rpc/PatcherIpcOpCode.cs create mode 100644 src/XIVLauncher2.Common/Patching/Rpc/PatcherIpcStartInstall.cs create mode 100644 src/XIVLauncher2.Common/Patching/Util/BinaryReaderHelpers.cs create mode 100644 src/XIVLauncher2.Common/Patching/Util/ChecksumBinaryReader.cs create mode 100644 src/XIVLauncher2.Common/Patching/Util/CircularMemoryStream.cs create mode 100644 src/XIVLauncher2.Common/Patching/Util/Crc32.cs create mode 100644 src/XIVLauncher2.Common/Patching/Util/FullDeflateStreamReader.cs create mode 100644 src/XIVLauncher2.Common/Patching/Util/MultipartResponseHandler.cs create mode 100644 src/XIVLauncher2.Common/Patching/Util/ReusableByteBufferManager.cs create mode 100644 src/XIVLauncher2.Common/Patching/ZiPatch/Chunk/AddDirectoryChunk.cs create mode 100644 src/XIVLauncher2.Common/Patching/ZiPatch/Chunk/ApplyFreeSpaceChunk.cs create mode 100644 src/XIVLauncher2.Common/Patching/ZiPatch/Chunk/ApplyOptionChunk.cs create mode 100644 src/XIVLauncher2.Common/Patching/ZiPatch/Chunk/DeleteDirectoryChunk.cs create mode 100644 src/XIVLauncher2.Common/Patching/ZiPatch/Chunk/EndOfFileChunk.cs create mode 100644 src/XIVLauncher2.Common/Patching/ZiPatch/Chunk/FileHeaderChunk.cs create mode 100644 src/XIVLauncher2.Common/Patching/ZiPatch/Chunk/SqpkChunk.cs create mode 100644 src/XIVLauncher2.Common/Patching/ZiPatch/Chunk/SqpkCommand/SqpkAddData.cs create mode 100644 src/XIVLauncher2.Common/Patching/ZiPatch/Chunk/SqpkCommand/SqpkDeleteData.cs create mode 100644 src/XIVLauncher2.Common/Patching/ZiPatch/Chunk/SqpkCommand/SqpkExpandData.cs create mode 100644 src/XIVLauncher2.Common/Patching/ZiPatch/Chunk/SqpkCommand/SqpkFile.cs create mode 100644 src/XIVLauncher2.Common/Patching/ZiPatch/Chunk/SqpkCommand/SqpkHeader.cs create mode 100644 src/XIVLauncher2.Common/Patching/ZiPatch/Chunk/SqpkCommand/SqpkIndex.cs create mode 100644 src/XIVLauncher2.Common/Patching/ZiPatch/Chunk/SqpkCommand/SqpkPatchInfo.cs create mode 100644 src/XIVLauncher2.Common/Patching/ZiPatch/Chunk/SqpkCommand/SqpkTargetInfo.cs create mode 100644 src/XIVLauncher2.Common/Patching/ZiPatch/Chunk/XXXXChunk.cs create mode 100644 src/XIVLauncher2.Common/Patching/ZiPatch/Chunk/ZiPatchChunk.cs create mode 100644 src/XIVLauncher2.Common/Patching/ZiPatch/Util/SqexFile.cs create mode 100644 src/XIVLauncher2.Common/Patching/ZiPatch/Util/SqexFileStream.cs create mode 100644 src/XIVLauncher2.Common/Patching/ZiPatch/Util/SqexFileStreamStore.cs create mode 100644 src/XIVLauncher2.Common/Patching/ZiPatch/Util/SqpackDatFile.cs create mode 100644 src/XIVLauncher2.Common/Patching/ZiPatch/Util/SqpackFile.cs create mode 100644 src/XIVLauncher2.Common/Patching/ZiPatch/Util/SqpackIndexFile.cs create mode 100644 src/XIVLauncher2.Common/Patching/ZiPatch/Util/SqpkCompressedBlock.cs create mode 100644 src/XIVLauncher2.Common/Patching/ZiPatch/ZiPatchConfig.cs create mode 100644 src/XIVLauncher2.Common/Patching/ZiPatch/ZiPatchException.cs create mode 100644 src/XIVLauncher2.Common/Patching/ZiPatch/ZiPatchFile.cs create mode 100644 src/XIVLauncher2.Common/Paths.cs create mode 100644 src/XIVLauncher2.Common/Platform.cs create mode 100644 src/XIVLauncher2.Common/PlatformAbstractions/IDalamudCompatibilityCheck.cs create mode 100644 src/XIVLauncher2.Common/PlatformAbstractions/IDalamudLoadingOverlay.cs create mode 100644 src/XIVLauncher2.Common/PlatformAbstractions/IDalamudRunner.cs create mode 100644 src/XIVLauncher2.Common/PlatformAbstractions/IGameRunner.cs create mode 100644 src/XIVLauncher2.Common/PlatformAbstractions/ISettings.cs create mode 100644 src/XIVLauncher2.Common/PlatformAbstractions/ISteam.cs create mode 100644 src/XIVLauncher2.Common/PlatformAbstractions/IUniqueIdCache.cs create mode 100644 src/XIVLauncher2.Common/Repository.cs create mode 100644 src/XIVLauncher2.Common/SeVersion.cs create mode 100644 src/XIVLauncher2.Common/SettingsAnnotation.cs create mode 100644 src/XIVLauncher2.Common/Storage.cs create mode 100644 src/XIVLauncher2.Common/Support/LogInit.cs create mode 100644 src/XIVLauncher2.Common/Support/SerilogEventSink.cs create mode 100644 src/XIVLauncher2.Common/Util/ApiHelpers.cs create mode 100644 src/XIVLauncher2.Common/Util/DebugHelpers.cs create mode 100644 src/XIVLauncher2.Common/Util/GameHelpers.cs create mode 100644 src/XIVLauncher2.Common/Util/HttpClientWithProgress.cs create mode 100644 src/XIVLauncher2.Common/Util/PlatformHelpers.cs diff --git a/src/XIVLauncher2.Common.Unix/Compatibility/CompatibilityTools.cs b/src/XIVLauncher2.Common.Unix/Compatibility/CompatibilityTools.cs new file mode 100644 index 0000000..48e59e2 --- /dev/null +++ b/src/XIVLauncher2.Common.Unix/Compatibility/CompatibilityTools.cs @@ -0,0 +1,281 @@ +using System; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Threading.Tasks; +using Serilog; +using XIVLauncher2.Common.Util; + +#if FLATPAK +#warning THIS IS A FLATPAK BUILD!!! +#endif + +namespace XIVLauncher2.Common.Unix.Compatibility; + +public class CompatibilityTools +{ + private DirectoryInfo toolDirectory; + private DirectoryInfo dxvkDirectory; + + private StreamWriter logWriter; + +#if WINE_XIV_ARCH_LINUX + private const string WINE_XIV_RELEASE_URL = "https://github.com/goatcorp/wine-xiv-git/releases/download/7.10.r3.g560db77d/wine-xiv-staging-fsync-git-arch-7.10.r3.g560db77d.tar.xz"; +#elif WINE_XIV_FEDORA_LINUX + private const string WINE_XIV_RELEASE_URL = "https://github.com/goatcorp/wine-xiv-git/releases/download/7.10.r3.g560db77d/wine-xiv-staging-fsync-git-fedora-7.10.r3.g560db77d.tar.xz"; +#else + private const string WINE_XIV_RELEASE_URL = "https://github.com/goatcorp/wine-xiv-git/releases/download/7.10.r3.g560db77d/wine-xiv-staging-fsync-git-ubuntu-7.10.r3.g560db77d.tar.xz"; +#endif + private const string WINE_XIV_RELEASE_NAME = "wine-xiv-staging-fsync-git-7.10.r3.g560db77d"; + + public bool IsToolReady { get; private set; } + + public WineSettings Settings { get; private set; } + + private string WineBinPath => Settings.StartupType == WineStartupType.Managed ? + Path.Combine(toolDirectory.FullName, WINE_XIV_RELEASE_NAME, "bin") : + Settings.CustomBinPath; + private string Wine64Path => Path.Combine(WineBinPath, "wine64"); + private string WineServerPath => Path.Combine(WineBinPath, "wineserver"); + + public bool IsToolDownloaded => File.Exists(Wine64Path) && Settings.Prefix.Exists; + + public DxvkSettings DxvkSettings { get; private set; } + + private readonly bool gamemodeOn; + + public CompatibilityTools(WineSettings wineSettings, DxvkSettings dxvkSettings, bool? gamemodeOn, DirectoryInfo toolsFolder) + { + this.Settings = wineSettings; + this.DxvkSettings = dxvkSettings; + this.gamemodeOn = gamemodeOn ?? false; + this.toolDirectory = new DirectoryInfo(Path.Combine(toolsFolder.FullName, "beta")); + this.dxvkDirectory = new DirectoryInfo(Path.Combine(toolsFolder.FullName, "dxvk")); + + this.logWriter = new StreamWriter(wineSettings.LogFile.FullName); + + if (wineSettings.StartupType == WineStartupType.Managed) + { + if (!this.toolDirectory.Exists) + this.toolDirectory.Create(); + + if (!this.dxvkDirectory.Exists) + this.dxvkDirectory.Create(); + } + } + + public async Task EnsureTool(DirectoryInfo tempPath) + { + if (!File.Exists(Wine64Path)) + { + Log.Information("Compatibility tool does not exist, downloading"); + await DownloadTool(tempPath).ConfigureAwait(false); + } + + EnsurePrefix(); + await Dxvk.InstallDxvk(Settings.Prefix, dxvkDirectory, DxvkSettings).ConfigureAwait(false); + + IsToolReady = true; + } + + private async Task DownloadTool(DirectoryInfo tempPath) + { + using var client = new HttpClient(); + var tempFilePath = Path.Combine(tempPath.FullName, $"{Guid.NewGuid()}"); + + await File.WriteAllBytesAsync(tempFilePath, await client.GetByteArrayAsync(WINE_XIV_RELEASE_URL).ConfigureAwait(false)).ConfigureAwait(false); + + PlatformHelpers.Untar(tempFilePath, this.toolDirectory.FullName); + + Log.Information("Compatibility tool successfully extracted to {Path}", this.toolDirectory.FullName); + + File.Delete(tempFilePath); + } + + private void ResetPrefix() + { + Settings.Prefix.Refresh(); + + if (Settings.Prefix.Exists) + Settings.Prefix.Delete(true); + + Settings.Prefix.Create(); + EnsurePrefix(); + } + + public void EnsurePrefix() + { + RunInPrefix("cmd /c dir %userprofile%/Documents > nul").WaitForExit(); + } + + public Process RunInPrefix(string command, string workingDirectory = "", IDictionary environment = null, bool redirectOutput = false, bool writeLog = false, bool wineD3D = false) + { + var psi = new ProcessStartInfo(Wine64Path); + psi.Arguments = command; + + Log.Verbose("Running in prefix: {FileName} {Arguments}", psi.FileName, command); + return RunInPrefix(psi, workingDirectory, environment, redirectOutput, writeLog, wineD3D); + } + + public Process RunInPrefix(string[] args, string workingDirectory = "", IDictionary environment = null, bool redirectOutput = false, bool writeLog = false, bool wineD3D = false) + { + var psi = new ProcessStartInfo(Wine64Path); + foreach (var arg in args) + psi.ArgumentList.Add(arg); + + Log.Verbose("Running in prefix: {FileName} {Arguments}", psi.FileName, psi.ArgumentList.Aggregate(string.Empty, (a, b) => a + " " + b)); + return RunInPrefix(psi, workingDirectory, environment, redirectOutput, writeLog, wineD3D); + } + + private void MergeDictionaries(StringDictionary a, IDictionary b) + { + if (b is null) + return; + + foreach (var keyValuePair in b) + { + if (a.ContainsKey(keyValuePair.Key)) + a[keyValuePair.Key] = keyValuePair.Value; + else + a.Add(keyValuePair.Key, keyValuePair.Value); + } + } + + private Process RunInPrefix(ProcessStartInfo psi, string workingDirectory, IDictionary environment, bool redirectOutput, bool writeLog, bool wineD3D) + { + psi.RedirectStandardOutput = redirectOutput; + psi.RedirectStandardError = writeLog; + psi.UseShellExecute = false; + psi.WorkingDirectory = workingDirectory; + + var wineEnviromentVariables = new Dictionary(); + wineEnviromentVariables.Add("WINEPREFIX", Settings.Prefix.FullName); + wineEnviromentVariables.Add("WINEDLLOVERRIDES", $"msquic=,mscoree=n,b;d3d9,d3d11,d3d10core,dxgi={(DxvkSettings.Enabled && !wineD3D ? "n" : "b")}"); + + if (!string.IsNullOrEmpty(Settings.DebugVars)) + { + wineEnviromentVariables.Add("WINEDEBUG", Settings.DebugVars); + } + + wineEnviromentVariables.Add("XL_WINEONLINUX", "true"); + string ldPreload = Environment.GetEnvironmentVariable("LD_PRELOAD") ?? ""; + + if (gamemodeOn && !ldPreload.Contains("libgamemodeauto.so.0")) + { + ldPreload = ldPreload.Equals("") ? "libgamemodeauto.so.0" : ldPreload + ":libgamemodeauto.so.0"; + } + + foreach (KeyValuePair dxvkVar in DxvkSettings.DxvkVars) + wineEnviromentVariables.Add(dxvkVar.Key, dxvkVar.Value); + + wineEnviromentVariables.Add("WINEESYNC", Settings.EsyncOn); + wineEnviromentVariables.Add("WINEFSYNC", Settings.FsyncOn); + + wineEnviromentVariables.Add("LD_PRELOAD", ldPreload); + + MergeDictionaries(psi.EnvironmentVariables, wineEnviromentVariables); + MergeDictionaries(psi.EnvironmentVariables, environment); + +#if FLATPAK_NOTRIGHTNOW + psi.FileName = "flatpak-spawn"; + + psi.ArgumentList.Insert(0, "--host"); + psi.ArgumentList.Insert(1, Wine64Path); + + foreach (KeyValuePair envVar in wineEnviromentVariables) + { + psi.ArgumentList.Insert(1, $"--env={envVar.Key}={envVar.Value}"); + } + + if (environment != null) + { + foreach (KeyValuePair envVar in environment) + { + psi.ArgumentList.Insert(1, $"--env=\"{envVar.Key}\"=\"{envVar.Value}\""); + } + } +#endif + + Process helperProcess = new(); + helperProcess.StartInfo = psi; + helperProcess.ErrorDataReceived += new DataReceivedEventHandler((_, errLine) => + { + if (String.IsNullOrEmpty(errLine.Data)) + return; + + try + { + logWriter.WriteLine(errLine.Data); + Console.Error.WriteLine(errLine.Data); + } + catch (Exception ex) when (ex is ArgumentOutOfRangeException || + ex is OverflowException || + ex is IndexOutOfRangeException) + { + // very long wine log lines get chopped off after a (seemingly) arbitrary limit resulting in strings that are not null terminated + //logWriter.WriteLine("Error writing Wine log line:"); + //logWriter.WriteLine(ex.Message); + } + }); + + helperProcess.Start(); + if (writeLog) + helperProcess.BeginErrorReadLine(); + + return helperProcess; + } + + public Int32[] GetProcessIds(string executableName) + { + var wineDbg = RunInPrefix("winedbg --command \"info proc\"", redirectOutput: true); + var output = wineDbg.StandardOutput.ReadToEnd(); + var matchingLines = output.Split('\n', StringSplitOptions.RemoveEmptyEntries).Where(l => l.Contains(executableName)); + return matchingLines.Select(l => int.Parse(l.Substring(1, 8), System.Globalization.NumberStyles.HexNumber)).ToArray(); + } + + public Int32 GetProcessId(string executableName) + { + return GetProcessIds(executableName).FirstOrDefault(); + } + + public Int32 GetUnixProcessId(Int32 winePid) + { + var wineDbg = RunInPrefix("winedbg --command \"info procmap\"", redirectOutput: true); + var output = wineDbg.StandardOutput.ReadToEnd(); + if (output.Contains("syntax error\n")) + return 0; + var matchingLines = output.Split('\n', StringSplitOptions.RemoveEmptyEntries).Skip(1).Where( + l => int.Parse(l.Substring(1, 8), System.Globalization.NumberStyles.HexNumber) == winePid); + var unixPids = matchingLines.Select(l => int.Parse(l.Substring(10, 8), System.Globalization.NumberStyles.HexNumber)).ToArray(); + return unixPids.FirstOrDefault(); + } + + public string UnixToWinePath(string unixPath) + { + var launchArguments = new string[] { "winepath", "--windows", unixPath }; + var winePath = RunInPrefix(launchArguments, redirectOutput: true); + var output = winePath.StandardOutput.ReadToEnd(); + return output.Split('\n', StringSplitOptions.RemoveEmptyEntries).LastOrDefault(); + } + + public void AddRegistryKey(string key, string value, string data) + { + var args = new string[] { "reg", "add", key, "/v", value, "/d", data, "/f" }; + var wineProcess = RunInPrefix(args); + wineProcess.WaitForExit(); + } + + public void Kill() + { + var psi = new ProcessStartInfo(WineServerPath) + { + Arguments = "-k" + }; + psi.EnvironmentVariables.Add("WINEPREFIX", Settings.Prefix.FullName); + + Process.Start(psi); + } +} diff --git a/src/XIVLauncher2.Common.Unix/Compatibility/Dxvk.cs b/src/XIVLauncher2.Common.Unix/Compatibility/Dxvk.cs new file mode 100644 index 0000000..4fea8ea --- /dev/null +++ b/src/XIVLauncher2.Common.Unix/Compatibility/Dxvk.cs @@ -0,0 +1,79 @@ +using System.IO; +using System.Net.Http; +using System.Threading.Tasks; +using Serilog; +using XIVLauncher2.Common.Util; + +namespace XIVLauncher2.Common.Unix.Compatibility; + +public static class Dxvk +{ + public static async Task InstallDxvk(DirectoryInfo prefix, DirectoryInfo installDirectory, DxvkSettings dxvkSettings) + { + var dxvkPath = Path.Combine(installDirectory.FullName, dxvkSettings.FolderName, "x64"); + + if (!Directory.Exists(dxvkPath)) + { + Log.Information("DXVK does not exist, downloading"); + await DownloadDxvk(installDirectory, dxvkSettings.DownloadURL).ConfigureAwait(false); + } + + var system32 = Path.Combine(prefix.FullName, "drive_c", "windows", "system32"); + var files = Directory.GetFiles(dxvkPath); + + foreach (string fileName in files) + { + File.Copy(fileName, Path.Combine(system32, Path.GetFileName(fileName)), true); + } + } + + private static async Task DownloadDxvk(DirectoryInfo installDirectory, string downloadURL) + { + using var client = new HttpClient(); + var tempPath = Path.GetTempFileName(); + + File.WriteAllBytes(tempPath, await client.GetByteArrayAsync(downloadURL)); + PlatformHelpers.Untar(tempPath, installDirectory.FullName); + + File.Delete(tempPath); + } + + public enum DxvkHudType + { + [SettingsDescription("None", "Show nothing")] + None, + + [SettingsDescription("FPS", "Only show FPS")] + Fps, + + [SettingsDescription("DXVK Hud Custom", "Use a custom DXVK_HUD string")] + Custom, + + [SettingsDescription("Full", "Show everything")] + Full, + + [SettingsDescription("MangoHud Default", "Uses no config file.")] + MangoHud, + + [SettingsDescription("MangoHud Custom", "Specify a custom config file")] + MangoHudCustom, + + [SettingsDescription("MangoHud Full", "Show (almost) everything")] + MangoHudFull, + } + + public enum DxvkVersion + { + [SettingsDescription("1.10.1", "The version of DXVK used with XIVLauncher.Core 1.0.2. Safe to use.")] + v1_10_1, + + [SettingsDescription("1.10.2", "Older version of 1.10 branch of DXVK. Safe to use.")] + v1_10_2, + + [SettingsDescription("1.10.3 (default)", "Current version of 1.10 branch of DXVK.")] + v1_10_3, + + [SettingsDescription("2.0 (might break Dalamud, GShade)", "Newest version of DXVK. May be faster, but not stable yet.")] + v2_0, + } +} diff --git a/src/XIVLauncher2.Common.Unix/Compatibility/DxvkSettings.cs b/src/XIVLauncher2.Common.Unix/Compatibility/DxvkSettings.cs new file mode 100644 index 0000000..5c907dc --- /dev/null +++ b/src/XIVLauncher2.Common.Unix/Compatibility/DxvkSettings.cs @@ -0,0 +1,131 @@ +#nullable enable +using System; +using System.IO; +using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; + +namespace XIVLauncher2.Common.Unix.Compatibility; + +public class DxvkSettings +{ + public bool Enabled { get; } + + public string DownloadURL { get; } + + public string FolderName { get; } + + public Dictionary DxvkVars { get; } + + public Dxvk.DxvkHudType DxvkHud { get; } + + public Dxvk.DxvkVersion DxvkVersion { get; } + + private const string ALLOWED_CHARS = "^[0-9a-zA-Z,=.]+$"; + + private const string ALLOWED_WORDS = "^(?:devinfo|fps|frametimes|submissions|drawcalls|pipelines|descriptors|memory|gpuload|version|api|cs|compiler|samplers|scale=(?:[0-9])*(?:.(?:[0-9])+)?)$"; + + public DxvkSettings(Dxvk.DxvkHudType hud, DirectoryInfo corePath, Dxvk.DxvkVersion version, bool enabled = true, string? dxvkHudCustom = null, FileInfo? mangoHudConfig = null, bool async = true, + int maxFrameRate = 0) + { + Enabled = enabled; + DxvkHud = hud; + var dxvkConfigPath = new DirectoryInfo(Path.Combine(corePath.FullName, "compatibilitytool", "dxvk")); + if (!dxvkConfigPath.Exists) + dxvkConfigPath.Create(); + DxvkVars = new Dictionary + { + { "DXVK_LOG_PATH", Path.Combine(corePath.FullName, "logs") }, + { "DXVK_CONFIG_FILE", Path.Combine(dxvkConfigPath.FullName, "dxvk.conf") }, + { "DXVK_ASYNC", async ? "1" : "0" }, + { "DXVK_FRAME_RATE", (maxFrameRate).ToString() } + }; + DxvkVersion = version; + var release = DxvkVersion switch + { + Dxvk.DxvkVersion.v1_10_1 => "1.10.1", + Dxvk.DxvkVersion.v1_10_2 => "1.10.2", + Dxvk.DxvkVersion.v1_10_3 => "1.10.3", + Dxvk.DxvkVersion.v2_0 => "2.0", + _ => throw new ArgumentOutOfRangeException(), + }; + DownloadURL = $"https://github.com/Sporif/dxvk-async/releases/download/{release}/dxvk-async-{release}.tar.gz"; + FolderName = $"dxvk-async-{release}"; + DirectoryInfo dxvkCachePath = new DirectoryInfo(Path.Combine(dxvkConfigPath.FullName, "cache")); + if (!dxvkCachePath.Exists) dxvkCachePath.Create(); + this.DxvkVars.Add("DXVK_STATE_CACHE_PATH", Path.Combine(dxvkCachePath.FullName, release + (async ? "-async" : ""))); + + switch(this.DxvkHud) + { + case Dxvk.DxvkHudType.Fps: + DxvkVars.Add("DXVK_HUD","fps"); + DxvkVars.Add("MANGOHUD","0"); + break; + + case Dxvk.DxvkHudType.Custom: + if (!CheckDxvkHudString(dxvkHudCustom)) + dxvkHudCustom = "fps,frametimes,gpuload,version"; + DxvkVars.Add("DXVK_HUD", dxvkHudCustom!); + DxvkVars.Add("MANGOHUD","0"); + break; + + case Dxvk.DxvkHudType.Full: + DxvkVars.Add("DXVK_HUD","full"); + DxvkVars.Add("MANGOHUD","0"); + break; + + case Dxvk.DxvkHudType.MangoHud: + DxvkVars.Add("DXVK_HUD","0"); + DxvkVars.Add("MANGOHUD","1"); + DxvkVars.Add("MANGOHUD_CONFIG", ""); + break; + + case Dxvk.DxvkHudType.MangoHudCustom: + DxvkVars.Add("DXVK_HUD","0"); + DxvkVars.Add("MANGOHUD","1"); + + if (mangoHudConfig is null) + { + var home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); + var conf1 = Path.Combine(corePath.FullName, "MangoHud.conf"); + var conf2 = Path.Combine(home, ".config", "MangoHud", "wine-ffxiv_dx11.conf"); + var conf3 = Path.Combine(home, ".config", "MangoHud", "MangoHud.conf"); + if (File.Exists(conf1)) + mangoHudConfig = new FileInfo(conf1); + else if (File.Exists(conf2)) + mangoHudConfig = new FileInfo(conf2); + else if (File.Exists(conf3)) + mangoHudConfig = new FileInfo(conf3); + } + + if (mangoHudConfig != null && mangoHudConfig.Exists) + DxvkVars.Add("MANGOHUD_CONFIGFILE", mangoHudConfig.FullName); + else + DxvkVars.Add("MANGOHUD_CONFIG", ""); + break; + + case Dxvk.DxvkHudType.MangoHudFull: + DxvkVars.Add("DXVK_HUD","0"); + DxvkVars.Add("MANGOHUD","1"); + DxvkVars.Add("MANGOHUD_CONFIG","full"); + break; + + case Dxvk.DxvkHudType.None: + break; + + default: + throw new ArgumentOutOfRangeException(); + } + } + + public static bool CheckDxvkHudString(string? customHud) + { + if (string.IsNullOrWhiteSpace(customHud)) return false; + if (customHud == "1") return true; + if (!Regex.IsMatch(customHud,ALLOWED_CHARS)) return false; + + string[] hudvars = customHud.Split(","); + + return hudvars.All(hudvar => Regex.IsMatch(hudvar, ALLOWED_WORDS)); + } +} diff --git a/src/XIVLauncher2.Common.Unix/Compatibility/GameFixes/GameFix.cs b/src/XIVLauncher2.Common.Unix/Compatibility/GameFixes/GameFix.cs new file mode 100644 index 0000000..a3b40e2 --- /dev/null +++ b/src/XIVLauncher2.Common.Unix/Compatibility/GameFixes/GameFix.cs @@ -0,0 +1,28 @@ +using System.IO; + +namespace XIVLauncher2.Common.Unix.Compatibility.GameFixes; + +public abstract class GameFix +{ + public GameFix(DirectoryInfo gameDirectory, DirectoryInfo configDirectory, DirectoryInfo winePrefixDirectory, DirectoryInfo tempDirectory) + { + GameDir = gameDirectory; + ConfigDir = configDirectory; + WinePrefixDir = winePrefixDirectory; + TempDir = tempDirectory; + } + + public abstract string LoadingTitle { get; } + + public GameFixApply.UpdateProgressDelegate UpdateProgress; + + public DirectoryInfo WinePrefixDir { get; private set; } + + public DirectoryInfo ConfigDir { get; private set; } + + public DirectoryInfo GameDir { get; private set; } + + public DirectoryInfo TempDir { get; private set; } + + public abstract void Apply(); +} \ No newline at end of file diff --git a/src/XIVLauncher2.Common.Unix/Compatibility/GameFixes/GameFixApply.cs b/src/XIVLauncher2.Common.Unix/Compatibility/GameFixes/GameFixApply.cs new file mode 100644 index 0000000..4f33a9c --- /dev/null +++ b/src/XIVLauncher2.Common.Unix/Compatibility/GameFixes/GameFixApply.cs @@ -0,0 +1,33 @@ +using System.IO; +using XIVLauncher2.Common.Unix.Compatibility.GameFixes.Implementations; + +namespace XIVLauncher2.Common.Unix.Compatibility.GameFixes; + +public class GameFixApply +{ + private readonly GameFix[] fixes; + + public delegate void UpdateProgressDelegate(string loadingText, bool hasProgress, float progress); + + public event UpdateProgressDelegate UpdateProgress; + + public GameFixApply(DirectoryInfo gameDirectory, DirectoryInfo configDirectory, DirectoryInfo winePrefixDirectory, DirectoryInfo tempDirectory) + { + this.fixes = new GameFix[] + { + new MacVideoFix(gameDirectory, configDirectory, winePrefixDirectory, tempDirectory), + }; + } + + public void Run() + { + foreach (GameFix fix in this.fixes) + { + this.UpdateProgress?.Invoke(fix.LoadingTitle, false, 0f); + + fix.UpdateProgress += this.UpdateProgress; + fix.Apply(); + fix.UpdateProgress -= this.UpdateProgress; + } + } +} \ No newline at end of file diff --git a/src/XIVLauncher2.Common.Unix/Compatibility/GameFixes/Implementations/MacVideoFix.cs b/src/XIVLauncher2.Common.Unix/Compatibility/GameFixes/Implementations/MacVideoFix.cs new file mode 100644 index 0000000..d05881d --- /dev/null +++ b/src/XIVLauncher2.Common.Unix/Compatibility/GameFixes/Implementations/MacVideoFix.cs @@ -0,0 +1,58 @@ +using System; +using System.IO; +using System.IO.Compression; +using System.Linq; +using XIVLauncher2.Common.Util; + +namespace XIVLauncher2.Common.Unix.Compatibility.GameFixes.Implementations; + +public class MacVideoFix : GameFix +{ + private const string MAC_ZIP_URL = "https://mac-dl.ffxiv.com/cw/finalfantasyxiv-1.0.8.zip"; + + public MacVideoFix(DirectoryInfo gameDirectory, DirectoryInfo configDirectory, DirectoryInfo winePrefixDirectory, DirectoryInfo tempDirectory) + : base(gameDirectory, configDirectory, winePrefixDirectory, tempDirectory) + { + } + + public override string LoadingTitle => "Preparing FMV cutscenes..."; + + public override void Apply() + { + var outputDirectory = new DirectoryInfo(Path.Combine(GameDir.FullName, "game", "movie", "ffxiv")); + var movieFileNames = new [] { "00000.bk2", "00001.bk2", "00002.bk2", "00003.bk2" }; + var movieFiles = movieFileNames.Select(movie => new FileInfo(Path.Combine(outputDirectory.FullName, movie))); + + if (movieFiles.All((movieFile) => movieFile.Exists)) + return; + + var zipFilePath = Path.Combine(TempDir.FullName, $"{Guid.NewGuid()}.zip"); + using var client = new HttpClientDownloadWithProgress(MAC_ZIP_URL, zipFilePath); + client.ProgressChanged += (size, downloaded, percentage) => + { + if (percentage != null && size != null) + { + this.UpdateProgress?.Invoke($"{LoadingTitle} ({ApiHelpers.BytesToString(downloaded)}/{ApiHelpers.BytesToString(size.Value)})", true, (float)(percentage.Value / 100f)); + } + }; + + client.Download().GetAwaiter().GetResult(); + + var zipMovieFileNames = movieFileNames.Select(movie => Path.Combine("game", "movie", "ffxiv", movie)); + + using (ZipArchive archive = ZipFile.OpenRead(zipFilePath)) + { + foreach (ZipArchiveEntry entry in archive.Entries) + { + if (zipMovieFileNames.Any((fileName) => entry.FullName.EndsWith(fileName, StringComparison.OrdinalIgnoreCase))) + { + string destinationPath = Path.Combine(outputDirectory.FullName, entry.Name); + if (!File.Exists(destinationPath)) + entry.ExtractToFile(destinationPath); + } + } + } + + File.Delete(zipFilePath); + } +} diff --git a/src/XIVLauncher2.Common.Unix/Compatibility/WineSettings.cs b/src/XIVLauncher2.Common.Unix/Compatibility/WineSettings.cs new file mode 100644 index 0000000..a1ee84d --- /dev/null +++ b/src/XIVLauncher2.Common.Unix/Compatibility/WineSettings.cs @@ -0,0 +1,37 @@ +using System.IO; + +namespace XIVLauncher2.Common.Unix.Compatibility; + +public enum WineStartupType +{ + [SettingsDescription("Managed by XIVLauncher", "The game installation and wine setup is managed by XIVLauncher - you can leave it up to us.")] + Managed, + + [SettingsDescription("Custom", "Point XIVLauncher to a custom location containing wine binaries to run the game with.")] + Custom, +} + +public class WineSettings +{ + public WineStartupType StartupType { get; private set; } + public string CustomBinPath { get; private set; } + + public string EsyncOn { get; private set; } + public string FsyncOn { get; private set; } + + public string DebugVars { get; private set; } + public FileInfo LogFile { get; private set; } + + public DirectoryInfo Prefix { get; private set; } + + public WineSettings(WineStartupType? startupType, string customBinPath, string debugVars, FileInfo logFile, DirectoryInfo prefix, bool? esyncOn, bool? fsyncOn) + { + this.StartupType = startupType ?? WineStartupType.Custom; + this.CustomBinPath = customBinPath; + this.EsyncOn = (esyncOn ?? false) ? "1" : "0"; + this.FsyncOn = (fsyncOn ?? false) ? "1" : "0"; + this.DebugVars = debugVars; + this.LogFile = logFile; + this.Prefix = prefix; + } +} \ No newline at end of file diff --git a/src/XIVLauncher2.Common.Unix/UnixDalamudCompatibilityCheck.cs b/src/XIVLauncher2.Common.Unix/UnixDalamudCompatibilityCheck.cs new file mode 100644 index 0000000..89cd07e --- /dev/null +++ b/src/XIVLauncher2.Common.Unix/UnixDalamudCompatibilityCheck.cs @@ -0,0 +1,33 @@ +using System.Runtime.InteropServices; +using XIVLauncher2.Common.PlatformAbstractions; + +namespace XIVLauncher2.Common.Unix; + +public class UnixDalamudCompatibilityCheck : IDalamudCompatibilityCheck +{ + public void EnsureCompatibility() + { + //Dalamud will work with wines built-in vcrun, so no need to check that + EnsureArchitecture(); + } + + private static void EnsureArchitecture() + { + var arch = RuntimeInformation.ProcessArchitecture; + + switch (arch) + { + case Architecture.X86: + throw new IDalamudCompatibilityCheck.ArchitectureNotSupportedException("Dalamud is not supported on x86 architecture."); + + case Architecture.X64: + break; + + case Architecture.Arm: + throw new IDalamudCompatibilityCheck.ArchitectureNotSupportedException("Dalamud is not supported on ARM32."); + + case Architecture.Arm64: + throw new IDalamudCompatibilityCheck.ArchitectureNotSupportedException("x64 emulation was not detected. Please make sure to run XIVLauncher with x64 emulation."); + } + } +} diff --git a/src/XIVLauncher2.Common.Unix/UnixDalamudRunner.cs b/src/XIVLauncher2.Common.Unix/UnixDalamudRunner.cs new file mode 100644 index 0000000..4ed3b0f --- /dev/null +++ b/src/XIVLauncher2.Common.Unix/UnixDalamudRunner.cs @@ -0,0 +1,116 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using System.Text.Json; +using Serilog; +using XIVLauncher2.Common.Dalamud; +using XIVLauncher2.Common.PlatformAbstractions; +using XIVLauncher2.Common.Unix.Compatibility; + +namespace XIVLauncher2.Common.Unix; + +public class UnixDalamudRunner : IDalamudRunner +{ + private readonly CompatibilityTools compatibility; + private readonly DirectoryInfo dotnetRuntime; + + public UnixDalamudRunner(CompatibilityTools compatibility, DirectoryInfo dotnetRuntime) + { + this.compatibility = compatibility; + this.dotnetRuntime = dotnetRuntime; + } + + public Process? Run(FileInfo runner, bool fakeLogin, bool noPlugins, bool noThirdPlugins, FileInfo gameExe, string gameArgs, IDictionary environment, DalamudLoadMethod loadMethod, DalamudStartInfo startInfo) + { + var gameExePath = ""; + var dotnetRuntimePath = ""; + + Parallel.Invoke( + () => { gameExePath = compatibility.UnixToWinePath(gameExe.FullName); }, + () => { dotnetRuntimePath = compatibility.UnixToWinePath(dotnetRuntime.FullName); }, + () => { startInfo.WorkingDirectory = compatibility.UnixToWinePath(startInfo.WorkingDirectory); }, + () => { startInfo.ConfigurationPath = compatibility.UnixToWinePath(startInfo.ConfigurationPath); }, + () => { startInfo.PluginDirectory = compatibility.UnixToWinePath(startInfo.PluginDirectory); }, + () => { startInfo.DefaultPluginDirectory = compatibility.UnixToWinePath(startInfo.DefaultPluginDirectory); }, + () => { startInfo.AssetDirectory = compatibility.UnixToWinePath(startInfo.AssetDirectory); } + ); + + var prevDalamudRuntime = Environment.GetEnvironmentVariable("DALAMUD_RUNTIME"); + if (string.IsNullOrWhiteSpace(prevDalamudRuntime)) + environment.Add("DALAMUD_RUNTIME", dotnetRuntimePath); + + var launchArguments = new List + { + $"\"{runner.FullName}\"", + DalamudInjectorArgs.Launch, + DalamudInjectorArgs.Mode(loadMethod == DalamudLoadMethod.EntryPoint ? "entrypoint" : "inject"), + DalamudInjectorArgs.Game(gameExePath), + DalamudInjectorArgs.WorkingDirectory(startInfo.WorkingDirectory), + DalamudInjectorArgs.ConfigurationPath(startInfo.ConfigurationPath), + DalamudInjectorArgs.PluginDirectory(startInfo.PluginDirectory), + DalamudInjectorArgs.PluginDevDirectory(startInfo.DefaultPluginDirectory), + DalamudInjectorArgs.AssetDirectory(startInfo.AssetDirectory), + DalamudInjectorArgs.ClientLanguage((int)startInfo.Language), + DalamudInjectorArgs.DelayInitialize(startInfo.DelayInitializeMs), + DalamudInjectorArgs.TSPackB64(Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes(startInfo.TroubleshootingPackData))), + }; + + if (loadMethod == DalamudLoadMethod.ACLonly) + launchArguments.Add(DalamudInjectorArgs.WithoutDalamud); + + if (fakeLogin) + launchArguments.Add(DalamudInjectorArgs.FakeArguments); + + if (noPlugins) + launchArguments.Add(DalamudInjectorArgs.NoPlugin); + + if (noThirdPlugins) + launchArguments.Add(DalamudInjectorArgs.NoThirdParty); + + launchArguments.Add("--"); + launchArguments.Add(gameArgs); + + var dalamudProcess = compatibility.RunInPrefix(string.Join(" ", launchArguments), environment: environment, redirectOutput: true, writeLog: true); + var output = dalamudProcess.StandardOutput.ReadLine(); + + if (output == null) + throw new DalamudRunnerException("An internal Dalamud error has occured"); + + Console.WriteLine(output); + + new Thread(() => + { + while (!dalamudProcess.StandardOutput.EndOfStream) + { + var output = dalamudProcess.StandardOutput.ReadLine(); + if (output != null) + Console.WriteLine(output); + } + + }).Start(); + + try + { + var dalamudConsoleOutput = JsonSerializer.Deserialize(output); + var unixPid = compatibility.GetUnixProcessId(dalamudConsoleOutput.Pid); + + if (unixPid == 0) + { + Log.Error("Could not retrive Unix process ID, this feature currently requires a patched wine version"); + return null; + } + + var gameProcess = Process.GetProcessById(unixPid); + Log.Verbose($"Got game process handle {gameProcess.Handle} with Unix pid {gameProcess.Id} and Wine pid {dalamudConsoleOutput.Pid}"); + return gameProcess; + } + catch (JsonException ex) + { + Log.Error(ex, $"Couldn't parse Dalamud output: {output}"); + return null; + } + } +} diff --git a/src/XIVLauncher2.Common.Unix/UnixGameRunner.cs b/src/XIVLauncher2.Common.Unix/UnixGameRunner.cs new file mode 100644 index 0000000..5ac2e46 --- /dev/null +++ b/src/XIVLauncher2.Common.Unix/UnixGameRunner.cs @@ -0,0 +1,34 @@ +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using XIVLauncher2.Common.Dalamud; +using XIVLauncher2.Common.PlatformAbstractions; +using XIVLauncher2.Common.Unix.Compatibility; + +namespace XIVLauncher2.Common.Unix; + +public class UnixGameRunner : IGameRunner +{ + private readonly CompatibilityTools compatibility; + private readonly DalamudLauncher dalamudLauncher; + private readonly bool dalamudOk; + + public UnixGameRunner(CompatibilityTools compatibility, DalamudLauncher dalamudLauncher, bool dalamudOk) + { + this.compatibility = compatibility; + this.dalamudLauncher = dalamudLauncher; + this.dalamudOk = dalamudOk; + } + + public Process? Start(string path, string workingDirectory, string arguments, IDictionary environment, DpiAwareness dpiAwareness) + { + if (dalamudOk) + { + return this.dalamudLauncher.Run(new FileInfo(path), arguments, environment); + } + else + { + return compatibility.RunInPrefix($"\"{path}\" {arguments}", workingDirectory, environment, writeLog: true); + } + } +} diff --git a/src/XIVLauncher2.Common.Unix/UnixSteam.cs b/src/XIVLauncher2.Common.Unix/UnixSteam.cs new file mode 100644 index 0000000..5d24d9d --- /dev/null +++ b/src/XIVLauncher2.Common.Unix/UnixSteam.cs @@ -0,0 +1,81 @@ +using System; +using System.Threading.Tasks; +using Steamworks; +using XIVLauncher2.Common.PlatformAbstractions; + +namespace XIVLauncher2.Common.Unix +{ + public class UnixSteam : ISteam + { + public UnixSteam() + { + SteamUtils.OnGamepadTextInputDismissed += b => OnGamepadTextInputDismissed?.Invoke(b); + } + + public void Initialize(uint appId) + { + // workaround because SetEnvironmentVariable doesn't actually touch the process environment on unix + [System.Runtime.InteropServices.DllImport("c")] + static extern int setenv(string name, string value, int overwrite); + + setenv("SteamAppId", appId.ToString(), 1); + setenv("SteamGameId", appId.ToString(), 1); + + SteamClient.Init(appId); + } + + public bool IsValid => SteamClient.IsValid; + + public bool BLoggedOn => SteamClient.IsLoggedOn; + + public bool BOverlayNeedsPresent => SteamUtils.DoesOverlayNeedPresent; + + public void Shutdown() + { + SteamClient.Shutdown(); + } + + public async Task GetAuthSessionTicketAsync() + { + var ticket = await SteamUser.GetAuthSessionTicketAsync().ConfigureAwait(true); + return ticket?.Data; + } + + public bool IsAppInstalled(uint appId) + { + return SteamApps.IsAppInstalled(appId); + } + + public string GetAppInstallDir(uint appId) + { + return SteamApps.AppInstallDir(appId); + } + + public bool ShowGamepadTextInput(bool password, bool multiline, string description, int maxChars, string existingText = "") + { + return SteamUtils.ShowGamepadTextInput(password ? GamepadTextInputMode.Password : GamepadTextInputMode.Normal, multiline ? GamepadTextInputLineMode.MultipleLines : GamepadTextInputLineMode.SingleLine, description, maxChars, existingText); + } + + public string GetEnteredGamepadText() + { + return SteamUtils.GetEnteredGamepadText(); + } + + public bool ShowFloatingGamepadTextInput(ISteam.EFloatingGamepadTextInputMode mode, int x, int y, int width, int height) + { + // Facepunch.Steamworks doesn't have this... + return false; + } + + public bool IsRunningOnSteamDeck() => SteamUtils.IsRunningOnSteamDeck; + + public uint GetServerRealTime() => (uint)((DateTimeOffset)SteamUtils.SteamServerTime).ToUnixTimeSeconds(); + + public void ActivateGameOverlayToWebPage(string url, bool modal = false) + { + SteamFriends.OpenWebOverlay(url, modal); + } + + public event Action OnGamepadTextInputDismissed; + } +} diff --git a/src/XIVLauncher2.Common.Unix/XIVLauncher2.Common.Unix.csproj b/src/XIVLauncher2.Common.Unix/XIVLauncher2.Common.Unix.csproj new file mode 100644 index 0000000..f2da5f2 --- /dev/null +++ b/src/XIVLauncher2.Common.Unix/XIVLauncher2.Common.Unix.csproj @@ -0,0 +1,50 @@ + + + XIVLauncher2.Common.Unix + XIVLauncher2.Common.Unix + Shared XIVLauncher platform-specific implementations for Unix-like systems. + 1.0.0 + disable + + + + Library + net7.0 + latest + true + true + + + + $(DefineConstants);$(ExtraDefineConstants) + + + + + + + + + $(MSBuildProjectDirectory)\ + $(AppOutputBase)=C:\goatsoft\xl\XIVLauncher.Common.Unix\ + + + + + + PreserveNewest + + + + + + + PreserveNewest + + + + + + + + diff --git a/src/XIVLauncher2.Common.Unix/libsteam_api64.dylib b/src/XIVLauncher2.Common.Unix/libsteam_api64.dylib new file mode 100644 index 0000000000000000000000000000000000000000..8d3c5eeb87d48eaa3a62ed8c63a1a484b920de82 GIT binary patch literal 609584 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�rgqYygTOdFi~Unb`X?GA{nJRs@uWj9Lgr(ubHQ^Kc;}$s=f%1@n)Lb%vyKM; zl9}r3%5X8y)I)7(laKxmD0p~*M}|%t@!|pLOlf+RwX7UWMN~0Q|p1dTjMGSi`A4KWT`V z7oOeE%zz#*AWhyMTfJM1)#sM?uS~$2+1))&BYDrVr`=ASGd@pD#^%GCv5Je5O1<0lg37g3eOApQC+879h4-4O>_z z<13zH&%x)`oz81MomY1{W#2J^TH!Zaj|`zMUssag$@BXVVxeQJXF>05Uv!+$g|FD_ zIZ&0I=BDN8Sa;a>J__F6&nzAsv&cB?&aW8jfnze3>jAX&9ktq~VtugNzkv2?tf=-K z5PeJkaU1vlN&h^ug}%RDh8wy4$NjgT-bV%=Z#C*S{LQLw%lp-_-tuzYAnJ0AY2lEA zI_$rXk0+`5JE)8Ak?yU-TFCRZ2DH7Vo+Nw@>|k0U>NWPPpECb)>~BIkpru<|WglCyIF8wdM?BX4uE>#-rmY zRn-lTweC)7&mPPPjxV7vtScuq@Ak-b%))^A{SoqhoeT$+dHNUBhndk?HfO^%xqI>m}B_HV0~bYuM7Trb1Bxv7tx;A&R_TN z#5&xEC-eJf<$1)ECk1jBU`@f?ZQ=DH6S$-Ao%pV7D*Hx`{W9R+hJs$BrHD0y@#Vfh zz3OSqQ+uvkF&QN#itZRsn|XdU=xvvfCvgeSs~p6sTY+EQ%~X%|sx{MV?m^(UGa<(Y zBGGRq{P{u1!~XI!=-~j?4}8rr^&R*pjwes|a*THp*2>goe0Lc!3dbkXXMB3qbN%eU zsCmte)D(cf%w^j^U%Xxd`<4ow^L$;r|2kk8%^3IQiWHQ)slFnGdhO}(-QhzWW4D}= zQA*i_c#QpHrU&a!z?WU8)AS6i^%AG#Lk$?yP^>%Q+jC~Tf;}I;Gn+aTIQQgfG<|Y_ zvU#mSoO|dX_$K52dX2IJh?5y#Yi(-79*wcU_h*Gpc)h}l5hWa-UygB{<)*G>7z<*W zE|wwROf6Xs4cT%^%GGCe7HHHp8D;sXlg_+=^JL^*kG2J7n+#r4P@V$5yqmCkiAUXHmlfk7JNO@<6CX9{Gv41Bon6tw4lIA!_PAf{p&3LrxU`WuD5c@M#S z3cy3~K|LN%3d#q?2mK|ZkK^Z**39uhBV^&&yLVi4QgbS7d4tYt_({FiaL&IK`PLfF zHo|cT$HA#=GY|WkGTpQ<8Tjy?iG3>D3ftjpr&%^=`+2tK4`8o6uUHVpQ%>$oiluv}7t6d60}ceta&6e7r}T z3;uJ#<5u*?;|iPU?I?Q&p}*$X|`ue zmJaNjcusNO*Q0M7;!H~c@9S~T*xv@(R9Sq85mJy3o1Ui51@m$Byt?D@S8`r9?OtI%kK6EqY z-TznCs%yULzH6Yhs&9<)|CUdD7BaC8zH@>rtXlDh{cF)%DavVBs zI%K{EbEbXU$a*c2C|ZI(x`$F1%g}ui)|51AZO^58oXc%)$9R}NfV%IVPxU(L??gWP ziuMx_GZ8K4cmZolkw;6OT%pm08u+hfxsxRpjTg8UX^^Gu4mqYE%J}S#`69;O%<|Sv zmvtK8+uMF5arrLlajfOrEZbd$`_bUtv8^z>gJ}BJyCk+ej(651iW*Sfy~Uf2S5Kzh zfiDjJyu6c`0KC zk1>jvzj58QojlIub6kr!uAPZBYOR|}cz+Jv)%T4Vc}xe~iK45~H|(k<*+umqATM7_ zemPAWxuYHZGe7=hWG5|}tfjnsnKp9YA*{X07;6Y)YQk7U=2&qCxZ@1Swg&mE|Hs+i zgCF$RwRXM4yL}t+xu?*FCsXQ!^SVx<%@3je>qyyuYxuCe^Vq&^?q`0AGB0`G=r(vi zr}a92;ktoWr4IGsi6R62pE5++5P_~UU}L~MguJx-wdEa%8F+5J`Lb-oWe0%MjVK?3 z_SU_{YY6hW55^+@4A&w8zMtZpZud*F{R@zd+pz3j^rxQXK;8k*jky&IfPor=0U!7RIef3gnmE!7PaotPW7e2U^^m)38&dWK`%#x|-m*vQ+@rs9weEG^eT+xN zSQ`)TNgV4MJ!j(Vcha_=n$7jn-gkG2^q`r3gcR{?d$H_$Hu|>cJ@DO(OQSCKGtL9+vz8&}=pKUpPrD@B>BkR9!+H&J4)0UAp2Yr;HZmB^nyLTJn ztmi!B`G)c{E7D{dvLfN`o$d-PSV())*I?WAs8^=2PL4 zK9zY?oSd}iAnG1GgPPJ;nR5kW%s0o~kz$YA1OCsz|4o2AqJOi@8ps+ z1r~lDo6n~w^29{hA~eZ>E79Rjl_@JX)A19;k`%U zckUY9??y39WBB;>IS zd(pOW-F4D`bQwMC&~9MI{t;sVes+v>70bhO0&RDrE!UZfJmKfp(LU?B^}FyT!XB?o zT7-Si$GoQq5pBxUs4$+Q;+uBVfV(bYpC+59>UQ^*jgs#kmLC ztMkY@hqmfgpggU{^dq%0^`uwxJe!u`lD>j8*c{>jmbuQ5c^=?Vp)2L+GZW=|G?@qa zan2&oym@WnwT{my)#u$j);)i-^^F*2Q{7j2;tYq^mz`+CxTVjRJiUbd)Lcn#dr;aT z30+2edS z9KiX{UFcJ^!~VcQ}p!vU57x=M$x(prM6>mHQ((4z5>vs9R!d>L0JPZ@v=o9Jkr?mejZJ z2huM242N!$V_OXw)A3yZo(GoSNZxPzjjfQ)I$yWW)-OXFo&)EDzcp96{kb?V=Kegx zOdo-B7Q98DV$C%!^A1f~#QxyR$P;+3r{Ou<@z3RxQ`Qx6-NM(6MTGUadx&pecW%1y z!5PT&!1fx~o!r?l-n5@Nax7Ost_0%f6s%oMXXyJnaF(1>as7Zw&5BQJ2T_CUEl2mKf)3lzn7!Io4^8U4@@tH{`MQ zbxHWR@p0i};G^TC;X{wH&1v355yyFB(bflF)jiXd&G}!9VP$9b5Ikc=IhEpBAb6)8PWw0?&z3y~Z7hXc$+WfzzWn7B>~%I8 z^fItoK3Us%ug6_?FQ4BWG&Y>VvL%B~L0#BxHlH0%F?`w2JV49afZ-JECBwjZ3(nbc zF;7lM{qTwM{vG(R5!%p=f6%CQ*>I}=2=cv}OxeRjXU%|Y^@ogM%^$(0a9$_cS782L z4_>E+&RTUU`oelMZ7A9d{Wxt#hUUA&kNp+!Ecq;n>bv#SLkBv$Oa2q_6R(Gi(Vul% z{u$OO-e2-P$3~3BGhd};TR0>EhtI&5eN14w)HUm731XF5XvdgynKoUjg>FG90#d5Ey3$#NM<1R_sl&FWWbi))hSp+0Q}$5!Cq)YXAG* zRkFW*H=HtRYLa$E+p}~9okpY5GHChK2{g((`8zil7~ek7Q~#5$nE4VdZ{+**efQ4P zPxyexz61Eo)Q2A01FXh`rp&ku_J6@>-wf{aBjdzoey+3wSaTh2OWN>_yC=+kUAsOo zV;I(~$AGow!rC>$_ro#Qm(KVb>YvE5UufzK#*uY=)Ex7Z80$$1j2}Mj0n;8~zg-WV zEyuxY*rUj2+igY4zMbhH=nmuj$#hd%apStHMQn1g#r846&jT~eal|IQzX$XA(i}Yd ze?dHBIU9Ti6W3{UbI}&mUGvq<d&Xj)mWVBt2 zGR9Ng)A4@QOZ=TTd!J|RVb!>l*Itf?>Tm`Vd(th|4CclAij}a>3Or-VA3-fVww5Mi z(K_A}O_Y6WcxJ}yrv@2B3^5_6i}yzdCdzUBWsHnLOOelUUIyZ@L<+TF+=a~VPkqY~ zpS8gs9hfBf|E_QOP3|uCR~Bx+>07=Qdn^yN1kCm?pu80KY(qlsNz(rYFo#&~*HOM% zPs(Ch66ej{g*w^mqj{aT{5`L8E&RdF6SVqLtR0EyoK7$1r4{ogVBf_%Y^*poIU87& z0;_fxwd9;3`}r~Wi`WL^7Wj~O=!|3+eS5H_AuBC%*M!~XLNoU1){ z-!QCI$Z^!&#cdOaO$G03CGRze>qR-v4up;T9dj9VMqoU@{Ss&8kV)O&tob|Z@$1ic z*Lvtetdkjg~=QpC_=WkGfLLA7h`Yu z3vAJn*^IflUC%hO;J+-fi9;ChdpYwEb0)1^XVBQSy?QF|Hb7R}~*Gu@CHxS6}~^{NkL}nfPwbSy>7n54Iitjh1if5}Ad?sVpQj7ukOtacu@mcTh z^z?T|R!nrb0$A;V-^ZD7Q3>w({1q|B4=+3*=hK$c&&xijAMJ#%Xt@(K&+$^UVH{qD z92rOt>YsLEEFTA8#~V(MW{1A&p1ucX?L4lGKe!jco?0-M1#X;2e{zp&6SCUt1b=UB zqL8_Fp1KF;wvE3(o?qg>-Q!9Hzuk=|2_ABfoR9HXI3I#+LY|cp=Tc4L90I-;uJ?V~ zJ@#>Y`fFEFBips<(w@Gnv}-||HhcvC*WHUa&1^=Bb=Rxa-HUjq#p2ndbzaCZDeIAK zN9TPL{G1OqBicu0KMlH+F9-c_e#LS#-z9DH+h%^okKJM~HXL@;fc-hw&3{$oElWop z>uEIB4v`;`Jlfro_hj&3d~w#%b6&#r>bBWD`JO)+54jiRV;m2iMEltOJse{pzcFm$ zk|c2%JzDMu*bc2T6t36NAbGxO>VfliZ*gNkPc1xO_&)!7$*a^QdFiHn)?Q8Y_n_qS zk;3PN2i%L!1Rt?qGkJ9^mAuH**WXetxuk9M_1o5WFL_^cCG6(86?t9jh<3hqtotse z5zn^p^AKJyVaTnX=M698( zOED+vt}_-HXrDNyi&$(c)>DpE8o}d-kd<-Y+Tvb>wWcM3zOAwH`MZ=^Vqu-fcjxN+ zK40p>9@&lWq$c5c%Mg6e(Sz@D>h(trck`!)3-4nX*q_MnL3aWlq5tnlIT}8fW5|XK zTMa48Es*68yQELL+Mw*KhEO)*KCJ8Gn%^0cDB6Q^{;siqh?YIBOxn+tzjrrjt5V1s?Pz<`>5_T>gv9bT036H znKH*4?;@`FM4oN${!C)VdVCP|51w&p<_8!0S`VJ3Hyu3#&zinVO_(d2kFCxJ4wnLt zIlv_kX9%;KfRi|5?7KIxo_pN!3prl>s9w$ZY|E8?ea)J9NwIH;;q;k!6QI*d)hc);;&}WyF3js5^J$KI@Ft zGuMjWCUgc6JMuXn_n%;2i1g-0%w5E9;!F?m+q{DG>_pLUeOYJt8Z#Y-8>nAX_U!4RU_j=C>?OMQ%nOuR?2%7C4SMN4N(eQO+nr}v(1m+F*fxcV4=qG{LIgLgiYJ(5)!FCp*4*SP;r2K4*@J#q> zZl@DsY{}=(xUb`Op$ldl#hh#$k+Z7~eD6qMo_sbV*Oa=8WdG^Ndk1YFX5L?Qi~eyI z0{_!{uWh|^!gyNm$UM{iZ%rOo+iQ)+1o0eHe0PAKg~GS-v(St;HWg)1L$q;S+AfdB z*V_18+ODlw2Qdfgjg@P~^Uq5#Z#&ZDSvH-xr>}m#8*F{Y!DI67?xv<>$Z!yL z&d*rC!{fmB7ceixS%m(%c;5WleBQ_59T3LgD%4^B!#?X;&^(9U`I9+EfJG7WK+4#% zZ(_O-bX=?@sm=312VfWQ)gl(i$dXuIiTpKVsFfg_7vpA~TIZwu&YE>b+FzXb3H-Hu zZmPp~Lmv3*zCh|qb)eE%JK?W8`TV(U3&Po*9K_XK3399WY)Q z_`bF_ddn${3xDST&)bVET;4~Y7B29Y%iGOoWfF&hZNKk(V(kA@G4`jx4}C5}tX0Cc zvDSofpO>V}h|L6}Z#@m!BcO!o&qO*>vC~%MMeU??%}gjEQY306oJW z3wvuh%~%BA(E89Fv=3w6jCE#u<8#;U;ykXGw$`_t{V>ND826}OpbqPp`(l2jz}W|z zn2mcG8`a(HL+6~{r>z-#k z&NI&7XR`9WD6fG$&d0uWMojYKc!^^nUZ>c%2-`)U+MUB%b;H+lE`jGlB|Yu2p1%8{ z#Dd$E;yVo-vvE0(cUHAqKHK5__77z}-gj_a!kpvI;}WaJN3LZLumO#_z(}v4m)gzy!?I;uS4%*uDp-;&;EiKg?W3iHs(Y2ldnkX zf_-&u{Di+#cS`elh*{smd5#w{99w-UFrNeL^Pq#-&_ym{1^7F}qO31#Z*eE)vd`Q< z@t7LF?OxOfJLH%-;}$#z1uxqd$vuP`H~iuviLbCK^I;md6ZzH|&ku)*xsZSiBHuc@!FtMLPJ>L$I}KyM;{v5`)GvmetU*0B z*5JBSCils_uD<;R_N*8`>)*25{KbgRa30 z!64hF^-FBM={YuSK>wxYxHRa$htE8_t#O52$F)$&zuL}MZ9xy%{I#IXQ-fVt{IyDj zhF$dJ*C=$YP3y1=ALO+3*>+g!^OrdL{W^WB^HbHQ4t+8|p_dcwap=&Wm9IlzJv8)X z(FXKYYPE&FELw-2tU5aMW6`R9bm+&**P)*tTG5XS`bk*)2hfiW{q(ejetKx=$D-ZP zPlLthT18GpKL+$uYSn>$dT8j!qIKxUXVrmztb7Cd>7k(?i`LiL_4Qj6dV@mWuF&;1 zZQQBwxyR1eA5iG6Hf=m&*VmeCdodnU@^>itJMDbERpIlrlD}KY|CODuy=e2%U$yJ# z`xN?3g?>w+|ESOhY}$C==5HLd=^-E4^V|5?&exCHZS^jNKBmwbo-bPZa^qQ=Nhc-e zbhVkZKHRROpQzBI75Zd_9&giHdQQ)L)hF5cdZt28k+d~m=?ytwU&FI4b-r5VZLiDn zm->_s)<0{*ZQW=m+Vnu{t?F~NNp^j84jWSx+^5?3CQZk8?@b)7*n;I${i>E{^Hlw+ zI&YJ%Q1~pg>l;_w{i=Rd3n+DhN}Z6MKagM5Yi#+oTD!jGS6Tj~wYEI!ywx_?^^eMSd@04{cebNh3pI3f^K0R)ZDa|!4*|zy4=(7joLFm&MZ{wk-+j8m9t+5A=iY}{h!8yjp|zulJ8fSyY&IicqsT2pAZ zLMPj_jygWGtr}bL*}cU_%~#jxZ>nS2TInyOtv!kTlj>X8rrqeL!IE8((G3}`eD`pL zhOPDFkG5$w7Wh|e&48_0;~q?`Zut~ztZk3Vu_pQ%tBRKkV@+7&R`f9#KdGH;&v$jK zYTRSRslxANDmtEGw^jE{R@`Ha!}k+8j`{tJW1#(262{SHj!BK*bm+`yjTJhx@O5Jx z4OYJ5AJlOelkEDI@1J#}?}x%4Vl3RBC3ERRQs(r2WOl*M5@ug2_V^xwoew+fsRNlU z`X7xsRiEYp_C0(cug{c0hn@A%(-k^fp%tHI*_qI1o?S;>yHua1?g`ZWp?0;ce;q!} zlEDbrIIBJ_DP-$3snVvkk$XVXL8wATpNTeh8Of3&f69Z>S$xAV1wlD2H$w^7>uVm$Ba z>togVYPjwGjbw!$Zqw>~Rc&A0AM4|79Q1UXR{f5aY0Inbw+8aDTDDUE5}T)L`>s42 z4_ARg&$DUw6}Df|7utMWSKD<|+gJC)deE+~hZMTfrrpbJ{;nFEXHu;_Kay^;Y1QVn zTkJ8aahv;gn`ctJ()Lb8FZb9o7!TO_25i36@@&OkB0 zYC8sO$C5#f`3=|(=UdoQL}DNBgS}PWRwc{Beagl9r`fou^ZFlMhx?n?{0zhT9Vs8R zU6Mc2*V=Iso~ap$OSo?j=KMQ&=13Z!qv7oPITB~zT;`r6ZCT~v1rPjoSZ^MgIEx#A zhXI{i_^JNFJ>8xs2K6HGn|95!>*&zAuaC|x{1aso|6%>$ zug0!!x81)QBdc+cdd_TMZdkIb_-o^B`*Q)~gkmf0eksdAd_V7hYo4mHr7DXCS-grY zzH6i`FZ3hJKzoXTa8cJA6_>&I3^mrW;zJQ5!G3ylI#r=zKRx-dAB$Gc2{hObk5%x2 z{aAH$*iR1)`?2W3#9FG)a0L|G3)*}JbN;HXH#*iEO9t(x-uj_Jsh{im=*P0_wgs~M z%0A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 literal 0 HcmV?d00001 diff --git a/src/XIVLauncher2.Common.Unix/libsteam_api64.so b/src/XIVLauncher2.Common.Unix/libsteam_api64.so new file mode 100644 index 0000000000000000000000000000000000000000..8bf6762bb35caa5309598a0168549099fd0f75eb GIT binary patch literal 416413 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

ZPiBFi@yz?_7C|eFTaWbvV1Yi#8 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 envVars, DpiAwareness dpiAwareness, Action beforeResume) + { + Process process = null; + + var userName = Environment.UserName; + + var pExplicitAccess = new PInvoke.EXPLICIT_ACCESS(); + PInvoke.BuildExplicitAccessWithName( + ref pExplicitAccess, + userName, + PInvoke.STANDARD_RIGHTS_ALL | PInvoke.SPECIFIC_RIGHTS_ALL & ~PInvoke.PROCESS_VM_WRITE, + PInvoke.GRANT_ACCESS, + 0); + + if (PInvoke.SetEntriesInAcl(1, ref pExplicitAccess, IntPtr.Zero, out var newAcl) != 0) + { + throw new Win32Exception(Marshal.GetLastWin32Error()); + } + + var secDesc = new PInvoke.SECURITY_DESCRIPTOR(); + + if (!PInvoke.InitializeSecurityDescriptor(out secDesc, PInvoke.SECURITY_DESCRIPTOR_REVISION)) + { + throw new Win32Exception(Marshal.GetLastWin32Error()); + } + + if (!PInvoke.SetSecurityDescriptorDacl(ref secDesc, true, newAcl, false)) + { + throw new Win32Exception(Marshal.GetLastWin32Error()); + } + + var psecDesc = Marshal.AllocHGlobal(Marshal.SizeOf()); + Marshal.StructureToPtr(secDesc, psecDesc, true); + + var lpProcessInformation = new PInvoke.PROCESS_INFORMATION(); + var lpEnvironment = IntPtr.Zero; + + try + { + if (envVars.Count > 0) + { + string envstr = string.Join("\0", envVars.Select(entry => entry.Key + "=" + entry.Value)); + + lpEnvironment = Marshal.StringToHGlobalAnsi(envstr); + } + + var lpProcessAttributes = new PInvoke.SECURITY_ATTRIBUTES + { + nLength = Marshal.SizeOf(), + lpSecurityDescriptor = psecDesc, + bInheritHandle = false + }; + + var lpStartupInfo = new PInvoke.STARTUPINFO + { + cb = Marshal.SizeOf() + }; + + var compatLayerPrev = Environment.GetEnvironmentVariable("__COMPAT_LAYER"); + + var compat = "RunAsInvoker "; + compat += dpiAwareness switch + { + DpiAwareness.Aware => "HighDPIAware", + DpiAwareness.Unaware => "DPIUnaware", + _ => throw new ArgumentOutOfRangeException() + }; + Environment.SetEnvironmentVariable("__COMPAT_LAYER", compat); + + if (!PInvoke.CreateProcess( + null, + $"\"{exePath}\" {arguments}", + ref lpProcessAttributes, + IntPtr.Zero, + false, + PInvoke.CREATE_SUSPENDED, + IntPtr.Zero, + workingDir, + ref lpStartupInfo, + out lpProcessInformation)) + { + throw new Win32Exception(Marshal.GetLastWin32Error()); + } + + Environment.SetEnvironmentVariable("__COMPAT_LAYER", compatLayerPrev); + + DisableSeDebug(lpProcessInformation.hProcess); + + process = new ExistingProcess(lpProcessInformation.hProcess); + + beforeResume?.Invoke(process); + + PInvoke.ResumeThread(lpProcessInformation.hThread); + + // Ensure that the game main window is prepared + try + { + do + { + process.WaitForInputIdle(); + + Thread.Sleep(100); + } while (IntPtr.Zero == TryFindGameWindow(process)); + } + catch (InvalidOperationException) + { + throw new GameExitedException(); + } + + if (PInvoke.GetSecurityInfo( + PInvoke.GetCurrentProcess(), + PInvoke.SE_OBJECT_TYPE.SE_KERNEL_OBJECT, + PInvoke.SECURITY_INFORMATION.DACL_SECURITY_INFORMATION, + IntPtr.Zero, IntPtr.Zero, + out var pACL, + IntPtr.Zero, IntPtr.Zero) != 0) + { + throw new Win32Exception(Marshal.GetLastWin32Error()); + } + + if (PInvoke.SetSecurityInfo( + lpProcessInformation.hProcess, + PInvoke.SE_OBJECT_TYPE.SE_KERNEL_OBJECT, + PInvoke.SECURITY_INFORMATION.DACL_SECURITY_INFORMATION | + PInvoke.SECURITY_INFORMATION.UNPROTECTED_DACL_SECURITY_INFORMATION, + IntPtr.Zero, IntPtr.Zero, pACL, IntPtr.Zero) != 0) + { + throw new Win32Exception(Marshal.GetLastWin32Error()); + } + } + catch (Exception ex) + { + Log.Error(ex, "[NativeAclFix] Uncaught error during initialization, trying to kill process"); + + try + { + process?.Kill(); + } + catch (Exception killEx) + { + Log.Error(killEx, "[NativeAclFix] Could not kill process"); + } + + throw; + } + finally + { + Marshal.FreeHGlobal(psecDesc); + + if (!IntPtr.Equals(lpEnvironment, IntPtr.Zero)) + { + Marshal.FreeHGlobal(lpEnvironment); + } + + PInvoke.CloseHandle(lpProcessInformation.hThread); + } + + return process; + } + + private static void DisableSeDebug(IntPtr ProcessHandle) + { + if (!PInvoke.OpenProcessToken(ProcessHandle, PInvoke.TOKEN_QUERY | PInvoke.TOKEN_ADJUST_PRIVILEGES, out var TokenHandle)) + { + throw new Win32Exception(Marshal.GetLastWin32Error()); + } + + var luidDebugPrivilege = new PInvoke.LUID(); + if (!PInvoke.LookupPrivilegeValue(null, "SeDebugPrivilege", ref luidDebugPrivilege)) + { + throw new Win32Exception(Marshal.GetLastWin32Error()); + } + + var RequiredPrivileges = new PInvoke.PRIVILEGE_SET + { + PrivilegeCount = 1, + Control = PInvoke.PRIVILEGE_SET_ALL_NECESSARY, + Privilege = new PInvoke.LUID_AND_ATTRIBUTES[1] + }; + + RequiredPrivileges.Privilege[0].Luid = luidDebugPrivilege; + RequiredPrivileges.Privilege[0].Attributes = PInvoke.SE_PRIVILEGE_ENABLED; + + if (!PInvoke.PrivilegeCheck(TokenHandle, ref RequiredPrivileges, out bool bResult)) + { + throw new Win32Exception(Marshal.GetLastWin32Error()); + } + + if (bResult) // SeDebugPrivilege is enabled; try disabling it + { + var TokenPrivileges = new PInvoke.TOKEN_PRIVILEGES + { + PrivilegeCount = 1, + Privileges = new PInvoke.LUID_AND_ATTRIBUTES[1] + }; + + TokenPrivileges.Privileges[0].Luid = luidDebugPrivilege; + TokenPrivileges.Privileges[0].Attributes = PInvoke.SE_PRIVILEGE_REMOVED; + + if (!PInvoke.AdjustTokenPrivileges(TokenHandle, false, ref TokenPrivileges, 0, IntPtr.Zero, 0)) + { + throw new Win32Exception(Marshal.GetLastWin32Error()); + } + } + + PInvoke.CloseHandle(TokenHandle); + } + + [DllImport("user32.dll", SetLastError = true)] + private static extern IntPtr FindWindowEx(IntPtr parentHandle, IntPtr hWndChildAfter, string className, IntPtr windowTitle); + [DllImport("user32.dll", SetLastError = true)] + private static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint lpdwProcessId); + [DllImport("user32.dll")] + [return: MarshalAs(UnmanagedType.Bool)] + static extern bool IsWindowVisible(IntPtr hWnd); + + private static IntPtr TryFindGameWindow(Process process) + { + IntPtr hwnd = IntPtr.Zero; + while (IntPtr.Zero != (hwnd = FindWindowEx(IntPtr.Zero, hwnd, "FFXIVGAME", IntPtr.Zero))) + { + GetWindowThreadProcessId(hwnd, out uint pid); + + if (pid == process.Id && IsWindowVisible(hwnd)) + { + break; + } + } + return hwnd; + } + } +} diff --git a/src/XIVLauncher2.Common.Windows/WindowsDalamudCompatibilityCheck.cs b/src/XIVLauncher2.Common.Windows/WindowsDalamudCompatibilityCheck.cs new file mode 100644 index 0000000..aaf59bc --- /dev/null +++ b/src/XIVLauncher2.Common.Windows/WindowsDalamudCompatibilityCheck.cs @@ -0,0 +1,124 @@ +using System; +using System.Collections.Generic; +using System.Runtime.InteropServices; +using Microsoft.Win32; +using Serilog; +using XIVLauncher2.Common.PlatformAbstractions; + +namespace XIVLauncher2.Common.Windows; + +public class WindowsDalamudCompatibilityCheck : IDalamudCompatibilityCheck +{ + public void EnsureCompatibility() + { + if (!CheckVcRedists()) + throw new IDalamudCompatibilityCheck.NoRedistsException(); + + EnsureArchitecture(); + } + + private static void EnsureArchitecture() + { + var arch = RuntimeInformation.ProcessArchitecture; + + switch (arch) + { + case Architecture.X86: + throw new IDalamudCompatibilityCheck.ArchitectureNotSupportedException("Dalamud is not supported on x86 architecture."); + + case Architecture.X64: + break; + + case Architecture.Arm: + throw new IDalamudCompatibilityCheck.ArchitectureNotSupportedException("Dalamud is not supported on ARM32."); + + case Architecture.Arm64: + throw new IDalamudCompatibilityCheck.ArchitectureNotSupportedException("x64 emulation was not detected. Please make sure to run XIVLauncher with x64 emulation."); + } + } + + [DllImport("kernel32", SetLastError = true)] + private static extern IntPtr LoadLibrary(string lpFileName); + + private static bool CheckLibrary(string fileName) + { + if (LoadLibrary(fileName) != IntPtr.Zero) + { + Log.Debug("Found " + fileName); + return true; + } + else + { + Log.Error("Could not find " + fileName); + } + return false; + } + + private static bool CheckVcRedists() + { + // snipped from https://stackoverflow.com/questions/12206314/detect-if-visual-c-redistributable-for-visual-studio-2012-is-installed + // and https://github.com/bitbeans/RedistributableChecker + + var vc2022Paths = new List + { + @"SOFTWARE\Microsoft\DevDiv\VC\Servicing\14.0\RuntimeMinimum", + @"SOFTWARE\Microsoft\VisualStudio\14.0\VC\Runtimes\X64", + @"SOFTWARE\Classes\Installer\Dependencies\Microsoft.VS.VC_RuntimeMinimumVSU_amd64,v14", + @"SOFTWARE\Classes\Installer\Dependencies\VC,redist.x64,amd64,14.31,bundle", + @"SOFTWARE\Classes\Installer\Dependencies\VC,redist.x64,amd64,14.30,bundle", + @"SOFTWARE\Classes\Installer\Dependencies\VC,redist.x64,amd64,14.29,bundle", + @"SOFTWARE\Classes\Installer\Dependencies\VC,redist.x64,amd64,14.28,bundle", + // technically, this was introduced in VCrun2017 with 14.16 + // but we shouldn't go that far + // here's a legacy vcrun2017 check + @"Installer\Dependencies\,,amd64,14.0,bundle", + // here's one for vcrun2015 + @"SOFTWARE\Classes\Installer\Dependencies\{d992c12e-cab2-426f-bde3-fb8c53950b0d}" + }; + + var dllPaths = new List + { + "ucrtbase_clr0400", + "vcruntime140_clr0400", + "vcruntime140" + }; + + var passedRegistry = false; + var passedDllChecks = true; + + foreach (var path in vc2022Paths) + { + Log.Debug("Checking Registry key: " + path); + var vcregcheck = Registry.LocalMachine.OpenSubKey(path, false); + if (vcregcheck == null) continue; + + var vcVersioncheck = vcregcheck.GetValue("Version") ?? ""; + + if (((string)vcVersioncheck).StartsWith("14", StringComparison.Ordinal)) + { + passedRegistry = true; + Log.Debug("Passed Registry Check with: " + path); + break; + } + } + + foreach (var path in dllPaths) + { + Log.Debug("Checking for DLL: " + path); + passedDllChecks = passedDllChecks && CheckLibrary(path); + } + + // Display our findings + if (!passedRegistry) + { + Log.Error("Failed all registry checks to find any Visual C++ 2015-2022 Runtimes."); + } + + if (!passedDllChecks) + { + Log.Error("Missing DLL files required by Dalamud."); + } + + return (passedRegistry && passedDllChecks); + } +} diff --git a/src/XIVLauncher2.Common.Windows/WindowsDalamudRunner.cs b/src/XIVLauncher2.Common.Windows/WindowsDalamudRunner.cs new file mode 100644 index 0000000..ed0097c --- /dev/null +++ b/src/XIVLauncher2.Common.Windows/WindowsDalamudRunner.cs @@ -0,0 +1,201 @@ +#nullable enable +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Runtime.InteropServices; +using System.Text.Json; +using Serilog; +using XIVLauncher2.Common.Dalamud; +using XIVLauncher2.Common.PlatformAbstractions; + +namespace XIVLauncher2.Common.Windows; + +public class WindowsDalamudRunner : IDalamudRunner +{ + public Process? Run(FileInfo runner, bool fakeLogin, bool noPlugins, bool noThirdPlugins, FileInfo gameExe, string gameArgs, IDictionary environment, DalamudLoadMethod loadMethod, DalamudStartInfo startInfo) + { + var inheritableCurrentProcess = GetInheritableCurrentProcessHandle(); + + var launchArguments = new List + { + DalamudInjectorArgs.Launch, + DalamudInjectorArgs.Mode(loadMethod == DalamudLoadMethod.EntryPoint ? "entrypoint" : "inject"), + DalamudInjectorArgs.HandleOwner((long)(inheritableCurrentProcess?.Handle ?? IntPtr.Zero)), + DalamudInjectorArgs.Game(gameExe.FullName), + DalamudInjectorArgs.WorkingDirectory(startInfo.WorkingDirectory), + DalamudInjectorArgs.ConfigurationPath(startInfo.ConfigurationPath), + DalamudInjectorArgs.PluginDirectory(startInfo.PluginDirectory), + DalamudInjectorArgs.PluginDevDirectory(startInfo.DefaultPluginDirectory), + DalamudInjectorArgs.AssetDirectory(startInfo.AssetDirectory), + DalamudInjectorArgs.ClientLanguage((int)startInfo.Language), + DalamudInjectorArgs.DelayInitialize(startInfo.DelayInitializeMs), + DalamudInjectorArgs.TSPackB64(Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes(startInfo.TroubleshootingPackData))), + }; + + if (loadMethod == DalamudLoadMethod.ACLonly) + launchArguments.Add(DalamudInjectorArgs.WithoutDalamud); + + if (fakeLogin) + launchArguments.Add(DalamudInjectorArgs.FakeArguments); + + if (noPlugins) + launchArguments.Add(DalamudInjectorArgs.NoPlugin); + + if (noThirdPlugins) + launchArguments.Add(DalamudInjectorArgs.NoThirdParty); + + launchArguments.Add("--"); + launchArguments.Add(gameArgs); + + var psi = new ProcessStartInfo(runner.FullName) { + Arguments = string.Join(" ", launchArguments), + RedirectStandardOutput = true, + UseShellExecute = false, + CreateNoWindow = true + }; + + foreach (var keyValuePair in environment) + { + if (psi.EnvironmentVariables.ContainsKey(keyValuePair.Key)) + psi.EnvironmentVariables[keyValuePair.Key] = keyValuePair.Value; + else + psi.EnvironmentVariables.Add(keyValuePair.Key, keyValuePair.Value); + } + + try + { + var dalamudProcess = Process.Start(psi); + var output = dalamudProcess.StandardOutput.ReadLine(); + + if (output == null) + throw new DalamudRunnerException("An internal Dalamud error has occured"); + + try + { + var dalamudConsoleOutput = JsonSerializer.Deserialize(output); + Process gameProcess; + + if (dalamudConsoleOutput.Handle == 0) + { + Log.Warning($"Dalamud returned NULL process handle, attempting to recover by creating a new one from pid {dalamudConsoleOutput.Pid}..."); + gameProcess = Process.GetProcessById(dalamudConsoleOutput.Pid); + } + else + { + gameProcess = new ExistingProcess((IntPtr)dalamudConsoleOutput.Handle); + } + + try + { + Log.Verbose($"Got game process handle {gameProcess.Handle} with pid {gameProcess.Id}"); + } + catch (InvalidOperationException ex) + { + Log.Error(ex, $"Dalamud returned invalid process handle {gameProcess.Handle}, attempting to recover by creating a new one from pid {dalamudConsoleOutput.Pid}..."); + gameProcess = Process.GetProcessById(dalamudConsoleOutput.Pid); + Log.Warning($"Recovered with process handle {gameProcess.Handle}"); + } + + if (gameProcess.Id != dalamudConsoleOutput.Pid) + Log.Warning($"Internal Process ID {gameProcess.Id} does not match Dalamud provided one {dalamudConsoleOutput.Pid}"); + + return gameProcess; + } + catch (JsonException ex) + { + Log.Error(ex, $"Couldn't parse Dalamud output: {output}"); + return null; + } + } + catch (Exception ex) + { + throw new DalamudRunnerException("Error trying to start Dalamud.", ex); + } + } + + ///

+ /// DUPLICATE_* values for DuplicateHandle's dwDesiredAccess. + /// + [Flags] + private enum DuplicateOptions : uint { + /// + /// Closes the source handle. This occurs regardless of any error status returned. + /// + CloseSource = 0x00000001, + + /// + /// Ignores the dwDesiredAccess parameter. The duplicate handle has the same access as the source handle. + /// + SameAccess = 0x00000002, + } + + /// + /// Duplicates an object handle. + /// + /// + /// A handle to the process with the handle to be duplicated. + /// + /// The handle must have the PROCESS_DUP_HANDLE access right. + /// + /// + /// The handle to be duplicated. This is an open object handle that is valid in the context of the source process. + /// For a list of objects whose handles can be duplicated, see the following Remarks section. + /// + /// + /// A handle to the process that is to receive the duplicated handle. + /// + /// The handle must have the PROCESS_DUP_HANDLE access right. + /// + /// + /// A pointer to a variable that receives the duplicate handle. This handle value is valid in the context of the target process. + /// + /// If hSourceHandle is a pseudo handle returned by GetCurrentProcess or GetCurrentThread, DuplicateHandle converts it to a real handle to a process or thread, respectively. + /// + /// If lpTargetHandle is NULL, the function duplicates the handle, but does not return the duplicate handle value to the caller. This behavior exists only for backward compatibility with previous versions of this function. You should not use this feature, as you will lose system resources until the target process terminates. + /// + /// This parameter is ignored if hTargetProcessHandle is NULL. + /// + /// + /// The access requested for the new handle. For the flags that can be specified for each object type, see the following Remarks section. + /// + /// This parameter is ignored if the dwOptions parameter specifies the DUPLICATE_SAME_ACCESS flag. Otherwise, the flags that can be specified depend on the type of object whose handle is to be duplicated. + /// + /// This parameter is ignored if hTargetProcessHandle is NULL. + /// + /// + /// A variable that indicates whether the handle is inheritable. If TRUE, the duplicate handle can be inherited by new processes created by the target process. If FALSE, the new handle cannot be inherited. + /// + /// This parameter is ignored if hTargetProcessHandle is NULL. + /// + /// + /// Optional actions. + /// + /// + /// If the function succeeds, the return value is nonzero. + /// + /// If the function fails, the return value is zero. To get extended error information, call GetLastError. + /// + /// + /// See https://docs.microsoft.com/en-us/windows/win32/api/handleapi/nf-handleapi-duplicatehandle. + /// + [DllImport("kernel32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + private static extern bool DuplicateHandle( + IntPtr hSourceProcessHandle, + IntPtr hSourceHandle, + IntPtr hTargetProcessHandle, + out IntPtr lpTargetHandle, + uint dwDesiredAccess, + [MarshalAs(UnmanagedType.Bool)] bool bInheritHandle, + DuplicateOptions dwOptions); + + private static Process? GetInheritableCurrentProcessHandle() { + if (!DuplicateHandle(Process.GetCurrentProcess().Handle, Process.GetCurrentProcess().Handle, Process.GetCurrentProcess().Handle, out var inheritableCurrentProcessHandle, 0, true, DuplicateOptions.SameAccess)) { + Log.Error("Failed to call DuplicateHandle: Win32 error code {0}", Marshal.GetLastWin32Error()); + return null; + } + + return new ExistingProcess(inheritableCurrentProcessHandle); + } +} diff --git a/src/XIVLauncher2.Common.Windows/WindowsGameRunner.cs b/src/XIVLauncher2.Common.Windows/WindowsGameRunner.cs new file mode 100644 index 0000000..bbeb895 --- /dev/null +++ b/src/XIVLauncher2.Common.Windows/WindowsGameRunner.cs @@ -0,0 +1,45 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using XIVLauncher.Common.Game; +using XIVLauncher2.Common.Dalamud; +using XIVLauncher2.Common.PlatformAbstractions; + +namespace XIVLauncher2.Common.Windows; + +public class WindowsGameRunner : IGameRunner +{ + private readonly DalamudLauncher dalamudLauncher; + private readonly bool dalamudOk; + private readonly DirectoryInfo dotnetRuntimePath; + + public WindowsGameRunner(DalamudLauncher dalamudLauncher, bool dalamudOk, DirectoryInfo dotnetRuntimePath) + { + this.dalamudLauncher = dalamudLauncher; + this.dalamudOk = dalamudOk; + this.dotnetRuntimePath = dotnetRuntimePath; + } + + public Process Start(string path, string workingDirectory, string arguments, IDictionary environment, DpiAwareness dpiAwareness) + { + if (dalamudOk) + { + var compat = "RunAsInvoker "; + compat += dpiAwareness switch { + DpiAwareness.Aware => "HighDPIAware", + DpiAwareness.Unaware => "DPIUnaware", + _ => throw new ArgumentOutOfRangeException() + }; + environment.Add("__COMPAT_LAYER", compat); + + var prevDalamudRuntime = Environment.GetEnvironmentVariable("DALAMUD_RUNTIME"); + if (string.IsNullOrWhiteSpace(prevDalamudRuntime)) + environment.Add("DALAMUD_RUNTIME", dotnetRuntimePath.FullName); + + return this.dalamudLauncher.Run(new FileInfo(path), arguments, environment); + } + + return NativeAclFix.LaunchGame(workingDirectory, path, arguments, environment, dpiAwareness, process => { }); + } +} diff --git a/src/XIVLauncher2.Common.Windows/WindowsRestartManager.cs b/src/XIVLauncher2.Common.Windows/WindowsRestartManager.cs new file mode 100644 index 0000000..7a1b964 --- /dev/null +++ b/src/XIVLauncher2.Common.Windows/WindowsRestartManager.cs @@ -0,0 +1,270 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices; +using System.Runtime.InteropServices.ComTypes; +using System.Text; +using Exception = System.Exception; + +// ReSharper disable FieldCanBeMadeReadOnly.Local +// ReSharper disable MemberCanBePrivate.Local + +namespace XIVLauncher2.Common.Windows; + +public class WindowsRestartManager : IDisposable +{ + public delegate void RmWriteStatusCallback(uint percentageCompleted); + + private const int RM_SESSION_KEY_LEN = 16; // sizeof GUID + private const int CCH_RM_SESSION_KEY = RM_SESSION_KEY_LEN * 2; + private const int CCH_RM_MAX_APP_NAME = 255; + private const int CCH_RM_MAX_SVC_NAME = 63; + private const int RM_INVALID_TS_SESSION = -1; + private const int RM_INVALID_PROCESS = -1; + private const int ERROR_MORE_DATA = 234; + + [StructLayout(LayoutKind.Sequential)] + public struct RmUniqueProcess + { + public int dwProcessId; // PID + public FILETIME ProcessStartTime; // Process creation time + } + + public enum RmAppType + { + /// + /// Application type cannot be classified in known categories + /// + RmUnknownApp = 0, + + /// + /// Application is a windows application that displays a top-level window + /// + RmMainWindow = 1, + + /// + /// Application is a windows app but does not display a top-level window + /// + RmOtherWindow = 2, + + /// + /// Application is an NT service + /// + RmService = 3, + + /// + /// Application is Explorer + /// + RmExplorer = 4, + + /// + /// Application is Console application + /// + RmConsole = 5, + + /// + /// Application is critical system process where a reboot is required to restart + /// + RmCritical = 1000, + } + + [Flags] + public enum RmRebootReason + { + /// + /// A system restart is not required. + /// + RmRebootReasonNone = 0x0, + + /// + /// The current user does not have sufficient privileges to shut down one or more processes. + /// + RmRebootReasonPermissionDenied = 0x1, + + /// + /// One or more processes are running in another Terminal Services session. + /// + RmRebootReasonSessionMismatch = 0x2, + + /// + /// A system restart is needed because one or more processes to be shut down are critical processes. + /// + RmRebootReasonCriticalProcess = 0x4, + + /// + /// A system restart is needed because one or more services to be shut down are critical services. + /// + RmRebootReasonCriticalService = 0x8, + + /// + /// A system restart is needed because the current process must be shut down. + /// + RmRebootReasonDetectedSelf = 0x10, + } + + [Flags] + private enum RmShutdownType + { + RmForceShutdown = 0x1, // Force app shutdown + RmShutdownOnlyRegistered = 0x10 // Only shutdown apps if all apps registered for restart + } + + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] + public struct RmProcessInfo + { + public RmUniqueProcess UniqueProcess; + + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = CCH_RM_MAX_APP_NAME + 1)] + public string AppName; + + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = CCH_RM_MAX_SVC_NAME + 1)] + public string ServiceShortName; + + public RmAppType ApplicationType; + public int AppStatus; + public int TSSessionId; + + [MarshalAs(UnmanagedType.Bool)] + public bool bRestartable; + + public Process Process + { + get + { + try + { + Process process = Process.GetProcessById(UniqueProcess.dwProcessId); + long fileTime = process.StartTime.ToFileTime(); + + if ((uint)UniqueProcess.ProcessStartTime.dwLowDateTime != (uint)(fileTime & uint.MaxValue)) + return null; + + if ((uint)UniqueProcess.ProcessStartTime.dwHighDateTime != (uint)(fileTime >> 32)) + return null; + + return process; + } + catch (Exception) + { + return null; + } + } + } + } + + [DllImport("rstrtmgr", CharSet = CharSet.Unicode)] + private static extern int RmStartSession(out int dwSessionHandle, int sessionFlags, StringBuilder strSessionKey); + + [DllImport("rstrtmgr")] + private static extern int RmEndSession(int dwSessionHandle); + + [DllImport("rstrtmgr")] + private static extern int RmShutdown(int dwSessionHandle, RmShutdownType lAtionFlags, RmWriteStatusCallback fnStatus); + + [DllImport("rstrtmgr")] + private static extern int RmRestart(int dwSessionHandle, int dwRestartFlags, RmWriteStatusCallback fnStatus); + + [DllImport("rstrtmgr")] + private static extern int RmGetList(int dwSessionHandle, out int nProcInfoNeeded, ref int nProcInfo, [In, Out] RmProcessInfo[] rgAffectedApps, out RmRebootReason dwRebootReasons); + + [DllImport("rstrtmgr", CharSet = CharSet.Unicode)] + private static extern int RmRegisterResources(int dwSessionHandle, + int nFiles, string[] rgsFileNames, + int nApplications, RmUniqueProcess[] rgApplications, + int nServices, string[] rgsServiceNames); + + private readonly int sessionHandle; + private readonly string sessionKey; + + public WindowsRestartManager() + { + var sessKey = new StringBuilder(CCH_RM_SESSION_KEY + 1); + ThrowOnFailure(RmStartSession(out sessionHandle, 0, sessKey)); + sessionKey = sessKey.ToString(); + } + + public void Register(IEnumerable files = null, IEnumerable processes = null, IEnumerable serviceNames = null) + { + string[] filesArray = files?.Select(f => f.FullName).ToArray() ?? Array.Empty(); + RmUniqueProcess[] processesArray = processes?.Select(f => new RmUniqueProcess + { + dwProcessId = f.Id, + ProcessStartTime = new FILETIME + { + dwLowDateTime = (int)(f.StartTime.ToFileTime() & uint.MaxValue), + dwHighDateTime = (int)(f.StartTime.ToFileTime() >> 32), + } + }).ToArray() ?? Array.Empty(); + string[] servicesArray = serviceNames?.ToArray() ?? Array.Empty(); + ThrowOnFailure(RmRegisterResources(sessionHandle, + filesArray.Length, filesArray, + processesArray.Length, processesArray, + servicesArray.Length, servicesArray)); + } + + public void Shutdown(bool forceShutdown = true, bool shutdownOnlyRegistered = false, RmWriteStatusCallback cb = null) + { + ThrowOnFailure(RmShutdown(sessionHandle, (forceShutdown ? RmShutdownType.RmForceShutdown : 0) | (shutdownOnlyRegistered ? RmShutdownType.RmShutdownOnlyRegistered : 0), cb)); + } + + public void Restart(RmWriteStatusCallback cb = null) + { + ThrowOnFailure(RmRestart(sessionHandle, 0, cb)); + } + + public List GetInterferingProcesses(out RmRebootReason rebootReason) + { + var count = 0; + var infos = new RmProcessInfo[count]; + var err = 0; + + for (var i = 0; i < 16; i++) + { + err = RmGetList(sessionHandle, out int needed, ref count, infos, out rebootReason); + + switch (err) + { + case 0: + return infos.Take(count).ToList(); + + case ERROR_MORE_DATA: + infos = new RmProcessInfo[count = needed]; + break; + + default: + ThrowOnFailure(err); + break; + } + } + + ThrowOnFailure(err); + + // should not reach + throw new InvalidOperationException(); + } + + private void ReleaseUnmanagedResources() + { + ThrowOnFailure(RmEndSession(sessionHandle)); + } + + public void Dispose() + { + ReleaseUnmanagedResources(); + GC.SuppressFinalize(this); + } + + ~WindowsRestartManager() + { + ReleaseUnmanagedResources(); + } + + private void ThrowOnFailure(int err) + { + if (err != 0) + throw new Win32Exception(err); + } +} diff --git a/src/XIVLauncher2.Common.Windows/WindowsSteam.cs b/src/XIVLauncher2.Common.Windows/WindowsSteam.cs new file mode 100644 index 0000000..2990a11 --- /dev/null +++ b/src/XIVLauncher2.Common.Windows/WindowsSteam.cs @@ -0,0 +1,175 @@ +using System; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.Win32; +using Serilog; +using Steamworks; +using XIVLauncher2.Common.Game.Exceptions; +using XIVLauncher2.Common.PlatformAbstractions; + +namespace XIVLauncher2.Common.Windows +{ + public class WindowsSteam : ISteam + { + private const int MAX_INIT_TRIES_AFTER_START = 15; + + public Task? AsyncStartTask { get; private set; } + + public WindowsSteam() + { + SteamUtils.OnGamepadTextInputDismissed += b => OnGamepadTextInputDismissed?.Invoke(b); + } + + public void KickoffAsyncStartup(uint appid) + { + AsyncStartTask = StartAndInitialize(appid); + } + + /// + /// Start Steam if not already running, and initialize our app. + /// + /// The app ID to init + private async Task StartAndInitialize(uint appId) + { + if (!Process.GetProcessesByName("steam").Any()) + StartSteam(); + + for (var i = 0; i < MAX_INIT_TRIES_AFTER_START; i++) + { + await Task.Delay(1000).ConfigureAwait(false); + + try + { + Initialize(appId); + + Log.Verbose("Steam started automatically"); + return; + } + catch (Exception ex) + { + Log.Verbose(ex, "Steam not ready yet, waiting a little longer..."); + } + } + + throw new SteamStartupTimedOutException(); + } + + public class SteamStartupTimedOutException : Exception + { + public SteamStartupTimedOutException() + : base("Could not init Steam in time") + { + } + } + + private static void StartSteam() + { + var path = FindSteam(); + + if (path == null || !path.Exists) + throw new SteamException($"Failed to find Steam at {path}"); + + var args = "-silent"; + + if (EnvironmentSettings.IsOpenSteamMinimal) + { + args += " -no-browser"; + } + + var psi = new ProcessStartInfo + { + FileName = path.FullName, + Arguments = args, + }; + + try + { + Process.Start(psi); + } + catch (Exception ex) + { + throw new SteamException("Steam Process.Start failed", ex); + } + } + + private static FileInfo? FindSteam() + { + var regValue = Registry.GetValue("HKEY_CLASSES_ROOT\\steam\\Shell\\Open\\Command", null, null) as string; + + if (regValue == null || !regValue.Contains("\"")) + return null; + + return new FileInfo(regValue.Substring(1, regValue.IndexOf('"', 1) - 1)); + } + + public void Initialize(uint appId) + { + // workaround because SetEnvironmentVariable doesn't actually touch the process environment on unix + if (Environment.OSVersion.Platform == PlatformID.Unix) + { + [System.Runtime.InteropServices.DllImport("c")] + static extern int setenv(string name, string value, int overwrite); + + setenv("SteamAppId", appId.ToString(), 1); + } + + SteamClient.Init(appId); + } + + public bool IsValid => SteamClient.IsValid; + + public bool BLoggedOn => SteamClient.IsLoggedOn; + + public bool BOverlayNeedsPresent => SteamUtils.DoesOverlayNeedPresent; + + public void Shutdown() + { + SteamClient.Shutdown(); + } + + public async Task GetAuthSessionTicketAsync() + { + var ticket = await SteamUser.GetAuthSessionTicketAsync().ConfigureAwait(true); + return ticket?.Data; + } + + public bool IsAppInstalled(uint appId) + { + return SteamApps.IsAppInstalled(appId); + } + + public string GetAppInstallDir(uint appId) + { + return SteamApps.AppInstallDir(appId); + } + + public bool ShowGamepadTextInput(bool password, bool multiline, string description, int maxChars, string existingText = "") + { + return SteamUtils.ShowGamepadTextInput(password ? GamepadTextInputMode.Password : GamepadTextInputMode.Normal, multiline ? GamepadTextInputLineMode.MultipleLines : GamepadTextInputLineMode.SingleLine, description, maxChars, existingText); + } + + public string GetEnteredGamepadText() + { + return SteamUtils.GetEnteredGamepadText(); + } + + public bool ShowFloatingGamepadTextInput(ISteam.EFloatingGamepadTextInputMode mode, int x, int y, int width, int height) + { + // Facepunch.Steamworks doesn't have this... + return false; + } + + public bool IsRunningOnSteamDeck() => SteamUtils.IsRunningOnSteamDeck; + + public uint GetServerRealTime() => (uint)((DateTimeOffset)SteamUtils.SteamServerTime).ToUnixTimeSeconds(); + + public void ActivateGameOverlayToWebPage(string url, bool modal = false) + { + SteamFriends.OpenWebOverlay(url, modal); + } + + public event Action OnGamepadTextInputDismissed; + } +} diff --git a/src/XIVLauncher2.Common.Windows/XIVLauncher2.Common.Windows.csproj b/src/XIVLauncher2.Common.Windows/XIVLauncher2.Common.Windows.csproj new file mode 100644 index 0000000..fe3a72e --- /dev/null +++ b/src/XIVLauncher2.Common.Windows/XIVLauncher2.Common.Windows.csproj @@ -0,0 +1,44 @@ + + + XIVLauncher2.Common.Windows + XIVLauncher2.Common.Windows + Shared XIVLauncher platform-specific implementations for Windows. + 2.0.0 + disable + + + + Library + net7.0 + latest + true + true + + + + + + + + $(MSBuildProjectDirectory)\ + $(AppOutputBase)=C:\goatsoft\xl\XIVLauncher.Common.Windows\ + + + + + + PreserveNewest + + + + PreserveNewest + + + + + + + + + + diff --git a/src/XIVLauncher2.Common.Windows/steam_api.dll b/src/XIVLauncher2.Common.Windows/steam_api.dll new file mode 100644 index 0000000000000000000000000000000000000000..319bb83a93bf48e9fb644d4098f0585c609b999d GIT binary patch literal 263080 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 a6W4EdC-FN)cIN7qFwE-Bi+sP6=)V9ps8bdI literal 0 HcmV?d00001 diff --git a/src/XIVLauncher2.Common/Addon/AddonEntry.cs b/src/XIVLauncher2.Common/Addon/AddonEntry.cs new file mode 100644 index 0000000..ced226a --- /dev/null +++ b/src/XIVLauncher2.Common/Addon/AddonEntry.cs @@ -0,0 +1,10 @@ +using XIVLauncher2.Common.Addon.Implementations; + +namespace XIVLauncher2.Common.Addon +{ + public class AddonEntry + { + public bool IsEnabled { get; set; } + public GenericAddon Addon { get; set; } + } +} diff --git a/src/XIVLauncher2.Common/Addon/AddonManager.cs b/src/XIVLauncher2.Common/Addon/AddonManager.cs new file mode 100644 index 0000000..ed08731 --- /dev/null +++ b/src/XIVLauncher2.Common/Addon/AddonManager.cs @@ -0,0 +1,70 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using Serilog; + +namespace XIVLauncher2.Common.Addon +{ + public class AddonManager + { + private List> _runningAddons; + + public bool IsRunning { get; private set; } + + public void RunAddons(int gamePid, List addonEntries) + { + if (_runningAddons != null) + throw new Exception("Addons still running?"); + + _runningAddons = new List>(); + + foreach (var addonEntry in addonEntries) + { + addonEntry.Setup(gamePid); + + if (addonEntry is IPersistentAddon persistentAddon) + { + Log.Information("Starting PersistentAddon {0}", persistentAddon.Name); + var cancellationTokenSource = new CancellationTokenSource(); + + var addonThread = new Thread(persistentAddon.DoWork); + addonThread.Start(cancellationTokenSource.Token); + + _runningAddons.Add(new Tuple(persistentAddon, addonThread, cancellationTokenSource)); + } + + if (addonEntry is IRunnableAddon runnableAddon) + { + Log.Information("Starting RunnableAddon {0}", runnableAddon.Name); + runnableAddon.Run(); + } + + if (addonEntry is INotifyAddonAfterClose notifiedAddon) + _runningAddons.Add(new Tuple(notifiedAddon, null, null)); + } + + IsRunning = true; + } + + public void StopAddons() + { + Log.Information("Stopping addons..."); + + if (_runningAddons != null) + { + foreach (var addon in _runningAddons) + { + addon.Item3?.Cancel(); + addon.Item2?.Join(); + + if (addon.Item1 is INotifyAddonAfterClose notifiedAddon) + notifiedAddon.GameClosed(); + } + + _runningAddons = null; + } + + IsRunning = false; + } + } +} diff --git a/src/XIVLauncher2.Common/Addon/IAddon.cs b/src/XIVLauncher2.Common/Addon/IAddon.cs new file mode 100644 index 0000000..b0bb062 --- /dev/null +++ b/src/XIVLauncher2.Common/Addon/IAddon.cs @@ -0,0 +1,9 @@ +namespace XIVLauncher2.Common.Addon +{ + public interface IAddon + { + string Name { get; } + + void Setup(int gamePid); + } +} diff --git a/src/XIVLauncher2.Common/Addon/INotifyAddonAfterClose.cs b/src/XIVLauncher2.Common/Addon/INotifyAddonAfterClose.cs new file mode 100644 index 0000000..24fbb18 --- /dev/null +++ b/src/XIVLauncher2.Common/Addon/INotifyAddonAfterClose.cs @@ -0,0 +1,7 @@ +namespace XIVLauncher2.Common.Addon +{ + interface INotifyAddonAfterClose : IAddon + { + void GameClosed(); + } +} diff --git a/src/XIVLauncher2.Common/Addon/IPersistentAddon.cs b/src/XIVLauncher2.Common/Addon/IPersistentAddon.cs new file mode 100644 index 0000000..34018ba --- /dev/null +++ b/src/XIVLauncher2.Common/Addon/IPersistentAddon.cs @@ -0,0 +1,7 @@ +namespace XIVLauncher2.Common.Addon +{ + interface IPersistentAddon : IAddon + { + void DoWork(object state); + } +} diff --git a/src/XIVLauncher2.Common/Addon/IRunnableAddon.cs b/src/XIVLauncher2.Common/Addon/IRunnableAddon.cs new file mode 100644 index 0000000..734ff9b --- /dev/null +++ b/src/XIVLauncher2.Common/Addon/IRunnableAddon.cs @@ -0,0 +1,7 @@ +namespace XIVLauncher2.Common.Addon +{ + interface IRunnableAddon : IAddon + { + void Run(); + } +} diff --git a/src/XIVLauncher2.Common/Addon/Implementations/GenericAddon.cs b/src/XIVLauncher2.Common/Addon/Implementations/GenericAddon.cs new file mode 100644 index 0000000..a115140 --- /dev/null +++ b/src/XIVLauncher2.Common/Addon/Implementations/GenericAddon.cs @@ -0,0 +1,208 @@ +using Serilog; +using System; +using System.ComponentModel; +using System.Diagnostics; +using System.IO; +using System.Linq; + +namespace XIVLauncher2.Common.Addon.Implementations +{ + public class GenericAddon : IRunnableAddon, INotifyAddonAfterClose + { + private Process _addonProcess; + + void IAddon.Setup(int gamePid) + { + } + + public void Run() => + Run(false); + + private void Run(bool gameClosed) + { + if (string.IsNullOrEmpty(Path)) + { + Log.Error("Generic addon path was null."); + return; + } + + if (RunOnClose && !gameClosed) + return; // This Addon only runs when the game is closed. + + try + { + var ext = System.IO.Path.GetExtension(Path).ToLower(); + + switch (ext) + { + case ".ps1": + RunPowershell(); + break; + + case ".bat": + RunBatch(); + break; + + default: + RunApp(); + break; + } + + Log.Information("Launched addon {0}.", System.IO.Path.GetFileNameWithoutExtension(Path)); + } + catch (Exception e) + { + Log.Error(e, "Could not launch generic addon."); + } + } + + public void GameClosed() + { + if (RunOnClose) + { + Run(true); + } + + if (!RunAsAdmin) + { + try + { + if (_addonProcess == null) + return; + + if (_addonProcess.Handle == IntPtr.Zero) + return; + + if (!_addonProcess.HasExited && KillAfterClose) + { + if (!_addonProcess.CloseMainWindow() || !_addonProcess.WaitForExit(1000)) + _addonProcess.Kill(); + + _addonProcess.Close(); + } + } + catch (Exception ex) + { + Log.Information(ex, "Could not kill addon process."); + } + } + } + + private void RunApp() + { + // If there already is a process like this running - we don't need to spawn another one. + if (Process.GetProcessesByName(System.IO.Path.GetFileNameWithoutExtension(Path)).Any()) + { + Log.Information("Addon {0} is already running.", Name); + return; + } + + _addonProcess = new Process + { + StartInfo = + { + FileName = Path, + Arguments = CommandLine, + WorkingDirectory = System.IO.Path.GetDirectoryName(Path), + }, + }; + + if (RunAsAdmin) + // Vista or higher check + // https://stackoverflow.com/a/2532775 + if (Environment.OSVersion.Version.Major >= 6) _addonProcess.StartInfo.Verb = "runas"; + + _addonProcess.StartInfo.WindowStyle = ProcessWindowStyle.Minimized; + + _addonProcess.Start(); + } + + private void RunPowershell() + { + var ps = new ProcessStartInfo + { + FileName = Powershell, + WorkingDirectory = System.IO.Path.GetDirectoryName(Path), + Arguments = $@"-File ""{Path}"" {CommandLine}", + UseShellExecute = false, + }; + + RunScript(ps); + } + + private void RunBatch() + { + var ps = new ProcessStartInfo + { + FileName = Environment.GetEnvironmentVariable("ComSpec"), + WorkingDirectory = System.IO.Path.GetDirectoryName(Path), + Arguments = $@"/C ""{Path}"" {CommandLine}", + UseShellExecute = false, + }; + + RunScript(ps); + } + + private void RunScript(ProcessStartInfo ps) + { + ps.WindowStyle = ProcessWindowStyle.Hidden; + ps.CreateNoWindow = true; + + if (RunAsAdmin) + // Vista or higher check + // https://stackoverflow.com/a/2532775 + if (Environment.OSVersion.Version.Major >= 6) ps.Verb = "runas"; + + try + { + _addonProcess = Process.Start(ps); + Log.Information("Launched addon {0}.", System.IO.Path.GetFileNameWithoutExtension(Path)); + } + catch (Win32Exception exc) + { + // If the user didn't cause this manually by dismissing the UAC prompt, we throw it + if ((uint)exc.HResult != 0x80004005) + throw; + } + } + + public string Name => + string.IsNullOrEmpty(Path) + ? "Invalid addon" + : $"Launch{(IsApp ? " EXE" : string.Empty)} : {System.IO.Path.GetFileNameWithoutExtension(Path)}"; + + private bool IsApp => + !string.IsNullOrEmpty(Path) && + System.IO.Path.GetExtension(Path).ToLower() == ".exe"; + + public string Path; + public string CommandLine; + public bool RunAsAdmin; + public bool RunOnClose; + public bool KillAfterClose; + + private static readonly Lazy LazyPowershell = new(GetPowershell); + + private static string Powershell => LazyPowershell.Value; + + private static string GetPowershell() + { + var result = "powershell.exe"; + + var path = Environment.GetEnvironmentVariable("Path"); + var values = path?.Split(';') ?? Array.Empty(); + + foreach (var dir in values) + { + var powershell = System.IO.Path.Combine(dir, "pwsh.exe"); + if (File.Exists(powershell)) + { + result = powershell; + break; + } + } + + return result; + } + } +} diff --git a/src/XIVLauncher2.Common/Class1.cs b/src/XIVLauncher2.Common/Class1.cs deleted file mode 100644 index 373f389..0000000 --- a/src/XIVLauncher2.Common/Class1.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace XIVLauncher2.Common; - -public class Class1 -{ - // pretend i put something here, got it? -} diff --git a/src/XIVLauncher2.Common/ClientLanguage.cs b/src/XIVLauncher2.Common/ClientLanguage.cs new file mode 100644 index 0000000..653d655 --- /dev/null +++ b/src/XIVLauncher2.Common/ClientLanguage.cs @@ -0,0 +1,39 @@ +using XIVLauncher2.Common.Util; + +namespace XIVLauncher2.Common +{ + public enum ClientLanguage + { + Japanese, + English, + German, + French + } + + public static class ClientLanguageExtensions + { + public static string GetLangCode(this ClientLanguage language, bool forceNa = false) + { + switch (language) + { + case ClientLanguage.Japanese: + return "ja"; + + case ClientLanguage.English when GameHelpers.IsRegionNorthAmerica() || forceNa: + return "en-us"; + + case ClientLanguage.English: + return "en-gb"; + + case ClientLanguage.German: + return "de"; + + case ClientLanguage.French: + return "fr"; + + default: + return "en-gb"; + } + } + } +} diff --git a/src/XIVLauncher2.Common/CommonUniqueIdCache.cs b/src/XIVLauncher2.Common/CommonUniqueIdCache.cs new file mode 100644 index 0000000..7eb7f74 --- /dev/null +++ b/src/XIVLauncher2.Common/CommonUniqueIdCache.cs @@ -0,0 +1,109 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text.Json; +using System.Text.Json.Serialization; +using XIVLauncher2.Common.PlatformAbstractions; + +namespace XIVLauncher.PlatformAbstractions +{ + public class CommonUniqueIdCache : IUniqueIdCache + { + private const int DAYS_TO_TIMEOUT = 1; + + private List _cache; + + public CommonUniqueIdCache(FileInfo saveFile) + { + this.configFile = saveFile; + + Load(); + } + + #region SaveLoad + + private readonly FileInfo configFile; + + public void Save() => + File.WriteAllText(configFile.FullName, JsonSerializer.Serialize(_cache, new JsonSerializerOptions { WriteIndented = true })); + + public void Load() + { + if (!File.Exists(configFile.FullName)) + { + _cache = new List(); + return; + } + + _cache = JsonSerializer.Deserialize>(File.ReadAllText(configFile.FullName)) ?? new List(); + } + + public void Reset() + { + _cache.Clear(); + Save(); + } + + #endregion + + private void DeleteOldCaches() + { + _cache.RemoveAll(entry => (DateTime.Now - entry.CreationDate).TotalDays > DAYS_TO_TIMEOUT); + } + + public bool HasValidCache(string userName) + { + return _cache.Any(entry => IsValidCache(entry, userName)); + } + + public void Add(string userName, string uid, int region, int expansionLevel) + { + _cache.Add(new UniqueIdCacheEntry + { + CreationDate = DateTime.Now, + UserName = userName, + UniqueId = uid, + Region = region, + ExpansionLevel = expansionLevel + }); + + Save(); + } + + public bool TryGet(string userName, out IUniqueIdCache.CachedUid cached) + { + DeleteOldCaches(); + + var cache = _cache.FirstOrDefault(entry => IsValidCache(entry, userName)); + + if (cache == null) + { + cached = default; + return false; + } + + cached = new IUniqueIdCache.CachedUid + { + UniqueId = cache.UniqueId, + Region = cache.Region, + MaxExpansion = cache.ExpansionLevel, + }; + return true; + } + + private bool IsValidCache(UniqueIdCacheEntry entry, string name) => entry.UserName == name && + (DateTime.Now - entry.CreationDate).TotalDays <= + DAYS_TO_TIMEOUT; + + public class UniqueIdCacheEntry + { + public string UserName { get; set; } + public string UniqueId { get; set; } + public int Region { get; set; } + public int ExpansionLevel { get; set; } + + public DateTime CreationDate { get; set; } + } + } +} diff --git a/src/XIVLauncher2.Common/Constants.cs b/src/XIVLauncher2.Common/Constants.cs new file mode 100644 index 0000000..63cf687 --- /dev/null +++ b/src/XIVLauncher2.Common/Constants.cs @@ -0,0 +1,32 @@ +using System; +using XIVLauncher2.Common.Util; + +namespace XIVLauncher2.Common +{ + public static class Constants + { + public const string BASE_GAME_VERSION = "2012.01.01.0000.0000"; + + public const uint STEAM_APP_ID = 39210; + public const uint STEAM_FT_APP_ID = 312060; + + public static string PatcherUserAgent => GetPatcherUserAgent(PlatformHelpers.GetPlatform()); + + private static string GetPatcherUserAgent(Platform platform) + { + switch (platform) + { + case Platform.Win32: + case Platform.Win32OnLinux: + case Platform.Linux: + return "FFXIV PATCH CLIENT"; + + case Platform.Mac: + return "FFXIV-MAC PATCH CLIENT"; + + default: + throw new ArgumentOutOfRangeException(nameof(platform), platform, null); + } + } + } +} diff --git a/src/XIVLauncher2.Common/Dalamud/AssetManager.cs b/src/XIVLauncher2.Common/Dalamud/AssetManager.cs new file mode 100644 index 0000000..b6c7ad9 --- /dev/null +++ b/src/XIVLauncher2.Common/Dalamud/AssetManager.cs @@ -0,0 +1,203 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Net; +using System.Net.Http; +using System.Net.Http.Headers; +using Serilog; +using System.Security.Cryptography; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Threading.Tasks; +using XIVLauncher2.Common.Util; + +namespace XIVLauncher2.Common.Dalamud +{ + public class AssetManager + { + private const string ASSET_STORE_URL = "https://kamori.goats.dev/Dalamud/Asset/Meta"; + + internal class AssetInfo + { + [JsonPropertyName("version")] + public int Version { get; set; } + + [JsonPropertyName("assets")] + public IReadOnlyList Assets { get; set; } + + public class Asset + { + [JsonPropertyName("url")] + public string Url { get; set; } + + [JsonPropertyName("fileName")] + public string FileName { get; set; } + + [JsonPropertyName("hash")] + public string Hash { get; set; } + } + } + + public static async Task EnsureAssets(DirectoryInfo baseDir, bool forceProxy) + { + using var client = new HttpClient + { + Timeout = TimeSpan.FromMinutes(4), + }; + + client.DefaultRequestHeaders.CacheControl = new CacheControlHeaderValue + { + NoCache = true, + }; + + using var sha1 = SHA1.Create(); + + Log.Verbose("[DASSET] Starting asset download"); + + var (isRefreshNeeded, info) = CheckAssetRefreshNeeded(baseDir); + + // NOTE(goat): We should use a junction instead of copying assets to a new folder. There is no C# API for junctions in .NET Framework. + + var assetsDir = new DirectoryInfo(Path.Combine(baseDir.FullName, info.Version.ToString())); + var devDir = new DirectoryInfo(Path.Combine(baseDir.FullName, "dev")); + + foreach (var entry in info.Assets) + { + var filePath = Path.Combine(assetsDir.FullName, entry.FileName); + var filePathDev = Path.Combine(devDir.FullName, entry.FileName); + + Directory.CreateDirectory(Path.GetDirectoryName(filePath)!); + + try + { + Directory.CreateDirectory(Path.GetDirectoryName(filePathDev)!); + } + catch + { + // ignored + } + + var refreshFile = false; + + if (File.Exists(filePath) && !string.IsNullOrEmpty(entry.Hash)) + { + try + { + using var file = File.OpenRead(filePath); + var fileHash = sha1.ComputeHash(file); + var stringHash = BitConverter.ToString(fileHash).Replace("-", ""); + refreshFile = stringHash != entry.Hash; + Log.Verbose("[DASSET] {0} has {1}, remote {2}", entry.FileName, stringHash, entry.Hash); + } + catch (Exception ex) + { + Log.Error(ex, "[DASSET] Could not read asset"); + } + } + + if (!File.Exists(filePath) || isRefreshNeeded || refreshFile) + { + var url = entry.Url; + + if (forceProxy && url.Contains("/File/Get/")) + { + url = url.Replace("/File/Get/", "/File/GetProxy/"); + } + + Log.Verbose("[DASSET] Downloading {0} to {1}...", url, entry.FileName); + + var request = await client.GetAsync(url).ConfigureAwait(true); + request.EnsureSuccessStatusCode(); + File.WriteAllBytes(filePath, await request.Content.ReadAsByteArrayAsync().ConfigureAwait(true)); + + try + { + File.Copy(filePath, filePathDev, true); + } + catch + { + // ignored + } + } + } + + if (isRefreshNeeded) + SetLocalAssetVer(baseDir, info.Version); + + Log.Verbose("[DASSET] Assets OK at {0}", assetsDir.FullName); + + CleanUpOld(baseDir, info.Version - 1); + + return assetsDir; + } + + private static string GetAssetVerPath(DirectoryInfo baseDir) + { + return Path.Combine(baseDir.FullName, "asset.ver"); + } + + /// + /// Check if an asset update is needed. When this fails, just return false - the route to github + /// might be bad, don't wanna just bail out in that case + /// + /// Base directory for assets + /// Update state + private static (bool isRefreshNeeded, AssetInfo info) CheckAssetRefreshNeeded(DirectoryInfo baseDir) + { + using var client = new WebClient(); + + var localVerFile = GetAssetVerPath(baseDir); + var localVer = 0; + + try + { + if (File.Exists(localVerFile)) + localVer = int.Parse(File.ReadAllText(localVerFile)); + } + catch (Exception ex) + { + // This means it'll stay on 0, which will redownload all assets - good by me + Log.Error(ex, "[DASSET] Could not read asset.ver"); + } + + var remoteVer = JsonSerializer.Deserialize(client.DownloadString(ASSET_STORE_URL)); + + Log.Verbose("[DASSET] Ver check - local:{0} remote:{1}", localVer, remoteVer.Version); + + var needsUpdate = remoteVer.Version > localVer; + + return (needsUpdate, remoteVer); + } + + private static void SetLocalAssetVer(DirectoryInfo baseDir, int version) + { + try + { + var localVerFile = GetAssetVerPath(baseDir); + File.WriteAllText(localVerFile, version.ToString()); + } + catch (Exception e) + { + Log.Error(e, "[DASSET] Could not write local asset version"); + } + } + + private static void CleanUpOld(DirectoryInfo baseDir, int version) + { + if (GameHelpers.CheckIsGameOpen()) + return; + + var toDelete = Path.Combine(baseDir.FullName, version.ToString()); + + try + { + if (Directory.Exists(toDelete)) + Directory.Delete(toDelete, true); + } + catch (Exception ex) + { + Log.Error(ex, "Could not clean up old assets"); + } + } + } +} diff --git a/src/XIVLauncher2.Common/Dalamud/DalamudConsoleOutput.cs b/src/XIVLauncher2.Common/Dalamud/DalamudConsoleOutput.cs new file mode 100644 index 0000000..ceabdae --- /dev/null +++ b/src/XIVLauncher2.Common/Dalamud/DalamudConsoleOutput.cs @@ -0,0 +1,13 @@ +using System.Text.Json.Serialization; + +namespace XIVLauncher2.Common.Dalamud +{ + public sealed class DalamudConsoleOutput + { + [JsonPropertyName("pid")] + public int Pid { get; set; } + + [JsonPropertyName("handle")] + public long Handle { get; set; } + } +} diff --git a/src/XIVLauncher2.Common/Dalamud/DalamudInjectorArgs.cs b/src/XIVLauncher2.Common/Dalamud/DalamudInjectorArgs.cs new file mode 100644 index 0000000..5cb08d2 --- /dev/null +++ b/src/XIVLauncher2.Common/Dalamud/DalamudInjectorArgs.cs @@ -0,0 +1,22 @@ +namespace XIVLauncher2.Common.Dalamud +{ + public static class DalamudInjectorArgs + { + public const string Launch = "launch"; + public const string WithoutDalamud = "--without-dalamud"; + public const string FakeArguments = "--fake-arguments"; + public const string NoPlugin = "--no-plugin"; + public const string NoThirdParty = "--no-3rd-plugin"; + public static string Mode(string method) => $"--mode={method}"; + public static string Game(string path) => $"--game=\"{path}\""; + public static string HandleOwner(long handle) => $"--handle-owner={handle}"; + public static string WorkingDirectory(string path) => $"--dalamud-working-directory=\"{path}\""; + public static string ConfigurationPath(string path) => $"--dalamud-configuration-path=\"{path}\""; + public static string PluginDirectory(string path) => $"--dalamud-plugin-directory=\"{path}\""; + public static string PluginDevDirectory(string path) => $"--dalamud-dev-plugin-directory=\"{path}\""; + public static string AssetDirectory(string path) => $"--dalamud-asset-directory=\"{path}\""; + public static string ClientLanguage(int language) => $"--dalamud-client-language={language}"; + public static string DelayInitialize(int delay) => $"--dalamud-delay-initialize={delay}"; + public static string TSPackB64(string data) => $"--dalamud-tspack-b64={data}"; + } +} diff --git a/src/XIVLauncher2.Common/Dalamud/DalamudLauncher.cs b/src/XIVLauncher2.Common/Dalamud/DalamudLauncher.cs new file mode 100644 index 0000000..0865c67 --- /dev/null +++ b/src/XIVLauncher2.Common/Dalamud/DalamudLauncher.cs @@ -0,0 +1,170 @@ +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Net; +using System.Threading; +using System.Text.Json; +using Serilog; +using XIVLauncher2.Common.PlatformAbstractions; + +namespace XIVLauncher2.Common.Dalamud +{ + public class DalamudLauncher + { + private readonly DalamudLoadMethod loadMethod; + private readonly DirectoryInfo gamePath; + private readonly DirectoryInfo configDirectory; + private readonly ClientLanguage language; + private readonly IDalamudRunner runner; + private readonly DalamudUpdater updater; + private readonly int injectionDelay; + private readonly bool fakeLogin; + private readonly bool noPlugin; + private readonly bool noThirdPlugin; + private readonly string troubleshootingData; + + public enum DalamudInstallState + { + Ok, + Failed, + OutOfDate, + } + + public DalamudLauncher(IDalamudRunner runner, DalamudUpdater updater, DalamudLoadMethod loadMethod, DirectoryInfo gamePath, DirectoryInfo configDirectory, ClientLanguage clientLanguage, int injectionDelay, bool fakeLogin, bool noPlugin, bool noThirdPlugin, string troubleshootingData) + { + this.runner = runner; + this.updater = updater; + this.loadMethod = loadMethod; + this.gamePath = gamePath; + this.configDirectory = configDirectory; + this.language = clientLanguage; + this.injectionDelay = injectionDelay; + this.fakeLogin = fakeLogin; + this.noPlugin = noPlugin; + this.noThirdPlugin = noThirdPlugin; + this.troubleshootingData = troubleshootingData; + } + + public const string REMOTE_BASE = "https://kamori.goats.dev/Dalamud/Release/VersionInfo?track="; + + public DalamudInstallState HoldForUpdate(DirectoryInfo gamePath) + { + Log.Information("[HOOKS] DalamudLauncher::HoldForUpdate(gp:{0})", gamePath.FullName); + + if (this.updater.State != DalamudUpdater.DownloadState.Done) + this.updater.ShowOverlay(); + + while (this.updater.State != DalamudUpdater.DownloadState.Done) + { + if (this.updater.State == DalamudUpdater.DownloadState.Failed) + { + this.updater.CloseOverlay(); + return DalamudInstallState.Failed; + } + + if (this.updater.State == DalamudUpdater.DownloadState.NoIntegrity) + { + this.updater.CloseOverlay(); + throw new DalamudRunnerException("No runner integrity"); + } + + Thread.Yield(); + } + + if (!this.updater.Runner.Exists) + throw new DalamudRunnerException("Runner not present"); + + if (!ReCheckVersion(gamePath)) + { + this.updater.SetOverlayProgress(IDalamudLoadingOverlay.DalamudUpdateStep.Unavailable); + this.updater.ShowOverlay(); + Log.Error("[HOOKS] ReCheckVersion fail"); + + return DalamudInstallState.OutOfDate; + } + + return DalamudInstallState.Ok; + } + + public Process Run(FileInfo gameExe, string gameArgs, IDictionary environment) + { + Log.Information("[HOOKS] DalamudLauncher::Run(gp:{0}, cl:{1})", this.gamePath.FullName, this.language); + + var ingamePluginPath = Path.Combine(this.configDirectory.FullName, "installedPlugins"); + var defaultPluginPath = Path.Combine(this.configDirectory.FullName, "devPlugins"); + + Directory.CreateDirectory(ingamePluginPath); + Directory.CreateDirectory(defaultPluginPath); + + var startInfo = new DalamudStartInfo + { + Language = language, + PluginDirectory = ingamePluginPath, + DefaultPluginDirectory = defaultPluginPath, + ConfigurationPath = DalamudSettings.GetConfigPath(this.configDirectory), + AssetDirectory = this.updater.AssetDirectory.FullName, + GameVersion = Repository.Ffxiv.GetVer(gamePath), + WorkingDirectory = this.updater.Runner.Directory?.FullName, + DelayInitializeMs = this.injectionDelay, + TroubleshootingPackData = this.troubleshootingData, + }; + + if (this.loadMethod != DalamudLoadMethod.ACLonly) + Log.Information("[HOOKS] DelayInitializeMs: {0}", startInfo.DelayInitializeMs); + + switch (this.loadMethod) + { + case DalamudLoadMethod.EntryPoint: + Log.Verbose("[HOOKS] Now running OEP rewrite"); + break; + + case DalamudLoadMethod.DllInject: + Log.Verbose("[HOOKS] Now running DLL inject"); + break; + + case DalamudLoadMethod.ACLonly: + Log.Verbose("[HOOKS] Now running ACL-only fix without injection"); + break; + } + + var process = this.runner.Run(this.updater.Runner, this.fakeLogin, this.noPlugin, this.noThirdPlugin, gameExe, gameArgs, environment, this.loadMethod, startInfo); + + this.updater.CloseOverlay(); + + if (this.loadMethod != DalamudLoadMethod.ACLonly) + Log.Information("[HOOKS] Started dalamud!"); + + return process; + } + + private bool ReCheckVersion(DirectoryInfo gamePath) + { + if (this.updater.State != DalamudUpdater.DownloadState.Done) + return false; + + if (this.updater.RunnerOverride != null) + return true; + + var info = DalamudVersionInfo.Load(new FileInfo(Path.Combine(this.updater.Runner.DirectoryName!, + "version.json"))); + + if (Repository.Ffxiv.GetVer(gamePath) != info.SupportedGameVer) + return false; + + return true; + } + + public static bool CanRunDalamud(DirectoryInfo gamePath) + { + using var client = new WebClient(); + + var versionInfoJson = client.DownloadString(REMOTE_BASE + "release"); + var remoteVersionInfo = JsonSerializer.Deserialize(versionInfoJson); + + if (Repository.Ffxiv.GetVer(gamePath) != remoteVersionInfo.SupportedGameVer) + return false; + + return true; + } + } +} diff --git a/src/XIVLauncher2.Common/Dalamud/DalamudLoadMethod.cs b/src/XIVLauncher2.Common/Dalamud/DalamudLoadMethod.cs new file mode 100644 index 0000000..47f3b93 --- /dev/null +++ b/src/XIVLauncher2.Common/Dalamud/DalamudLoadMethod.cs @@ -0,0 +1,14 @@ +namespace XIVLauncher2.Common.Dalamud +{ + public enum DalamudLoadMethod + { + [SettingsDescription("Entrypoint", "dummy")] + EntryPoint, + + [SettingsDescription("DLL Injection", "dummy")] + DllInject, + + [SettingsDescription("ACL-only fix", "dummy")] + ACLonly, + }; +} diff --git a/src/XIVLauncher2.Common/Dalamud/DalamudRunnerException.cs b/src/XIVLauncher2.Common/Dalamud/DalamudRunnerException.cs new file mode 100644 index 0000000..5278a58 --- /dev/null +++ b/src/XIVLauncher2.Common/Dalamud/DalamudRunnerException.cs @@ -0,0 +1,11 @@ +using System; + +namespace XIVLauncher2.Common.Dalamud; + +public class DalamudRunnerException : Exception +{ + public DalamudRunnerException(string message, Exception innerException = null) + : base(message, innerException) + { + } +} diff --git a/src/XIVLauncher2.Common/Dalamud/DalamudSettings.cs b/src/XIVLauncher2.Common/Dalamud/DalamudSettings.cs new file mode 100644 index 0000000..c2e1bfb --- /dev/null +++ b/src/XIVLauncher2.Common/Dalamud/DalamudSettings.cs @@ -0,0 +1,34 @@ +using System; +using System.IO; +using System.Text.Json; +using Serilog; + +namespace XIVLauncher2.Common.Dalamud +{ + public class DalamudSettings + { + public string? DalamudBetaKey { get; set; } = null; + public bool DoDalamudRuntime { get; set; } = false; + public string DalamudBetaKind { get; set; } + + public static string GetConfigPath(DirectoryInfo configFolder) => Path.Combine(configFolder.FullName, "dalamudConfig.json"); + + public static DalamudSettings GetSettings(DirectoryInfo configFolder) + { + var configPath = GetConfigPath(configFolder); + DalamudSettings deserialized = null; + + try + { + deserialized = File.Exists(configPath) ? JsonSerializer.Deserialize(File.ReadAllText(configPath)) : new DalamudSettings(); + } + catch (Exception ex) + { + Log.Error(ex, "Couldn't deserialize Dalamud settings"); + } + + deserialized ??= new DalamudSettings(); // In case the .json is corrupted + return deserialized; + } + } +} diff --git a/src/XIVLauncher2.Common/Dalamud/DalamudStartInfo.cs b/src/XIVLauncher2.Common/Dalamud/DalamudStartInfo.cs new file mode 100644 index 0000000..6f243a3 --- /dev/null +++ b/src/XIVLauncher2.Common/Dalamud/DalamudStartInfo.cs @@ -0,0 +1,20 @@ +using System; + +namespace XIVLauncher2.Common.Dalamud +{ + [Serializable] + public sealed class DalamudStartInfo + { + public string WorkingDirectory; + public string ConfigurationPath; + + public string PluginDirectory; + public string DefaultPluginDirectory; + public string AssetDirectory; + public ClientLanguage Language; + public int DelayInitializeMs; + + public string GameVersion; + public string TroubleshootingPackData; + } +} diff --git a/src/XIVLauncher2.Common/Dalamud/DalamudUpdater.cs b/src/XIVLauncher2.Common/Dalamud/DalamudUpdater.cs new file mode 100644 index 0000000..ac87bf1 --- /dev/null +++ b/src/XIVLauncher2.Common/Dalamud/DalamudUpdater.cs @@ -0,0 +1,501 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.IO.Compression; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Security.Cryptography; +using System.Threading.Tasks; +using System.Text.Json; +using Serilog; +using XIVLauncher2.Common.PlatformAbstractions; +using XIVLauncher2.Common.Util; + +namespace XIVLauncher2.Common.Dalamud +{ + public class DalamudUpdater + { + private readonly DirectoryInfo addonDirectory; + private readonly DirectoryInfo runtimeDirectory; + private readonly DirectoryInfo assetDirectory; + private readonly DirectoryInfo configDirectory; + private readonly IUniqueIdCache? cache; + + private readonly TimeSpan defaultTimeout = TimeSpan.FromMinutes(15); + + private bool forceProxy = false; + + public DownloadState State { get; private set; } = DownloadState.Unknown; + public bool IsStaging { get; private set; } = false; + + private FileInfo runnerInternal; + + public FileInfo Runner + { + get + { + if (RunnerOverride != null) + return RunnerOverride; + + return runnerInternal; + } + private set => runnerInternal = value; + } + + public DirectoryInfo Runtime => this.runtimeDirectory; + + public FileInfo RunnerOverride { get; set; } + + public DirectoryInfo AssetDirectory { get; private set; } + + public IDalamudLoadingOverlay Overlay { get; set; } + + public string RolloutBucket { get; set; } + + public enum DownloadState + { + Unknown, + Done, + Failed, + NoIntegrity + } + + public DalamudUpdater(DirectoryInfo addonDirectory, DirectoryInfo runtimeDirectory, DirectoryInfo assetDirectory, DirectoryInfo configDirectory, IUniqueIdCache? cache, string? dalamudRolloutBucket) + { + this.addonDirectory = addonDirectory; + this.runtimeDirectory = runtimeDirectory; + this.assetDirectory = assetDirectory; + this.configDirectory = configDirectory; + this.cache = cache; + + this.RolloutBucket = dalamudRolloutBucket; + + if (this.RolloutBucket == null) + { + var rng = new Random(); + this.RolloutBucket = rng.Next(0, 9) >= 7 ? "Canary" : "Control"; + } + } + + public void SetOverlayProgress(IDalamudLoadingOverlay.DalamudUpdateStep progress) + { + Overlay.SetStep(progress); + } + + public void ShowOverlay() + { + Overlay.SetVisible(); + } + + public void CloseOverlay() + { + Overlay.SetInvisible(); + } + + private void ReportOverlayProgress(long? size, long downloaded, double? progress) + { + Overlay.ReportProgress(size, downloaded, progress); + } + + public void Run() + { + Log.Information("[DUPDATE] Starting..."); + + Task.Run(async () => + { + const int MAX_TRIES = 10; + + for (var tries = 0; tries < MAX_TRIES; tries++) + { + try + { + await UpdateDalamud().ConfigureAwait(true); + break; + } + catch (Exception ex) + { + Log.Error(ex, "[DUPDATE] Update failed, try {TryCnt}/{MaxTries}...", tries, MAX_TRIES); + this.forceProxy = true; + } + } + + if (this.State != DownloadState.Done) this.State = DownloadState.Failed; + }); + } + + private static string GetBetaTrackName(DalamudSettings settings) => + string.IsNullOrEmpty(settings.DalamudBetaKind) ? "staging" : settings.DalamudBetaKind; + + private async Task<(DalamudVersionInfo release, DalamudVersionInfo? staging)> GetVersionInfo(DalamudSettings settings) + { + using var client = new HttpClient + { + Timeout = this.defaultTimeout, + }; + + client.DefaultRequestHeaders.CacheControl = new CacheControlHeaderValue + { + NoCache = true, + }; + + var versionInfoJsonRelease = await client.GetStringAsync(DalamudLauncher.REMOTE_BASE + $"release&bucket={this.RolloutBucket}").ConfigureAwait(false); + + DalamudVersionInfo versionInfoRelease = JsonSerializer.Deserialize(versionInfoJsonRelease); + + DalamudVersionInfo? versionInfoStaging = null; + + if (!string.IsNullOrEmpty(settings.DalamudBetaKey)) + { + var versionInfoJsonStaging = await client.GetAsync(DalamudLauncher.REMOTE_BASE + GetBetaTrackName(settings)).ConfigureAwait(false); + + if (versionInfoJsonStaging.StatusCode != HttpStatusCode.BadRequest) + versionInfoStaging = JsonSerializer.Deserialize(await versionInfoJsonStaging.Content.ReadAsStringAsync().ConfigureAwait(false)); + } + + return (versionInfoRelease, versionInfoStaging); + } + + private async Task UpdateDalamud() + { + var settings = DalamudSettings.GetSettings(this.configDirectory); + + // GitHub requires TLS 1.2, we need to hardcode this for Windows 7 + ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12; + + var (versionInfoRelease, versionInfoStaging) = await GetVersionInfo(settings).ConfigureAwait(false); + + var remoteVersionInfo = versionInfoRelease; + + if (versionInfoStaging?.Key != null && versionInfoStaging.Key == settings.DalamudBetaKey) + { + remoteVersionInfo = versionInfoStaging; + IsStaging = true; + Log.Information("[DUPDATE] Using staging version {Kind} with key {Key} ({Hash})", settings.DalamudBetaKind, settings.DalamudBetaKey, remoteVersionInfo.AssemblyVersion); + } + else + { + Log.Information("[DUPDATE] Using release version ({Hash})", remoteVersionInfo.AssemblyVersion); + } + + var versionInfoJson = JsonSerializer.Serialize(remoteVersionInfo); + + var addonPath = new DirectoryInfo(Path.Combine(this.addonDirectory.FullName, "Hooks")); + var currentVersionPath = new DirectoryInfo(Path.Combine(addonPath.FullName, remoteVersionInfo.AssemblyVersion)); + var runtimePaths = new DirectoryInfo[] + { + new(Path.Combine(this.runtimeDirectory.FullName, "host", "fxr", remoteVersionInfo.RuntimeVersion)), + new(Path.Combine(this.runtimeDirectory.FullName, "shared", "Microsoft.NETCore.App", remoteVersionInfo.RuntimeVersion)), + new(Path.Combine(this.runtimeDirectory.FullName, "shared", "Microsoft.WindowsDesktop.App", remoteVersionInfo.RuntimeVersion)), + }; + + if (!currentVersionPath.Exists || !IsIntegrity(currentVersionPath)) + { + Log.Information("[DUPDATE] Not found, redownloading"); + + SetOverlayProgress(IDalamudLoadingOverlay.DalamudUpdateStep.Dalamud); + + try + { + await DownloadDalamud(currentVersionPath, remoteVersionInfo).ConfigureAwait(true); + CleanUpOld(addonPath, remoteVersionInfo.AssemblyVersion); + + // This is a good indicator that we should clear the UID cache + cache?.Reset(); + } + catch (Exception ex) + { + Log.Error(ex, "[DUPDATE] Could not download dalamud"); + + State = DownloadState.NoIntegrity; + return; + } + } + + if (remoteVersionInfo.RuntimeRequired || settings.DoDalamudRuntime) + { + Log.Information("[DUPDATE] Now starting for .NET Runtime {0}", remoteVersionInfo.RuntimeVersion); + + var versionFile = new FileInfo(Path.Combine(this.runtimeDirectory.FullName, "version")); + var localVersion = "5.0.6"; // This is the version we first shipped. We didn't write out a version file, so we can't check it. + if (versionFile.Exists) + localVersion = File.ReadAllText(versionFile.FullName); + + if (!this.runtimeDirectory.Exists) + Directory.CreateDirectory(this.runtimeDirectory.FullName); + + var integrity = await CheckRuntimeHashes(runtimeDirectory, localVersion).ConfigureAwait(false); + + if (runtimePaths.Any(p => !p.Exists) || localVersion != remoteVersionInfo.RuntimeVersion || !integrity) + { + Log.Information("[DUPDATE] Not found, outdated or no integrity: {LocalVer} - {RemoteVer}", localVersion, remoteVersionInfo.RuntimeVersion); + + SetOverlayProgress(IDalamudLoadingOverlay.DalamudUpdateStep.Runtime); + + try + { + await DownloadRuntime(this.runtimeDirectory, remoteVersionInfo.RuntimeVersion).ConfigureAwait(false); + File.WriteAllText(versionFile.FullName, remoteVersionInfo.RuntimeVersion); + } + catch (Exception ex) + { + Log.Error(ex, "[DUPDATE] Could not download runtime"); + + State = DownloadState.Failed; + return; + } + } + } + + try + { + this.SetOverlayProgress(IDalamudLoadingOverlay.DalamudUpdateStep.Assets); + this.ReportOverlayProgress(null, 0, null); + AssetDirectory = await AssetManager.EnsureAssets(this.assetDirectory, this.forceProxy).ConfigureAwait(true); + } + catch (Exception ex) + { + Log.Error(ex, "[DUPDATE] Asset ensurement error, bailing out..."); + State = DownloadState.Failed; + return; + } + + if (!IsIntegrity(currentVersionPath)) + { + Log.Error("[DUPDATE] Integrity check failed after ensurement."); + + State = DownloadState.NoIntegrity; + return; + } + + WriteVersionJson(currentVersionPath, versionInfoJson); + + Log.Information("[DUPDATE] All set for " + remoteVersionInfo.SupportedGameVer); + + Runner = new FileInfo(Path.Combine(currentVersionPath.FullName, "Dalamud.Injector.exe")); + + State = DownloadState.Done; + SetOverlayProgress(IDalamudLoadingOverlay.DalamudUpdateStep.Starting); + } + + private static bool CanRead(FileInfo info) + { + try + { + using var stream = info.OpenRead(); + stream.ReadByte(); + } + catch + { + return false; + } + + return true; + } + + public static bool IsIntegrity(DirectoryInfo addonPath) + { + var files = addonPath.GetFiles(); + + try + { + if (!CanRead(files.First(x => x.Name == "Dalamud.Injector.exe")) + || !CanRead(files.First(x => x.Name == "Dalamud.dll")) + || !CanRead(files.First(x => x.Name == "ImGuiScene.dll"))) + { + Log.Error("[DUPDATE] Can't open files for read"); + return false; + } + + var hashesPath = Path.Combine(addonPath.FullName, "hashes.json"); + + if (!File.Exists(hashesPath)) + { + Log.Error("[DUPDATE] No hashes.json"); + return false; + } + + return CheckIntegrity(addonPath, File.ReadAllText(hashesPath)); + } + catch (Exception ex) + { + Log.Error(ex, "[DUPDATE] No dalamud integrity"); + return false; + } + } + + private static bool CheckIntegrity(DirectoryInfo directory, string hashesJson) + { + try + { + Log.Verbose("[DUPDATE] Checking integrity of {Directory}", directory.FullName); + + var hashes = JsonSerializer.Deserialize>(hashesJson); + + foreach (var hash in hashes) + { + var file = Path.Combine(directory.FullName, hash.Key.Replace("\\", "/")); + using var fileStream = File.OpenRead(file); + using var md5 = MD5.Create(); + + var hashed = BitConverter.ToString(md5.ComputeHash(fileStream)).ToUpperInvariant().Replace("-", string.Empty); + + if (hashed != hash.Value) + { + Log.Error("[DUPDATE] Integrity check failed for {0} ({1} - {2})", file, hash.Value, hashed); + return false; + } + + Log.Verbose("[DUPDATE] Integrity check OK for {0} ({1})", file, hashed); + } + } + catch (Exception ex) + { + Log.Error(ex, "[DUPDATE] Integrity check failed"); + return false; + } + + return true; + } + + private static void CleanUpOld(DirectoryInfo addonPath, string currentVer) + { + if (!addonPath.Exists) + return; + + foreach (var directory in addonPath.GetDirectories()) + { + if (directory.Name == "dev" || directory.Name == currentVer) continue; + + try + { + directory.Delete(true); + } + catch + { + // ignored + } + } + } + + private static void WriteVersionJson(DirectoryInfo addonPath, string info) + { + File.WriteAllText(Path.Combine(addonPath.FullName, "version.json"), info); + } + + private async Task DownloadDalamud(DirectoryInfo addonPath, DalamudVersionInfo version) + { + // Ensure directory exists + if (!addonPath.Exists) + addonPath.Create(); + else + { + addonPath.Delete(true); + addonPath.Create(); + } + + var downloadPath = PlatformHelpers.GetTempFileName(); + + if (File.Exists(downloadPath)) + File.Delete(downloadPath); + + await this.DownloadFile(version.DownloadUrl, downloadPath, this.defaultTimeout).ConfigureAwait(false); + ZipFile.ExtractToDirectory(downloadPath, addonPath.FullName); + + File.Delete(downloadPath); + + try + { + var devPath = new DirectoryInfo(Path.Combine(addonPath.FullName, "..", "dev")); + + if (!devPath.Exists) + devPath.Create(); + else + { + devPath.Delete(true); + devPath.Create(); + } + + foreach (var fileInfo in addonPath.GetFiles()) + { + fileInfo.CopyTo(Path.Combine(devPath.FullName, fileInfo.Name)); + } + } + catch (Exception ex) + { + Log.Error(ex, "[DUPDATE] Could not copy to dev folder."); + } + } + + private async Task CheckRuntimeHashes(DirectoryInfo runtimePath, string version) + { +#if DEBUG + Log.Warning("Debug build, ignoring runtime hash check"); + return true; +#endif + + var hashesFile = new FileInfo(Path.Combine(runtimePath.FullName, $"hashes-{version}.json")); + string? runtimeHashes = null; + + if (!hashesFile.Exists) + { + Log.Verbose("Hashes file does not exist, redownloading..."); + + using var client = new HttpClient(); + runtimeHashes = await client.GetStringAsync($"https://kamori.goats.dev/Dalamud/Release/Runtime/Hashes/{version}").ConfigureAwait(false); + + File.WriteAllText(hashesFile.FullName, runtimeHashes); + } + else + { + runtimeHashes = File.ReadAllText(hashesFile.FullName); + } + + return CheckIntegrity(runtimePath, runtimeHashes); + } + + private async Task DownloadRuntime(DirectoryInfo runtimePath, string version) + { + // Ensure directory exists + if (!runtimePath.Exists) + { + runtimePath.Create(); + } + else + { + runtimePath.Delete(true); + runtimePath.Create(); + } + + var dotnetUrl = $"https://kamori.goats.dev/Dalamud/Release/Runtime/DotNet/{version}"; + var desktopUrl = $"https://kamori.goats.dev/Dalamud/Release/Runtime/WindowsDesktop/{version}"; + + var downloadPath = PlatformHelpers.GetTempFileName(); + + if (File.Exists(downloadPath)) + File.Delete(downloadPath); + + await this.DownloadFile(dotnetUrl, downloadPath, this.defaultTimeout).ConfigureAwait(false); + ZipFile.ExtractToDirectory(downloadPath, runtimePath.FullName); + + await this.DownloadFile(desktopUrl, downloadPath, this.defaultTimeout).ConfigureAwait(false); + ZipFile.ExtractToDirectory(downloadPath, runtimePath.FullName); + + File.Delete(downloadPath); + } + + private async Task DownloadFile(string url, string path, TimeSpan timeout) + { + if (this.forceProxy && url.Contains("/File/Get/")) + { + url = url.Replace("/File/Get/", "/File/GetProxy/"); + } + + using var downloader = new HttpClientDownloadWithProgress(url, path); + downloader.ProgressChanged += this.ReportOverlayProgress; + + await downloader.Download(timeout).ConfigureAwait(false); + } + } +} diff --git a/src/XIVLauncher2.Common/Dalamud/DalamudVersionInfo.cs b/src/XIVLauncher2.Common/Dalamud/DalamudVersionInfo.cs new file mode 100644 index 0000000..456b737 --- /dev/null +++ b/src/XIVLauncher2.Common/Dalamud/DalamudVersionInfo.cs @@ -0,0 +1,30 @@ +using System.IO; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace XIVLauncher2.Common.Dalamud +{ + internal class DalamudVersionInfo + { + [JsonPropertyName("assemblyVersion")] + public string AssemblyVersion { get; set; } + + [JsonPropertyName("supportedGameVer")] + public string SupportedGameVer { get; set; } + + [JsonPropertyName("runtimeVersion")] + public string RuntimeVersion { get; set; } + + [JsonPropertyName("runtimeRequired")] + public bool RuntimeRequired { get; set; } + + [JsonPropertyName("key")] + public string Key { get; set; } + + [JsonPropertyName("downloadUrl")] + public string DownloadUrl { get; set; } + + public static DalamudVersionInfo Load(FileInfo file) => + JsonSerializer.Deserialize(File.ReadAllText(file.FullName)); + } +} diff --git a/src/XIVLauncher2.Common/DpiAwareness.cs b/src/XIVLauncher2.Common/DpiAwareness.cs new file mode 100644 index 0000000..7038f00 --- /dev/null +++ b/src/XIVLauncher2.Common/DpiAwareness.cs @@ -0,0 +1,8 @@ +namespace XIVLauncher2.Common +{ + public enum DpiAwareness + { + Aware, + Unaware, + } +} diff --git a/src/XIVLauncher2.Common/Encryption/ArgumentBuilder.cs b/src/XIVLauncher2.Common/Encryption/ArgumentBuilder.cs new file mode 100644 index 0000000..300a5df --- /dev/null +++ b/src/XIVLauncher2.Common/Encryption/ArgumentBuilder.cs @@ -0,0 +1,139 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Linq; +using Serilog; +using System.Runtime.InteropServices; +using XIVLauncher2.Common.Util; + +namespace XIVLauncher2.Common.Encryption +{ + public sealed class ArgumentBuilder + { + private static readonly uint version = 3; + + private static readonly char[] checksumTable = + { + 'f', 'X', '1', 'p', 'G', 't', 'd', 'S', + '5', 'C', 'A', 'P', '4', '_', 'V', 'L' + }; + + private static char DeriveChecksum(uint key) + { + var index = (key & 0x000F_0000) >> 16; + + try + { + return checksumTable[index]; + } + catch (IndexOutOfRangeException) + { + return '!'; // Conceivably, this shouldn't happen... + } + } + + private readonly List> arguments; + + public ArgumentBuilder() + { + this.arguments = new List>(); + } + + public ArgumentBuilder(IEnumerable> items) + { + this.arguments = new List>(items); + } + + public ArgumentBuilder Append(string key, string value) + { + return Append(new KeyValuePair(key, value)); + } + + public ArgumentBuilder Append(KeyValuePair item) + { + this.arguments.Add(item); + + return this; + } + + public ArgumentBuilder Append(IEnumerable> items) + { + this.arguments.AddRange(items); + + return this; + } + + public string Build() + { + return this.arguments.Aggregate(new StringBuilder(), + (whole, part) => whole.Append($" {part.Key}={part.Value}")) + .ToString(); + } + + public string BuildEncrypted(uint key) + { + var arguments = this.arguments.Aggregate(new StringBuilder(), + // Yes, they do have a space prepended even for the first argument. + (whole, part) => whole.Append($" /{EscapeValue(part.Key)} ={EscapeValue(part.Value)}")) + .ToString(); + + var blowfish = new LegacyBlowfish(GetKeyBytes(key)); + var ciphertext = blowfish.Encrypt(Encoding.UTF8.GetBytes(arguments)); + var base64Str = GameHelpers.ToMangledSeBase64(ciphertext); + var checksum = DeriveChecksum(key); + + Log.Information("ArgumentBuilder::BuildEncrypted() checksum:{0}", checksum); + + return $"//**sqex{version:D04}{base64Str}{checksum}**//"; + } + + public string BuildEncrypted() + { + var key = DeriveKey(); + + return BuildEncrypted(key); + } + + private uint DeriveKey() + { + var rawTickCount = (uint)Environment.TickCount; + + if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + [System.Runtime.InteropServices.DllImport("c")] + // ReSharper disable once InconsistentNaming + static extern ulong clock_gettime_nsec_np(int clock_id); + + const int CLOCK_MONOTONIC_RAW = 4; + var rawTickCountFixed = (clock_gettime_nsec_np(CLOCK_MONOTONIC_RAW) / 1000000); + Log.Information("ArgumentBuilder::DeriveKey() fixing up rawTickCount from {0} to {1} on macOS", rawTickCount, rawTickCountFixed); + rawTickCount = (uint)rawTickCountFixed; + } + + var ticks = rawTickCount & 0xFFFF_FFFFu; + var key = ticks & 0xFFFF_0000u; + + Log.Information("ArgumentBuilder::DeriveKey() rawTickCount:{0} ticks:{1} key:{2}", rawTickCount, ticks, key); + + var keyPair = new KeyValuePair("T", Convert.ToString(ticks)); + if (this.arguments.Count > 0 && this.arguments[0].Key == "T") + this.arguments[0] = keyPair; + else + this.arguments.Insert(0, keyPair); + + return key; + } + + private static byte[] GetKeyBytes(uint key) + { + var format = $"{key:x08}"; + + return Encoding.UTF8.GetBytes(format); + } + + private static string EscapeValue(string input) + { + return input.Replace(" ", " "); + } + } +} diff --git a/src/XIVLauncher2.Common/Encryption/BlockCipher/Blowfish.cs b/src/XIVLauncher2.Common/Encryption/BlockCipher/Blowfish.cs new file mode 100644 index 0000000..9e94c23 --- /dev/null +++ b/src/XIVLauncher2.Common/Encryption/BlockCipher/Blowfish.cs @@ -0,0 +1,93 @@ +// NOTE: This file is copy-pasted almost *as-is* from the previous work `Aither.Crypto` +// hence currently it does not follow XL's naming convetions. +// +// It's totally okay to change this. But for now, this is what it is atm. +// ReSharper disable InconsistentNaming + +using System; +using System.Buffers.Binary; + +namespace XIVLauncher2.Common.Encryption.BlockCipher +{ + public sealed class Blowfish : IBlockCipher + { + /// + public int BlockSize => 8; + + // NOTE: this field should never be marked as readonly as it actually creates a defensive copy on every access. (it's a trap) + // https://devblogs.microsoft.com/premier-developer/the-in-modifier-and-the-readonly-structs-in-c/ + + private BlowfishState m_state; + + /// + /// Initializes a new instance of the Blowfish class. + /// + /// + /// This function also calculates P-array and S-boxes from the given key. This is most expensive operation in blowfish algorithm. + /// + /// A secret key used for blowfish. Key length must be between 32 and 448 bits. + /// Length of the key is either too short or too long. + public Blowfish(ReadOnlySpan key) + { + m_state = new BlowfishState(key); + } + + public unsafe void EncryptBlockUnsafe(byte* input, byte* output) + { + var inputBlock = (uint*)input; + var outputBlock = (uint*)output; + + var xl = inputBlock[0]; + var xr = inputBlock[1]; + + // will be elided by JIT + if (BitConverter.IsLittleEndian) + { + xl = BinaryPrimitives.ReverseEndianness(xl); + xr = BinaryPrimitives.ReverseEndianness(xr); + } + + (xl, xr) = m_state.EncryptBlock(xl, xr); + + // will be elided by JIT + if (BitConverter.IsLittleEndian) + { + xl = BinaryPrimitives.ReverseEndianness(xl); + xr = BinaryPrimitives.ReverseEndianness(xr); + } + + outputBlock[0] = xl; + outputBlock[1] = xr; + } + + public unsafe void DecryptBlockUnsafe(byte* input, byte* output) + { + var inputBlock = (uint*)input; + var outputBlock = (uint*)output; + + var xl = inputBlock[0]; + var xr = inputBlock[1]; + + // will be elided by JIT + if (BitConverter.IsLittleEndian) + { + xl = BinaryPrimitives.ReverseEndianness(xl); + xr = BinaryPrimitives.ReverseEndianness(xr); + } + + (xl, xr) = m_state.DecryptBlock(xl, xr); + + // will be elided by JIT + if (BitConverter.IsLittleEndian) + { + xl = BinaryPrimitives.ReverseEndianness(xl); + xr = BinaryPrimitives.ReverseEndianness(xr); + } + + outputBlock[0] = xl; + outputBlock[1] = xr; + } + } +} + +// ReSharper restore InconsistentNaming diff --git a/src/XIVLauncher2.Common/Encryption/BlockCipher/BlowfishState.cs b/src/XIVLauncher2.Common/Encryption/BlockCipher/BlowfishState.cs new file mode 100644 index 0000000..c1616c6 --- /dev/null +++ b/src/XIVLauncher2.Common/Encryption/BlockCipher/BlowfishState.cs @@ -0,0 +1,369 @@ +// NOTE: This file is copy-pasted almost *as-is* from the previous work `Aither.Crypto` +// hence currently it does not follow XL's naming convetions. +// +// It's totally okay to change this. But for now, this is what it is atm. +// ReSharper disable InconsistentNaming + +using System; +using System.Runtime.CompilerServices; + +namespace XIVLauncher2.Common.Encryption.BlockCipher +{ + internal unsafe struct BlowfishState + { + // References: + // https://www.schneier.com/academic/archives/1994/09/description_of_a_new.html + // http://www.herongyang.com/Blowfish/Algorithm-Blowfish-Cipher-Encryption-Algorithm.html + // https://en.wikipedia.org/wiki/Blowfish_(cipher) + + private const int Rounds = 16; + public const int PSize = 18; + public const int SSize = 256; + + private static readonly uint[] PInit = new uint[] + { + 0x243F6A88, 0x85A308D3, 0x13198A2E, 0x03707344, 0xA4093822, 0x299F31D0, + 0x082EFA98, 0xEC4E6C89, 0x452821E6, 0x38D01377, 0xBE5466CF, 0x34E90C6C, + 0xC0AC29B7, 0xC97C50DD, 0x3F84D5B5, 0xB5470917, 0x9216D5D9, 0x8979FB1B + }; + + private static readonly uint[] S0Init = new uint[] + { + 0xD1310BA6, 0x98DFB5AC, 0x2FFD72DB, 0xD01ADFB7, 0xB8E1AFED, 0x6A267E96, 0xBA7C9045, 0xF12C7F99, + 0x24A19947, 0xB3916CF7, 0x0801F2E2, 0x858EFC16, 0x636920D8, 0x71574E69, 0xA458FEA3, 0xF4933D7E, + 0x0D95748F, 0x728EB658, 0x718BCD58, 0x82154AEE, 0x7B54A41D, 0xC25A59B5, 0x9C30D539, 0x2AF26013, + 0xC5D1B023, 0x286085F0, 0xCA417918, 0xB8DB38EF, 0x8E79DCB0, 0x603A180E, 0x6C9E0E8B, 0xB01E8A3E, + 0xD71577C1, 0xBD314B27, 0x78AF2FDA, 0x55605C60, 0xE65525F3, 0xAA55AB94, 0x57489862, 0x63E81440, + 0x55CA396A, 0x2AAB10B6, 0xB4CC5C34, 0x1141E8CE, 0xA15486AF, 0x7C72E993, 0xB3EE1411, 0x636FBC2A, + 0x2BA9C55D, 0x741831F6, 0xCE5C3E16, 0x9B87931E, 0xAFD6BA33, 0x6C24CF5C, 0x7A325381, 0x28958677, + 0x3B8F4898, 0x6B4BB9AF, 0xC4BFE81B, 0x66282193, 0x61D809CC, 0xFB21A991, 0x487CAC60, 0x5DEC8032, + 0xEF845D5D, 0xE98575B1, 0xDC262302, 0xEB651B88, 0x23893E81, 0xD396ACC5, 0x0F6D6FF3, 0x83F44239, + 0x2E0B4482, 0xA4842004, 0x69C8F04A, 0x9E1F9B5E, 0x21C66842, 0xF6E96C9A, 0x670C9C61, 0xABD388F0, + 0x6A51A0D2, 0xD8542F68, 0x960FA728, 0xAB5133A3, 0x6EEF0B6C, 0x137A3BE4, 0xBA3BF050, 0x7EFB2A98, + 0xA1F1651D, 0x39AF0176, 0x66CA593E, 0x82430E88, 0x8CEE8619, 0x456F9FB4, 0x7D84A5C3, 0x3B8B5EBE, + 0xE06F75D8, 0x85C12073, 0x401A449F, 0x56C16AA6, 0x4ED3AA62, 0x363F7706, 0x1BFEDF72, 0x429B023D, + 0x37D0D724, 0xD00A1248, 0xDB0FEAD3, 0x49F1C09B, 0x075372C9, 0x80991B7B, 0x25D479D8, 0xF6E8DEF7, + 0xE3FE501A, 0xB6794C3B, 0x976CE0BD, 0x04C006BA, 0xC1A94FB6, 0x409F60C4, 0x5E5C9EC2, 0x196A2463, + 0x68FB6FAF, 0x3E6C53B5, 0x1339B2EB, 0x3B52EC6F, 0x6DFC511F, 0x9B30952C, 0xCC814544, 0xAF5EBD09, + 0xBEE3D004, 0xDE334AFD, 0x660F2807, 0x192E4BB3, 0xC0CBA857, 0x45C8740F, 0xD20B5F39, 0xB9D3FBDB, + 0x5579C0BD, 0x1A60320A, 0xD6A100C6, 0x402C7279, 0x679F25FE, 0xFB1FA3CC, 0x8EA5E9F8, 0xDB3222F8, + 0x3C7516DF, 0xFD616B15, 0x2F501EC8, 0xAD0552AB, 0x323DB5FA, 0xFD238760, 0x53317B48, 0x3E00DF82, + 0x9E5C57BB, 0xCA6F8CA0, 0x1A87562E, 0xDF1769DB, 0xD542A8F6, 0x287EFFC3, 0xAC6732C6, 0x8C4F5573, + 0x695B27B0, 0xBBCA58C8, 0xE1FFA35D, 0xB8F011A0, 0x10FA3D98, 0xFD2183B8, 0x4AFCB56C, 0x2DD1D35B, + 0x9A53E479, 0xB6F84565, 0xD28E49BC, 0x4BFB9790, 0xE1DDF2DA, 0xA4CB7E33, 0x62FB1341, 0xCEE4C6E8, + 0xEF20CADA, 0x36774C01, 0xD07E9EFE, 0x2BF11FB4, 0x95DBDA4D, 0xAE909198, 0xEAAD8E71, 0x6B93D5A0, + 0xD08ED1D0, 0xAFC725E0, 0x8E3C5B2F, 0x8E7594B7, 0x8FF6E2FB, 0xF2122B64, 0x8888B812, 0x900DF01C, + 0x4FAD5EA0, 0x688FC31C, 0xD1CFF191, 0xB3A8C1AD, 0x2F2F2218, 0xBE0E1777, 0xEA752DFE, 0x8B021FA1, + 0xE5A0CC0F, 0xB56F74E8, 0x18ACF3D6, 0xCE89E299, 0xB4A84FE0, 0xFD13E0B7, 0x7CC43B81, 0xD2ADA8D9, + 0x165FA266, 0x80957705, 0x93CC7314, 0x211A1477, 0xE6AD2065, 0x77B5FA86, 0xC75442F5, 0xFB9D35CF, + 0xEBCDAF0C, 0x7B3E89A0, 0xD6411BD3, 0xAE1E7E49, 0x00250E2D, 0x2071B35E, 0x226800BB, 0x57B8E0AF, + 0x2464369B, 0xF009B91E, 0x5563911D, 0x59DFA6AA, 0x78C14389, 0xD95A537F, 0x207D5BA2, 0x02E5B9C5, + 0x83260376, 0x6295CFA9, 0x11C81968, 0x4E734A41, 0xB3472DCA, 0x7B14A94A, 0x1B510052, 0x9A532915, + 0xD60F573F, 0xBC9BC6E4, 0x2B60A476, 0x81E67400, 0x08BA6FB5, 0x571BE91F, 0xF296EC6B, 0x2A0DD915, + 0xB6636521, 0xE7B9F9B6, 0xFF34052E, 0xC5855664, 0x53B02D5D, 0xA99F8FA1, 0x08BA4799, 0x6E85076A + }; + + private static readonly uint[] S1Init = new uint[] + { + 0x4B7A70E9, 0xB5B32944, 0xDB75092E, 0xC4192623, 0xAD6EA6B0, 0x49A7DF7D, 0x9CEE60B8, 0x8FEDB266, + 0xECAA8C71, 0x699A17FF, 0x5664526C, 0xC2B19EE1, 0x193602A5, 0x75094C29, 0xA0591340, 0xE4183A3E, + 0x3F54989A, 0x5B429D65, 0x6B8FE4D6, 0x99F73FD6, 0xA1D29C07, 0xEFE830F5, 0x4D2D38E6, 0xF0255DC1, + 0x4CDD2086, 0x8470EB26, 0x6382E9C6, 0x021ECC5E, 0x09686B3F, 0x3EBAEFC9, 0x3C971814, 0x6B6A70A1, + 0x687F3584, 0x52A0E286, 0xB79C5305, 0xAA500737, 0x3E07841C, 0x7FDEAE5C, 0x8E7D44EC, 0x5716F2B8, + 0xB03ADA37, 0xF0500C0D, 0xF01C1F04, 0x0200B3FF, 0xAE0CF51A, 0x3CB574B2, 0x25837A58, 0xDC0921BD, + 0xD19113F9, 0x7CA92FF6, 0x94324773, 0x22F54701, 0x3AE5E581, 0x37C2DADC, 0xC8B57634, 0x9AF3DDA7, + 0xA9446146, 0x0FD0030E, 0xECC8C73E, 0xA4751E41, 0xE238CD99, 0x3BEA0E2F, 0x3280BBA1, 0x183EB331, + 0x4E548B38, 0x4F6DB908, 0x6F420D03, 0xF60A04BF, 0x2CB81290, 0x24977C79, 0x5679B072, 0xBCAF89AF, + 0xDE9A771F, 0xD9930810, 0xB38BAE12, 0xDCCF3F2E, 0x5512721F, 0x2E6B7124, 0x501ADDE6, 0x9F84CD87, + 0x7A584718, 0x7408DA17, 0xBC9F9ABC, 0xE94B7D8C, 0xEC7AEC3A, 0xDB851DFA, 0x63094366, 0xC464C3D2, + 0xEF1C1847, 0x3215D908, 0xDD433B37, 0x24C2BA16, 0x12A14D43, 0x2A65C451, 0x50940002, 0x133AE4DD, + 0x71DFF89E, 0x10314E55, 0x81AC77D6, 0x5F11199B, 0x043556F1, 0xD7A3C76B, 0x3C11183B, 0x5924A509, + 0xF28FE6ED, 0x97F1FBFA, 0x9EBABF2C, 0x1E153C6E, 0x86E34570, 0xEAE96FB1, 0x860E5E0A, 0x5A3E2AB3, + 0x771FE71C, 0x4E3D06FA, 0x2965DCB9, 0x99E71D0F, 0x803E89D6, 0x5266C825, 0x2E4CC978, 0x9C10B36A, + 0xC6150EBA, 0x94E2EA78, 0xA5FC3C53, 0x1E0A2DF4, 0xF2F74EA7, 0x361D2B3D, 0x1939260F, 0x19C27960, + 0x5223A708, 0xF71312B6, 0xEBADFE6E, 0xEAC31F66, 0xE3BC4595, 0xA67BC883, 0xB17F37D1, 0x018CFF28, + 0xC332DDEF, 0xBE6C5AA5, 0x65582185, 0x68AB9802, 0xEECEA50F, 0xDB2F953B, 0x2AEF7DAD, 0x5B6E2F84, + 0x1521B628, 0x29076170, 0xECDD4775, 0x619F1510, 0x13CCA830, 0xEB61BD96, 0x0334FE1E, 0xAA0363CF, + 0xB5735C90, 0x4C70A239, 0xD59E9E0B, 0xCBAADE14, 0xEECC86BC, 0x60622CA7, 0x9CAB5CAB, 0xB2F3846E, + 0x648B1EAF, 0x19BDF0CA, 0xA02369B9, 0x655ABB50, 0x40685A32, 0x3C2AB4B3, 0x319EE9D5, 0xC021B8F7, + 0x9B540B19, 0x875FA099, 0x95F7997E, 0x623D7DA8, 0xF837889A, 0x97E32D77, 0x11ED935F, 0x16681281, + 0x0E358829, 0xC7E61FD6, 0x96DEDFA1, 0x7858BA99, 0x57F584A5, 0x1B227263, 0x9B83C3FF, 0x1AC24696, + 0xCDB30AEB, 0x532E3054, 0x8FD948E4, 0x6DBC3128, 0x58EBF2EF, 0x34C6FFEA, 0xFE28ED61, 0xEE7C3C73, + 0x5D4A14D9, 0xE864B7E3, 0x42105D14, 0x203E13E0, 0x45EEE2B6, 0xA3AAABEA, 0xDB6C4F15, 0xFACB4FD0, + 0xC742F442, 0xEF6ABBB5, 0x654F3B1D, 0x41CD2105, 0xD81E799E, 0x86854DC7, 0xE44B476A, 0x3D816250, + 0xCF62A1F2, 0x5B8D2646, 0xFC8883A0, 0xC1C7B6A3, 0x7F1524C3, 0x69CB7492, 0x47848A0B, 0x5692B285, + 0x095BBF00, 0xAD19489D, 0x1462B174, 0x23820E00, 0x58428D2A, 0x0C55F5EA, 0x1DADF43E, 0x233F7061, + 0x3372F092, 0x8D937E41, 0xD65FECF1, 0x6C223BDB, 0x7CDE3759, 0xCBEE7460, 0x4085F2A7, 0xCE77326E, + 0xA6078084, 0x19F8509E, 0xE8EFD855, 0x61D99735, 0xA969A7AA, 0xC50C06C2, 0x5A04ABFC, 0x800BCADC, + 0x9E447A2E, 0xC3453484, 0xFDD56705, 0x0E1E9EC9, 0xDB73DBD3, 0x105588CD, 0x675FDA79, 0xE3674340, + 0xC5C43465, 0x713E38D8, 0x3D28F89E, 0xF16DFF20, 0x153E21E7, 0x8FB03D4A, 0xE6E39F2B, 0xDB83ADF7 + }; + + private static readonly uint[] S2Init = new uint[] + { + 0xE93D5A68, 0x948140F7, 0xF64C261C, 0x94692934, 0x411520F7, 0x7602D4F7, 0xBCF46B2E, 0xD4A20068, + 0xD4082471, 0x3320F46A, 0x43B7D4B7, 0x500061AF, 0x1E39F62E, 0x97244546, 0x14214F74, 0xBF8B8840, + 0x4D95FC1D, 0x96B591AF, 0x70F4DDD3, 0x66A02F45, 0xBFBC09EC, 0x03BD9785, 0x7FAC6DD0, 0x31CB8504, + 0x96EB27B3, 0x55FD3941, 0xDA2547E6, 0xABCA0A9A, 0x28507825, 0x530429F4, 0x0A2C86DA, 0xE9B66DFB, + 0x68DC1462, 0xD7486900, 0x680EC0A4, 0x27A18DEE, 0x4F3FFEA2, 0xE887AD8C, 0xB58CE006, 0x7AF4D6B6, + 0xAACE1E7C, 0xD3375FEC, 0xCE78A399, 0x406B2A42, 0x20FE9E35, 0xD9F385B9, 0xEE39D7AB, 0x3B124E8B, + 0x1DC9FAF7, 0x4B6D1856, 0x26A36631, 0xEAE397B2, 0x3A6EFA74, 0xDD5B4332, 0x6841E7F7, 0xCA7820FB, + 0xFB0AF54E, 0xD8FEB397, 0x454056AC, 0xBA489527, 0x55533A3A, 0x20838D87, 0xFE6BA9B7, 0xD096954B, + 0x55A867BC, 0xA1159A58, 0xCCA92963, 0x99E1DB33, 0xA62A4A56, 0x3F3125F9, 0x5EF47E1C, 0x9029317C, + 0xFDF8E802, 0x04272F70, 0x80BB155C, 0x05282CE3, 0x95C11548, 0xE4C66D22, 0x48C1133F, 0xC70F86DC, + 0x07F9C9EE, 0x41041F0F, 0x404779A4, 0x5D886E17, 0x325F51EB, 0xD59BC0D1, 0xF2BCC18F, 0x41113564, + 0x257B7834, 0x602A9C60, 0xDFF8E8A3, 0x1F636C1B, 0x0E12B4C2, 0x02E1329E, 0xAF664FD1, 0xCAD18115, + 0x6B2395E0, 0x333E92E1, 0x3B240B62, 0xEEBEB922, 0x85B2A20E, 0xE6BA0D99, 0xDE720C8C, 0x2DA2F728, + 0xD0127845, 0x95B794FD, 0x647D0862, 0xE7CCF5F0, 0x5449A36F, 0x877D48FA, 0xC39DFD27, 0xF33E8D1E, + 0x0A476341, 0x992EFF74, 0x3A6F6EAB, 0xF4F8FD37, 0xA812DC60, 0xA1EBDDF8, 0x991BE14C, 0xDB6E6B0D, + 0xC67B5510, 0x6D672C37, 0x2765D43B, 0xDCD0E804, 0xF1290DC7, 0xCC00FFA3, 0xB5390F92, 0x690FED0B, + 0x667B9FFB, 0xCEDB7D9C, 0xA091CF0B, 0xD9155EA3, 0xBB132F88, 0x515BAD24, 0x7B9479BF, 0x763BD6EB, + 0x37392EB3, 0xCC115979, 0x8026E297, 0xF42E312D, 0x6842ADA7, 0xC66A2B3B, 0x12754CCC, 0x782EF11C, + 0x6A124237, 0xB79251E7, 0x06A1BBE6, 0x4BFB6350, 0x1A6B1018, 0x11CAEDFA, 0x3D25BDD8, 0xE2E1C3C9, + 0x44421659, 0x0A121386, 0xD90CEC6E, 0xD5ABEA2A, 0x64AF674E, 0xDA86A85F, 0xBEBFE988, 0x64E4C3FE, + 0x9DBC8057, 0xF0F7C086, 0x60787BF8, 0x6003604D, 0xD1FD8346, 0xF6381FB0, 0x7745AE04, 0xD736FCCC, + 0x83426B33, 0xF01EAB71, 0xB0804187, 0x3C005E5F, 0x77A057BE, 0xBDE8AE24, 0x55464299, 0xBF582E61, + 0x4E58F48F, 0xF2DDFDA2, 0xF474EF38, 0x8789BDC2, 0x5366F9C3, 0xC8B38E74, 0xB475F255, 0x46FCD9B9, + 0x7AEB2661, 0x8B1DDF84, 0x846A0E79, 0x915F95E2, 0x466E598E, 0x20B45770, 0x8CD55591, 0xC902DE4C, + 0xB90BACE1, 0xBB8205D0, 0x11A86248, 0x7574A99E, 0xB77F19B6, 0xE0A9DC09, 0x662D09A1, 0xC4324633, + 0xE85A1F02, 0x09F0BE8C, 0x4A99A025, 0x1D6EFE10, 0x1AB93D1D, 0x0BA5A4DF, 0xA186F20F, 0x2868F169, + 0xDCB7DA83, 0x573906FE, 0xA1E2CE9B, 0x4FCD7F52, 0x50115E01, 0xA70683FA, 0xA002B5C4, 0x0DE6D027, + 0x9AF88C27, 0x773F8641, 0xC3604C06, 0x61A806B5, 0xF0177A28, 0xC0F586E0, 0x006058AA, 0x30DC7D62, + 0x11E69ED7, 0x2338EA63, 0x53C2DD94, 0xC2C21634, 0xBBCBEE56, 0x90BCB6DE, 0xEBFC7DA1, 0xCE591D76, + 0x6F05E409, 0x4B7C0188, 0x39720A3D, 0x7C927C24, 0x86E3725F, 0x724D9DB9, 0x1AC15BB4, 0xD39EB8FC, + 0xED545578, 0x08FCA5B5, 0xD83D7CD3, 0x4DAD0FC4, 0x1E50EF5E, 0xB161E6F8, 0xA28514D9, 0x6C51133C, + 0x6FD5C7E7, 0x56E14EC4, 0x362ABFCE, 0xDDC6C837, 0xD79A3234, 0x92638212, 0x670EFA8E, 0x406000E0 + }; + + private static readonly uint[] S3Init = new uint[] + { + 0x3A39CE37, 0xD3FAF5CF, 0xABC27737, 0x5AC52D1B, 0x5CB0679E, 0x4FA33742, 0xD3822740, 0x99BC9BBE, + 0xD5118E9D, 0xBF0F7315, 0xD62D1C7E, 0xC700C47B, 0xB78C1B6B, 0x21A19045, 0xB26EB1BE, 0x6A366EB4, + 0x5748AB2F, 0xBC946E79, 0xC6A376D2, 0x6549C2C8, 0x530FF8EE, 0x468DDE7D, 0xD5730A1D, 0x4CD04DC6, + 0x2939BBDB, 0xA9BA4650, 0xAC9526E8, 0xBE5EE304, 0xA1FAD5F0, 0x6A2D519A, 0x63EF8CE2, 0x9A86EE22, + 0xC089C2B8, 0x43242EF6, 0xA51E03AA, 0x9CF2D0A4, 0x83C061BA, 0x9BE96A4D, 0x8FE51550, 0xBA645BD6, + 0x2826A2F9, 0xA73A3AE1, 0x4BA99586, 0xEF5562E9, 0xC72FEFD3, 0xF752F7DA, 0x3F046F69, 0x77FA0A59, + 0x80E4A915, 0x87B08601, 0x9B09E6AD, 0x3B3EE593, 0xE990FD5A, 0x9E34D797, 0x2CF0B7D9, 0x022B8B51, + 0x96D5AC3A, 0x017DA67D, 0xD1CF3ED6, 0x7C7D2D28, 0x1F9F25CF, 0xADF2B89B, 0x5AD6B472, 0x5A88F54C, + 0xE029AC71, 0xE019A5E6, 0x47B0ACFD, 0xED93FA9B, 0xE8D3C48D, 0x283B57CC, 0xF8D56629, 0x79132E28, + 0x785F0191, 0xED756055, 0xF7960E44, 0xE3D35E8C, 0x15056DD4, 0x88F46DBA, 0x03A16125, 0x0564F0BD, + 0xC3EB9E15, 0x3C9057A2, 0x97271AEC, 0xA93A072A, 0x1B3F6D9B, 0x1E6321F5, 0xF59C66FB, 0x26DCF319, + 0x7533D928, 0xB155FDF5, 0x03563482, 0x8ABA3CBB, 0x28517711, 0xC20AD9F8, 0xABCC5167, 0xCCAD925F, + 0x4DE81751, 0x3830DC8E, 0x379D5862, 0x9320F991, 0xEA7A90C2, 0xFB3E7BCE, 0x5121CE64, 0x774FBE32, + 0xA8B6E37E, 0xC3293D46, 0x48DE5369, 0x6413E680, 0xA2AE0810, 0xDD6DB224, 0x69852DFD, 0x09072166, + 0xB39A460A, 0x6445C0DD, 0x586CDECF, 0x1C20C8AE, 0x5BBEF7DD, 0x1B588D40, 0xCCD2017F, 0x6BB4E3BB, + 0xDDA26A7E, 0x3A59FF45, 0x3E350A44, 0xBCB4CDD5, 0x72EACEA8, 0xFA6484BB, 0x8D6612AE, 0xBF3C6F47, + 0xD29BE463, 0x542F5D9E, 0xAEC2771B, 0xF64E6370, 0x740E0D8D, 0xE75B1357, 0xF8721671, 0xAF537D5D, + 0x4040CB08, 0x4EB4E2CC, 0x34D2466A, 0x0115AF84, 0xE1B00428, 0x95983A1D, 0x06B89FB4, 0xCE6EA048, + 0x6F3F3B82, 0x3520AB82, 0x011A1D4B, 0x277227F8, 0x611560B1, 0xE7933FDC, 0xBB3A792B, 0x344525BD, + 0xA08839E1, 0x51CE794B, 0x2F32C9B7, 0xA01FBAC9, 0xE01CC87E, 0xBCC7D1F6, 0xCF0111C3, 0xA1E8AAC7, + 0x1A908749, 0xD44FBD9A, 0xD0DADECB, 0xD50ADA38, 0x0339C32A, 0xC6913667, 0x8DF9317C, 0xE0B12B4F, + 0xF79E59B7, 0x43F5BB3A, 0xF2D519FF, 0x27D9459C, 0xBF97222C, 0x15E6FC2A, 0x0F91FC71, 0x9B941525, + 0xFAE59361, 0xCEB69CEB, 0xC2A86459, 0x12BAA8D1, 0xB6C1075E, 0xE3056A0C, 0x10D25065, 0xCB03A442, + 0xE0EC6E0E, 0x1698DB3B, 0x4C98A0BE, 0x3278E964, 0x9F1F9532, 0xE0D392DF, 0xD3A0342B, 0x8971F21E, + 0x1B0A7441, 0x4BA3348C, 0xC5BE7120, 0xC37632D8, 0xDF359F8D, 0x9B992F2E, 0xE60B6F47, 0x0FE3F11D, + 0xE54CDA54, 0x1EDAD891, 0xCE6279CF, 0xCD3E7E6F, 0x1618B166, 0xFD2C1D05, 0x848FD2C5, 0xF6FB2299, + 0xF523F357, 0xA6327623, 0x93A83531, 0x56CCCD02, 0xACF08162, 0x5A75EBB5, 0x6E163697, 0x88D273CC, + 0xDE966292, 0x81B949D0, 0x4C50901B, 0x71C65614, 0xE6C6C7BD, 0x327A140A, 0x45E1D006, 0xC3F27B9A, + 0xC9AA53FD, 0x62A80F00, 0xBB25BFE2, 0x35BDD2F6, 0x71126905, 0xB2040222, 0xB6CBCF7C, 0xCD769C2B, + 0x53113EC0, 0x1640E3D3, 0x38ABBD60, 0x2547ADF0, 0xBA38209C, 0xF746CE76, 0x77AFA1C5, 0x20756060, + 0x85CBFE4E, 0x8AE88DD8, 0x7AAAF9B0, 0x4CF9AA7E, 0x1948C25C, 0x02FB8A8C, 0x01C36AE4, 0xD6EBE1F9, + 0x90D4F869, 0xA65CDEA0, 0x3F09252D, 0xC208E69F, 0xB74E6132, 0xCE77E25B, 0x578FDFE3, 0x3AC372E6 + }; + + private fixed uint m_p[PSize]; + private fixed uint m_s0[SSize]; + private fixed uint m_s1[SSize]; + private fixed uint m_s2[SSize]; + private fixed uint m_s3[SSize]; + + public BlowfishState(ReadOnlySpan key) + { + CheckKeyLength(key); + + // initializes P-array and S-boxes to initial values. + fixed (uint* pSrc = PInit) + fixed (uint* pDst = m_p) + { + Buffer.MemoryCopy(pSrc, pDst, PSize * 4, PSize * 4); + } + + fixed (uint* pSrc = S0Init) + fixed (uint* pDst = m_s0) + { + Buffer.MemoryCopy(pSrc, pDst, SSize * 4, SSize * 4); + } + + fixed (uint* pSrc = S1Init) + fixed (uint* pDst = m_s1) + { + Buffer.MemoryCopy(pSrc, pDst, SSize * 4, SSize * 4); + } + + fixed (uint* pSrc = S2Init) + fixed (uint* pDst = m_s2) + { + Buffer.MemoryCopy(pSrc, pDst, SSize * 4, SSize * 4); + } + + fixed (uint* pSrc = S3Init) + fixed (uint* pDst = m_s3) + { + Buffer.MemoryCopy(pSrc, pDst, SSize * 4, SSize * 4); + } + + InitKey(key); + } + + private void CheckKeyLength(ReadOnlySpan key) + { + // Supported key sizes: 32–448 bits + // https://en.wikipedia.org/wiki/Blowfish_(cipher)#The_algorithm + if (key.Length < 4 || key.Length > 56) + { + throw new ArgumentException("Key length must be between from 32 to 448 bits.", nameof(key)); + } + } + + /// + /// Encrypts a block. + /// + /// A left side of the block. + /// A right side of the block. + public (uint, uint) EncryptBlock(uint xl, uint xr) + { + // https://en.wikipedia.org/wiki/Feistel_cipher#Construction_details + for (var i = 0; i < Rounds; i += 2) + { + xl ^= m_p[i]; + xr ^= Round(xl); + xr ^= m_p[i + 1]; + xl ^= Round(xr); + } + + xl ^= m_p[16]; + xr ^= m_p[17]; + + // swap(L, R) + var temp = xl; + xl = xr; + xr = temp; + + return (xl, xr); + } + + /// + /// Decrypts a block. + /// + /// A left side of the block. + /// A right side of the blick. + public (uint, uint) DecryptBlock(uint xl, uint xr) + { + // https://en.wikipedia.org/wiki/Feistel_cipher#Construction_details + for (var i = Rounds; i > 0; i -= 2) + { + xl ^= m_p[i + 1]; + xr ^= Round(xr); + xr ^= m_p[i]; + xr ^= Round(xr); + } + + xl ^= m_p[1]; + xr ^= m_p[0]; + + // swap(L, R); + var temp = xl; + xl = xr; + xr = temp; + + return (xl, xr); + } + + /// + /// Initializes P-array and S-boxes with given key. P-array and S-boxes must be initialized to initial values beforehand. + /// + /// A setup key. + private void InitKey(ReadOnlySpan key) + { + { + var keyPos = 0; + + for (var i = 0; i < PSize; i++) + { + var val = 0u; + + // wrapping u32 (be) + // eg. key = { 12 34 56 78 AB CD EF GH HI JK } + // => {0x12345678, 0xABCDEFGH, 0xHIJK1234, 0x5678ABCD, ..} + for (var j = 0; j < 4; j++) + { + // wrap to the start when we reached the end. + if (keyPos >= key.Length) + { + keyPos = 0; + } + + val = (val << 8) | key[keyPos++]; + } + + m_p[i] ^= val; + } + } + + { + var xl = 0u; + var xr = 0u; + + for (var i = 0; i < PSize; i += 2) + { + (xl, xr) = EncryptBlock(xl, xr); + + m_p[i] = xl; + m_p[i + 1] = xr; + } + + for (var i = 0; i < SSize; i += 2) + { + (xl, xr) = EncryptBlock(xl, xr); + + m_s0[i] = xl; + m_s0[i + 1] = xr; + } + + for (var i = 0; i < SSize; i += 2) + { + (xl, xr) = EncryptBlock(xl, xr); + + m_s1[i] = xl; + m_s1[i + 1] = xr; + } + + for (var i = 0; i < SSize; i += 2) + { + (xl, xr) = EncryptBlock(xl, xr); + + m_s2[i] = xl; + m_s2[i + 1] = xr; + } + + for (var i = 0; i < SSize; i += 2) + { + (xl, xr) = EncryptBlock(xl, xr); + + m_s3[i] = xl; + m_s3[i + 1] = xr; + } + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining /* | MethodImplOptions.AggressiveOptimization */)] + private uint Round(uint x) + { + return unchecked( + ((m_s0[x >> 24] + m_s1[(byte)(x >> 16)]) ^ m_s2[(byte)(x >> 8)]) + m_s3[(byte)x] + ); + } + } +} + +// ReSharper restore InconsistentNaming diff --git a/src/XIVLauncher2.Common/Encryption/BlockCipher/Ecb.cs b/src/XIVLauncher2.Common/Encryption/BlockCipher/Ecb.cs new file mode 100644 index 0000000..3fcd1ab --- /dev/null +++ b/src/XIVLauncher2.Common/Encryption/BlockCipher/Ecb.cs @@ -0,0 +1,72 @@ +// NOTE: This file is copy-pasted almost *as-is* from the previous work `Aither.Crypto` +// hence currently it does not follow XL's naming convetions. +// +// It's totally okay to change this. But for now, this is what it is atm. +// ReSharper disable InconsistentNaming + +using System; + +namespace XIVLauncher2.Common.Encryption.BlockCipher +{ + public sealed class Ecb : IBlockMode where T : IBlockCipher + { + private T m_cipher; + + public Ecb(T cipher) + { + m_cipher = cipher; + } + + private void AssertSliceLength(ReadOnlySpan input, ReadOnlySpan output) + { + if (input.Length > output.Length) + { + throw new ArgumentException("The output slice must be larger than the input.", nameof(output)); + } + + var blockSize = m_cipher.BlockSize; + + if (input.Length % blockSize != 0) + { + throw new ArgumentException("The length of the input slice must be divisible by the block length.", + nameof(input)); + } + } + + public void Encrypt(ReadOnlySpan input, Span output) + { + AssertSliceLength(input, output); + + unsafe + { + fixed (byte* pInput = input) + fixed (byte* pOutput = output) + { + for (var i = 0; i < input.Length; i += m_cipher.BlockSize) + { + m_cipher.EncryptBlockUnsafe(pInput + i, pOutput + i); + } + } + } + } + + public void Decrypt(ReadOnlySpan input, Span output) + { + AssertSliceLength(input, output); + + unsafe + { + fixed (byte* pInput = input) + fixed (byte* pOutput = output) + { + for (var i = 0; i < input.Length; i += m_cipher.BlockSize) + { + m_cipher.DecryptBlockUnsafe(pInput + i, pOutput + i); + } + } + } + } + } +} + +// ReSharper restore InconsistentNaming diff --git a/src/XIVLauncher2.Common/Encryption/BlockCipher/IBlockCipher.cs b/src/XIVLauncher2.Common/Encryption/BlockCipher/IBlockCipher.cs new file mode 100644 index 0000000..308e95e --- /dev/null +++ b/src/XIVLauncher2.Common/Encryption/BlockCipher/IBlockCipher.cs @@ -0,0 +1,54 @@ +// NOTE: This file is copy-pasted almost *as-is* from the previous work `Aither.Crypto` +// hence currently it does not follow XL's naming convetions. +// +// It's totally okay to change this. But for now, this is what it is atm. +// ReSharper disable InconsistentNaming + +namespace XIVLauncher2.Common.Encryption.BlockCipher +{ + public interface IBlockCipher + { + /// + /// A number of bytes that can be processed in a single operation. + /// + /// + /// This property is assumed to be immutable once the block cipher object is created. + /// Breaking this assumption may cause an undefined behavior. + /// + int BlockSize { get; } + + /// + /// Encrypts a single block. + /// + /// + /// A pointer to the data needs to be encrypted. + /// It must be valid to read bytes from the pointer where size is indicated by BlockSize property. + /// + /// + /// A pointer to the buffer to store the result of the operation. + /// It must be valid to write bytes to the pointer where size is indicated by BlockSize property. + /// + /// + /// A pointer to input and output **can** overlap to perform in-place operation. + /// + unsafe void EncryptBlockUnsafe(byte* input, byte* output); + + /// + /// Decrypts a single block. + /// + /// + /// A pointer to the data needs to be decrypted. + /// It must be valid to read bytes from the pointer where size is indicated by BlockSize property. + /// + /// + /// A pointer to the buffer to store the result of the operation. + /// It must be valid to write bytes to the pointer where size is indicated by BlockSize property. + /// + /// + /// A pointer to input and output **can** overlap to perform in-place operation. + /// + unsafe void DecryptBlockUnsafe(byte* input, byte* output); + } +} + +// ReSharper restore InconsistentNaming diff --git a/src/XIVLauncher2.Common/Encryption/BlockCipher/IBlockMode.cs b/src/XIVLauncher2.Common/Encryption/BlockCipher/IBlockMode.cs new file mode 100644 index 0000000..b995c34 --- /dev/null +++ b/src/XIVLauncher2.Common/Encryption/BlockCipher/IBlockMode.cs @@ -0,0 +1,18 @@ +// NOTE: This file is copy-pasted almost *as-is* from the previous work `Aither.Crypto` +// hence currently it does not follow XL's naming convetions. +// +// It's totally okay to change this. But for now, this is what it is atm. +// ReSharper disable InconsistentNaming + +using System; + +namespace XIVLauncher2.Common.Encryption.BlockCipher +{ + public interface IBlockMode + { + void Encrypt(ReadOnlySpan input, Span output); + void Decrypt(ReadOnlySpan input, Span output); + } +} + +// ReSharper restore InconsistentNaming diff --git a/src/XIVLauncher2.Common/Encryption/CrtRand.cs b/src/XIVLauncher2.Common/Encryption/CrtRand.cs new file mode 100644 index 0000000..7053378 --- /dev/null +++ b/src/XIVLauncher2.Common/Encryption/CrtRand.cs @@ -0,0 +1,17 @@ +namespace XIVLauncher2.Common.Encryption; + +public class CrtRand +{ + private uint seed; + + public CrtRand(uint seed) + { + this.seed = seed; + } + + public uint Next() + { + this.seed = 0x343FD * this.seed + 0x269EC3; + return ((this.seed >> 16) & 0xFFFF) & 0x7FFF; + } +} diff --git a/src/XIVLauncher2.Common/Encryption/LegacyBlowfish.cs b/src/XIVLauncher2.Common/Encryption/LegacyBlowfish.cs new file mode 100644 index 0000000..c5926a2 --- /dev/null +++ b/src/XIVLauncher2.Common/Encryption/LegacyBlowfish.cs @@ -0,0 +1,316 @@ +using System; +using System.Collections.Generic; + +namespace XIVLauncher2.Common.Encryption +{ + public class LegacyBlowfish + { + #region P-Array and S-Boxes + + private readonly uint[] p = + { + 0x243f6a88, 0x85a308d3, 0x13198a2e, 0x03707344, 0xa4093822, 0x299f31d0, + 0x082efa98, 0xec4e6c89, 0x452821e6, 0x38d01377, 0xbe5466cf, 0x34e90c6c, + 0xc0ac29b7, 0xc97c50dd, 0x3f84d5b5, 0xb5470917, 0x9216d5d9, 0x8979fb1b + }; + + private readonly uint[,] s = + { + { + 0xd1310ba6, 0x98dfb5ac, 0x2ffd72db, 0xd01adfb7, 0xb8e1afed, 0x6a267e96, + 0xba7c9045, 0xf12c7f99, 0x24a19947, 0xb3916cf7, 0x0801f2e2, 0x858efc16, + 0x636920d8, 0x71574e69, 0xa458fea3, 0xf4933d7e, 0x0d95748f, 0x728eb658, + 0x718bcd58, 0x82154aee, 0x7b54a41d, 0xc25a59b5, 0x9c30d539, 0x2af26013, + 0xc5d1b023, 0x286085f0, 0xca417918, 0xb8db38ef, 0x8e79dcb0, 0x603a180e, + 0x6c9e0e8b, 0xb01e8a3e, 0xd71577c1, 0xbd314b27, 0x78af2fda, 0x55605c60, + 0xe65525f3, 0xaa55ab94, 0x57489862, 0x63e81440, 0x55ca396a, 0x2aab10b6, + 0xb4cc5c34, 0x1141e8ce, 0xa15486af, 0x7c72e993, 0xb3ee1411, 0x636fbc2a, + 0x2ba9c55d, 0x741831f6, 0xce5c3e16, 0x9b87931e, 0xafd6ba33, 0x6c24cf5c, + 0x7a325381, 0x28958677, 0x3b8f4898, 0x6b4bb9af, 0xc4bfe81b, 0x66282193, + 0x61d809cc, 0xfb21a991, 0x487cac60, 0x5dec8032, 0xef845d5d, 0xe98575b1, + 0xdc262302, 0xeb651b88, 0x23893e81, 0xd396acc5, 0x0f6d6ff3, 0x83f44239, + 0x2e0b4482, 0xa4842004, 0x69c8f04a, 0x9e1f9b5e, 0x21c66842, 0xf6e96c9a, + 0x670c9c61, 0xabd388f0, 0x6a51a0d2, 0xd8542f68, 0x960fa728, 0xab5133a3, + 0x6eef0b6c, 0x137a3be4, 0xba3bf050, 0x7efb2a98, 0xa1f1651d, 0x39af0176, + 0x66ca593e, 0x82430e88, 0x8cee8619, 0x456f9fb4, 0x7d84a5c3, 0x3b8b5ebe, + 0xe06f75d8, 0x85c12073, 0x401a449f, 0x56c16aa6, 0x4ed3aa62, 0x363f7706, + 0x1bfedf72, 0x429b023d, 0x37d0d724, 0xd00a1248, 0xdb0fead3, 0x49f1c09b, + 0x075372c9, 0x80991b7b, 0x25d479d8, 0xf6e8def7, 0xe3fe501a, 0xb6794c3b, + 0x976ce0bd, 0x04c006ba, 0xc1a94fb6, 0x409f60c4, 0x5e5c9ec2, 0x196a2463, + 0x68fb6faf, 0x3e6c53b5, 0x1339b2eb, 0x3b52ec6f, 0x6dfc511f, 0x9b30952c, + 0xcc814544, 0xaf5ebd09, 0xbee3d004, 0xde334afd, 0x660f2807, 0x192e4bb3, + 0xc0cba857, 0x45c8740f, 0xd20b5f39, 0xb9d3fbdb, 0x5579c0bd, 0x1a60320a, + 0xd6a100c6, 0x402c7279, 0x679f25fe, 0xfb1fa3cc, 0x8ea5e9f8, 0xdb3222f8, + 0x3c7516df, 0xfd616b15, 0x2f501ec8, 0xad0552ab, 0x323db5fa, 0xfd238760, + 0x53317b48, 0x3e00df82, 0x9e5c57bb, 0xca6f8ca0, 0x1a87562e, 0xdf1769db, + 0xd542a8f6, 0x287effc3, 0xac6732c6, 0x8c4f5573, 0x695b27b0, 0xbbca58c8, + 0xe1ffa35d, 0xb8f011a0, 0x10fa3d98, 0xfd2183b8, 0x4afcb56c, 0x2dd1d35b, + 0x9a53e479, 0xb6f84565, 0xd28e49bc, 0x4bfb9790, 0xe1ddf2da, 0xa4cb7e33, + 0x62fb1341, 0xcee4c6e8, 0xef20cada, 0x36774c01, 0xd07e9efe, 0x2bf11fb4, + 0x95dbda4d, 0xae909198, 0xeaad8e71, 0x6b93d5a0, 0xd08ed1d0, 0xafc725e0, + 0x8e3c5b2f, 0x8e7594b7, 0x8ff6e2fb, 0xf2122b64, 0x8888b812, 0x900df01c, + 0x4fad5ea0, 0x688fc31c, 0xd1cff191, 0xb3a8c1ad, 0x2f2f2218, 0xbe0e1777, + 0xea752dfe, 0x8b021fa1, 0xe5a0cc0f, 0xb56f74e8, 0x18acf3d6, 0xce89e299, + 0xb4a84fe0, 0xfd13e0b7, 0x7cc43b81, 0xd2ada8d9, 0x165fa266, 0x80957705, + 0x93cc7314, 0x211a1477, 0xe6ad2065, 0x77b5fa86, 0xc75442f5, 0xfb9d35cf, + 0xebcdaf0c, 0x7b3e89a0, 0xd6411bd3, 0xae1e7e49, 0x00250e2d, 0x2071b35e, + 0x226800bb, 0x57b8e0af, 0x2464369b, 0xf009b91e, 0x5563911d, 0x59dfa6aa, + 0x78c14389, 0xd95a537f, 0x207d5ba2, 0x02e5b9c5, 0x83260376, 0x6295cfa9, + 0x11c81968, 0x4e734a41, 0xb3472dca, 0x7b14a94a, 0x1b510052, 0x9a532915, + 0xd60f573f, 0xbc9bc6e4, 0x2b60a476, 0x81e67400, 0x08ba6fb5, 0x571be91f, + 0xf296ec6b, 0x2a0dd915, 0xb6636521, 0xe7b9f9b6, 0xff34052e, 0xc5855664, + 0x53b02d5d, 0xa99f8fa1, 0x08ba4799, 0x6e85076a + }, + { + 0x4b7a70e9, 0xb5b32944, 0xdb75092e, 0xc4192623, 0xad6ea6b0, 0x49a7df7d, + 0x9cee60b8, 0x8fedb266, 0xecaa8c71, 0x699a17ff, 0x5664526c, 0xc2b19ee1, + 0x193602a5, 0x75094c29, 0xa0591340, 0xe4183a3e, 0x3f54989a, 0x5b429d65, + 0x6b8fe4d6, 0x99f73fd6, 0xa1d29c07, 0xefe830f5, 0x4d2d38e6, 0xf0255dc1, + 0x4cdd2086, 0x8470eb26, 0x6382e9c6, 0x021ecc5e, 0x09686b3f, 0x3ebaefc9, + 0x3c971814, 0x6b6a70a1, 0x687f3584, 0x52a0e286, 0xb79c5305, 0xaa500737, + 0x3e07841c, 0x7fdeae5c, 0x8e7d44ec, 0x5716f2b8, 0xb03ada37, 0xf0500c0d, + 0xf01c1f04, 0x0200b3ff, 0xae0cf51a, 0x3cb574b2, 0x25837a58, 0xdc0921bd, + 0xd19113f9, 0x7ca92ff6, 0x94324773, 0x22f54701, 0x3ae5e581, 0x37c2dadc, + 0xc8b57634, 0x9af3dda7, 0xa9446146, 0x0fd0030e, 0xecc8c73e, 0xa4751e41, + 0xe238cd99, 0x3bea0e2f, 0x3280bba1, 0x183eb331, 0x4e548b38, 0x4f6db908, + 0x6f420d03, 0xf60a04bf, 0x2cb81290, 0x24977c79, 0x5679b072, 0xbcaf89af, + 0xde9a771f, 0xd9930810, 0xb38bae12, 0xdccf3f2e, 0x5512721f, 0x2e6b7124, + 0x501adde6, 0x9f84cd87, 0x7a584718, 0x7408da17, 0xbc9f9abc, 0xe94b7d8c, + 0xec7aec3a, 0xdb851dfa, 0x63094366, 0xc464c3d2, 0xef1c1847, 0x3215d908, + 0xdd433b37, 0x24c2ba16, 0x12a14d43, 0x2a65c451, 0x50940002, 0x133ae4dd, + 0x71dff89e, 0x10314e55, 0x81ac77d6, 0x5f11199b, 0x043556f1, 0xd7a3c76b, + 0x3c11183b, 0x5924a509, 0xf28fe6ed, 0x97f1fbfa, 0x9ebabf2c, 0x1e153c6e, + 0x86e34570, 0xeae96fb1, 0x860e5e0a, 0x5a3e2ab3, 0x771fe71c, 0x4e3d06fa, + 0x2965dcb9, 0x99e71d0f, 0x803e89d6, 0x5266c825, 0x2e4cc978, 0x9c10b36a, + 0xc6150eba, 0x94e2ea78, 0xa5fc3c53, 0x1e0a2df4, 0xf2f74ea7, 0x361d2b3d, + 0x1939260f, 0x19c27960, 0x5223a708, 0xf71312b6, 0xebadfe6e, 0xeac31f66, + 0xe3bc4595, 0xa67bc883, 0xb17f37d1, 0x018cff28, 0xc332ddef, 0xbe6c5aa5, + 0x65582185, 0x68ab9802, 0xeecea50f, 0xdb2f953b, 0x2aef7dad, 0x5b6e2f84, + 0x1521b628, 0x29076170, 0xecdd4775, 0x619f1510, 0x13cca830, 0xeb61bd96, + 0x0334fe1e, 0xaa0363cf, 0xb5735c90, 0x4c70a239, 0xd59e9e0b, 0xcbaade14, + 0xeecc86bc, 0x60622ca7, 0x9cab5cab, 0xb2f3846e, 0x648b1eaf, 0x19bdf0ca, + 0xa02369b9, 0x655abb50, 0x40685a32, 0x3c2ab4b3, 0x319ee9d5, 0xc021b8f7, + 0x9b540b19, 0x875fa099, 0x95f7997e, 0x623d7da8, 0xf837889a, 0x97e32d77, + 0x11ed935f, 0x16681281, 0x0e358829, 0xc7e61fd6, 0x96dedfa1, 0x7858ba99, + 0x57f584a5, 0x1b227263, 0x9b83c3ff, 0x1ac24696, 0xcdb30aeb, 0x532e3054, + 0x8fd948e4, 0x6dbc3128, 0x58ebf2ef, 0x34c6ffea, 0xfe28ed61, 0xee7c3c73, + 0x5d4a14d9, 0xe864b7e3, 0x42105d14, 0x203e13e0, 0x45eee2b6, 0xa3aaabea, + 0xdb6c4f15, 0xfacb4fd0, 0xc742f442, 0xef6abbb5, 0x654f3b1d, 0x41cd2105, + 0xd81e799e, 0x86854dc7, 0xe44b476a, 0x3d816250, 0xcf62a1f2, 0x5b8d2646, + 0xfc8883a0, 0xc1c7b6a3, 0x7f1524c3, 0x69cb7492, 0x47848a0b, 0x5692b285, + 0x095bbf00, 0xad19489d, 0x1462b174, 0x23820e00, 0x58428d2a, 0x0c55f5ea, + 0x1dadf43e, 0x233f7061, 0x3372f092, 0x8d937e41, 0xd65fecf1, 0x6c223bdb, + 0x7cde3759, 0xcbee7460, 0x4085f2a7, 0xce77326e, 0xa6078084, 0x19f8509e, + 0xe8efd855, 0x61d99735, 0xa969a7aa, 0xc50c06c2, 0x5a04abfc, 0x800bcadc, + 0x9e447a2e, 0xc3453484, 0xfdd56705, 0x0e1e9ec9, 0xdb73dbd3, 0x105588cd, + 0x675fda79, 0xe3674340, 0xc5c43465, 0x713e38d8, 0x3d28f89e, 0xf16dff20, + 0x153e21e7, 0x8fb03d4a, 0xe6e39f2b, 0xdb83adf7 + }, + { + 0xe93d5a68, 0x948140f7, 0xf64c261c, 0x94692934, 0x411520f7, 0x7602d4f7, + 0xbcf46b2e, 0xd4a20068, 0xd4082471, 0x3320f46a, 0x43b7d4b7, 0x500061af, + 0x1e39f62e, 0x97244546, 0x14214f74, 0xbf8b8840, 0x4d95fc1d, 0x96b591af, + 0x70f4ddd3, 0x66a02f45, 0xbfbc09ec, 0x03bd9785, 0x7fac6dd0, 0x31cb8504, + 0x96eb27b3, 0x55fd3941, 0xda2547e6, 0xabca0a9a, 0x28507825, 0x530429f4, + 0x0a2c86da, 0xe9b66dfb, 0x68dc1462, 0xd7486900, 0x680ec0a4, 0x27a18dee, + 0x4f3ffea2, 0xe887ad8c, 0xb58ce006, 0x7af4d6b6, 0xaace1e7c, 0xd3375fec, + 0xce78a399, 0x406b2a42, 0x20fe9e35, 0xd9f385b9, 0xee39d7ab, 0x3b124e8b, + 0x1dc9faf7, 0x4b6d1856, 0x26a36631, 0xeae397b2, 0x3a6efa74, 0xdd5b4332, + 0x6841e7f7, 0xca7820fb, 0xfb0af54e, 0xd8feb397, 0x454056ac, 0xba489527, + 0x55533a3a, 0x20838d87, 0xfe6ba9b7, 0xd096954b, 0x55a867bc, 0xa1159a58, + 0xcca92963, 0x99e1db33, 0xa62a4a56, 0x3f3125f9, 0x5ef47e1c, 0x9029317c, + 0xfdf8e802, 0x04272f70, 0x80bb155c, 0x05282ce3, 0x95c11548, 0xe4c66d22, + 0x48c1133f, 0xc70f86dc, 0x07f9c9ee, 0x41041f0f, 0x404779a4, 0x5d886e17, + 0x325f51eb, 0xd59bc0d1, 0xf2bcc18f, 0x41113564, 0x257b7834, 0x602a9c60, + 0xdff8e8a3, 0x1f636c1b, 0x0e12b4c2, 0x02e1329e, 0xaf664fd1, 0xcad18115, + 0x6b2395e0, 0x333e92e1, 0x3b240b62, 0xeebeb922, 0x85b2a20e, 0xe6ba0d99, + 0xde720c8c, 0x2da2f728, 0xd0127845, 0x95b794fd, 0x647d0862, 0xe7ccf5f0, + 0x5449a36f, 0x877d48fa, 0xc39dfd27, 0xf33e8d1e, 0x0a476341, 0x992eff74, + 0x3a6f6eab, 0xf4f8fd37, 0xa812dc60, 0xa1ebddf8, 0x991be14c, 0xdb6e6b0d, + 0xc67b5510, 0x6d672c37, 0x2765d43b, 0xdcd0e804, 0xf1290dc7, 0xcc00ffa3, + 0xb5390f92, 0x690fed0b, 0x667b9ffb, 0xcedb7d9c, 0xa091cf0b, 0xd9155ea3, + 0xbb132f88, 0x515bad24, 0x7b9479bf, 0x763bd6eb, 0x37392eb3, 0xcc115979, + 0x8026e297, 0xf42e312d, 0x6842ada7, 0xc66a2b3b, 0x12754ccc, 0x782ef11c, + 0x6a124237, 0xb79251e7, 0x06a1bbe6, 0x4bfb6350, 0x1a6b1018, 0x11caedfa, + 0x3d25bdd8, 0xe2e1c3c9, 0x44421659, 0x0a121386, 0xd90cec6e, 0xd5abea2a, + 0x64af674e, 0xda86a85f, 0xbebfe988, 0x64e4c3fe, 0x9dbc8057, 0xf0f7c086, + 0x60787bf8, 0x6003604d, 0xd1fd8346, 0xf6381fb0, 0x7745ae04, 0xd736fccc, + 0x83426b33, 0xf01eab71, 0xb0804187, 0x3c005e5f, 0x77a057be, 0xbde8ae24, + 0x55464299, 0xbf582e61, 0x4e58f48f, 0xf2ddfda2, 0xf474ef38, 0x8789bdc2, + 0x5366f9c3, 0xc8b38e74, 0xb475f255, 0x46fcd9b9, 0x7aeb2661, 0x8b1ddf84, + 0x846a0e79, 0x915f95e2, 0x466e598e, 0x20b45770, 0x8cd55591, 0xc902de4c, + 0xb90bace1, 0xbb8205d0, 0x11a86248, 0x7574a99e, 0xb77f19b6, 0xe0a9dc09, + 0x662d09a1, 0xc4324633, 0xe85a1f02, 0x09f0be8c, 0x4a99a025, 0x1d6efe10, + 0x1ab93d1d, 0x0ba5a4df, 0xa186f20f, 0x2868f169, 0xdcb7da83, 0x573906fe, + 0xa1e2ce9b, 0x4fcd7f52, 0x50115e01, 0xa70683fa, 0xa002b5c4, 0x0de6d027, + 0x9af88c27, 0x773f8641, 0xc3604c06, 0x61a806b5, 0xf0177a28, 0xc0f586e0, + 0x006058aa, 0x30dc7d62, 0x11e69ed7, 0x2338ea63, 0x53c2dd94, 0xc2c21634, + 0xbbcbee56, 0x90bcb6de, 0xebfc7da1, 0xce591d76, 0x6f05e409, 0x4b7c0188, + 0x39720a3d, 0x7c927c24, 0x86e3725f, 0x724d9db9, 0x1ac15bb4, 0xd39eb8fc, + 0xed545578, 0x08fca5b5, 0xd83d7cd3, 0x4dad0fc4, 0x1e50ef5e, 0xb161e6f8, + 0xa28514d9, 0x6c51133c, 0x6fd5c7e7, 0x56e14ec4, 0x362abfce, 0xddc6c837, + 0xd79a3234, 0x92638212, 0x670efa8e, 0x406000e0 + }, + { + 0x3a39ce37, 0xd3faf5cf, 0xabc27737, 0x5ac52d1b, 0x5cb0679e, 0x4fa33742, + 0xd3822740, 0x99bc9bbe, 0xd5118e9d, 0xbf0f7315, 0xd62d1c7e, 0xc700c47b, + 0xb78c1b6b, 0x21a19045, 0xb26eb1be, 0x6a366eb4, 0x5748ab2f, 0xbc946e79, + 0xc6a376d2, 0x6549c2c8, 0x530ff8ee, 0x468dde7d, 0xd5730a1d, 0x4cd04dc6, + 0x2939bbdb, 0xa9ba4650, 0xac9526e8, 0xbe5ee304, 0xa1fad5f0, 0x6a2d519a, + 0x63ef8ce2, 0x9a86ee22, 0xc089c2b8, 0x43242ef6, 0xa51e03aa, 0x9cf2d0a4, + 0x83c061ba, 0x9be96a4d, 0x8fe51550, 0xba645bd6, 0x2826a2f9, 0xa73a3ae1, + 0x4ba99586, 0xef5562e9, 0xc72fefd3, 0xf752f7da, 0x3f046f69, 0x77fa0a59, + 0x80e4a915, 0x87b08601, 0x9b09e6ad, 0x3b3ee593, 0xe990fd5a, 0x9e34d797, + 0x2cf0b7d9, 0x022b8b51, 0x96d5ac3a, 0x017da67d, 0xd1cf3ed6, 0x7c7d2d28, + 0x1f9f25cf, 0xadf2b89b, 0x5ad6b472, 0x5a88f54c, 0xe029ac71, 0xe019a5e6, + 0x47b0acfd, 0xed93fa9b, 0xe8d3c48d, 0x283b57cc, 0xf8d56629, 0x79132e28, + 0x785f0191, 0xed756055, 0xf7960e44, 0xe3d35e8c, 0x15056dd4, 0x88f46dba, + 0x03a16125, 0x0564f0bd, 0xc3eb9e15, 0x3c9057a2, 0x97271aec, 0xa93a072a, + 0x1b3f6d9b, 0x1e6321f5, 0xf59c66fb, 0x26dcf319, 0x7533d928, 0xb155fdf5, + 0x03563482, 0x8aba3cbb, 0x28517711, 0xc20ad9f8, 0xabcc5167, 0xccad925f, + 0x4de81751, 0x3830dc8e, 0x379d5862, 0x9320f991, 0xea7a90c2, 0xfb3e7bce, + 0x5121ce64, 0x774fbe32, 0xa8b6e37e, 0xc3293d46, 0x48de5369, 0x6413e680, + 0xa2ae0810, 0xdd6db224, 0x69852dfd, 0x09072166, 0xb39a460a, 0x6445c0dd, + 0x586cdecf, 0x1c20c8ae, 0x5bbef7dd, 0x1b588d40, 0xccd2017f, 0x6bb4e3bb, + 0xdda26a7e, 0x3a59ff45, 0x3e350a44, 0xbcb4cdd5, 0x72eacea8, 0xfa6484bb, + 0x8d6612ae, 0xbf3c6f47, 0xd29be463, 0x542f5d9e, 0xaec2771b, 0xf64e6370, + 0x740e0d8d, 0xe75b1357, 0xf8721671, 0xaf537d5d, 0x4040cb08, 0x4eb4e2cc, + 0x34d2466a, 0x0115af84, 0xe1b00428, 0x95983a1d, 0x06b89fb4, 0xce6ea048, + 0x6f3f3b82, 0x3520ab82, 0x011a1d4b, 0x277227f8, 0x611560b1, 0xe7933fdc, + 0xbb3a792b, 0x344525bd, 0xa08839e1, 0x51ce794b, 0x2f32c9b7, 0xa01fbac9, + 0xe01cc87e, 0xbcc7d1f6, 0xcf0111c3, 0xa1e8aac7, 0x1a908749, 0xd44fbd9a, + 0xd0dadecb, 0xd50ada38, 0x0339c32a, 0xc6913667, 0x8df9317c, 0xe0b12b4f, + 0xf79e59b7, 0x43f5bb3a, 0xf2d519ff, 0x27d9459c, 0xbf97222c, 0x15e6fc2a, + 0x0f91fc71, 0x9b941525, 0xfae59361, 0xceb69ceb, 0xc2a86459, 0x12baa8d1, + 0xb6c1075e, 0xe3056a0c, 0x10d25065, 0xcb03a442, 0xe0ec6e0e, 0x1698db3b, + 0x4c98a0be, 0x3278e964, 0x9f1f9532, 0xe0d392df, 0xd3a0342b, 0x8971f21e, + 0x1b0a7441, 0x4ba3348c, 0xc5be7120, 0xc37632d8, 0xdf359f8d, 0x9b992f2e, + 0xe60b6f47, 0x0fe3f11d, 0xe54cda54, 0x1edad891, 0xce6279cf, 0xcd3e7e6f, + 0x1618b166, 0xfd2c1d05, 0x848fd2c5, 0xf6fb2299, 0xf523f357, 0xa6327623, + 0x93a83531, 0x56cccd02, 0xacf08162, 0x5a75ebb5, 0x6e163697, 0x88d273cc, + 0xde966292, 0x81b949d0, 0x4c50901b, 0x71c65614, 0xe6c6c7bd, 0x327a140a, + 0x45e1d006, 0xc3f27b9a, 0xc9aa53fd, 0x62a80f00, 0xbb25bfe2, 0x35bdd2f6, + 0x71126905, 0xb2040222, 0xb6cbcf7c, 0xcd769c2b, 0x53113ec0, 0x1640e3d3, + 0x38abbd60, 0x2547adf0, 0xba38209c, 0xf746ce76, 0x77afa1c5, 0x20756060, + 0x85cbfe4e, 0x8ae88dd8, 0x7aaaf9b0, 0x4cf9aa7e, 0x1948c25c, 0x02fb8a8c, + 0x01c36ae4, 0xd6ebe1f9, 0x90d4f869, 0xa65cdea0, 0x3f09252d, 0xc208e69f, + 0xb74e6132, 0xce77e25b, 0x578fdfe3, 0x3ac372e6 + } + }; + + #endregion + + private static readonly int Rounds = 16; + + /// + /// Initialize a new blowfish. + /// + /// The key to use. + /// Whether or not a sign confusion should be introduced during key init. This is needed for SE's implementation of blowfish. + public LegacyBlowfish(byte[] key) + { + foreach (var (i, keyFragment) in WrappingUInt32(key, this.p.Length)) + this.p[i] ^= keyFragment; + + uint l = 0, r = 0; + for (int i = 0; i < this.p.Length; i += 2) + (l, r) = (this.p[i], this.p[i + 1]) = Encrypt(l, r); + + for (int i = 0; i < this.s.GetLength(0); i++) + for (int j = 0; j < this.s.GetLength(1); j += 2) + (l, r) = (this.s[i, j], this.s[i, j + 1]) = Encrypt(l, r); + } + + public byte[] Encrypt(byte[] data) + { + var paddedLength = data.Length % 8 == 0 ? data.Length : data.Length + (8 - (data.Length % 8)); + var buffer = new byte[paddedLength]; + Buffer.BlockCopy(data, 0, buffer, 0, data.Length); + + for (int i = 0; i < paddedLength; i += 8) + { + var (l, r) = Encrypt(BitConverter.ToUInt32(buffer, i), BitConverter.ToUInt32(buffer, i + 4)); + CopyUInt32IntoArray(buffer, l, i); + CopyUInt32IntoArray(buffer, r, i + 4); + } + + return buffer; + } + + public void Decrypt(ref byte[] data) + { + for (int i = 0; i < data.Length; i += 8) + { + var (l, r) = Decrypt(BitConverter.ToUInt32(data, i), BitConverter.ToUInt32(data, i + 4)); + CopyUInt32IntoArray(data, l, i); + CopyUInt32IntoArray(data, r, i + 4); + } + } + + private static void CopyUInt32IntoArray(byte[] dest, uint val, int offset) + { + dest[offset] = (byte)(val & 0xFF); + dest[offset + 1] = (byte)((val >> 8) & 0xFF); + dest[offset + 2] = (byte)((val >> 16) & 0xFF); + dest[offset + 3] = (byte)((val >> 24) & 0xFF); + } + + private uint F(uint i) + { + return ((this.s[0, i >> 24] + + this.s[1, (i >> 16) & 0xFF]) + ^ this.s[2, (i >> 8) & 0xFF]) + + this.s[3, i & 0xFF]; + } + + private (uint, uint) Encrypt(uint l, uint r) + { + for (int i = 0; i < Rounds; i += 2) + { + l ^= this.p[i]; + r ^= F(l); + r ^= this.p[i + 1]; + l ^= F(r); + } + + return (r ^ this.p[17], l ^ this.p[16]); + } + + private (uint, uint) Decrypt(uint l, uint r) + { + for (int i = Rounds; i > 0; i -= 2) + { + l ^= this.p[i + 1]; + r ^= F(l); + r ^= this.p[i]; + l ^= F(r); + } + + return (r ^ this.p[0], l ^ this.p[1]); + } + + private static IEnumerable Cycle(IEnumerable source) + { + while (true) + foreach (TSource t in source) + yield return t; + } + + private IEnumerable<(int, uint)> WrappingUInt32(IEnumerable source, int count) + { + var enumerator = Cycle(source).GetEnumerator(); + + for (int i = 0; i < count; i++) + { + var n = 0u; + + for (var j = 0; j < 4 && enumerator.MoveNext(); j++) + { + n = (uint)((n << 8) | (sbyte)enumerator.Current); // NOTE(goat): THIS IS A BUG! SE's implementation wrongly uses signed numbers for this, so we need to as well. + } + + yield return (i, n); + } + } + } +} diff --git a/src/XIVLauncher2.Common/Encryption/Ticket.cs b/src/XIVLauncher2.Common/Encryption/Ticket.cs new file mode 100644 index 0000000..8d5b367 --- /dev/null +++ b/src/XIVLauncher2.Common/Encryption/Ticket.cs @@ -0,0 +1,122 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Serilog; +using XIVLauncher2.Common.Encryption.BlockCipher; +using XIVLauncher2.Common.PlatformAbstractions; +using XIVLauncher2.Common.Util; + +namespace XIVLauncher2.Common.Encryption; + +public class Ticket +{ + public string Text { get; } + public int Length { get; } + + private const string FUCKED_GARBAGE_ALPHABET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-_"; + + private Ticket(string text, int length) + { + this.Text = text; + this.Length = length; + } + + public static async Task Get(ISteam steam) + { + var ticketBytes = await steam.GetAuthSessionTicketAsync().ConfigureAwait(true); + + if (ticketBytes == null) + return null; + + return EncryptAuthSessionTicket(ticketBytes, steam.GetServerRealTime()); + } + + public static Ticket EncryptAuthSessionTicket(byte[] ticket, uint time) + { + time -= 5; + time -= time % 60; // Time should be rounded to nearest minute. + + var ticketString = BitConverter.ToString(ticket).Replace("-", "").ToLower(); + var rawTicketBytes = Encoding.ASCII.GetBytes(ticketString); + + var rawTicket = new byte[rawTicketBytes.Length + 1]; + Array.Copy(rawTicketBytes, rawTicket, rawTicketBytes.Length); + rawTicket[rawTicket.Length - 1] = 0; + + var blowfishKey = $"{time:x08}#un@e=x>"; + + using var memorySteam = new MemoryStream(); + using var binaryWriter = new BinaryWriter(memorySteam); + + /* REGULAR SUM + TICKET */ + ushort ticketSum = 0; + + foreach (byte b in rawTicket) + { + ticketSum += b; + } + + binaryWriter.Write(ticketSum); + binaryWriter.Write(rawTicket); + + /* GARBAGE */ + int castTicketSum = unchecked((short)ticketSum); + var seed = time ^ castTicketSum; + var rand = new CrtRand((uint)seed); + + var numRandomBytes = ((ulong)(rawTicket.Length + 9) & 0xFFFFFFFFFFFFFFF8) - 2 - (ulong)rawTicket.Length; + var garbage = new byte[numRandomBytes]; + + uint fuckedSum = BitConverter.ToUInt32(memorySteam.ToArray(), 0); + + for (var i = 0u; i < numRandomBytes; i++) + { + var randChar = FUCKED_GARBAGE_ALPHABET[(int)(fuckedSum + rand.Next()) & 0x3F]; + garbage[i] = (byte)randChar; + fuckedSum += randChar; + } + + binaryWriter.Write(garbage); + + memorySteam.Seek(0, SeekOrigin.Begin); + binaryWriter.Write(fuckedSum); + + Log.Verbose("[STEAM] time: {Time}, bfKey: {FishKey}, rawTicket.Length: {TicketLen}, ticketSum: {TicketSum}, fuckedSum: {FuckedSum}, seed: {Seed}, numRandomBytes: {NumRandomBytes}", time, + blowfishKey, rawTicket.Length, ticketSum, fuckedSum, seed, numRandomBytes); + + /* ENC + SPLIT */ + var finalBytes = memorySteam.ToArray(); + + var t = finalBytes[0]; + finalBytes[0] = finalBytes[1]; + finalBytes[1] = t; + + var keyBytes = Encoding.ASCII.GetBytes(blowfishKey); + + var blowfish = new Blowfish(keyBytes); + var ecb = new Ecb(blowfish); + + var encBytes = new byte[finalBytes.Length]; + Debug.Assert(encBytes.Length % 8 == 0); + + ecb.Encrypt(finalBytes, encBytes); + var encString = GameHelpers.ToMangledSeBase64(encBytes); + + const int SPLIT_SIZE = 300; + var parts = ChunksUpto(encString, SPLIT_SIZE).ToArray(); + + var finalString = string.Join(",", parts); + + return new Ticket(finalString, finalString.Length - (parts.Length - 1)); + } + + private static IEnumerable ChunksUpto(string str, int maxChunkSize) + { + for (var i = 0; i < str.Length; i += maxChunkSize) + yield return str.Substring(i, Math.Min(maxChunkSize, str.Length - i)); + } +} diff --git a/src/XIVLauncher2.Common/EnvironmentSettings.cs b/src/XIVLauncher2.Common/EnvironmentSettings.cs new file mode 100644 index 0000000..2114bc8 --- /dev/null +++ b/src/XIVLauncher2.Common/EnvironmentSettings.cs @@ -0,0 +1,14 @@ +namespace XIVLauncher2.Common +{ + public static class EnvironmentSettings + { + public static bool IsWine => CheckEnvBool("XL_WINEONLINUX"); + public static bool IsHardwareRendered => CheckEnvBool("XL_HWRENDER"); + public static bool IsDisableUpdates => CheckEnvBool("XL_NOAUTOUPDATE"); + public static bool IsPreRelease => CheckEnvBool("XL_PRERELEASE"); + public static bool IsNoRunas => CheckEnvBool("XL_NO_RUNAS"); + public static bool IsIgnoreSpaceRequirements => CheckEnvBool("XL_NO_SPACE_REQUIREMENTS"); + public static bool IsOpenSteamMinimal => CheckEnvBool("XL_OPEN_STEAM_MINIMAL"); + private static bool CheckEnvBool(string var) => bool.Parse(System.Environment.GetEnvironmentVariable(var) ?? "false"); + } +} diff --git a/src/XIVLauncher2.Common/ExistingProcess.cs b/src/XIVLauncher2.Common/ExistingProcess.cs new file mode 100644 index 0000000..61dbaca --- /dev/null +++ b/src/XIVLauncher2.Common/ExistingProcess.cs @@ -0,0 +1,28 @@ +using System; +using System.Diagnostics; +using System.Reflection; +using Microsoft.Win32.SafeHandles; + +namespace XIVLauncher2.Common; + +/// +/// Class allowing the creation of a Process object based on an already held handle. +/// +public class ExistingProcess : Process +{ + public ExistingProcess(IntPtr handle) + { + SetHandle(handle); + } + + private void SetHandle(IntPtr handle) + { + var baseType = GetType().BaseType; + if (baseType == null) + return; + + var setProcessHandleMethod = baseType.GetMethod("SetProcessHandle", + BindingFlags.NonPublic | BindingFlags.Instance); + setProcessHandleMethod?.Invoke(this, new object[] { new SafeProcessHandle(handle, true) }); + } +} diff --git a/src/XIVLauncher2.Common/Game/Exceptions/BinaryNotPresentException.cs b/src/XIVLauncher2.Common/Game/Exceptions/BinaryNotPresentException.cs new file mode 100644 index 0000000..48c09b6 --- /dev/null +++ b/src/XIVLauncher2.Common/Game/Exceptions/BinaryNotPresentException.cs @@ -0,0 +1,14 @@ +using System; + +namespace XIVLauncher2.Common.Game.Exceptions; + +public class BinaryNotPresentException : Exception +{ + public string Path { get; private set; } + + public BinaryNotPresentException(string path) + : base("Game binary was not found") + { + this.Path = path; + } +} diff --git a/src/XIVLauncher2.Common/Game/Exceptions/GameExitedException.cs b/src/XIVLauncher2.Common/Game/Exceptions/GameExitedException.cs new file mode 100644 index 0000000..3311613 --- /dev/null +++ b/src/XIVLauncher2.Common/Game/Exceptions/GameExitedException.cs @@ -0,0 +1,11 @@ +using System; + +namespace XIVLauncher2.Common.Game.Exceptions; + +public class GameExitedException : Exception +{ + public GameExitedException() + : base("Game exited prematurely.") + { + } +} diff --git a/src/XIVLauncher2.Common/Game/Exceptions/InvalidResponseException.cs b/src/XIVLauncher2.Common/Game/Exceptions/InvalidResponseException.cs new file mode 100644 index 0000000..9a2d754 --- /dev/null +++ b/src/XIVLauncher2.Common/Game/Exceptions/InvalidResponseException.cs @@ -0,0 +1,14 @@ +using System; + +namespace XIVLauncher2.Common.Game.Exceptions; + +public class InvalidResponseException : Exception +{ + public string Document { get; set; } + + public InvalidResponseException(string message, string document) + : base(message) + { + this.Document = document; + } +} diff --git a/src/XIVLauncher2.Common/Game/Exceptions/InvalidVersionFilesException.cs b/src/XIVLauncher2.Common/Game/Exceptions/InvalidVersionFilesException.cs new file mode 100644 index 0000000..37dadb0 --- /dev/null +++ b/src/XIVLauncher2.Common/Game/Exceptions/InvalidVersionFilesException.cs @@ -0,0 +1,11 @@ +using System; + +namespace XIVLauncher2.Common.Game.Exceptions; + +public class InvalidVersionFilesException : Exception +{ + public InvalidVersionFilesException() + : base("Version files are invalid.") + { + } +} diff --git a/src/XIVLauncher2.Common/Game/Exceptions/NoVersionReferenceException.cs b/src/XIVLauncher2.Common/Game/Exceptions/NoVersionReferenceException.cs new file mode 100644 index 0000000..e1dc011 --- /dev/null +++ b/src/XIVLauncher2.Common/Game/Exceptions/NoVersionReferenceException.cs @@ -0,0 +1,11 @@ +using System; + +namespace XIVLauncher2.Common.Game.Exceptions; + +public class NoVersionReferenceException : Exception +{ + public NoVersionReferenceException(Repository repo, string version) + : base($"No version reference found for {repo}({version})") + { + } +} diff --git a/src/XIVLauncher2.Common/Game/Exceptions/OauthLoginException.cs b/src/XIVLauncher2.Common/Game/Exceptions/OauthLoginException.cs new file mode 100644 index 0000000..90fbed6 --- /dev/null +++ b/src/XIVLauncher2.Common/Game/Exceptions/OauthLoginException.cs @@ -0,0 +1,33 @@ +using System; +using System.Text.RegularExpressions; +using Serilog; + +namespace XIVLauncher2.Common.Game.Exceptions; + +[Serializable] +public class OauthLoginException : Exception +{ + private static Regex errorMessageRegex = + new(@"window.external.user\(""login=auth,ng,err,(?.*)\""\);", RegexOptions.Compiled | RegexOptions.CultureInvariant); + + public string? OauthErrorMessage { get; private set; } + + public OauthLoginException(string document) + : base(GetErrorMessage(document) ?? "Unknown error") + { + this.OauthErrorMessage = GetErrorMessage(document); + } + + private static string? GetErrorMessage(string document) + { + var matches = errorMessageRegex.Matches(document); + + if (matches.Count is 0 or > 1) + { + Log.Error("Could not get login error\n{Doc}", document); + return null; + } + + return matches[0].Groups["errorMessage"].Value; + } +} diff --git a/src/XIVLauncher2.Common/Game/Exceptions/SteamException.cs b/src/XIVLauncher2.Common/Game/Exceptions/SteamException.cs new file mode 100644 index 0000000..4124bd1 --- /dev/null +++ b/src/XIVLauncher2.Common/Game/Exceptions/SteamException.cs @@ -0,0 +1,11 @@ +using System; + +namespace XIVLauncher2.Common.Game.Exceptions; + +public class SteamException : Exception +{ + public SteamException(string message, Exception innerException = null) + : base(message, innerException) + { + } +} diff --git a/src/XIVLauncher2.Common/Game/Exceptions/SteamLinkNeededException.cs b/src/XIVLauncher2.Common/Game/Exceptions/SteamLinkNeededException.cs new file mode 100644 index 0000000..7bfeb58 --- /dev/null +++ b/src/XIVLauncher2.Common/Game/Exceptions/SteamLinkNeededException.cs @@ -0,0 +1,11 @@ +using System; + +namespace XIVLauncher2.Common.Game.Exceptions; + +public class SteamLinkNeededException : Exception +{ + public SteamLinkNeededException() + : base("No steam account linked.") + { + } +} diff --git a/src/XIVLauncher2.Common/Game/Exceptions/SteamWrongAccountException.cs b/src/XIVLauncher2.Common/Game/Exceptions/SteamWrongAccountException.cs new file mode 100644 index 0000000..6712995 --- /dev/null +++ b/src/XIVLauncher2.Common/Game/Exceptions/SteamWrongAccountException.cs @@ -0,0 +1,11 @@ +using System; + +namespace XIVLauncher2.Common.Game.Exceptions; + +public class SteamWrongAccountException : Exception +{ + public SteamWrongAccountException(string chosenUserName, string imposedUserName) + : base($"Wrong username! chosen: {chosenUserName}, imposed: {imposedUserName}") + { + } +} diff --git a/src/XIVLauncher2.Common/Game/GateStatus.cs b/src/XIVLauncher2.Common/Game/GateStatus.cs new file mode 100644 index 0000000..1e3a2a0 --- /dev/null +++ b/src/XIVLauncher2.Common/Game/GateStatus.cs @@ -0,0 +1,16 @@ +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace XIVLauncher2.Common.Game; + +public class GateStatus +{ + [JsonPropertyName("status")] + public bool Status { get; set; } + + [JsonPropertyName("message")] + public List Message { get; set; } + + [JsonPropertyName("news")] + public List News { get; set; } +} diff --git a/src/XIVLauncher2.Common/Game/Headlines.cs b/src/XIVLauncher2.Common/Game/Headlines.cs new file mode 100644 index 0000000..3f4eb73 --- /dev/null +++ b/src/XIVLauncher2.Common/Game/Headlines.cs @@ -0,0 +1,67 @@ +using System; +using System.Globalization; +using System.Text; +using System.Threading.Tasks; +using System.Text.Json; +using System.Text.Json.Serialization; +using XIVLauncher2.Common.Util; + +namespace XIVLauncher2.Common.Game +{ + public partial class Headlines + { + [JsonPropertyName("news")] + public News[] News { get; set; } + + [JsonPropertyName("topics")] + public News[] Topics { get; set; } + + [JsonPropertyName("pinned")] + public News[] Pinned { get; set; } + + [JsonPropertyName("banner")] + public Banner[] Banner { get; set; } + } + + public class Banner + { + [JsonPropertyName("lsb_banner")] + public Uri LsbBanner { get; set; } + + [JsonPropertyName("link")] + public Uri Link { get; set; } + } + + public class News + { + [JsonPropertyName("date")] + public DateTimeOffset Date { get; set; } + + [JsonPropertyName("title")] + public string Title { get; set; } + + [JsonPropertyName("url")] + public string Url { get; set; } + + [JsonPropertyName("id")] + public string Id { get; set; } + + [JsonPropertyName("tag")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string Tag { get; set; } + } + + public partial class Headlines + { + public static async Task Get(Launcher game, ClientLanguage language, bool forceNa = false) + { + var unixTimestamp = ApiHelpers.GetUnixMillis(); + var langCode = language.GetLangCode(forceNa); + var url = $"https://frontier.ffxiv.com/news/headline.json?lang={langCode}&media=pcapp&_={unixTimestamp}"; + + var json = Encoding.UTF8.GetString(await game.DownloadAsLauncher(url, language, "application/json, text/plain, */*").ConfigureAwait(false)); + + return JsonSerializer.Deserialize(json); + } + } +} diff --git a/src/XIVLauncher2.Common/Game/IntegrityCheck.cs b/src/XIVLauncher2.Common/Game/IntegrityCheck.cs new file mode 100644 index 0000000..e15d0e7 --- /dev/null +++ b/src/XIVLauncher2.Common/Game/IntegrityCheck.cs @@ -0,0 +1,152 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net; +using System.Security.Cryptography; +using System.Text.Json; +using System.Threading.Tasks; + +namespace XIVLauncher2.Common.Game +{ + public static class IntegrityCheck + { + private const string INTEGRITY_CHECK_BASE_URL = "https://goatcorp.github.io/integrity/"; + + public class IntegrityCheckResult + { + public Dictionary Hashes { get; set; } + public string GameVersion { get; set; } + public string LastGameVersion { get; set; } + } + + public class IntegrityCheckProgress + { + public string CurrentFile { get; set; } + } + + public enum CompareResult + { + Valid, + Invalid, + ReferenceNotFound, + ReferenceFetchFailure, + } + + public static async Task<(CompareResult compareResult, string report, IntegrityCheckResult remoteIntegrity)> + CompareIntegrityAsync(IProgress progress, DirectoryInfo gamePath, bool onlyIndex = false) + { + IntegrityCheckResult remoteIntegrity; + + try + { + remoteIntegrity = DownloadIntegrityCheckForVersion(Repository.Ffxiv.GetVer(gamePath)); + } + catch (WebException e) + { + if (e.Response is HttpWebResponse resp && resp.StatusCode == HttpStatusCode.NotFound) + return (CompareResult.ReferenceNotFound, null, null); + return (CompareResult.ReferenceFetchFailure, null, null); + } + + var localIntegrity = await RunIntegrityCheckAsync(gamePath, progress, onlyIndex).ConfigureAwait(false); + + var report = ""; + var failed = false; + + foreach (var hashEntry in remoteIntegrity.Hashes) + { + if (onlyIndex && (!hashEntry.Key.EndsWith(".index", StringComparison.Ordinal) && !hashEntry.Key.EndsWith(".index2", StringComparison.Ordinal))) + continue; + + if (localIntegrity.Hashes.Any(h => h.Key == hashEntry.Key)) + { + if (localIntegrity.Hashes.First(h => h.Key == hashEntry.Key).Value != hashEntry.Value) + { + report += $"Mismatch: {hashEntry.Key}\n"; + failed = true; + } + } + else + { + report += $"Missing: {hashEntry.Key}\n"; + } + } + + return (failed ? CompareResult.Invalid : CompareResult.Valid, report, remoteIntegrity); + } + + private static IntegrityCheckResult DownloadIntegrityCheckForVersion(string gameVersion) + { + using (var client = new WebClient()) + { + return JsonSerializer.Deserialize( + client.DownloadString(INTEGRITY_CHECK_BASE_URL + gameVersion + ".json")); + } + } + + public static async Task RunIntegrityCheckAsync(DirectoryInfo gamePath, + IProgress progress, bool onlyIndex = false) + { + var hashes = new Dictionary(); + + using (var sha1 = SHA1.Create()) + { + CheckDirectory(gamePath, sha1, gamePath.FullName, ref hashes, progress, onlyIndex); + } + + return new IntegrityCheckResult + { + GameVersion = Repository.Ffxiv.GetVer(gamePath), + Hashes = hashes + }; + } + + private static void CheckDirectory(DirectoryInfo directory, SHA1 sha1, string rootDirectory, + ref Dictionary results, IProgress progress, bool onlyIndex = false) + { + foreach (var file in directory.GetFiles()) + { + var relativePath = file.FullName.Substring(rootDirectory.Length); + + // for unix compatibility with windows-generated integrity files. + relativePath = relativePath.Replace("/", "\\"); + + if (!relativePath.StartsWith("\\", StringComparison.Ordinal)) + relativePath = "\\" + relativePath; + + if (!relativePath.StartsWith("\\game", StringComparison.Ordinal)) + continue; + + if (onlyIndex && (!relativePath.EndsWith(".index", StringComparison.Ordinal) && !relativePath.EndsWith(".index2", StringComparison.Ordinal))) + continue; + + try + { + using (var stream = + new BufferedStream(file.Open(FileMode.Open, FileAccess.Read, FileShare.ReadWrite), 1200000)) + { + var hash = sha1.ComputeHash(stream); + + results.Add(relativePath, BitConverter.ToString(hash).Replace('-', ' ')); + + progress?.Report(new IntegrityCheckProgress + { + CurrentFile = relativePath + }); + } + } + catch (IOException) + { + // Ignore + } + } + + foreach (var dir in directory.GetDirectories()) + { + if (!dir.FullName.ToLower().Contains("shade")) //skip gshade directories. They just waste cpu + CheckDirectory(dir, sha1, rootDirectory, ref results, progress, onlyIndex); + } + } + } +} diff --git a/src/XIVLauncher2.Common/Game/Launcher.cs b/src/XIVLauncher2.Common/Game/Launcher.cs new file mode 100644 index 0000000..1068396 --- /dev/null +++ b/src/XIVLauncher2.Common/Game/Launcher.cs @@ -0,0 +1,702 @@ + + +#nullable enable + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Net; +using System.Net.Http; + +#if NET6_0_OR_GREATER && !WIN32 +using System.Net.Security; +#endif + +using System.Security.Cryptography; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using System.Text.Json; +using Serilog; +using XIVLauncher2.Common.Encryption; +using XIVLauncher2.Common.Game.Exceptions; +using XIVLauncher2.Common.Game.Patch.PatchList; +using XIVLauncher2.Common.PlatformAbstractions; +using XIVLauncher2.Common.Util; + +namespace XIVLauncher2.Common.Game; + +public class Launcher +{ + private readonly ISteam? steam; + private readonly byte[]? steamTicket; + private readonly IUniqueIdCache uniqueIdCache; + private readonly ISettings settings; + private readonly HttpClient client; + private readonly string frontierUrlTemplate; + + private const string FALLBACK_FRONTIER_URL_TEMPLATE = "https://launcher.finalfantasyxiv.com/v620/index.html?rc_lang={0}&time={1}"; + + public Launcher(ISteam? steam, IUniqueIdCache uniqueIdCache, ISettings settings, string? frontierUrl = null) + { + this.steam = steam; + this.uniqueIdCache = uniqueIdCache; + this.settings = settings; + this.frontierUrlTemplate = + string.IsNullOrWhiteSpace(frontierUrl) ? FALLBACK_FRONTIER_URL_TEMPLATE : frontierUrl; + + ServicePointManager.Expect100Continue = false; + +#if NET6_0_OR_GREATER && !WIN32 + var sslOptions = new SslClientAuthenticationOptions() + { + CipherSuitesPolicy = new CipherSuitesPolicy(new[] { TlsCipherSuite.TLS_DHE_RSA_WITH_AES_256_GCM_SHA384 }) + }; + + var handler = new SocketsHttpHandler + { + UseCookies = false, + SslOptions = sslOptions, + }; +#else + var handler = new HttpClientHandler + { + UseCookies = false, + }; +#endif + + this.client = new HttpClient(handler); + } + + public Launcher(byte[] steamTicket, IUniqueIdCache uniqueIdCache, ISettings settings, string? frontierUrl = null) + : this(steam: null, uniqueIdCache, settings, frontierUrl) + { + this.steamTicket = steamTicket; + } + + // The user agent for frontier pages. {0} has to be replaced by a unique computer id and its checksum + private const string USER_AGENT_TEMPLATE = "SQEXAuthor/2.0.0(Windows 6.2; ja-jp; {0})"; + private readonly string _userAgent = GenerateUserAgent(); + + private static readonly string[] FilesToHash = + { + "ffxivboot.exe", + "ffxivboot64.exe", + "ffxivlauncher.exe", + "ffxivlauncher64.exe", + "ffxivupdater.exe", + "ffxivupdater64.exe" + }; + + public enum LoginState + { + Unknown, + Ok, + NeedsPatchGame, + NeedsPatchBoot, + NoService, + NoTerms + } + + public class LoginResult + { + public LoginState State { get; set; } + public PatchListEntry[] PendingPatches { get; set; } + public OauthLoginResult OauthLogin { get; set; } + public string UniqueId { get; set; } + } + + public async Task Login(string userName, string password, string otp, bool isSteam, bool useCache, DirectoryInfo gamePath, bool forceBaseVersion, bool isFreeTrial) + { + string uid; + PatchListEntry[] pendingPatches = null; + + OauthLoginResult oauthLoginResult; + + LoginState loginState; + + Log.Information("XivGame::Login(steamServiceAccount:{IsSteam}, cache:{UseCache})", isSteam, useCache); + + Ticket? steamTicket = null; + + if (isSteam) + { + if (this.steamTicket != null) + { + steamTicket = Ticket.EncryptAuthSessionTicket(this.steamTicket, (uint) DateTimeOffset.UtcNow.ToUnixTimeSeconds()); + Log.Information("Using predefined steam ticket"); + } + else + { + Debug.Assert(this.steam != null); + + try + { + if (!this.steam.IsValid) + { + this.steam.Initialize(isFreeTrial ? Constants.STEAM_FT_APP_ID : Constants.STEAM_APP_ID); + } + } + catch (Exception ex) + { + Log.Error(ex, "Could not initialize Steam"); + throw new SteamException("SteamAPI_Init() failed.", ex); + } + + if (!this.steam.IsValid) + { + throw new SteamException("Steam did not initialize successfully. Please restart Steam and try again."); + } + + if (!this.steam.BLoggedOn) + { + throw new SteamException("Not logged into Steam, or Steam is running in offline mode. Please log in and try again."); + } + + try + { + steamTicket = await Ticket.Get(steam).ConfigureAwait(true); + } + catch (Exception ex) + { + throw new SteamException("Could not request auth ticket.", ex); + } + } + + if (steamTicket == null) + { + throw new SteamException("Steam auth ticket was null."); + } + } + + if (!useCache || !this.uniqueIdCache.TryGet(userName, out var cached)) + { + oauthLoginResult = await OauthLogin(userName, password, otp, isFreeTrial, isSteam, 3, steamTicket); + + Log.Information($"OAuth login successful - playable:{oauthLoginResult.Playable} terms:{oauthLoginResult.TermsAccepted} region:{oauthLoginResult.Region} expack:{oauthLoginResult.MaxExpansion}"); + + if (!oauthLoginResult.Playable) + { + return new LoginResult + { + State = LoginState.NoService + }; + } + + if (!oauthLoginResult.TermsAccepted) + { + return new LoginResult + { + State = LoginState.NoTerms + }; + } + + (uid, loginState, pendingPatches) = await RegisterSession(oauthLoginResult, gamePath, forceBaseVersion); + + if (useCache) + this.uniqueIdCache.Add(userName, uid, oauthLoginResult.Region, oauthLoginResult.MaxExpansion); + } + else + { + Log.Information("Cached UID found, using instead"); + uid = cached.UniqueId; + loginState = LoginState.Ok; + + oauthLoginResult = new OauthLoginResult + { + Playable = true, + Region = cached.Region, + TermsAccepted = true, + MaxExpansion = cached.MaxExpansion + }; + } + + return new LoginResult + { + PendingPatches = pendingPatches, + OauthLogin = oauthLoginResult, + State = loginState, + UniqueId = uid + }; + } + + public Process? LaunchGame(IGameRunner runner, string sessionId, int region, int expansionLevel, + bool isSteamServiceAccount, string additionalArguments, + DirectoryInfo gamePath, bool isDx11, ClientLanguage language, + bool encryptArguments, DpiAwareness dpiAwareness) + { + Log.Information( + $"XivGame::LaunchGame(steamServiceAccount:{isSteamServiceAccount}, args:{additionalArguments})"); + + var exePath = Path.Combine(gamePath.FullName, "game", "ffxiv_dx11.exe"); + if (!isDx11) + exePath = Path.Combine(gamePath.FullName, "game", "ffxiv.exe"); + + var environment = new Dictionary(); + + var argumentBuilder = new ArgumentBuilder() + .Append("DEV.DataPathType", "1") + .Append("DEV.MaxEntitledExpansionID", expansionLevel.ToString()) + .Append("DEV.TestSID", sessionId) + .Append("DEV.UseSqPack", "1") + .Append("SYS.Region", region.ToString()) + .Append("language", ((int)language).ToString()) + .Append("resetConfig", "0") + .Append("ver", Repository.Ffxiv.GetVer(gamePath)); + + if (isSteamServiceAccount) + { + // These environment variable and arguments seems to be set when ffxivboot is started with "-issteam" (27.08.2019) + environment.Add("IS_FFXIV_LAUNCH_FROM_STEAM", "1"); + argumentBuilder.Append("IsSteam", "1"); + } + + // This is a bit of a hack; ideally additionalArguments would be a dictionary or some KeyValue structure + if (!string.IsNullOrEmpty(additionalArguments)) + { + var regex = new Regex(@"\s*(?[^\s=]+)\s*=\s*(?([^=]*$|[^=]*\s(?=[^\s=]+)))\s*", RegexOptions.Compiled); + foreach (Match match in regex.Matches(additionalArguments)) + argumentBuilder.Append(match.Groups["key"].Value, match.Groups["value"].Value.Trim()); + } + + if (!File.Exists(exePath)) + throw new BinaryNotPresentException(exePath); + + var workingDir = Path.Combine(gamePath.FullName, "game"); + + var arguments = encryptArguments + ? argumentBuilder.BuildEncrypted() + : argumentBuilder.Build(); + + return runner.Start(exePath, workingDir, arguments, environment, dpiAwareness); + } + + private static string GetVersionReport(DirectoryInfo gamePath, int exLevel, bool forceBaseVersion) + { + var verReport = $"{GetBootVersionHash(gamePath)}"; + + if (exLevel >= 1) + verReport += $"\nex1\t{(forceBaseVersion ? Constants.BASE_GAME_VERSION : Repository.Ex1.GetVer(gamePath))}"; + + if (exLevel >= 2) + verReport += $"\nex2\t{(forceBaseVersion ? Constants.BASE_GAME_VERSION : Repository.Ex2.GetVer(gamePath))}"; + + if (exLevel >= 3) + verReport += $"\nex3\t{(forceBaseVersion ? Constants.BASE_GAME_VERSION : Repository.Ex3.GetVer(gamePath))}"; + + if (exLevel >= 4) + verReport += $"\nex4\t{(forceBaseVersion ? Constants.BASE_GAME_VERSION : Repository.Ex4.GetVer(gamePath))}"; + + return verReport; + } + + /// + /// Check ver & bck files for sanity. + /// + /// + /// + private static void EnsureVersionSanity(DirectoryInfo gamePath, int exLevel) + { + var failed = IsBadVersionSanity(gamePath, Repository.Ffxiv); + failed |= IsBadVersionSanity(gamePath, Repository.Ffxiv, true); + + if (exLevel >= 1) + { + failed |= IsBadVersionSanity(gamePath, Repository.Ex1); + failed |= IsBadVersionSanity(gamePath, Repository.Ex1, true); + } + + if (exLevel >= 2) + { + failed |= IsBadVersionSanity(gamePath, Repository.Ex2); + failed |= IsBadVersionSanity(gamePath, Repository.Ex2, true); + } + + if (exLevel >= 3) + { + failed |= IsBadVersionSanity(gamePath, Repository.Ex3); + failed |= IsBadVersionSanity(gamePath, Repository.Ex3, true); + } + + if (exLevel >= 4) + { + failed |= IsBadVersionSanity(gamePath, Repository.Ex4); + failed |= IsBadVersionSanity(gamePath, Repository.Ex4, true); + } + + if (failed) + throw new InvalidVersionFilesException(); + } + + private static bool IsBadVersionSanity(DirectoryInfo gamePath, Repository repo, bool isBck = false) + { + var text = repo.GetVer(gamePath, isBck); + + var nullOrWhitespace = string.IsNullOrWhiteSpace(text); + var containsNewline = text.Contains("\n"); + var allNullBytes = Encoding.UTF8.GetBytes(text).All(x => x == 0x00); + + if (nullOrWhitespace || containsNewline || allNullBytes) + { + Log.Error("Sanity check failed for {repo}/{isBck}: {NullOrWhitespace}, {ContainsNewline}, {AllNullBytes}", repo, isBck, nullOrWhitespace, containsNewline, allNullBytes); + return true; + } + + return false; + } + + /// + /// Calculate the hash that is sent to patch-gamever for version verification/tamper protection. + /// This same hash is also sent in lobby, but for ffxiv.exe and ffxiv_dx11.exe. + /// + /// String of hashed EXE files. + private static string GetBootVersionHash(DirectoryInfo gamePath) + { + var result = Repository.Boot.GetVer(gamePath) + "="; + + for (var i = 0; i < FilesToHash.Length; i++) + { + result += + $"{FilesToHash[i]}/{GetFileHash(Path.Combine(gamePath.FullName, "boot", FilesToHash[i]))}"; + + if (i != FilesToHash.Length - 1) + result += ","; + } + + return result; + } + + public async Task CheckBootVersion(DirectoryInfo gamePath) + { + var request = new HttpRequestMessage(HttpMethod.Get, + $"http://patch-bootver.ffxiv.com/http/win32/ffxivneo_release_boot/{Repository.Boot.GetVer(gamePath)}/?time=" + + GetLauncherFormattedTimeLongRounded()); + + request.Headers.AddWithoutValidation("User-Agent", Constants.PatcherUserAgent); + request.Headers.AddWithoutValidation("Host", "patch-bootver.ffxiv.com"); + + var resp = await this.client.SendAsync(request); + var text = await resp.Content.ReadAsStringAsync(); + + if (text == string.Empty) + return null; + + Log.Verbose("Boot patching is needed... List:\n{PatchList}", resp); + + try + { + return PatchListParser.Parse(text); + } + catch (PatchListParseException ex) + { + Log.Information("Patch list:\n{PatchList}", ex.List); + throw; + } + } + + private async Task<(string Uid, LoginState result, PatchListEntry[] PendingGamePatches)> RegisterSession(OauthLoginResult loginResult, DirectoryInfo gamePath, bool forceBaseVersion) + { + var request = new HttpRequestMessage(HttpMethod.Post, + $"https://patch-gamever.ffxiv.com/http/win32/ffxivneo_release_game/{(forceBaseVersion ? Constants.BASE_GAME_VERSION : Repository.Ffxiv.GetVer(gamePath))}/{loginResult.SessionId}"); + + request.Headers.AddWithoutValidation("X-Hash-Check", "enabled"); + request.Headers.AddWithoutValidation("User-Agent", Constants.PatcherUserAgent); + + if (!forceBaseVersion) + EnsureVersionSanity(gamePath, loginResult.MaxExpansion); + request.Content = new StringContent(GetVersionReport(gamePath, loginResult.MaxExpansion, forceBaseVersion)); + + var resp = await this.client.SendAsync(request); + var text = await resp.Content.ReadAsStringAsync(); + + // Conflict indicates that boot needs to update, we do not get a patch list or a unique ID to download patches with in this case + if (resp.StatusCode == HttpStatusCode.Conflict) + return (null, LoginState.NeedsPatchBoot, null); + + if (resp.StatusCode == HttpStatusCode.Gone) + throw new InvalidResponseException("The server indicated that the version requested is no longer being serviced or not present.", text); + + if (!resp.Headers.TryGetValues("X-Patch-Unique-Id", out var uidVals)) + throw new InvalidResponseException($"Could not get X-Patch-Unique-Id. ({resp.StatusCode})", text); + + var uid = uidVals.First(); + + if (string.IsNullOrEmpty(text)) + return (uid, LoginState.Ok, null); + + Log.Verbose("Game Patching is needed... List:\n{PatchList}", text); + + var pendingPatches = PatchListParser.Parse(text); + return (uid, LoginState.NeedsPatchGame, pendingPatches); + } + + public async Task GenPatchToken(string patchUrl, string uniqueId) + { + // Yes, Square does use HTTP for this and sends tokens in headers. IT'S NOT MY FAULT. + var request = new HttpRequestMessage(HttpMethod.Post, "http://patch-gamever.ffxiv.com/gen_token"); + + request.Headers.AddWithoutValidation("Connection", "Keep-Alive"); + request.Headers.AddWithoutValidation("X-Patch-Unique-Id", uniqueId); + request.Headers.AddWithoutValidation("User-Agent", Constants.PatcherUserAgent); + + request.Content = new StringContent(patchUrl); + + var resp = await this.client.SendAsync(request); + resp.EnsureSuccessStatusCode(); + + return await resp.Content.ReadAsStringAsync(); + } + + private async Task<(string Stored, string? SteamLinkedId)> GetOauthTop(string url, bool isSteam) + { + // This is needed to be able to access the login site correctly + var request = new HttpRequestMessage(HttpMethod.Get, url); + request.Headers.AddWithoutValidation("Accept", "image/gif, image/jpeg, image/pjpeg, application/x-ms-application, application/xaml+xml, application/x-ms-xbap, */*"); + request.Headers.AddWithoutValidation("Referer", GenerateFrontierReferer(this.settings.ClientLanguage.GetValueOrDefault(ClientLanguage.English))); + request.Headers.AddWithoutValidation("Accept-Encoding", "gzip, deflate"); + request.Headers.AddWithoutValidation("Accept-Language", this.settings.AcceptLanguage); + request.Headers.AddWithoutValidation("User-Agent", _userAgent); + request.Headers.AddWithoutValidation("Connection", "Keep-Alive"); + request.Headers.AddWithoutValidation("Cookie", "_rsid=\"\""); + + var reply = await this.client.SendAsync(request); + + var text = await reply.Content.ReadAsStringAsync(); + + if (text.Contains("window.external.user(\"restartup\");")) + { + if (isSteam) + throw new SteamLinkNeededException(); + + throw new InvalidResponseException("restartup, but not isSteam?", text); + } + + var storedRegex = new Regex(@"\t<\s*input .* name=""_STORED_"" value=""(?.*)"">"); + var matches = storedRegex.Matches(text); + + if (matches.Count == 0) + { + Log.Error(text); + throw new InvalidResponseException("Could not get STORED.", text); + } + + string? steamUsername = null; + + if (isSteam) + { + var steamRegex = new Regex(@".*)""\/>"); + var steamMatches = steamRegex.Matches(text); + + if (steamMatches.Count == 0) + { + Log.Error(text); + throw new InvalidResponseException("Could not get steam username.", text); + } + + steamUsername = steamMatches[0].Groups["sqexid"].Value; + } + + return (matches[0].Groups["stored"].Value, steamUsername); + } + + public class OauthLoginResult + { + public string SessionId { get; set; } + public int Region { get; set; } + public bool TermsAccepted { get; set; } + public bool Playable { get; set; } + public int MaxExpansion { get; set; } + } + + private static string GetOauthTopUrl(int region, bool isFreeTrial, bool isSteam, Ticket steamTicket) + { + var url = + $"https://ffxiv-login.square-enix.com/oauth/ffxivarr/login/top?lng=en&rgn={region}&isft={(isFreeTrial ? "1" : "0")}&cssmode=1&isnew=1&launchver=3"; + + if (isSteam) + { + url += "&issteam=1"; + + url += $"&session_ticket={steamTicket.Text}"; + url += $"&ticket_size={steamTicket.Length}"; + } + + return url; + } + + private async Task OauthLogin(string userName, string password, string otp, bool isFreeTrial, bool isSteam, int region, Ticket? steamTicket) + { + if (isSteam && steamTicket == null) + throw new ArgumentNullException(nameof(steamTicket), "isSteam, but steamTicket == null"); + + var topUrl = GetOauthTopUrl(region, isFreeTrial, isSteam, steamTicket); + var topResult = await GetOauthTop(topUrl, isSteam); + + var request = new HttpRequestMessage(HttpMethod.Post, + "https://ffxiv-login.square-enix.com/oauth/ffxivarr/login/login.send"); + + request.Headers.AddWithoutValidation("Accept", "image/gif, image/jpeg, image/pjpeg, application/x-ms-application, application/xaml+xml, application/x-ms-xbap, */*"); + request.Headers.AddWithoutValidation("Referer", topUrl); + request.Headers.AddWithoutValidation("Accept-Language", this.settings.AcceptLanguage); + request.Headers.AddWithoutValidation("User-Agent", _userAgent); + //request.Headers.AddWithoutValidation("Content-Type", "application/x-www-form-urlencoded"); + request.Headers.AddWithoutValidation("Accept-Encoding", "gzip, deflate"); + request.Headers.AddWithoutValidation("Host", "ffxiv-login.square-enix.com"); + request.Headers.AddWithoutValidation("Connection", "Keep-Alive"); + request.Headers.AddWithoutValidation("Cache-Control", "no-cache"); + request.Headers.AddWithoutValidation("Cookie", "_rsid=\"\""); + + if (isSteam) + { + if (!String.Equals(userName, topResult.SteamLinkedId, StringComparison.OrdinalIgnoreCase)) + throw new SteamWrongAccountException(userName, topResult.SteamLinkedId); + + userName = topResult.SteamLinkedId; + } + + request.Content = new FormUrlEncodedContent( + new Dictionary() + { + { "_STORED_", topResult.Stored }, + { "sqexid", userName }, + { "password", password }, + { "otppw", otp }, + // { "saveid", "1" } // NOTE(goat): This adds a Set-Cookie with a filled-out _rsid value in the login response. + }); + + var response = await this.client.SendAsync(request); + + var reply = await response.Content.ReadAsStringAsync(); + + var regex = new Regex(@"window.external.user\(""login=auth,ok,(?.*)\);"); + var matches = regex.Matches(reply); + + if (matches.Count == 0) + throw new OauthLoginException(reply); + + var launchParams = matches[0].Groups["launchParams"].Value.Split(','); + + return new OauthLoginResult + { + SessionId = launchParams[1], + Region = int.Parse(launchParams[5]), + TermsAccepted = launchParams[3] != "0", + Playable = launchParams[9] != "0", + MaxExpansion = int.Parse(launchParams[13]) + }; + } + + private static string GetFileHash(string file) + { + var bytes = File.ReadAllBytes(file); + + var hash = SHA1.Create().ComputeHash(bytes); + var hashstring = string.Join("", hash.Select(b => b.ToString("x2")).ToArray()); + + var length = new FileInfo(file).Length; + + return length + "/" + hashstring; + } + + public async Task GetGateStatus(ClientLanguage language) + { + try + { + var reply = Encoding.UTF8.GetString( + await DownloadAsLauncher( + $"https://frontier.ffxiv.com/worldStatus/gate_status.json?lang={language.GetLangCode()}&_={ApiHelpers.GetUnixMillis()}", language).ConfigureAwait(true)); + + return JsonSerializer.Deserialize(reply); + } + catch (Exception exc) + { + throw new Exception("Could not get gate status", exc); + } + } + + public async Task GetLoginStatus() + { + try + { + var reply = Encoding.UTF8.GetString( + await DownloadAsLauncher( + $"https://frontier.ffxiv.com/worldStatus/login_status.json?_={ApiHelpers.GetUnixMillis()}", ClientLanguage.English).ConfigureAwait(true)); + + return Convert.ToBoolean(int.Parse(reply[10].ToString())); + } + catch (Exception exc) + { + throw new Exception("Could not get gate status", exc); + } + } + + private static string MakeComputerId() + { + var hashString = Environment.MachineName + Environment.UserName + Environment.OSVersion + + Environment.ProcessorCount; + + using var sha1 = HashAlgorithm.Create("SHA1"); + + var bytes = new byte[5]; + + Array.Copy(sha1.ComputeHash(Encoding.Unicode.GetBytes(hashString)), 0, bytes, 1, 4); + + var checkSum = (byte) -(bytes[1] + bytes[2] + bytes[3] + bytes[4]); + bytes[0] = checkSum; + + return BitConverter.ToString(bytes).Replace("-", "").ToLower(); + } + + public async Task DownloadAsLauncher(string url, ClientLanguage language, string contentType = "") + { + var request = new HttpRequestMessage(HttpMethod.Get, url); + + request.Headers.AddWithoutValidation("User-Agent", _userAgent); + + if (!string.IsNullOrEmpty(contentType)) + { + request.Headers.AddWithoutValidation("Accept", contentType); + } + + request.Headers.AddWithoutValidation("Accept-Encoding", "gzip, deflate"); + request.Headers.AddWithoutValidation("Accept-Language", this.settings.AcceptLanguage); + + request.Headers.AddWithoutValidation("Origin", "https://launcher.finalfantasyxiv.com"); + + request.Headers.AddWithoutValidation("Referer", GenerateFrontierReferer(language)); + + var resp = await this.client.SendAsync(request); + return await resp.Content.ReadAsByteArrayAsync(); + } + + private string GenerateFrontierReferer(ClientLanguage language) + { + var langCode = language.GetLangCode().Replace("-", "_"); + var formattedTime = GetLauncherFormattedTimeLong(); + + return string.Format(this.frontierUrlTemplate, langCode, formattedTime); + } + + // Used to be used for frontier top, they now use the un-rounded long timestamp + private static string GetLauncherFormattedTime() => DateTime.UtcNow.ToString("yyyy-MM-dd-HH"); + + private static string GetLauncherFormattedTimeLong() => DateTime.UtcNow.ToString("yyyy-MM-dd-HH-mm"); + + private static string GetLauncherFormattedTimeLongRounded() + { + var formatted = DateTime.UtcNow.ToString("yyyy-MM-dd-HH-mm", new CultureInfo("en-US")).ToCharArray(); + formatted[15] = '0'; + + return new string(formatted); + } + + private static string GenerateUserAgent() + { + return string.Format(USER_AGENT_TEMPLATE, MakeComputerId()); + } +} + +#nullable restore diff --git a/src/XIVLauncher2.Common/Game/Patch/Acquisition/AcquisitionMethod.cs b/src/XIVLauncher2.Common/Game/Patch/Acquisition/AcquisitionMethod.cs new file mode 100644 index 0000000..95926e5 --- /dev/null +++ b/src/XIVLauncher2.Common/Game/Patch/Acquisition/AcquisitionMethod.cs @@ -0,0 +1,17 @@ +namespace XIVLauncher2.Common.Game.Patch.Acquisition +{ + public enum AcquisitionMethod + { + [SettingsDescription(".NET", "Basic .NET downloads")] + NetDownloader, + + [SettingsDescription("Torrent (+ .NET)", "Torrent downloads, with .NET as a fallback")] + MonoTorrentNetFallback, + + [SettingsDescription("Torrent (+ Aria)", "Torrent downloads, with Aria as a fallback")] + MonoTorrentAriaFallback, + + [SettingsDescription("Aria2c", "Aria2c downloads (recommended)")] + Aria, + } +} diff --git a/src/XIVLauncher2.Common/Game/Patch/Acquisition/AcquisitionProgress.cs b/src/XIVLauncher2.Common/Game/Patch/Acquisition/AcquisitionProgress.cs new file mode 100644 index 0000000..440cbd1 --- /dev/null +++ b/src/XIVLauncher2.Common/Game/Patch/Acquisition/AcquisitionProgress.cs @@ -0,0 +1,8 @@ +namespace XIVLauncher2.Common.Game.Patch.Acquisition +{ + public class AcquisitionProgress + { + public long Progress { get; set; } + public long BytesPerSecondSpeed { get; set; } + } +} diff --git a/src/XIVLauncher2.Common/Game/Patch/Acquisition/AcquisitionResult.cs b/src/XIVLauncher2.Common/Game/Patch/Acquisition/AcquisitionResult.cs new file mode 100644 index 0000000..5ff2d15 --- /dev/null +++ b/src/XIVLauncher2.Common/Game/Patch/Acquisition/AcquisitionResult.cs @@ -0,0 +1,9 @@ +namespace XIVLauncher2.Common.Game.Patch.Acquisition +{ + public enum AcquisitionResult + { + Success, + Error, + Cancelled, + } +} diff --git a/src/XIVLauncher2.Common/Game/Patch/Acquisition/Aria/AriaHttpPatchAcquisition.cs b/src/XIVLauncher2.Common/Game/Patch/Acquisition/Aria/AriaHttpPatchAcquisition.cs new file mode 100644 index 0000000..611e46e --- /dev/null +++ b/src/XIVLauncher2.Common/Game/Patch/Acquisition/Aria/AriaHttpPatchAcquisition.cs @@ -0,0 +1,187 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Runtime.InteropServices; +using System.Security.Cryptography; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using AriaNet; +using Serilog; +using XIVLauncher2.Common.Util; + +namespace XIVLauncher2.Common.Game.Patch.Acquisition.Aria +{ + public class AriaHttpPatchAcquisition : PatchAcquisition + { + private static Process ariaProcess; + private static AriaManager manager; + private static long maxDownloadSpeed; + + public static async Task InitializeAsync(long maxDownloadSpeed, FileInfo logFile) + { + AriaHttpPatchAcquisition.maxDownloadSpeed = maxDownloadSpeed; + + if (ariaProcess == null || ariaProcess.HasExited) + { + // Kill stray aria2c-xl processes + var stray = Process.GetProcessesByName("aria2c-xl"); + + foreach (var process in stray) + { + try + { + process.Kill(); + } + catch (Exception ex) + { + Log.Error(ex, "[ARIA] Could not kill stray process."); + } + } + + // I don't really see the point of this, but aria complains if we don't provide a secret + var rng = new Random(); + var secret = BitConverter.ToString(MD5.Create().ComputeHash(Encoding.UTF8.GetBytes($"{rng.Next()}{rng.Next()}{rng.Next()}{rng.Next()}"))); + + var ariaPath = Path.Combine(Paths.ResourcesPath, "aria2c-xl.exe"); + + if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + ariaPath = "aria2c"; + } + + var ariaPort = PlatformHelpers.GetAvailablePort(); + var ariaHost = $"http://localhost:{ariaPort}/jsonrpc"; + + var ariaArgs = + $"--enable-rpc --rpc-secret={secret} --rpc-listen-port={ariaPort} --log=\"{logFile.FullName}\" --log-level=notice --max-connection-per-server=8 --auto-file-renaming=false --allow-overwrite=true"; + + Log.Verbose($"[ARIA] Aria process not there, creating from {ariaPath} {ariaArgs}..."); + + var startInfo = new ProcessStartInfo(ariaPath, ariaArgs) + { +#if !DEBUG + CreateNoWindow = true, + WindowStyle = ProcessWindowStyle.Hidden, +#endif + UseShellExecute = false, + }; + + ariaProcess = Process.Start(startInfo); + + Thread.Sleep(400); + + if (ariaProcess == null) + throw new Exception("ariaProcess was null."); + + if (ariaProcess.HasExited) + throw new Exception("ariaProcess has exited."); + + manager = new AriaManager(secret, ariaHost); + } + } + + public static async Task UnInitializeAsync() + { + if (ariaProcess is {HasExited: false}) + { + try + { + await manager.Shutdown(); + } + catch (Exception) + { + // ignored + } + + Thread.Sleep(1000); + + if (!ariaProcess.HasExited) + ariaProcess.Kill(); + } + } + + public override async Task StartDownloadAsync(string url, FileInfo outFile) + { + await manager.AddUri(new List() + { + url + }, new Dictionary() + { + {"user-agent", Constants.PatcherUserAgent}, + {"out", outFile.Name}, + {"dir", outFile.Directory.FullName}, + {"max-connection-per-server", "8"}, + {"max-tries", "100"}, + {"max-download-limit", maxDownloadSpeed.ToString()}, + {"auto-file-renaming", "false"}, + {"allow-overwrite", "true"}, + }).ContinueWith(t => + { + if (t.IsFaulted || t.IsCanceled) + { + Log.Error(t.Exception, $"[ARIA] Could not send download RPC for {url}"); + OnComplete(AcquisitionResult.Error); + return; + } + + var gid = t.Result; + + Log.Verbose($"[ARIA] GID# {gid} for {url}"); + + var _ = Task.Run(async () => + { + while (true) + { + try + { + var status = await manager.GetStatus(gid); + + if (status.Status == "complete") + { + Log.Verbose($"[ARIA] GID# {gid} for {url} SUCCESS"); + + OnComplete(AcquisitionResult.Success); + return; + } + + if (status.Status == "removed") + { + Log.Verbose($"[ARIA] GID# {gid} for {url} CANCEL"); + + OnComplete(AcquisitionResult.Cancelled); + return; + } + + if (status.Status == "error") + { + Log.Verbose($"[ARIA] GID# {gid} for {url} FAULTED"); + + OnComplete(AcquisitionResult.Error); + return; + } + + OnProgressChanged(new AcquisitionProgress + { + BytesPerSecondSpeed = long.Parse(status.DownloadSpeed), + Progress = long.Parse(status.CompletedLength), + }); + } + catch (Exception ex) + { + Log.Error(ex, $"[ARIA] Failed to get status for GID# {gid} ({url})"); + } + + Thread.Sleep(500); + } + }); + }); + } + + public override async Task CancelAsync() + { + await manager.PauseAllTasks(); + } + } +} diff --git a/src/XIVLauncher2.Common/Game/Patch/Acquisition/Aria/AriaManager.cs b/src/XIVLauncher2.Common/Game/Patch/Acquisition/Aria/AriaManager.cs new file mode 100644 index 0000000..b9b6727 --- /dev/null +++ b/src/XIVLauncher2.Common/Game/Patch/Acquisition/Aria/AriaManager.cs @@ -0,0 +1,201 @@ +/** + * This file is part of AriaNet by huming2207, licensed under the CC-BY-NC-SA 3.0 Australian Licence. + * You can find the original code in this GitHub repository: https://github.com/huming2207/AriaNet + */ + +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading.Tasks; +using AriaNet.Attributes; +using XIVLauncher2.Common.Game.Patch.Acquisition.Aria.JsonRpc; + +namespace AriaNet +{ + public class AriaManager + { + private readonly JsonRpcHttpClient rpcClient; + private readonly string secret; + + public AriaManager(string secret, string rpcUrl = "http://localhost:6800/jsonrpc") + { + this.secret = secret; + this.rpcClient = new JsonRpcHttpClient(rpcUrl); + } + + private async Task Invoke(string method, params object[] arguments) + { + var args = new object[arguments.Length + 1]; + args[0] = $"token:{this.secret}"; + Array.Copy(arguments, 0, args, 1, arguments.Length); + + return await this.rpcClient.Invoke(method, args); + } + + public async Task AddUri(List uriList) + { + return await Invoke("aria2.addUri", uriList); + } + + public async Task AddUri(List uriList, string userAgent, string referrer) + { + return await Invoke("aria2.addUri", uriList, + new Dictionary + { + {"user-agent", userAgent}, + {"referer", referrer} + }); + } + + public async Task AddUri(List uriList, Dictionary options) + { + return await Invoke("aria2.addUri", uriList, options); + } + + public async Task AddMetaLink(string filePath) + { + var metaLinkBase64 = Convert.ToBase64String(File.ReadAllBytes(filePath)); + return await Invoke("aria2.addMetalink", metaLinkBase64); + } + + public async Task AddTorrent(string filePath) + { + var torrentBase64 = Convert.ToBase64String(File.ReadAllBytes(filePath)); + return await Invoke("aria2.addTorrent", torrentBase64); + } + + public async Task RemoveTask(string gid, bool forceRemove = false) + { + if (!forceRemove) + { + return await Invoke("aria2.remove", gid); + } + else + { + return await Invoke("aria2.forceRemove", gid); + } + } + + public async Task PauseTask(string gid, bool forcePause = false) + { + if (!forcePause) + { + return await Invoke("aria2.pause", gid); + } + else + { + return await Invoke("aria2.forcePause", gid); + } + } + + public async Task PauseAllTasks() + { + return (await Invoke("aria2.pauseAll")).Contains("OK"); + } + + public async Task UnpauseAllTasks() + { + return (await Invoke("aria2.unpauseAll")).Contains("OK"); + } + + public async Task UnpauseTask(string gid) + { + return await Invoke("aria2.unpause", gid); + } + + public async Task GetStatus(string gid) + { + return await Invoke("aria2.tellStatus", gid); + } + + public async Task GetUris(string gid) + { + return await Invoke("aria2.getUris", gid); + } + + public async Task GetFiles(string gid) + { + return await Invoke("aria2.getFiles", gid); + } + + public async Task GetPeers(string gid) + { + return await Invoke("aria2.getPeers", gid); + } + + public async Task GetServers(string gid) + { + return await Invoke("aria2.getServers", gid); + } + + public async Task GetActiveStatus(string gid) + { + return await Invoke("aria2.tellActive", gid); + } + public async Task GetOption(string gid) + { + return await Invoke("aria2.getOption", gid); + } + + + public async Task ChangeOption(string gid, AriaOption option) + { + return (await Invoke("aria2.changeOption", gid, option)) + .Contains("OK"); + } + + public async Task GetGlobalOption() + { + return await Invoke("aria2.getGlobalOption"); + } + + public async Task ChangeGlobalOption(AriaOption option) + { + return (await Invoke("aria2.changeGlobalOption", option)) + .Contains("OK"); + } + + public async Task GetGlobalStatus() + { + return await Invoke("aria2.getGlobalStat"); + } + + public async Task PurgeDownloadResult() + { + return (await Invoke("aria2.purgeDownloadResult")).Contains("OK"); + } + + public async Task RemoveDownloadResult(string gid) + { + return (await Invoke("aria2.removeDownloadResult", gid)) + .Contains("OK"); + } + + public async Task GetVersion() + { + return await Invoke("aria2.getVersion"); + } + + public async Task GetSessionInfo() + { + return await Invoke("aria2.getSessionInfo"); + } + + public async Task Shutdown(bool forceShutdown = false) + { + if (!forceShutdown) + { + return (await Invoke("aria2.shutdown")).Contains("OK"); + } + else + { + return (await Invoke("aria2.forceShutdown")).Contains("OK"); + } + } + + public async Task SaveSession() + { + return (await Invoke("aria2.saveSession")).Contains("OK"); + } + } +} diff --git a/src/XIVLauncher2.Common/Game/Patch/Acquisition/Aria/Attributes/AriaFile.cs b/src/XIVLauncher2.Common/Game/Patch/Acquisition/Aria/Attributes/AriaFile.cs new file mode 100644 index 0000000..bfd2631 --- /dev/null +++ b/src/XIVLauncher2.Common/Game/Patch/Acquisition/Aria/Attributes/AriaFile.cs @@ -0,0 +1,31 @@ +/** + * This file is part of AriaNet by huming2207, licensed under the CC-BY-NC-SA 3.0 Australian Licence. + * You can find the original code in this GitHub repository: https://github.com/huming2207/AriaNet + */ + +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace AriaNet.Attributes +{ + public class AriaFile + { + [JsonPropertyName("index")] + public string Index { get; set; } + + [JsonPropertyName("length")] + public string Length { get; set; } + + [JsonPropertyName("completedLength")] + public string CompletedLength { get; set; } + + [JsonPropertyName("path")] + public string Path { get; set; } + + [JsonPropertyName("selected")] + public string Selected { get; set; } + + [JsonPropertyName("uris")] + public List Uris { get; set; } + } +} diff --git a/src/XIVLauncher2.Common/Game/Patch/Acquisition/Aria/Attributes/AriaGlobalStatus.cs b/src/XIVLauncher2.Common/Game/Patch/Acquisition/Aria/Attributes/AriaGlobalStatus.cs new file mode 100644 index 0000000..d6ab0cb --- /dev/null +++ b/src/XIVLauncher2.Common/Game/Patch/Acquisition/Aria/Attributes/AriaGlobalStatus.cs @@ -0,0 +1,27 @@ +/** + * This file is part of AriaNet by huming2207, licensed under the CC-BY-NC-SA 3.0 Australian Licence. + * You can find the original code in this GitHub repository: https://github.com/huming2207/AriaNet + */ + +using System.Text.Json.Serialization; + +namespace AriaNet.Attributes +{ + public class AriaGlobalStatus + { + [JsonPropertyName("downloadSpeed")] + public int DownloadSpeed { get; set; } + + [JsonPropertyName("numActive")] + public int ActiveTaskCount { get; set; } + + [JsonPropertyName("numStopped")] + public int StoppedTaskCount { get; set; } + + [JsonPropertyName("numWaiting")] + public int WaitingTaskCount { get; set; } + + [JsonPropertyName("uploadSpeed")] + public int UploadSpeed { get; set; } + } +} \ No newline at end of file diff --git a/src/XIVLauncher2.Common/Game/Patch/Acquisition/Aria/Attributes/AriaOption.cs b/src/XIVLauncher2.Common/Game/Patch/Acquisition/Aria/Attributes/AriaOption.cs new file mode 100644 index 0000000..2092ce3 --- /dev/null +++ b/src/XIVLauncher2.Common/Game/Patch/Acquisition/Aria/Attributes/AriaOption.cs @@ -0,0 +1,339 @@ +/** + * This file is part of AriaNet by huming2207, licensed under the CC-BY-NC-SA 3.0 Australian Licence. + * You can find the original code in this GitHub repository: https://github.com/huming2207/AriaNet + */ + +using System.Text.Json.Serialization; + +namespace AriaNet.Attributes +{ + public class AriaOption + { + [JsonPropertyName("all-proxy")] + public string AllProxy { get; set; } + + [JsonPropertyName("all-proxy-passwd")] + public string AllProxyPasswd { get; set; } + + [JsonPropertyName("all-proxy-user")] + public string AllProxyUser { get; set; } + + [JsonPropertyName("allow-overwrite")] + public string AllowOverwrite { get; set; } + + [JsonPropertyName("allow-piece-length-change")] + public string AllowPieceLengthChange { get; set; } + + [JsonPropertyName("always-resume")] + public string AlwaysResume { get; set; } + + [JsonPropertyName("async-dns")] + public string AsyncDns { get; set; } + + [JsonPropertyName("auto-file-renaming")] + public string AutoFileRenaming { get; set; } + + [JsonPropertyName("bt-enable-hook-after-hash-check")] + public string BtEnableHookAfterHashCheck { get; set; } + + [JsonPropertyName("bt-enable-lpd")] + public string BtEnableLpd { get; set; } + + [JsonPropertyName("bt-exclude-tracker")] + public string BtExcludeTracker { get; set; } + + [JsonPropertyName("bt-external-ip")] + public string BtExternalIp { get; set; } + + [JsonPropertyName("bt-force-encryption")] + public string BtForceEncryption { get; set; } + + [JsonPropertyName("bt-hash-check-seed")] + public string BtHashCheckSeed { get; set; } + + [JsonPropertyName("bt-max-peers")] + public string BtMaxPeers { get; set; } + + [JsonPropertyName("bt-metadata-only")] + public string BtMetadataOnly { get; set; } + + [JsonPropertyName("bt-min-crypto-level")] + public string BtMinCryptoLevel { get; set; } + + [JsonPropertyName("bt-prioritize-piece")] + public string BtPrioritizePiece { get; set; } + + [JsonPropertyName("bt-remove-unselected-file")] + public string BtRemoveUnselectedFile { get; set; } + + [JsonPropertyName("bt-request-peer-speed-limit")] + public string BtRequestPeerSpeedLimit { get; set; } + + [JsonPropertyName("bt-require-crypto")] + public string BtRequireCrypto { get; set; } + + [JsonPropertyName("bt-save-metadata")] + public string BtSaveMetadata { get; set; } + + [JsonPropertyName("bt-seed-unverified")] + public string BtSeedUnverified { get; set; } + + [JsonPropertyName("bt-stop-timeout")] + public string BtStopTimeout { get; set; } + + [JsonPropertyName("bt-tracker")] + public string BtTracker { get; set; } + + [JsonPropertyName("bt-tracker-connect-timeout")] + public string BtTrackerConnectTimeout { get; set; } + + [JsonPropertyName("bt-tracker-interval")] + public string BtTrackerInterval { get; set; } + + [JsonPropertyName("bt-tracker-timeout")] + public string BtTrackerTimeout { get; set; } + + [JsonPropertyName("check-integrity")] + public string CheckIntegrity { get; set; } + + [JsonPropertyName("checksum")] + public string Checksum { get; set; } + + [JsonPropertyName("conditional-get")] + public string ConditionalGet { get; set; } + + [JsonPropertyName("connect-timeout")] + public string ConnectTimeout { get; set; } + + [JsonPropertyName("content-disposition-default-utf8")] + public string ContentDispositionDefaultUtf8 { get; set; } + + [JsonPropertyName("continue")] + public string Continue { get; set; } + + [JsonPropertyName("dir")] + public string Dir { get; set; } + + [JsonPropertyName("dry-run")] + public string DryRun { get; set; } + + [JsonPropertyName("enable-http-keep-alive")] + public string EnableHttpKeepAlive { get; set; } + + [JsonPropertyName("enable-http-pipelining")] + public string EnableHttpPipelining { get; set; } + + [JsonPropertyName("enable-mmap")] + public string EnableMmap { get; set; } + + [JsonPropertyName("enable-peer-exchange")] + public string EnablePeerExchange { get; set; } + + [JsonPropertyName("file-allocation")] + public string FileAllocation { get; set; } + + [JsonPropertyName("follow-metalink")] + public string FollowMetalink { get; set; } + + [JsonPropertyName("follow-torrent")] + public string FollowTorrent { get; set; } + + [JsonPropertyName("force-save")] + public string ForceSave { get; set; } + + [JsonPropertyName("ftp-passwd")] + public string FtpPasswd { get; set; } + + [JsonPropertyName("ftp-pasv")] + public string FtpPasv { get; set; } + + [JsonPropertyName("ftp-proxy")] + public string FtpProxy { get; set; } + + [JsonPropertyName("ftp-proxy-passwd")] + public string FtpProxyPasswd { get; set; } + + [JsonPropertyName("ftp-proxy-user")] + public string FtpProxyUser { get; set; } + + [JsonPropertyName("ftp-reuse-connection")] + public string FtpReuseConnection { get; set; } + + [JsonPropertyName("ftp-type")] + public string FtpType { get; set; } + + [JsonPropertyName("ftp-user")] + public string FtpUser { get; set; } + + [JsonPropertyName("gid")] + public string Gid { get; set; } + + [JsonPropertyName("hash-check-only")] + public string HashCheckOnly { get; set; } + + [JsonPropertyName("header")] + public string Header { get; set; } + + [JsonPropertyName("http-accept-gzip")] + public string HttpAcceptGzip { get; set; } + + [JsonPropertyName("http-auth-challenge")] + public string HttpAuthChallenge { get; set; } + + [JsonPropertyName("http-no-cache")] + public string HttpNoCache { get; set; } + + [JsonPropertyName("http-passwd")] + public string HttpPasswd { get; set; } + + [JsonPropertyName("http-proxy")] + public string HttpProxy { get; set; } + + [JsonPropertyName("http-proxy-passwd")] + public string HttpProxyPasswd { get; set; } + + [JsonPropertyName("http-proxy-user")] + public string HttpProxyUser { get; set; } + + [JsonPropertyName("http-user")] + public string HttpUser { get; set; } + + [JsonPropertyName("https-proxy")] + public string HttpsProxy { get; set; } + + [JsonPropertyName("https-proxy-passwd")] + public string HttpsProxyPasswd { get; set; } + + [JsonPropertyName("https-proxy-user")] + public string HttpsProxyUser { get; set; } + + [JsonPropertyName("index-out")] + public string IndexOut { get; set; } + + [JsonPropertyName("lowest-speed-limit")] + public string LowestSpeedLimit { get; set; } + + [JsonPropertyName("max-connection-per-server")] + public string MaxConnectionPerServer { get; set; } + + [JsonPropertyName("max-download-limit")] + public string MaxDownloadLimit { get; set; } + + [JsonPropertyName("max-file-not-found")] + public string MaxFileNotFound { get; set; } + + [JsonPropertyName("max-mmap-limit")] + public string MaxMmapLimit { get; set; } + + [JsonPropertyName("max-resume-failure-tries")] + public string MaxResumeFailureTries { get; set; } + + [JsonPropertyName("max-tries")] + public string MaxTries { get; set; } + + [JsonPropertyName("max-upload-limit")] + public string MaxUploadLimit { get; set; } + + [JsonPropertyName("metalink-base-uri")] + public string MetalinkBaseUri { get; set; } + + [JsonPropertyName("metalink-enable-unique-protocol")] + public string MetalinkEnableUniqueProtocol { get; set; } + + [JsonPropertyName("metalink-language")] + public string MetalinkLanguage { get; set; } + + [JsonPropertyName("metalink-location")] + public string MetalinkLocation { get; set; } + + [JsonPropertyName("metalink-os")] + public string MetalinkOs { get; set; } + + [JsonPropertyName("metalink-preferred-protocol")] + public string MetalinkPreferredProtocol { get; set; } + + [JsonPropertyName("metalink-version")] + public string MetalinkVersion { get; set; } + + [JsonPropertyName("min-split-size")] + public string MinSplitSize { get; set; } + + [JsonPropertyName("no-file-allocation-limit")] + public string NoFileAllocationLimit { get; set; } + + [JsonPropertyName("no-netrc")] + public string NoNetrc { get; set; } + + [JsonPropertyName("no-proxy")] + public string NoProxy { get; set; } + + [JsonPropertyName("out")] + public string Out { get; set; } + + [JsonPropertyName("parameterized-uri")] + public string ParameterizedUri { get; set; } + + [JsonPropertyName("pause")] + public string Pause { get; set; } + + [JsonPropertyName("pause-metadata")] + public string PauseMetadata { get; set; } + + [JsonPropertyName("piece-length")] + public string PieceLength { get; set; } + + [JsonPropertyName("proxy-method")] + public string ProxyMethod { get; set; } + + [JsonPropertyName("realtime-chunk-checksum")] + public string RealtimeChunkChecksum { get; set; } + + [JsonPropertyName("referer")] + public string Referer { get; set; } + + [JsonPropertyName("remote-time")] + public string RemoteTime { get; set; } + + [JsonPropertyName("remove-control-file")] + public string RemoveControlFile { get; set; } + + [JsonPropertyName("retry-wait")] + public string RetryWait { get; set; } + + [JsonPropertyName("reuse-uri")] + public string ReuseUri { get; set; } + + [JsonPropertyName("rpc-save-upload-metadata")] + public string RpcSaveUploadMetadata { get; set; } + + [JsonPropertyName("seed-ratio")] + public string SeedRatio { get; set; } + + [JsonPropertyName("seed-time")] + public string SeedTime { get; set; } + + [JsonPropertyName("select-file")] + public string SelectFile { get; set; } + + [JsonPropertyName("split")] + public string Split { get; set; } + + [JsonPropertyName("ssh-host-key-md")] + public string SshHostKeyMd { get; set; } + + [JsonPropertyName("stream-piece-selector")] + public string StreamPieceSelector { get; set; } + + [JsonPropertyName("timeout")] + public string Timeout { get; set; } + + [JsonPropertyName("uri-selector")] + public string UriSelector { get; set; } + + [JsonPropertyName("use-head")] + public string UseHead { get; set; } + + [JsonPropertyName("user-agent")] + public string UserAgent { get; set; } + } +} \ No newline at end of file diff --git a/src/XIVLauncher2.Common/Game/Patch/Acquisition/Aria/Attributes/AriaServer.cs b/src/XIVLauncher2.Common/Game/Patch/Acquisition/Aria/Attributes/AriaServer.cs new file mode 100644 index 0000000..a107b72 --- /dev/null +++ b/src/XIVLauncher2.Common/Game/Patch/Acquisition/Aria/Attributes/AriaServer.cs @@ -0,0 +1,32 @@ +/** + * This file is part of AriaNet by huming2207, licensed under the CC-BY-NC-SA 3.0 Australian Licence. + * You can find the original code in this GitHub repository: https://github.com/huming2207/AriaNet + */ + +using System.Collections.Generic; +using System.Text.Json.Serialization; + + +namespace AriaNet.Attributes +{ + public class ServerDetail + { + [JsonPropertyName("currentUri")] + public string CurrentUri { get; set; } + + [JsonPropertyName("downloadSpeed")] + public string DownloadSpeed { get; set; } + + [JsonPropertyName("uri")] + public string Uri { get; set; } + } + + public class AriaServer + { + [JsonPropertyName("index")] + public string Index { get; set; } + + [JsonPropertyName("servers")] + public List Servers { get; set; } + } +} diff --git a/src/XIVLauncher2.Common/Game/Patch/Acquisition/Aria/Attributes/AriaSession.cs b/src/XIVLauncher2.Common/Game/Patch/Acquisition/Aria/Attributes/AriaSession.cs new file mode 100644 index 0000000..9774da1 --- /dev/null +++ b/src/XIVLauncher2.Common/Game/Patch/Acquisition/Aria/Attributes/AriaSession.cs @@ -0,0 +1,15 @@ +/** + * This file is part of AriaNet by huming2207, licensed under the CC-BY-NC-SA 3.0 Australian Licence. + * You can find the original code in this GitHub repository: https://github.com/huming2207/AriaNet + */ + +using System.Text.Json.Serialization; + +namespace AriaNet.Attributes +{ + public class AriaSession + { + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } + } +} \ No newline at end of file diff --git a/src/XIVLauncher2.Common/Game/Patch/Acquisition/Aria/Attributes/AriaStatus.cs b/src/XIVLauncher2.Common/Game/Patch/Acquisition/Aria/Attributes/AriaStatus.cs new file mode 100644 index 0000000..28f568a --- /dev/null +++ b/src/XIVLauncher2.Common/Game/Patch/Acquisition/Aria/Attributes/AriaStatus.cs @@ -0,0 +1,53 @@ +/** + * This file is part of AriaNet by huming2207, licensed under the CC-BY-NC-SA 3.0 Australian Licence. + * You can find the original code in this GitHub repository: https://github.com/huming2207/AriaNet + */ + +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace AriaNet.Attributes +{ + public class AriaStatus + { + + [JsonPropertyName("bitfield")] + public string Bitfield { get; set; } + + [JsonPropertyName("completedLength")] + public string CompletedLength { get; set; } + + [JsonPropertyName("connections")] + public string Connections { get; set; } + + [JsonPropertyName("dir")] + public string Dir { get; set; } + + [JsonPropertyName("downloadSpeed")] + public string DownloadSpeed { get; set; } + + [JsonPropertyName("files")] + public List Files { get; set; } + + [JsonPropertyName("gid")] + public string TaskId { get; set; } + + [JsonPropertyName("numPieces")] + public string NumPieces { get; set; } + + [JsonPropertyName("pieceLength")] + public string PieceLength { get; set; } + + [JsonPropertyName("status")] + public string Status { get; set; } + + [JsonPropertyName("totalLength")] + public string TotalLength { get; set; } + + [JsonPropertyName("uploadLength")] + public string UploadLength { get; set; } + + [JsonPropertyName("uploadSpeed")] + public string UploadSpeed { get; set; } + } +} diff --git a/src/XIVLauncher2.Common/Game/Patch/Acquisition/Aria/Attributes/AriaTorrent.cs b/src/XIVLauncher2.Common/Game/Patch/Acquisition/Aria/Attributes/AriaTorrent.cs new file mode 100644 index 0000000..afd337f --- /dev/null +++ b/src/XIVLauncher2.Common/Game/Patch/Acquisition/Aria/Attributes/AriaTorrent.cs @@ -0,0 +1,39 @@ +/** + * This file is part of AriaNet by huming2207, licensed under the CC-BY-NC-SA 3.0 Australian Licence. + * You can find the original code in this GitHub repository: https://github.com/huming2207/AriaNet + */ + +using System.Text.Json.Serialization; + +namespace AriaNet.Attributes +{ + public class AriaTorrent + { + [JsonPropertyName("amChoking")] + public string AmChoking { get; set; } + + [JsonPropertyName("bitfield")] + public string BitField { get; set; } + + [JsonPropertyName("downloadSpeed")] + public string DownloadSpeed { get; set; } + + [JsonPropertyName("ip")] + public string Ip { get; set; } + + [JsonPropertyName("peerChoking")] + public string PeerChoking { get; set; } + + [JsonPropertyName("peerId")] + public string PeerId { get; set; } + + [JsonPropertyName("port")] + public string Port { get; set; } + + [JsonPropertyName("seeder")] + public string Seeder { get; set; } + + [JsonPropertyName("uploadSpeed")] + public string UploadSpeed { get; set; } + } +} diff --git a/src/XIVLauncher2.Common/Game/Patch/Acquisition/Aria/Attributes/AriaUri.cs b/src/XIVLauncher2.Common/Game/Patch/Acquisition/Aria/Attributes/AriaUri.cs new file mode 100644 index 0000000..2685644 --- /dev/null +++ b/src/XIVLauncher2.Common/Game/Patch/Acquisition/Aria/Attributes/AriaUri.cs @@ -0,0 +1,18 @@ +/** + * This file is part of AriaNet by huming2207, licensed under the CC-BY-NC-SA 3.0 Australian Licence. + * You can find the original code in this GitHub repository: https://github.com/huming2207/AriaNet + */ + +using System.Text.Json.Serialization; + +namespace AriaNet.Attributes +{ + public class AriaUri + { + [JsonPropertyName("status")] + public string Status { get; set; } + + [JsonPropertyName("uri")] + public string Uri { get; set; } + } +} diff --git a/src/XIVLauncher2.Common/Game/Patch/Acquisition/Aria/Attributes/AriaVersionInfo.cs b/src/XIVLauncher2.Common/Game/Patch/Acquisition/Aria/Attributes/AriaVersionInfo.cs new file mode 100644 index 0000000..2a3e283 --- /dev/null +++ b/src/XIVLauncher2.Common/Game/Patch/Acquisition/Aria/Attributes/AriaVersionInfo.cs @@ -0,0 +1,19 @@ +/** + * This file is part of AriaNet by huming2207, licensed under the CC-BY-NC-SA 3.0 Australian Licence. + * You can find the original code in this GitHub repository: https://github.com/huming2207/AriaNet + */ + +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace AriaNet.Attributes +{ + public class AriaVersionInfo + { + [JsonPropertyName("enabledFeatures")] + public List EnabledFeatures { get; set; } + + [JsonPropertyName("version")] + public string Version { get; set; } + } +} \ No newline at end of file diff --git a/src/XIVLauncher2.Common/Game/Patch/Acquisition/Aria/JsonRpc/JsonRpcHttpClient.cs b/src/XIVLauncher2.Common/Game/Patch/Acquisition/Aria/JsonRpc/JsonRpcHttpClient.cs new file mode 100644 index 0000000..751342e --- /dev/null +++ b/src/XIVLauncher2.Common/Game/Patch/Acquisition/Aria/JsonRpc/JsonRpcHttpClient.cs @@ -0,0 +1,44 @@ +using System; +using System.Net.Http; +using System.Text; +using System.Threading.Tasks; +using System.Text.Json; +using Serilog; + +namespace XIVLauncher2.Common.Game.Patch.Acquisition.Aria.JsonRpc +{ + /// + /// Bodge JSON-RPC 2.0 http client implementation + /// + public class JsonRpcHttpClient + { + private readonly string _endpoint; + private readonly HttpClient _client; + + public JsonRpcHttpClient(string endpoint) + { + _endpoint = endpoint; + _client = new HttpClient + { + Timeout = new TimeSpan(0, 5, 0) + }; + } + + private static string Base64Encode(string plainText) { + var plainTextBytes = Encoding.UTF8.GetBytes(plainText); + return Convert.ToBase64String(plainTextBytes); + } + + public async Task Invoke(string method, params object[] args) + { + var argsJson = JsonSerializer.Serialize(args); + Log.Debug($"[JSONRPC] method({method}) arg({argsJson})"); + + var httpResponse = await _client.GetAsync(_endpoint + $"?method={method}&id={Guid.NewGuid()}¶ms={Base64Encode(argsJson)}"); + httpResponse.EnsureSuccessStatusCode(); + + var rpcResponse = JsonSerializer.Deserialize>(await httpResponse.Content.ReadAsStringAsync()); + return rpcResponse.Result; + } + } +} diff --git a/src/XIVLauncher2.Common/Game/Patch/Acquisition/Aria/JsonRpc/JsonRpcResponse.cs b/src/XIVLauncher2.Common/Game/Patch/Acquisition/Aria/JsonRpc/JsonRpcResponse.cs new file mode 100644 index 0000000..0150b5e --- /dev/null +++ b/src/XIVLauncher2.Common/Game/Patch/Acquisition/Aria/JsonRpc/JsonRpcResponse.cs @@ -0,0 +1,16 @@ +using System.Text.Json.Serialization; + +namespace XIVLauncher2.Common.Game.Patch.Acquisition.Aria.JsonRpc +{ + public class JsonRpcResponse + { + [JsonPropertyName("id")] + public string Id { get; set; } + + [JsonPropertyName("jsonrpc")] + public string Version { get; set; } + + [JsonPropertyName("result")] + public T Result { get; set; } + } +} diff --git a/src/XIVLauncher2.Common/Game/Patch/Acquisition/NetDownloaderPatchAcquisition.cs b/src/XIVLauncher2.Common/Game/Patch/Acquisition/NetDownloaderPatchAcquisition.cs new file mode 100644 index 0000000..80466b1 --- /dev/null +++ b/src/XIVLauncher2.Common/Game/Patch/Acquisition/NetDownloaderPatchAcquisition.cs @@ -0,0 +1,92 @@ +using System; +using System.IO; +using System.Threading.Tasks; +using Downloader; +using Serilog; + +namespace XIVLauncher2.Common.Game.Patch.Acquisition +{ + internal class NetDownloaderPatchAcquisition : PatchAcquisition + { + private readonly DirectoryInfo _patchStore; + private DownloadService _dlService; + + private string DownloadTempPath => Path.Combine(_patchStore.FullName, "temp"); + + private DownloadConfiguration _downloadOpt = new DownloadConfiguration + { + ParallelDownload = true, // download parts of file as parallel or not + BufferBlockSize = 8000, // usually, hosts support max to 8000 bytes + ChunkCount = 8, // file parts to download + MaxTryAgainOnFailover = int.MaxValue, // the maximum number of times to fail. + OnTheFlyDownload = false, // caching in-memory mode + Timeout = 10000, // timeout (millisecond) per stream block reader + TempDirectory = Path.GetTempPath(), // this is the library default + RequestConfiguration = new RequestConfiguration + { + UserAgent = Constants.PatcherUserAgent, + Accept = "*/*" + }, + //MaximumBytesPerSecond = App.Settings.SpeedLimitBytes / PatchManager.MAX_DOWNLOADS_AT_ONCE, + }; + + public NetDownloaderPatchAcquisition(DirectoryInfo patchStore, long maxBytesPerSecond) + { + this._patchStore = patchStore; + + this._downloadOpt.TempDirectory = this.DownloadTempPath; + } + + public override async Task StartDownloadAsync(string url, FileInfo outFile) + { + _dlService = new DownloadService(_downloadOpt); + + _dlService.DownloadProgressChanged += (sender, args) => + { + OnProgressChanged(new AcquisitionProgress + { + BytesPerSecondSpeed = (long) args.BytesPerSecondSpeed, + Progress = args.ReceivedBytesSize + }); + }; + + _dlService.DownloadFileCompleted += (sender, args) => + { + if (args.Error != null) + { + Log.Error(args.Error, "[WEB] Download failed for {0} with reason {1}", url, args.Error); + + // If we cancel downloads, we don't want to see an error message + if (args.Error is OperationCanceledException) + { + OnComplete(AcquisitionResult.Cancelled); + return; + } + + OnComplete(AcquisitionResult.Error); + return; + } + + if (args.Cancelled) + { + Log.Error("[WEB] Download cancelled for {0} with reason {1}", url, args.Error); + + /* + Cancellation should not produce an error message, since it is always triggered by another error or the user. + */ + OnComplete(AcquisitionResult.Cancelled); + return; + } + + OnComplete(AcquisitionResult.Success); + }; + + await _dlService.DownloadFileTaskAsync(url, outFile.FullName); + } + + public override async Task CancelAsync() + { + this._dlService.CancelAsync(); + } + } +} diff --git a/src/XIVLauncher2.Common/Game/Patch/Acquisition/PatchAcquisition.cs b/src/XIVLauncher2.Common/Game/Patch/Acquisition/PatchAcquisition.cs new file mode 100644 index 0000000..8f1b86b --- /dev/null +++ b/src/XIVLauncher2.Common/Game/Patch/Acquisition/PatchAcquisition.cs @@ -0,0 +1,26 @@ +using System; +using System.IO; +using System.Threading.Tasks; + +namespace XIVLauncher2.Common.Game.Patch.Acquisition +{ + public abstract class PatchAcquisition + { + public abstract Task StartDownloadAsync(string url, FileInfo outFile); + public abstract Task CancelAsync(); + + public event EventHandler ProgressChanged; + + protected void OnProgressChanged(AcquisitionProgress progress) + { + this.ProgressChanged?.Invoke(this, progress); + } + + public event EventHandler Complete; + + protected void OnComplete(AcquisitionResult result) + { + this.Complete?.Invoke(this, result); + } + } +} diff --git a/src/XIVLauncher2.Common/Game/Patch/Acquisition/TorrentPatchAcquisition.cs b/src/XIVLauncher2.Common/Game/Patch/Acquisition/TorrentPatchAcquisition.cs new file mode 100644 index 0000000..cee3d93 --- /dev/null +++ b/src/XIVLauncher2.Common/Game/Patch/Acquisition/TorrentPatchAcquisition.cs @@ -0,0 +1,104 @@ +using System; +using System.IO; +using System.Net; +using System.Threading.Tasks; +using MonoTorrent.Client; +using Serilog; +using XIVLauncher2.Common.Game.Patch.PatchList; + +namespace XIVLauncher2.Common.Game.Patch.Acquisition +{ + public class TorrentPatchAcquisition : PatchAcquisition + { + private static ClientEngine torrentEngine; + + private TorrentManager _torrentManager; + private byte[] _torrentBytes; + + public static async Task InitializeAsync(long maxDownloadSpeed) + { + if (torrentEngine == null) + { + torrentEngine = new ClientEngine(); + + var builder = new EngineSettingsBuilder(torrentEngine.Settings) {MaximumDownloadSpeed = (int)maxDownloadSpeed}; + + await torrentEngine.UpdateSettingsAsync(builder.ToSettings()); + } + } + + public static async Task UnInitializeAsync() + { + if (torrentEngine != null) + { + await torrentEngine.StopAllAsync(); + torrentEngine = null; + } + } + + public bool IsApplicable(PatchListEntry patch) + { + try + { + using var client = new WebClient(); + + _torrentBytes = client.DownloadData("http://goaaats.github.io/patchtorrent/" + patch.GetUrlPath() + ".torrent"); + } + catch (Exception ex) + { + Log.Error(ex, $"[TORRENT] Could not get torrent for patch: {patch.GetUrlPath()}"); + return false; + } + + return true; + } + + public override async Task StartDownloadAsync(string url, FileInfo outFile) + { + throw new NotImplementedException("WIP"); + + /* + if (_torrentBytes == null) + { + if (!IsApplicable(patch)) + throw new Exception("This patch is not applicable to be downloaded with this acquisition method."); + } + + var torrent = await Torrent.LoadAsync(_torrentBytes); + var hasSignaledComplete = false; + + _torrentManager = await torrentEngine.AddAsync(torrent, outFile.Directory.FullName); + _torrentManager.TorrentStateChanged += async (sender, args) => + { + if ((int) _torrentManager.Progress == 100 && !hasSignaledComplete && args.NewState == TorrentState.Seeding) + { + OnComplete(AcquisitionResult.Success); + hasSignaledComplete = true; + await _torrentManager.StopAsync(); + } + }; + + _torrentManager.PieceHashed += (sender, args) => + { + OnProgressChanged(new AcquisitionProgress + { + Progress = _torrentManager.Monitor.DataBytesDownloaded, + BytesPerSecondSpeed = _torrentManager.Monitor.DownloadSpeed + }); + }; + + await _torrentManager.StartAsync(); + await _torrentManager.DhtAnnounceAsync(); + */ + } + + public override async Task CancelAsync() + { + if (_torrentManager == null) + return; + + await _torrentManager.StopAsync(); + await torrentEngine.RemoveAsync(_torrentManager); + } + } +} diff --git a/src/XIVLauncher2.Common/Game/Patch/GamePatchType.cs b/src/XIVLauncher2.Common/Game/Patch/GamePatchType.cs new file mode 100644 index 0000000..acd5980 --- /dev/null +++ b/src/XIVLauncher2.Common/Game/Patch/GamePatchType.cs @@ -0,0 +1,8 @@ +namespace XIVLauncher2.Common.Game.Patch +{ + public enum GamePatchType + { + Boot, + Game + } +} diff --git a/src/XIVLauncher2.Common/Game/Patch/NotEnoughSpaceException.cs b/src/XIVLauncher2.Common/Game/Patch/NotEnoughSpaceException.cs new file mode 100644 index 0000000..6c2f35d --- /dev/null +++ b/src/XIVLauncher2.Common/Game/Patch/NotEnoughSpaceException.cs @@ -0,0 +1,26 @@ +using System; + +namespace XIVLauncher2.Common.Game.Patch; + +public class NotEnoughSpaceException : Exception +{ + public enum SpaceKind + { + Patches, + AllPatches, + Game, + } + + public SpaceKind Kind { get; private set; } + + public long BytesRequired { get; set; } + + public long BytesFree { get; set; } + + public NotEnoughSpaceException(SpaceKind kind, long required, long free) + { + this.Kind = kind; + this.BytesRequired = required; + this.BytesFree = free; + } +} diff --git a/src/XIVLauncher2.Common/Game/Patch/PatchInstaller.cs b/src/XIVLauncher2.Common/Game/Patch/PatchInstaller.cs new file mode 100644 index 0000000..a674645 --- /dev/null +++ b/src/XIVLauncher2.Common/Game/Patch/PatchInstaller.cs @@ -0,0 +1,167 @@ +using System; +using System.Diagnostics; +using System.IO; +using System.Threading; +using Serilog; +using XIVLauncher2.Common.Game.Patch.PatchList; +using XIVLauncher2.Common.PatcherIpc; +using XIVLauncher2.Common.Patching; +using XIVLauncher2.Common.Patching.Rpc; +using XIVLauncher2.Common.Patching.Rpc.Implementations; + +namespace XIVLauncher2.Common.Game.Patch +{ + public class PatchInstaller : IDisposable + { + private readonly bool keepPatches; + private IRpc rpc; + + private RemotePatchInstaller? internalPatchInstaller; + + public enum InstallerState + { + NotStarted, + NotReady, + Ready, + Busy, + Failed + } + + public InstallerState State { get; private set; } = InstallerState.NotStarted; + + public event Action OnFail; + + public PatchInstaller(bool keepPatches) + { + this.keepPatches = keepPatches; + } + + public void StartIfNeeded(bool external = true) + { + var rpcName = "XLPatcher" + Guid.NewGuid().ToString(); + + Log.Information("[PATCHERIPC] Starting patcher with '{0}'", rpcName); + + if (external) + { + this.rpc = new SharedMemoryRpc(rpcName); + this.rpc.MessageReceived += RemoteCallHandler; + + var path = Path.Combine(AppContext.BaseDirectory, + "XIVLauncher.PatchInstaller.exe"); + + var startInfo = new ProcessStartInfo(path); + startInfo.UseShellExecute = true; + + //Start as admin if needed + if (!EnvironmentSettings.IsNoRunas && Environment.OSVersion.Version.Major >= 6) + startInfo.Verb = "runas"; + + startInfo.Arguments = $"rpc {rpcName}"; + + State = InstallerState.NotReady; + + try + { + Process.Start(startInfo); + } + catch (Exception ex) + { + Log.Error(ex, "Could not launch Patch Installer"); + throw new PatchInstallerException("Start failed.", ex); + } + } + else + { + this.rpc = new InProcessRpc(rpcName); + this.rpc.MessageReceived += RemoteCallHandler; + + this.internalPatchInstaller = new RemotePatchInstaller(new InProcessRpc(rpcName)); + this.internalPatchInstaller.Start(); + } + } + + private void RemoteCallHandler(PatcherIpcEnvelope envelope) + { + switch (envelope.OpCode) + { + case PatcherIpcOpCode.Hello: + //_client.Initialize(_clientPort); + Log.Information("[PATCHERIPC] GOT HELLO"); + State = InstallerState.Ready; + break; + + case PatcherIpcOpCode.InstallOk: + Log.Information("[PATCHERIPC] INSTALL OK"); + State = InstallerState.Ready; + break; + + case PatcherIpcOpCode.InstallFailed: + State = InstallerState.Failed; + OnFail?.Invoke(); + + Stop(); + Environment.Exit(0); + break; + + default: + throw new ArgumentOutOfRangeException(); + } + } + + public void WaitOnHello() + { + for (var i = 0; i < 40; i++) + { + if (State == InstallerState.Ready) + return; + + Thread.Sleep(500); + } + + throw new PatchInstallerException("Installer RPC timed out."); + } + + public void Stop() + { + if (State == InstallerState.NotReady || State == InstallerState.NotStarted || State == InstallerState.Busy) + return; + + this.rpc.SendMessage(new PatcherIpcEnvelope + { + OpCode = PatcherIpcOpCode.Bye + }); + } + + public void StartInstall(DirectoryInfo gameDirectory, FileInfo file, PatchListEntry patch, Repository repo) + { + State = InstallerState.Busy; + this.rpc.SendMessage(new PatcherIpcEnvelope + { + OpCode = PatcherIpcOpCode.StartInstall, + StartInstallInfo = new PatcherIpcStartInstall + { + GameDirectory = gameDirectory, + PatchFile = file, + Repo = repo, + VersionId = patch.VersionId, + KeepPatch = this.keepPatches, + } + }); + } + + public void FinishInstall(DirectoryInfo gameDirectory) + { + this.rpc.SendMessage(new PatcherIpcEnvelope + { + OpCode = PatcherIpcOpCode.Finish, + GameDirectory = gameDirectory + }); + } + + public void Dispose() + { + Stop(); + } + } +} diff --git a/src/XIVLauncher2.Common/Game/Patch/PatchInstallerException.cs b/src/XIVLauncher2.Common/Game/Patch/PatchInstallerException.cs new file mode 100644 index 0000000..a908778 --- /dev/null +++ b/src/XIVLauncher2.Common/Game/Patch/PatchInstallerException.cs @@ -0,0 +1,11 @@ +using System; + +namespace XIVLauncher2.Common.Game.Patch; + +public class PatchInstallerException : Exception +{ + public PatchInstallerException(string message, Exception? inner = null) : base(message, inner) + { + // ignored + } +} diff --git a/src/XIVLauncher2.Common/Game/Patch/PatchList/PatchListEntry.cs b/src/XIVLauncher2.Common/Game/Patch/PatchList/PatchListEntry.cs new file mode 100644 index 0000000..f6d19a4 --- /dev/null +++ b/src/XIVLauncher2.Common/Game/Patch/PatchList/PatchListEntry.cs @@ -0,0 +1,33 @@ +using System.IO; +using System.Text.RegularExpressions; + +namespace XIVLauncher2.Common.Game.Patch.PatchList +{ + public class PatchListEntry + { + private static Regex urlRegex = new Regex(".*/((game|boot)/([a-zA-Z0-9]+)/.*)", RegexOptions.Compiled | RegexOptions.CultureInvariant); + + public string VersionId { get; set; } + public string HashType { get; set; } + public string Url { get; set; } + public long HashBlockSize { get; set; } + public string[] Hashes { get; set; } + public long Length { get; set; } + + public override string ToString() => $"{this.GetRepoName()}/{VersionId}"; + + private Match Deconstruct() => urlRegex.Match(this.Url); + + public string GetRepoName() + { + var name = this.Deconstruct().Groups[3].Captures[0].Value; + + // The URL doesn't have the "ffxiv" part for ffxiv repo. Let's fake it for readability. + return name == "4e9a232b" ? "ffxiv" : name; + } + + public string GetUrlPath() => this.Deconstruct().Groups[1].Captures[0].Value; + + public string GetFilePath() => GetUrlPath().Replace('/', Path.DirectorySeparatorChar); + } +} diff --git a/src/XIVLauncher2.Common/Game/Patch/PatchList/PatchListParseException.cs b/src/XIVLauncher2.Common/Game/Patch/PatchList/PatchListParseException.cs new file mode 100644 index 0000000..9295657 --- /dev/null +++ b/src/XIVLauncher2.Common/Game/Patch/PatchList/PatchListParseException.cs @@ -0,0 +1,14 @@ +using System; + +namespace XIVLauncher2.Common.Game.Patch.PatchList; + +public class PatchListParseException : Exception +{ + public string List { get; private set; } + + public PatchListParseException(string list, Exception innerException) + : base("Failed to parse patch list", innerException) + { + List = list; + } +} diff --git a/src/XIVLauncher2.Common/Game/Patch/PatchList/PatchListParser.cs b/src/XIVLauncher2.Common/Game/Patch/PatchList/PatchListParser.cs new file mode 100644 index 0000000..7bc59e3 --- /dev/null +++ b/src/XIVLauncher2.Common/Game/Patch/PatchList/PatchListParser.cs @@ -0,0 +1,44 @@ +using System; +using System.Collections.Generic; + +namespace XIVLauncher2.Common.Game.Patch.PatchList +{ + class PatchListParser + { + public static PatchListEntry[] Parse(string list) + { + try + { + var lines = list.Split( + new[] { "\r\n", "\r", "\n", Environment.NewLine }, + StringSplitOptions.None + ); + + var output = new List(); + + for (var i = 5; i < lines.Length - 2; i++) + { + var fields = lines[i].Split('\t'); + output.Add(new PatchListEntry() + { + Length = long.Parse(fields[0]), + VersionId = fields[4], + HashType = fields[5], + + HashBlockSize = fields.Length == 9 ? long.Parse(fields[6]) : 0, + + // bootver patchlists don't have a hash field + Hashes = fields.Length == 9 ? (fields[7].Split(',')) : null, + Url = fields[fields.Length == 9 ? 8 : 5] + }); + } + + return output.ToArray(); + } + catch (Exception ex) + { + throw new PatchListParseException(list, ex); + } + } + } +} diff --git a/src/XIVLauncher2.Common/Game/Patch/PatchManager.cs b/src/XIVLauncher2.Common/Game/Patch/PatchManager.cs new file mode 100644 index 0000000..312b552 --- /dev/null +++ b/src/XIVLauncher2.Common/Game/Patch/PatchManager.cs @@ -0,0 +1,524 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Security.Cryptography; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Serilog; +using XIVLauncher2.Common.Game.Patch.Acquisition; +using XIVLauncher2.Common.Game.Patch.Acquisition.Aria; +using XIVLauncher2.Common.Game.Patch.PatchList; +using XIVLauncher2.Common.Util; + +namespace XIVLauncher2.Common.Game.Patch +{ + public enum PatchState + { + Nothing, + IsDownloading, + Downloaded, + IsInstalling, + Finished + } + + public class PatchDownload + { + public PatchListEntry Patch { get; set; } + public PatchState State { get; set; } + } + + public class PatchManager + { + public const int MAX_DOWNLOADS_AT_ONCE = 4; + + private readonly CancellationTokenSource _cancelTokenSource = new(); + + private readonly AcquisitionMethod acquisitionMethod; + private readonly long speedLimitBytes; + private readonly Repository repo; + private readonly DirectoryInfo gamePath; + private readonly DirectoryInfo patchStore; + private readonly PatchInstaller installer; + private readonly Launcher launcher; + private readonly string sid; + + public readonly IReadOnlyList Downloads; + + public int CurrentInstallIndex { get; private set; } + + public enum SlotState + { + InProgress, + Checking, + Done, + } + + public readonly long[] Progresses = new long[MAX_DOWNLOADS_AT_ONCE]; + public readonly double[] Speeds = new double[MAX_DOWNLOADS_AT_ONCE]; + public readonly PatchDownload[] Actives = new PatchDownload[MAX_DOWNLOADS_AT_ONCE]; + public readonly SlotState[] Slots = new SlotState[MAX_DOWNLOADS_AT_ONCE]; + public readonly PatchAcquisition[] DownloadServices = new PatchAcquisition[MAX_DOWNLOADS_AT_ONCE]; + + public bool IsInstallerBusy { get; private set; } + + public bool DownloadsDone { get; private set; } + + public long AllDownloadsLength => GetDownloadLength(); + + private bool hasError = false; + + public event Action OnFail; + + public enum FailReason + { + DownloadProblem, + HashCheck, + } + + public PatchManager(AcquisitionMethod acquisitionMethod, long speedLimitBytes, Repository repo, IEnumerable patches, DirectoryInfo gamePath, DirectoryInfo patchStore, PatchInstaller installer, Launcher launcher, string sid) + { + Debug.Assert(patches != null, "patches != null ASSERTION FAILED"); + + this.acquisitionMethod = acquisitionMethod; + this.speedLimitBytes = speedLimitBytes; + this.repo = repo; + this.gamePath = gamePath; + this.patchStore = patchStore; + this.installer = installer; + this.launcher = launcher; + this.sid = sid; + + if (!this.patchStore.Exists) + this.patchStore.Create(); + + Downloads = patches.Select(patchListEntry => new PatchDownload {Patch = patchListEntry, State = PatchState.Nothing}).ToList().AsReadOnly(); + + // All dl slots are available at the start + for (var i = 0; i < MAX_DOWNLOADS_AT_ONCE; i++) + { + Slots[i] = SlotState.Done; + } + } + + public async Task PatchAsync(FileInfo aria2LogFile, bool external = true) + { + if (!EnvironmentSettings.IsIgnoreSpaceRequirements) + { + var freeSpaceDownload = PlatformHelpers.GetDiskFreeSpace(this.patchStore); + + if (Downloads.Any(x => x.Patch.Length > freeSpaceDownload)) + { + throw new NotEnoughSpaceException(NotEnoughSpaceException.SpaceKind.Patches, + Downloads.OrderByDescending(x => x.Patch.Length).First().Patch.Length, freeSpaceDownload); + } + + // If the first 6 patches altogether are bigger than the patch drive, we might run out of space + if (freeSpaceDownload < GetDownloadLength(6)) + { + throw new NotEnoughSpaceException(NotEnoughSpaceException.SpaceKind.AllPatches, AllDownloadsLength, + freeSpaceDownload); + } + + var freeSpaceGame = PlatformHelpers.GetDiskFreeSpace(this.gamePath); + + if (freeSpaceGame < AllDownloadsLength) + { + throw new NotEnoughSpaceException(NotEnoughSpaceException.SpaceKind.Game, AllDownloadsLength, + freeSpaceGame); + } + } + + this.installer.StartIfNeeded(external); + this.installer.WaitOnHello(); + + await InitializeAcquisition(aria2LogFile).ConfigureAwait(false); + + try + { + await Task.WhenAll(new Task[] { + Task.Run(RunDownloadQueue, _cancelTokenSource.Token), + Task.Run(RunApplyQueue, _cancelTokenSource.Token), + }).ConfigureAwait(false); + } + finally + { + // Only PatchManager uses Aria (or Torrent), so it's safe to shut it down here. + await UnInitializeAcquisition().ConfigureAwait(false); + } + } + + public async Task InitializeAcquisition(FileInfo aria2LogFile) + { + // TODO: Come up with a better pattern for initialization. This sucks. + switch (this.acquisitionMethod) + { + case AcquisitionMethod.NetDownloader: + // ignored + break; + + case AcquisitionMethod.MonoTorrentNetFallback: + await TorrentPatchAcquisition.InitializeAsync(this.speedLimitBytes / MAX_DOWNLOADS_AT_ONCE); + break; + + case AcquisitionMethod.MonoTorrentAriaFallback: + await AriaHttpPatchAcquisition.InitializeAsync(this.speedLimitBytes / MAX_DOWNLOADS_AT_ONCE, aria2LogFile); + await TorrentPatchAcquisition.InitializeAsync(this.speedLimitBytes / MAX_DOWNLOADS_AT_ONCE); + break; + + case AcquisitionMethod.Aria: + await AriaHttpPatchAcquisition.InitializeAsync(this.speedLimitBytes / MAX_DOWNLOADS_AT_ONCE, aria2LogFile); + break; + + default: + throw new ArgumentOutOfRangeException(); + } + } + + public static async Task UnInitializeAcquisition() + { + try + { + await AriaHttpPatchAcquisition.UnInitializeAsync(); + await TorrentPatchAcquisition.UnInitializeAsync(); + } + catch (Exception ex) + { + Log.Error(ex, "Could not uninitialize patch acquisition."); + } + } + + private async Task DownloadPatchAsync(PatchDownload download, int index) + { + var outFile = GetPatchFile(download.Patch); + + var realUrl = download.Patch.Url; + if (this.repo != Repository.Boot && false) // Disabled for now, waiting on SE to patch this + { + realUrl = await this.launcher.GenPatchToken(download.Patch.Url, this.sid); + } + + Log.Information("Downloading patch {0} at {1} to {2}", download.Patch.VersionId, realUrl, outFile.FullName); + + Actives[index] = download; + + if (outFile.Exists && CheckPatchValidity(download.Patch, outFile) == HashCheckResult.Pass) + { + download.State = PatchState.Downloaded; + Slots[index] = SlotState.Done; + Progresses[index] = download.Patch.Length; + return; + } + + PatchAcquisition acquisition; + + switch (this.acquisitionMethod) + { + case AcquisitionMethod.NetDownloader: + acquisition = new NetDownloaderPatchAcquisition(this.patchStore, this.speedLimitBytes / MAX_DOWNLOADS_AT_ONCE); + break; + + case AcquisitionMethod.MonoTorrentNetFallback: + acquisition = new TorrentPatchAcquisition(); + + var torrentAcquisition = acquisition as TorrentPatchAcquisition; + if (!torrentAcquisition.IsApplicable(download.Patch)) + acquisition = new NetDownloaderPatchAcquisition(this.patchStore, this.speedLimitBytes / MAX_DOWNLOADS_AT_ONCE); + break; + + case AcquisitionMethod.MonoTorrentAriaFallback: + acquisition = new TorrentPatchAcquisition(); + + torrentAcquisition = acquisition as TorrentPatchAcquisition; + if (!torrentAcquisition.IsApplicable(download.Patch)) + acquisition = new AriaHttpPatchAcquisition(); + break; + case AcquisitionMethod.Aria: + acquisition = new AriaHttpPatchAcquisition(); + break; + default: + throw new ArgumentOutOfRangeException(); + } + + acquisition.ProgressChanged += (sender, args) => + { + Progresses[index] = args.Progress; + Speeds[index] = args.BytesPerSecondSpeed; + }; + + acquisition.Complete += (sender, args) => + { + if (args == AcquisitionResult.Error) + { + if (this.hasError) + return; + + Log.Error("Download failed for {0}", download.Patch.VersionId); + + hasError = true; + + OnFail?.Invoke(FailReason.DownloadProblem, download.Patch.VersionId); + + CancelAllDownloads(); + + Environment.Exit(0); + return; + } + + if (args == AcquisitionResult.Cancelled) + { + // Cancellation should not produce an error message, since it is always triggered by another error or the user. + Log.Error("Download cancelled for {0}", download.Patch.VersionId); + + return; + } + + // Indicate "Checking..." + Slots[index] = SlotState.Checking; + + var checkResult = CheckPatchValidity(download.Patch, outFile); + + // Let's just bail for now, need better handling of this later + if (checkResult != HashCheckResult.Pass) + { + if (this.hasError) + return; + + Log.Error("IsHashCheckPass failed with {Result} for {VersionId} after DL", checkResult, download.Patch.VersionId); + + hasError = true; + + OnFail?.Invoke(FailReason.HashCheck, download.Patch.VersionId); + + CancelAllDownloads(); + + outFile.Delete(); + Environment.Exit(0); + return; + } + + download.State = PatchState.Downloaded; + Slots[index] = SlotState.Done; + Progresses[index] = 0; + Speeds[index] = 0; + + Log.Information("Patch at {0} downloaded completely", download.Patch.Url); + + this.CheckIsDone(); + }; + + DownloadServices[index] = acquisition; + + await acquisition.StartDownloadAsync(realUrl, outFile); + } + + public void CancelAllDownloads() + { + #if !DEBUG + return; + #endif + + foreach (var downloadService in DownloadServices) + { + try + { + downloadService?.CancelAsync().GetAwaiter().GetResult(); + Thread.Sleep(200); + } + catch (Exception ex) + { + Log.Error(ex, "Could not cancel download."); + } + } + } + + private void RunDownloadQueue() + { + while (Downloads.Any(x => x.State == PatchState.Nothing)) + { + Thread.Sleep(500); + for (var i = 0; i < MAX_DOWNLOADS_AT_ONCE; i++) + { + if (Slots[i] != SlotState.Done) + continue; + + Slots[i] = SlotState.InProgress; + + var toDl = Downloads.FirstOrDefault(x => x.State == PatchState.Nothing); + + if (toDl == null) + return; + + toDl.State = PatchState.IsDownloading; + var curIndex = i; + Task.Run(async () => + { + try + { + await DownloadPatchAsync(toDl, curIndex); + } + catch (Exception ex) + { + Log.Error(ex, "Exception in DownloadPatchAsync"); + throw; + } + }); + } + } + } + + private void CheckIsDone() + { + Log.Information("CheckIsDone!!"); + + if (!Downloads.Any(x => x.State is PatchState.Nothing or PatchState.IsDownloading)) + { + Log.Information("All patches downloaded."); + + DownloadsDone = true; + + for (var j = 0; j < Progresses.Length; j++) + { + Progresses[j] = 0; + } + + for (var j = 0; j < Speeds.Length; j++) + { + Speeds[j] = 0; + } + + return; + } + } + + private void RunApplyQueue() + { + while (CurrentInstallIndex < Downloads.Count) + { + Thread.Sleep(500); + + var toInstall = Downloads[CurrentInstallIndex]; + + if (toInstall.State != PatchState.Downloaded) + continue; + + toInstall.State = PatchState.IsInstalling; + + Log.Information("Starting patch install for {0} at {1}({2})", toInstall.Patch.VersionId, toInstall.Patch.Url, CurrentInstallIndex); + + IsInstallerBusy = true; + + this.installer.StartInstall(this.gamePath, GetPatchFile(toInstall.Patch), toInstall.Patch, GetRepoForPatch(toInstall.Patch)); + + while (this.installer.State != PatchInstaller.InstallerState.Ready) + { + Thread.Yield(); + } + + // TODO need to handle this better + if (this.installer.State == PatchInstaller.InstallerState.Failed) + return; + + Log.Information($"Patch at {CurrentInstallIndex} installed"); + + IsInstallerBusy = false; + + toInstall.State = PatchState.Finished; + CurrentInstallIndex++; + } + + Log.Information("PATCHING finish"); + this.installer.FinishInstall(this.gamePath); + } + + private enum HashCheckResult + { + Pass, + BadHash, + BadLength, + } + + private static HashCheckResult CheckPatchValidity(PatchListEntry patchListEntry, FileInfo path) + { + if (patchListEntry.HashType != "sha1") + { + Log.Error("??? Unknown HashType: {0} for {1}", patchListEntry.HashType, patchListEntry.Url); + return HashCheckResult.Pass; + } + + var stream = path.OpenRead(); + + if (stream.Length != patchListEntry.Length) + { + return HashCheckResult.BadLength; + } + + var parts = (int) Math.Ceiling((double) patchListEntry.Length / patchListEntry.HashBlockSize); + var block = new byte[patchListEntry.HashBlockSize]; + + for (var i = 0; i < parts; i++) + { + var read = stream.Read(block, 0, (int) patchListEntry.HashBlockSize); + + if (read < patchListEntry.HashBlockSize) + { + var trimmedBlock = new byte[read]; + Array.Copy(block, 0, trimmedBlock, 0, read); + block = trimmedBlock; + } + + using var sha1 = new SHA1Managed(); + + var hash = sha1.ComputeHash(block); + var sb = new StringBuilder(hash.Length * 2); + + foreach (var b in hash) + { + sb.Append(b.ToString("x2")); + } + + if (sb.ToString() == patchListEntry.Hashes[i]) + continue; + + stream.Close(); + return HashCheckResult.BadHash; + } + + stream.Close(); + return HashCheckResult.Pass; + } + + private FileInfo GetPatchFile(PatchListEntry patch) + { + var file = new FileInfo(Path.Combine(this.patchStore.FullName, patch.GetFilePath())); + file.Directory.Create(); + + return file; + } + + private Repository GetRepoForPatch(PatchListEntry patch) + { + if (patch.Url.Contains("boot")) + return Repository.Boot; + + if (patch.Url.Contains("ex1")) + return Repository.Ex1; + + if (patch.Url.Contains("ex2")) + return Repository.Ex2; + + if (patch.Url.Contains("ex3")) + return Repository.Ex3; + + if (patch.Url.Contains("ex4")) + return Repository.Ex4; + + return Repository.Ffxiv; + } + + private long GetDownloadLength() => GetDownloadLength(Downloads.Count); + + private long GetDownloadLength(int takeAmount) => Downloads.Take(takeAmount).Where(x => x.State == PatchState.Nothing || x.State == PatchState.IsDownloading).Sum(x => x.Patch.Length) - Progresses.Sum(); } +} diff --git a/src/XIVLauncher2.Common/Game/Patch/PatchVerifier.cs b/src/XIVLauncher2.Common/Game/Patch/PatchVerifier.cs new file mode 100644 index 0000000..5b29f8a --- /dev/null +++ b/src/XIVLauncher2.Common/Game/Patch/PatchVerifier.cs @@ -0,0 +1,666 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Diagnostics; +using System.IO; +using System.IO.Compression; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Text.RegularExpressions; +using System.Threading; +using System.Threading.Tasks; +using System.Text.Json; +using System.Text.Json.Serialization; +using Serilog; +using XIVLauncher2.Common.Game.Exceptions; +using XIVLauncher2.Common.Patching.IndexedZiPatch; +using XIVLauncher2.Common.Patching.Util; +using XIVLauncher2.Common.PlatformAbstractions; + +namespace XIVLauncher2.Common.Game.Patch +{ + public class PatchVerifier : IDisposable + { + private const string REPAIR_RECYCLER_DIRECTORY = "repair_recycler"; + + private static readonly Regex[] GameIgnoreUnnecessaryFilePatterns = new Regex[] + { + // Base game version files. + new Regex(@"^ffxivgame\.(?:bck|ver)$", RegexOptions.IgnoreCase), + + // Expansion version files. + new Regex(@"^sqpack/ex([1-9][0-9]*)/ex\1\.(?:bck|ver)$", RegexOptions.IgnoreCase), + + // Under WINE, since .dat files are actually WMV videos, the game will become unusable. + // Bink videos will be used instead in those cases. + new Regex(@"^movie/ffxiv/0000[0-3]\.bk2$", RegexOptions.IgnoreCase), + + // DXVK can deal with corrupted cache files by itself, so let it do the job by itself. + new Regex(@"^ffxiv_dx11\.dxvk-cache$", RegexOptions.IgnoreCase), + + // Repair recycle bin folder. + new Regex(@"^repair_recycler/.*$", RegexOptions.IgnoreCase), + + // Ignore gshade folders. Unless someone wants to handle the symlinked folder, just skip recycling them. + new Regex(@"^gshade-(shader|preset)s$", RegexOptions.IgnoreCase), + }; + + private readonly ISettings _settings; + private readonly int _maxExpansionToCheck; + private readonly bool _external; + private HttpClient _client; + private CancellationTokenSource _cancellationTokenSource = new(); + + private Dictionary _repoMetaPaths = new(); + private Dictionary _patchSources = new(); + + private Task _verificationTask; + private List> _reportedProgresses = new(); + + public int ProgressUpdateInterval { get; private set; } + public int NumBrokenFiles { get; private set; } = 0; + public string MovedFileToDir { get; private set; } = null; + public List MovedFiles { get; private set; } = new(); + public int PatchSetIndex { get; private set; } + public int PatchSetCount { get; private set; } + public int TaskIndex { get; private set; } + public long Progress { get; private set; } + public long Total { get; private set; } + public int TaskCount { get; private set; } + public IndexedZiPatchInstaller.InstallTaskState CurrentMetaInstallState { get; private set; } = IndexedZiPatchInstaller.InstallTaskState.NotStarted; + public string CurrentFile { get; private set; } + public long Speed { get; private set; } + public Exception LastException { get; private set; } + + private const string BASE_URL = "https://raw.githubusercontent.com/goatcorp/patchinfo/main/"; + + public enum VerifyState + { + NotStarted, + DownloadMeta, + VerifyAndRepair, + Done, + Cancelled, + Error + } + + private struct PatchSource + { + public FileInfo FileInfo; + public Uri Uri; + } + + private class VerifyVersions + { + [JsonPropertyName("boot")] + public string Boot { get; set; } + + [JsonPropertyName("bootRevision")] + public int BootRevision { get; set; } + + [JsonPropertyName("game")] + public string Game { get; set; } + + [JsonPropertyName("gameRevision")] + public int GameRevision { get; set; } + + [JsonPropertyName("ex1")] + public string Ex1 { get; set; } + + [JsonPropertyName("ex1Revision")] + public int Ex1Revision { get; set; } + + [JsonPropertyName("ex2")] + public string Ex2 { get; set; } + + [JsonPropertyName("ex2Revision")] + public int Ex2Revision { get; set; } + + [JsonPropertyName("ex3")] + public string Ex3 { get; set; } + + [JsonPropertyName("ex3Revision")] + public int Ex3Revision { get; set; } + + [JsonPropertyName("ex4")] + public string Ex4 { get; set; } + + [JsonPropertyName("ex4Revision")] + public int Ex4Revision { get; set; } + } + + public VerifyState State { get; private set; } = VerifyState.NotStarted; + + public PatchVerifier(ISettings settings, Launcher.LoginResult loginResult, int progressUpdateInterval, int maxExpansion, bool external = true) + { + this._settings = settings; + _client = new HttpClient(); + ProgressUpdateInterval = progressUpdateInterval; + _maxExpansionToCheck = maxExpansion; + _external = external; + + SetLoginState(loginResult); + } + + public void Start() + { + Debug.Assert(_patchSources.Count != 0); + Debug.Assert(_verificationTask == null || _verificationTask.IsCompleted); + + _cancellationTokenSource = new(); + _reportedProgresses.Clear(); + NumBrokenFiles = 0; + PatchSetIndex = 0; + PatchSetCount = 0; + TaskIndex = 0; + Progress = 0; + Total = 0; + TaskCount = 0; + CurrentFile = null; + Speed = 0; + CurrentMetaInstallState = IndexedZiPatchInstaller.InstallTaskState.NotStarted; + LastException = null; + + _verificationTask = Task.Run(this.RunVerifier, _cancellationTokenSource.Token); + } + + public Task Cancel() + { + _cancellationTokenSource.Cancel(); + return WaitForCompletion(); + } + + public Task WaitForCompletion() + { + return _verificationTask ?? Task.CompletedTask; + } + + private void SetLoginState(Launcher.LoginResult result) + { + _patchSources.Clear(); + + foreach (var patch in result.PendingPatches) + { + var repoName = patch.GetRepoName(); + if (repoName == "ffxiv") + repoName = "ex0"; + + _patchSources.Add($"{repoName}:{Path.GetFileName(patch.GetFilePath())}", new PatchSource() + { + FileInfo = new FileInfo(Path.Combine(_settings.PatchPath.FullName, patch.GetFilePath())), + Uri = new Uri(patch.Url), + }); + } + } + + private bool AdminAccessRequired(string gameRootPath) + { + string tempFn; + do + { + tempFn = Path.Combine(gameRootPath, Guid.NewGuid().ToString()); + } while (File.Exists(tempFn)); + try + { + File.WriteAllText(tempFn, ""); + File.Delete(tempFn); + } + catch (UnauthorizedAccessException) + { + return true; + } + return false; + } + + private void RecordProgressForEstimation() + { + var now = DateTime.Now.Ticks; + _reportedProgresses.Add(Tuple.Create(now, Progress)); + while ((now - _reportedProgresses.First().Item1) > 10 * 1000 * 8000) + _reportedProgresses.RemoveAt(0); + + var elapsedMs = _reportedProgresses.Last().Item1 - _reportedProgresses.First().Item1; + if (elapsedMs == 0) + Speed = 0; + else + Speed = (_reportedProgresses.Last().Item2 - _reportedProgresses.First().Item2) * 10 * 1000 * 1000 / elapsedMs; + } + + public async Task MoveUnnecessaryFiles(IIndexedZiPatchIndexInstaller installer, string gamePath, HashSet targetRelativePaths) + { + this.MovedFileToDir = Path.Combine(gamePath, REPAIR_RECYCLER_DIRECTORY, DateTime.Now.ToString("yyyyMMdd_HHmmss")); + + var rootPathInfo = new DirectoryInfo(gamePath); + gamePath = rootPathInfo.FullName; + + Queue directoriesToVisit = new(); + HashSet directoriesVisited = new(); + directoriesToVisit.Enqueue(rootPathInfo); + directoriesVisited.Add(rootPathInfo); + + while (directoriesToVisit.Any()) + { + var dir = directoriesToVisit.Dequeue(); + + // For directories, ignore if final path does not belong in the root path. + if (!dir.FullName.ToLowerInvariant().Replace('\\', '/').StartsWith(gamePath.ToLowerInvariant().Replace('\\', '/'))) + continue; + + var relativeDirPath = dir == rootPathInfo ? "" : dir.FullName.Substring(gamePath.Length + 1).Replace('\\', '/'); + if (GameIgnoreUnnecessaryFilePatterns.Any(x => x.IsMatch(relativeDirPath))) + continue; + + if (!dir.EnumerateFileSystemInfos().Any()) + { + await installer.RemoveDirectory(dir.FullName); + await installer.CreateDirectory(Path.Combine(this.MovedFileToDir, relativeDirPath)); + continue; + } + + foreach (var subdir in dir.EnumerateDirectories()) + { + if (directoriesVisited.Contains(subdir)) + continue; + + directoriesVisited.Add(subdir); + directoriesToVisit.Enqueue(subdir); + } + + foreach (var file in dir.EnumerateFiles()) + { + if (!file.FullName.ToLowerInvariant().Replace('\\', '/').StartsWith(gamePath.ToLowerInvariant().Replace('\\', '/'))) + continue; + + var relativePath = file.FullName.Substring(gamePath.Length + 1).Replace('\\', '/'); + if (targetRelativePaths.Any(x => x.Replace('\\', '/').ToLowerInvariant() == relativePath.ToLowerInvariant())) + continue; + + if (GameIgnoreUnnecessaryFilePatterns.Any(x => x.IsMatch(relativePath))) + continue; + + await installer.MoveFile(file.FullName, Path.Combine(this.MovedFileToDir, relativePath)); + MovedFiles.Add(relativePath); + } + } + } + + private async Task RunVerifier() + { + State = VerifyState.NotStarted; + LastException = null; + IIndexedZiPatchIndexInstaller indexedZiPatchIndexInstaller = null; + try + { + var assemblyLocation = AppContext.BaseDirectory; + if (_external) + indexedZiPatchIndexInstaller = new IndexedZiPatchIndexRemoteInstaller(Path.Combine(assemblyLocation!, "XIVLauncher.PatchInstaller.exe"), + AdminAccessRequired(_settings.GamePath.FullName)); + else + indexedZiPatchIndexInstaller = new IndexedZiPatchIndexLocalInstaller(); + + await indexedZiPatchIndexInstaller.SetWorkerProcessPriority(ProcessPriorityClass.Idle).ConfigureAwait(false); + + while (!_cancellationTokenSource.IsCancellationRequested && State != VerifyState.Done) + { + switch (State) + { + + case VerifyState.NotStarted: + State = VerifyState.DownloadMeta; + break; + + case VerifyState.DownloadMeta: + await this.GetPatchMeta().ConfigureAwait(false); + State = VerifyState.VerifyAndRepair; + break; + + case VerifyState.VerifyAndRepair: + Debug.Assert(_repoMetaPaths.Count != 0); + + const int MAX_CONCURRENT_CONNECTIONS_FOR_PATCH_SET = 8; + const int REATTEMPT_COUNT = 5; + + CurrentFile = null; + TaskIndex = 0; + PatchSetIndex = 0; + PatchSetCount = _repoMetaPaths.Count; + Progress = Total = 0; + + HashSet targetRelativePaths = new(); + + var bootPath = Path.Combine(_settings.GamePath.FullName, "boot"); + var gamePath = Path.Combine(_settings.GamePath.FullName, "game"); + + foreach (var metaPath in _repoMetaPaths) + { + var patchIndex = new IndexedZiPatchIndex(new BinaryReader(new DeflateStream(new FileStream(metaPath.Value, FileMode.Open, FileAccess.Read), CompressionMode.Decompress))); + var adjustedGamePath = patchIndex.ExpacVersion == IndexedZiPatchIndex.EXPAC_VERSION_BOOT ? bootPath : gamePath; + + foreach (var target in patchIndex.Targets) + targetRelativePaths.Add(target.RelativePath); + + void UpdateVerifyProgress(int targetIndex, long progress, long max) + { + CurrentFile = patchIndex[Math.Min(targetIndex, patchIndex.Length - 1)].RelativePath; + TaskIndex = targetIndex; + Progress = Math.Min(progress, max); + Total = max; + RecordProgressForEstimation(); + } + + void UpdateInstallProgress(int sourceIndex, long progress, long max, IndexedZiPatchInstaller.InstallTaskState state) + { + CurrentFile = patchIndex.Sources[Math.Min(sourceIndex, patchIndex.Sources.Count - 1)]; + TaskIndex = sourceIndex; + Progress = Math.Min(progress, max); + Total = max; + CurrentMetaInstallState = state; + RecordProgressForEstimation(); + } + + try + { + indexedZiPatchIndexInstaller.OnVerifyProgress += UpdateVerifyProgress; + indexedZiPatchIndexInstaller.OnInstallProgress += UpdateInstallProgress; + await indexedZiPatchIndexInstaller.ConstructFromPatchFile(patchIndex, ProgressUpdateInterval).ConfigureAwait(false); + + var fileBroken = new bool[patchIndex.Length].ToList(); + var repaired = false; + for (var attemptIndex = 0; attemptIndex < REATTEMPT_COUNT; attemptIndex++) + { + CurrentMetaInstallState = IndexedZiPatchInstaller.InstallTaskState.NotStarted; + + TaskCount = patchIndex.Length; + Progress = Total = TaskIndex = 0; + _reportedProgresses.Clear(); + + await indexedZiPatchIndexInstaller.SetTargetStreamsFromPathReadOnly(adjustedGamePath).ConfigureAwait(false); + // TODO: check one at a time if random access is slow? + await indexedZiPatchIndexInstaller.VerifyFiles(attemptIndex > 0, Environment.ProcessorCount, _cancellationTokenSource.Token).ConfigureAwait(false); + + var missingPartIndicesPerTargetFile = await indexedZiPatchIndexInstaller.GetMissingPartIndicesPerTargetFile().ConfigureAwait(false); + if ((repaired = missingPartIndicesPerTargetFile.All(x => !x.Any()))) + break; + else if (attemptIndex == 1) + Log.Warning("One or more of local copies of patch files seem to be corrupt, if any. Ignoring local patch files for further attempts."); + + for (var i = 0; i < missingPartIndicesPerTargetFile.Count; i++) + if (missingPartIndicesPerTargetFile[i].Any()) + fileBroken[i] = true; + + TaskCount = patchIndex.Sources.Count; + Progress = Total = TaskIndex = 0; + _reportedProgresses.Clear(); + var missing = await indexedZiPatchIndexInstaller.GetMissingPartIndicesPerPatch().ConfigureAwait(false); + + await indexedZiPatchIndexInstaller.SetTargetStreamsFromPathReadWriteForMissingFiles(adjustedGamePath).ConfigureAwait(false); + var prefix = patchIndex.ExpacVersion == IndexedZiPatchIndex.EXPAC_VERSION_BOOT ? "boot:" : $"ex{patchIndex.ExpacVersion}:"; + for (var i = 0; i < patchIndex.Sources.Count; i++) + { + var patchSourceKey = prefix + patchIndex.Sources[i]; + + if (!missing[i].Any()) + continue; + else + Log.Information("Looking for patch file {0} (key: \"{1}\")", patchIndex.Sources[i], patchSourceKey); + + if (!_patchSources.TryGetValue(patchSourceKey, out var source)) + throw new InvalidOperationException($"Key \"{patchSourceKey}\" not found in _patchSources"); + + // We might be trying again because local copy of the patch file might be corrupt, so refer to the local copy only for the first attempt. + if (attemptIndex == 0 && source.FileInfo.Exists) + await indexedZiPatchIndexInstaller.QueueInstall(i, source.FileInfo, MAX_CONCURRENT_CONNECTIONS_FOR_PATCH_SET).ConfigureAwait(false); + else + await indexedZiPatchIndexInstaller.QueueInstall(i, source.Uri, null, MAX_CONCURRENT_CONNECTIONS_FOR_PATCH_SET).ConfigureAwait(false); + } + + CurrentMetaInstallState = IndexedZiPatchInstaller.InstallTaskState.Connecting; + try + { + await indexedZiPatchIndexInstaller.Install(MAX_CONCURRENT_CONNECTIONS_FOR_PATCH_SET, _cancellationTokenSource.Token).ConfigureAwait(false); + } + catch (Exception e) + { + Log.Error(e, "IndexedZiPatchIndexInstaller.Install"); + if (attemptIndex == REATTEMPT_COUNT - 1) + throw; + } + } + + if (!repaired) + throw new IOException($"Failed to repair after {REATTEMPT_COUNT} attempts"); + + await indexedZiPatchIndexInstaller.WriteVersionFiles(adjustedGamePath).ConfigureAwait(false); + + NumBrokenFiles += fileBroken.Count(x => x); + PatchSetIndex++; + } + finally + { + indexedZiPatchIndexInstaller.OnVerifyProgress -= UpdateVerifyProgress; + indexedZiPatchIndexInstaller.OnInstallProgress -= UpdateInstallProgress; + } + } + + await MoveUnnecessaryFiles(indexedZiPatchIndexInstaller, gamePath, targetRelativePaths); + + State = VerifyState.Done; + break; + + case VerifyState.Done: + break; + + default: + throw new ArgumentOutOfRangeException(); + } + } + } + catch (Exception ex) + { + if (ex is OperationCanceledException) + State = VerifyState.Cancelled; + else if (_cancellationTokenSource.IsCancellationRequested) + State = VerifyState.Cancelled; + else if (ex is Win32Exception winex && (uint)winex.HResult == 0x80004005u) // The operation was canceled by the user (UAC dialog cancellation) + State = VerifyState.Cancelled; + else + { + Log.Error(ex, "Unexpected error occurred in RunVerifier"); + Log.Information("_patchSources had following:"); + foreach (var kvp in _patchSources) + { + Log.Information("* \"{0}\" = {1} / {2}({3})", kvp.Key, kvp.Value.Uri.ToString(), kvp.Value.FileInfo.FullName, kvp.Value.FileInfo.Exists ? "Exists" : "Nonexistent"); + } + + LastException = ex; + State = VerifyState.Error; + } + } + finally + { + indexedZiPatchIndexInstaller?.Dispose(); + } + } + + private async Task GetPatchMeta() + { + PatchSetCount = 6; + PatchSetIndex = 0; + + _repoMetaPaths.Clear(); + + var metaFolder = Path.Combine(Paths.RoamingPath, "patchMeta"); + Directory.CreateDirectory(metaFolder); + + CurrentFile = "latest.json"; + Total = Progress = 0; + + var latestVersionJson = await _client.GetStringAsync(BASE_URL + "latest.json").ConfigureAwait(false); + _cancellationTokenSource.Token.ThrowIfCancellationRequested(); + + var latestVersion = JsonSerializer.Deserialize(latestVersionJson); + + PatchSetIndex++; + await this.GetRepoMeta(Repository.Ffxiv, latestVersion.Game, metaFolder, latestVersion.GameRevision).ConfigureAwait(false); + _cancellationTokenSource.Token.ThrowIfCancellationRequested(); + + PatchSetIndex++; + if (_maxExpansionToCheck >= 1) + await this.GetRepoMeta(Repository.Ex1, latestVersion.Ex1, metaFolder, latestVersion.Ex1Revision).ConfigureAwait(false); + _cancellationTokenSource.Token.ThrowIfCancellationRequested(); + + PatchSetIndex++; + if (_maxExpansionToCheck >= 2) + await this.GetRepoMeta(Repository.Ex2, latestVersion.Ex2, metaFolder, latestVersion.Ex2Revision).ConfigureAwait(false); + _cancellationTokenSource.Token.ThrowIfCancellationRequested(); + + PatchSetIndex++; + if (_maxExpansionToCheck >= 3) + await this.GetRepoMeta(Repository.Ex3, latestVersion.Ex3, metaFolder, latestVersion.Ex3Revision).ConfigureAwait(false); + _cancellationTokenSource.Token.ThrowIfCancellationRequested(); + + PatchSetIndex++; + if (_maxExpansionToCheck >= 4) + await this.GetRepoMeta(Repository.Ex4, latestVersion.Ex4, metaFolder, latestVersion.Ex4Revision).ConfigureAwait(false); + _cancellationTokenSource.Token.ThrowIfCancellationRequested(); + + PatchSetIndex++; + } + + private async Task GetRepoMeta(Repository repo, string latestVersion, string baseDir, int patchIndexFileRevision) + { + _reportedProgresses.Clear(); + CurrentFile = latestVersion; + Total = 32 * 1048576; + Progress = 0; + + var version = repo.GetVer(_settings.GamePath); + + // TODO: We should not assume that this always has a "D". We should just store them by the patchlist VersionId instead. + var repoShorthand = repo == Repository.Ffxiv ? "game" : repo.ToString().ToLower(); + var fileName = $"{latestVersion}.patch.index"; + + var metaPath = Path.Combine(baseDir, repoShorthand); + var filePath = Path.Combine(metaPath, fileName) + (patchIndexFileRevision > 0 ? $".v{patchIndexFileRevision}" : ""); + Directory.CreateDirectory(metaPath); + + if (!File.Exists(filePath)) + { + var request = await _client.GetAsync($"{BASE_URL}{repoShorthand}/{fileName}", HttpCompletionOption.ResponseHeadersRead, _cancellationTokenSource.Token).ConfigureAwait(false); + if (request.StatusCode == HttpStatusCode.NotFound) + throw new NoVersionReferenceException(repo, latestVersion); + + request.EnsureSuccessStatusCode(); + + Total = request.Content.Headers.ContentLength.GetValueOrDefault(Total); + + var tempFile = new FileInfo(filePath + ".tmp"); + var complete = false; + + try + { + using var sourceStream = await request.Content.ReadAsStreamAsync().ConfigureAwait(false); + using var buffer = ReusableByteBufferManager.GetBuffer(); + + using (var targetStream = tempFile.OpenWrite()) + { + while (true) + { + _cancellationTokenSource.Token.ThrowIfCancellationRequested(); + + int read = await sourceStream.ReadAsync(buffer.Buffer, 0, buffer.Buffer.Length, _cancellationTokenSource.Token).ConfigureAwait(false); + if (read == 0) + break; + + Total = Math.Max(Total, Progress + read); + Progress += read; + RecordProgressForEstimation(); + await targetStream.WriteAsync(buffer.Buffer, 0, read, _cancellationTokenSource.Token).ConfigureAwait(false); + } + } + complete = true; + } + finally + { + if (complete) + tempFile.MoveTo(filePath); + else + { + try + { + if (tempFile.Exists) + tempFile.Delete(); + } + catch (Exception ex) + { + Log.Warning(ex, "Failed to delete temp file at {0}", tempFile.FullName); + } + } + } + } + + _repoMetaPaths.Add(repo, filePath); + Log.Verbose("Downloaded patch index for {Repo}({Version})", repo, latestVersion); + } + + public static List GetRelevantFiles(string gamePath) + { + var rootPathInfo = new DirectoryInfo(gamePath); + gamePath = rootPathInfo.FullName; + + Queue directoriesToVisit = new(); + HashSet directoriesVisited = new(); + directoriesToVisit.Enqueue(rootPathInfo); + directoriesVisited.Add(rootPathInfo); + + List files = new(); + + while (directoriesToVisit.Any()) + { + var dir = directoriesToVisit.Dequeue(); + + // For directories, ignore if final path does not belong in the root path. + if (!dir.FullName.ToLowerInvariant().Replace('\\', '/').StartsWith(gamePath.ToLowerInvariant().Replace('\\', '/'), StringComparison.Ordinal)) + continue; + + var relativeDirPath = dir == rootPathInfo ? "" : dir.FullName.Substring(gamePath.Length + 1).Replace('\\', '/'); + if (GameIgnoreUnnecessaryFilePatterns.Any(x => x.IsMatch(relativeDirPath))) + continue; + + foreach (var subdir in dir.EnumerateDirectories()) + { + if (directoriesVisited.Contains(subdir)) + continue; + + directoriesVisited.Add(subdir); + directoriesToVisit.Enqueue(subdir); + } + + foreach (var file in dir.EnumerateFiles()) + { + if (!file.FullName.ToLowerInvariant().Replace('\\', '/').StartsWith(gamePath.ToLowerInvariant().Replace('\\', '/'), StringComparison.Ordinal)) + continue; + + var relativePath = file.FullName.Substring(gamePath.Length + 1).Replace('\\', '/'); + + if (GameIgnoreUnnecessaryFilePatterns.Any(x => x.IsMatch(relativePath))) + continue; + + files.Add(file); + } + } + + return files; + } + + public void Dispose() + { + if (_verificationTask != null && !_verificationTask.IsCompleted) + { + _cancellationTokenSource.Cancel(); + _verificationTask.Wait(); + } + } + } +} diff --git a/src/XIVLauncher2.Common/Http/HttpServer.cs b/src/XIVLauncher2.Common/Http/HttpServer.cs new file mode 100644 index 0000000..b0c574a --- /dev/null +++ b/src/XIVLauncher2.Common/Http/HttpServer.cs @@ -0,0 +1,90 @@ +using System; +using System.Diagnostics; +using System.Net; +using System.Net.Sockets; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading; + +namespace XIVLauncher2.Common.Http +{ + // This is a very dumb HTTP server that just accepts GETs and fires events with the requested URL + internal class HttpServer + { + private readonly TcpListener listener; + + private readonly byte[] httpResponse; + + public EventHandler GetReceived; + + private bool _isRunning = false; + + public class HttpServerGetEvent + { + public string Path { get; set; } + } + + public HttpServer(int port, string version) + { + this.listener = new TcpListener(IPAddress.Any, port); + + this.httpResponse = Encoding.Default.GetBytes( + "HTTP/1.0 200 OK\n" + + "Content-Type: application/json; charset=UTF-8\n" + + "\n{\"app\":\"XIVLauncher\", \"version\":\"" + version + "\"}" + ); + } + + public void Start() + { + try + { + this.listener.Start(); + _isRunning = true; + + while (_isRunning) + { + if (!this.listener.Pending()) + { + Thread.Sleep(200); + continue; + } + + var client = this.listener.AcceptTcpClient(); + + while (client.Connected) + { + var networkStream = client.GetStream(); + + var message = new byte[1024]; + networkStream.Read(message, 0, message.Length); + + var messageString = Encoding.Default.GetString(message); + Debug.WriteLine(Encoding.Default.GetString(message)); + + networkStream.Write(httpResponse, 0, httpResponse.Length); + + networkStream.Close(3); + + GetReceived?.Invoke(this, new HttpServerGetEvent + { + Path = Regex.Match(messageString, "GET (?.+) HTTP").Groups["url"].Value + }); + } + + client.Close(); + } + } + catch + { + // ignored + } + } + + public void Stop() + { + _isRunning = false; + this.listener.Stop(); + } + } +} diff --git a/src/XIVLauncher2.Common/Http/OtpListener.cs b/src/XIVLauncher2.Common/Http/OtpListener.cs new file mode 100644 index 0000000..847dd90 --- /dev/null +++ b/src/XIVLauncher2.Common/Http/OtpListener.cs @@ -0,0 +1,46 @@ +using System; +using System.Threading; + +namespace XIVLauncher2.Common.Http +{ + public class OtpListener + { + private volatile HttpServer server; + + private const int HTTP_PORT = 4646; + + public event LoginEvent OnOtpReceived; + + public delegate void LoginEvent(string onetimePassword); + + private readonly Thread serverThread; + + public OtpListener(string version) + { + this.server = new HttpServer(HTTP_PORT, version); + this.server.GetReceived += this.GetReceived; + + this.serverThread = new Thread(this.server.Start) { Name = "OtpListenerServerThread", IsBackground = true }; + } + + private void GetReceived(object sender, HttpServer.HttpServerGetEvent e) + { + if (e.Path.StartsWith("/ffxivlauncher/", StringComparison.Ordinal)) + { + var otp = e.Path.Substring(15); + + OnOtpReceived?.Invoke(otp); + } + } + + public void Start() + { + this.serverThread.Start(); + } + + public void Stop() + { + this.server?.Stop(); + } + } +} diff --git a/src/XIVLauncher2.Common/Patching/IndexedZiPatch/IIndexedZiPatchIndexInstaller.cs b/src/XIVLauncher2.Common/Patching/IndexedZiPatch/IIndexedZiPatchIndexInstaller.cs new file mode 100644 index 0000000..cce3086 --- /dev/null +++ b/src/XIVLauncher2.Common/Patching/IndexedZiPatch/IIndexedZiPatchIndexInstaller.cs @@ -0,0 +1,53 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace XIVLauncher2.Common.Patching.IndexedZiPatch +{ + public interface IIndexedZiPatchIndexInstaller : IDisposable + { + public event IndexedZiPatchInstaller.OnInstallProgressDelegate OnInstallProgress; + public event IndexedZiPatchInstaller.OnVerifyProgressDelegate OnVerifyProgress; + + public Task ConstructFromPatchFile(IndexedZiPatchIndex patchIndex, int progressReportInterval = 250); + + public Task VerifyFiles(bool refine = false, int concurrentCount = 8, CancellationToken? cancellationToken = null); + + public Task MarkFileAsMissing(int targetIndex, CancellationToken? cancellationToken = null); + + public Task SetTargetStreamFromPathReadOnly(int targetIndex, string path, CancellationToken? cancellationToken = null); + + public Task SetTargetStreamFromPathReadWrite(int targetIndex, string path, CancellationToken? cancellationToken = null); + + public Task SetTargetStreamsFromPathReadOnly(string rootPath, CancellationToken? cancellationToken = null); + + public Task SetTargetStreamsFromPathReadWriteForMissingFiles(string rootPath, CancellationToken? cancellationToken = null); + + public Task RepairNonPatchData(CancellationToken? cancellationToken = null); + + public Task WriteVersionFiles(string rootPath, CancellationToken? cancellationToken = null); + + public Task QueueInstall(int sourceIndex, Uri sourceUrl, string sid, int splitBy = 8, CancellationToken? cancellationToken = null); + + public Task QueueInstall(int sourceIndex, FileInfo sourceFile, int splitBy = 8, CancellationToken? cancellationToken = null); + + public Task Install(int concurrentCount, CancellationToken? cancellationToken = null); + + public Task>>> GetMissingPartIndicesPerPatch(CancellationToken? cancellationToken = null); + + public Task>> GetMissingPartIndicesPerTargetFile(CancellationToken? cancellationToken = null); + + public Task> GetSizeMismatchTargetFileIndices(CancellationToken? cancellationToken = null); + + public Task SetWorkerProcessPriority(ProcessPriorityClass subprocessPriority, CancellationToken? cancellationToken = null); + + public Task MoveFile(string sourceFile, string targetFile, CancellationToken? cancellationToken = null); + + public Task CreateDirectory(string dir, CancellationToken? cancellationToken = null); + + public Task RemoveDirectory(string dir, bool recursive = false, CancellationToken? cancellationToken = null); + } +} diff --git a/src/XIVLauncher2.Common/Patching/IndexedZiPatch/IndexedZiPatchIndex.cs b/src/XIVLauncher2.Common/Patching/IndexedZiPatch/IndexedZiPatchIndex.cs new file mode 100644 index 0000000..a7fdefa --- /dev/null +++ b/src/XIVLauncher2.Common/Patching/IndexedZiPatch/IndexedZiPatchIndex.cs @@ -0,0 +1,325 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using XIVLauncher2.Common.Patching.ZiPatch; +using XIVLauncher2.Common.Patching.ZiPatch.Chunk; +using XIVLauncher2.Common.Patching.ZiPatch.Chunk.SqpkCommand; +using XIVLauncher2.Common.Patching.ZiPatch.Util; + +namespace XIVLauncher2.Common.Patching.IndexedZiPatch +{ + public class IndexedZiPatchIndex + { + public const int EXPAC_VERSION_BOOT = -1; + public const int EXPAC_VERSION_BASE_GAME = 0; + + public readonly int ExpacVersion; + + private readonly List sourceFiles = new(); + private readonly List sourceFileLastPtr = new(); + private readonly List targetFiles = new(); + private readonly List>> sourceFilePartsCache = new(); + + public IndexedZiPatchIndex(int expacVersion) + { + ExpacVersion = expacVersion; + } + + public IndexedZiPatchIndex(BinaryReader reader, bool disposeReader = true) + { + try + { + ExpacVersion = reader.ReadInt32(); + + for (int i = 0, readIndex = reader.ReadInt32(); i < readIndex; i++) + this.sourceFiles.Add(reader.ReadString()); + foreach (var _ in this.sourceFiles) + this.sourceFileLastPtr.Add(reader.ReadInt32()); + + for (int i = 0, readIndex = reader.ReadInt32(); i < readIndex; i++) + this.targetFiles.Add(new IndexedZiPatchTargetFile(reader, false)); + } + finally + { + if (disposeReader) + { + reader.Dispose(); + } + } + } + + public IList Sources => this.sourceFiles.AsReadOnly(); + public int GetSourceLastPtr(int index) => this.sourceFileLastPtr[index]; + public IList Targets => this.targetFiles.AsReadOnly(); + + public IList>> SourceParts + { + get + { + for (var sourceFileIndex = this.sourceFilePartsCache.Count; sourceFileIndex < this.sourceFiles.Count; sourceFileIndex++) + { + var list = new List>(); + for (var i = 0; i < this.targetFiles.Count; i++) + for (var j = 0; j < this.targetFiles[i].Count; j++) + if (this.targetFiles[i][j].SourceIndex == sourceFileIndex) + list.Add(Tuple.Create(i, j)); + list.Sort((x, y) => this.targetFiles[x.Item1][x.Item2].SourceOffset.CompareTo(this.targetFiles[y.Item1][y.Item2].SourceOffset)); + this.sourceFilePartsCache.Add(list.AsReadOnly()); + } + + return this.sourceFilePartsCache.AsReadOnly(); + } + } + + public IndexedZiPatchTargetFile this[int index] => this.targetFiles[index]; + public IndexedZiPatchTargetFile this[string name] => this.targetFiles[IndexOf(name)]; + public int IndexOf(string name) => this.targetFiles.FindIndex(x => x.RelativePath == NormalizePath(name)); + public int Length => this.targetFiles.Count; + public string VersionName => this.sourceFiles.Last().Substring(1, this.sourceFiles.Last().Length - 7); + public string VersionFileBase => ExpacVersion == EXPAC_VERSION_BOOT ? "ffxivboot" : ExpacVersion == EXPAC_VERSION_BASE_GAME ? "ffxivgame" : $"sqpack/ex{ExpacVersion}/ex{ExpacVersion}"; + public string VersionFileVer => VersionFileBase + ".ver"; + public string VersionFileBck => VersionFileBase + ".bck"; + + private void ReassignTargetIndices() + { + for (int i = 0; i < this.targetFiles.Count; i++) + { + for (var j = 0; j < this.targetFiles[i].Count; j++) + { + var obj = this.targetFiles[i][j]; + obj.TargetIndex = i; + this.targetFiles[i][j] = obj; + } + } + } + + private Tuple AllocFile(string target) + { + target = NormalizePath(target); + var targetFileIndex = IndexOf(target); + if (targetFileIndex == -1) + { + this.targetFiles.Add(new(target)); + targetFileIndex = this.targetFiles.Count - 1; + } + return Tuple.Create(targetFileIndex, this.targetFiles[targetFileIndex]); + } + + public async Task ApplyZiPatch(string patchFileName, ZiPatchFile patchFile, CancellationToken? cancellationToken = null) + { + await Task.Run(() => + { + var sourceIndex = this.sourceFiles.Count; + this.sourceFiles.Add(patchFileName); + this.sourceFileLastPtr.Add(0); + this.sourceFilePartsCache.Clear(); + + var platform = ZiPatchConfig.PlatformId.Win32; + foreach (var patchChunk in patchFile.GetChunks()) + { + if (cancellationToken.HasValue) + cancellationToken.Value.ThrowIfCancellationRequested(); + + if (patchChunk is DeleteDirectoryChunk deleteDirectoryChunk) + { + var prefix = NormalizePath(deleteDirectoryChunk.DirName.ToLowerInvariant()); + this.targetFiles.RemoveAll(x => x.RelativePath.ToLowerInvariant().StartsWith(prefix)); + ReassignTargetIndices(); + } + else if (patchChunk is SqpkTargetInfo sqpkTargetInfo) + { + platform = sqpkTargetInfo.Platform; + } + else if (patchChunk is SqpkFile sqpkFile) + { + switch (sqpkFile.Operation) + { + case SqpkFile.OperationKind.AddFile: + var (targetIndex, file) = AllocFile(sqpkFile.TargetFile.RelativePath); + if (sqpkFile.FileOffset == 0) + file.Clear(); + + var offset = sqpkFile.FileOffset; + for (var i = 0; i < sqpkFile.CompressedData.Count; ++i) + { + if (cancellationToken.HasValue) + cancellationToken.Value.ThrowIfCancellationRequested(); + + var block = sqpkFile.CompressedData[i]; + var dataOffset = (int)sqpkFile.CompressedDataSourceOffsets[i]; + if (block.IsCompressed) + { + file.Update(new IndexedZiPatchPartLocator + { + TargetOffset = offset, + TargetSize = block.DecompressedSize, + TargetIndex = targetIndex, + SourceIndex = sourceIndex, + SourceOffset = dataOffset, + IsDeflatedBlockData = true, + }); + this.sourceFileLastPtr[this.sourceFileLastPtr.Count - 1] = dataOffset + block.CompressedSize; + } + else + { + file.Update(new IndexedZiPatchPartLocator + { + TargetOffset = offset, + TargetSize = block.DecompressedSize, + TargetIndex = targetIndex, + SourceIndex = sourceIndex, + SourceOffset = dataOffset, + }); + this.sourceFileLastPtr[this.sourceFileLastPtr.Count - 1] = dataOffset + block.DecompressedSize; + } + offset += block.DecompressedSize; + } + + break; + + case SqpkFile.OperationKind.RemoveAll: + var xpacPath = SqexFile.GetExpansionFolder((byte)sqpkFile.ExpansionId); + + this.targetFiles.RemoveAll(x => x.RelativePath.ToLowerInvariant().StartsWith($"sqpack/{xpacPath}")); + this.targetFiles.RemoveAll(x => x.RelativePath.ToLowerInvariant().StartsWith($"movie/{xpacPath}")); + ReassignTargetIndices(); + break; + + case SqpkFile.OperationKind.DeleteFile: + this.targetFiles.RemoveAll(x => x.RelativePath.ToLowerInvariant() == sqpkFile.TargetFile.RelativePath.ToLowerInvariant()); + ReassignTargetIndices(); + break; + } + } + else if (patchChunk is SqpkAddData sqpkAddData) + { + sqpkAddData.TargetFile.ResolvePath(platform); + var (targetIndex, file) = AllocFile(sqpkAddData.TargetFile.RelativePath); + file.Update(new IndexedZiPatchPartLocator + { + TargetOffset = sqpkAddData.BlockOffset, + TargetSize = sqpkAddData.BlockNumber, + TargetIndex = targetIndex, + SourceIndex = sourceIndex, + SourceOffset = sqpkAddData.BlockDataSourceOffset, + Crc32OrPlaceholderEntryDataUnits = (uint)(sqpkAddData.BlockNumber >> 7) - 1, + }); + this.sourceFileLastPtr[this.sourceFileLastPtr.Count - 1] = (int)(sqpkAddData.BlockDataSourceOffset + sqpkAddData.BlockNumber); + file.Update(new IndexedZiPatchPartLocator + { + TargetOffset = sqpkAddData.BlockOffset + sqpkAddData.BlockNumber, + TargetSize = sqpkAddData.BlockDeleteNumber, + TargetIndex = targetIndex, + SourceIndex = IndexedZiPatchPartLocator.SOURCE_INDEX_ZEROS, + Crc32OrPlaceholderEntryDataUnits = (uint)(sqpkAddData.BlockDeleteNumber >> 7) - 1, + }); + } + else if (patchChunk is SqpkDeleteData sqpkDeleteData) + { + sqpkDeleteData.TargetFile.ResolvePath(platform); + var (targetIndex, file) = AllocFile(sqpkDeleteData.TargetFile.RelativePath); + if (sqpkDeleteData.BlockNumber > 0) + { + file.Update(new IndexedZiPatchPartLocator + { + TargetOffset = sqpkDeleteData.BlockOffset, + TargetSize = 1 << 7, + TargetIndex = targetIndex, + SourceIndex = IndexedZiPatchPartLocator.SOURCE_INDEX_EMPTY_BLOCK, + Crc32OrPlaceholderEntryDataUnits = (uint)sqpkDeleteData.BlockNumber - 1, + }); + if (sqpkDeleteData.BlockNumber > 1) + { + file.Update(new IndexedZiPatchPartLocator + { + TargetOffset = sqpkDeleteData.BlockOffset + (1 << 7), + TargetSize = (sqpkDeleteData.BlockNumber - 1) << 7, + TargetIndex = targetIndex, + SourceIndex = IndexedZiPatchPartLocator.SOURCE_INDEX_ZEROS, + }); + } + } + } + else if (patchChunk is SqpkExpandData sqpkExpandData) + { + sqpkExpandData.TargetFile.ResolvePath(platform); + var (targetIndex, file) = AllocFile(sqpkExpandData.TargetFile.RelativePath); + if (sqpkExpandData.BlockNumber > 0) + { + file.Update(new IndexedZiPatchPartLocator + { + TargetOffset = sqpkExpandData.BlockOffset, + TargetSize = 1 << 7, + TargetIndex = targetIndex, + SourceIndex = IndexedZiPatchPartLocator.SOURCE_INDEX_EMPTY_BLOCK, + Crc32OrPlaceholderEntryDataUnits = (uint)sqpkExpandData.BlockNumber - 1, + }); + if (sqpkExpandData.BlockNumber > 1) + { + file.Update(new IndexedZiPatchPartLocator + { + TargetOffset = sqpkExpandData.BlockOffset + (1 << 7), + TargetSize = (sqpkExpandData.BlockNumber - 1) << 7, + TargetIndex = targetIndex, + SourceIndex = IndexedZiPatchPartLocator.SOURCE_INDEX_ZEROS, + }); + } + } + } + else if (patchChunk is SqpkHeader sqpkHeader) + { + sqpkHeader.TargetFile.ResolvePath(platform); + var (targetIndex, file) = AllocFile(sqpkHeader.TargetFile.RelativePath); + file.Update(new IndexedZiPatchPartLocator + { + TargetOffset = sqpkHeader.HeaderKind == SqpkHeader.TargetHeaderKind.Version ? 0 : SqpkHeader.HEADER_SIZE, + TargetSize = SqpkHeader.HEADER_SIZE, + TargetIndex = targetIndex, + SourceIndex = sourceIndex, + SourceOffset = sqpkHeader.HeaderDataSourceOffset, + }); + this.sourceFileLastPtr[this.sourceFileLastPtr.Count - 1] = (int)(sqpkHeader.HeaderDataSourceOffset + SqpkHeader.HEADER_SIZE); + } + } + }); + } + + public async Task CalculateCrc32(List sources, CancellationToken? cancellationToken = null) + { + foreach (var file in this.targetFiles) + { + if (cancellationToken.HasValue) + cancellationToken.Value.ThrowIfCancellationRequested(); + await file.CalculateCrc32(sources, cancellationToken); + } + } + + public void WriteTo(BinaryWriter writer) + { + writer.Write(ExpacVersion); + + writer.Write(this.sourceFiles.Count); + foreach (var file in this.sourceFiles) + writer.Write(file); + foreach (var file in this.sourceFileLastPtr) + writer.Write(file); + + writer.Write(this.targetFiles.Count); + foreach (var file in this.targetFiles) + file.WriteTo(writer); + } + + private static string NormalizePath(string path) + { + if (path == "") + return path; + path = path.Replace("\\", "/"); + while (path[0] == '/') + path = path.Substring(1); + return path; + } + } +} diff --git a/src/XIVLauncher2.Common/Patching/IndexedZiPatch/IndexedZiPatchIndexLocalInstaller.cs b/src/XIVLauncher2.Common/Patching/IndexedZiPatch/IndexedZiPatchIndexLocalInstaller.cs new file mode 100644 index 0000000..b93ead4 --- /dev/null +++ b/src/XIVLauncher2.Common/Patching/IndexedZiPatch/IndexedZiPatchIndexLocalInstaller.cs @@ -0,0 +1,156 @@ +using Serilog; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace XIVLauncher2.Common.Patching.IndexedZiPatch +{ + public class IndexedZiPatchIndexLocalInstaller : IIndexedZiPatchIndexInstaller + { + private int cancellationTokenCounter = 1; + private long lastProgressUpdateCounter = 0; + private bool isDisposed = false; + private IndexedZiPatchInstaller? instance; + + public event IndexedZiPatchInstaller.OnInstallProgressDelegate OnInstallProgress; + public event IndexedZiPatchInstaller.OnVerifyProgressDelegate OnVerifyProgress; + + public IndexedZiPatchIndexLocalInstaller() + { + this.instance = null; + } + + public void Dispose() + { + if (this.isDisposed) + throw new ObjectDisposedException(GetType().FullName); + + this.isDisposed = true; + } + + public Task ConstructFromPatchFile(IndexedZiPatchIndex patchIndex, int progressReportInterval = 250) + { + this.instance?.Dispose(); + this.instance = new(patchIndex) + { + ProgressReportInterval = progressReportInterval, + }; + this.instance.OnInstallProgress += OnInstallProgress; + this.instance.OnVerifyProgress += OnVerifyProgress; + return Task.CompletedTask; + } + + public async Task VerifyFiles(bool refine = false, int concurrentCount = 8, CancellationToken? cancellationToken = null) + { + await this.instance.VerifyFiles(refine, concurrentCount, cancellationToken); + } + + public Task MarkFileAsMissing(int targetIndex, CancellationToken? cancellationToken = null) + { + this.instance.MarkFileAsMissing(targetIndex); + return Task.CompletedTask; + } + + public Task SetTargetStreamFromPathReadOnly(int targetIndex, string path, CancellationToken? cancellationToken = null) + { + this.instance.SetTargetStreamForRead(targetIndex, new FileStream(path, FileMode.Open, FileAccess.Read)); + return Task.CompletedTask; + } + + public Task SetTargetStreamFromPathReadWrite(int targetIndex, string path, CancellationToken? cancellationToken = null) + { + this.instance.SetTargetStreamForWriteFromFile(targetIndex, new FileInfo(path)); + return Task.CompletedTask; + } + + public Task SetTargetStreamsFromPathReadOnly(string rootPath, CancellationToken? cancellationToken = null) + { + this.instance.SetTargetStreamsFromPathReadOnly(rootPath); + return Task.CompletedTask; + } + + public Task SetTargetStreamsFromPathReadWriteForMissingFiles(string rootPath, CancellationToken? cancellationToken = null) + { + this.instance.SetTargetStreamsFromPathReadWriteForMissingFiles(rootPath); + return Task.CompletedTask; + } + + public async Task RepairNonPatchData(CancellationToken? cancellationToken = null) + { + await this.instance.RepairNonPatchData(cancellationToken); + } + + public Task WriteVersionFiles(string rootPath, CancellationToken? cancellationToken = null) + { + this.instance.WriteVersionFiles(rootPath); + return Task.CompletedTask; + } + + public Task QueueInstall(int sourceIndex, Uri sourceUrl, string sid, int splitBy = 8, CancellationToken? cancellationToken = null) + { + this.instance.QueueInstall(sourceIndex, sourceUrl.OriginalString, sid, splitBy); + return Task.CompletedTask; + } + + public Task QueueInstall(int sourceIndex, FileInfo sourceFile, int splitBy = 8, CancellationToken? cancellationToken = null) + { + this.instance.QueueInstall(sourceIndex, sourceFile, splitBy); + return Task.CompletedTask; + } + + public async Task Install(int concurrentCount, CancellationToken? cancellationToken = null) + { + await this.instance.Install(concurrentCount, cancellationToken); + } + + public Task>>> GetMissingPartIndicesPerPatch(CancellationToken? cancellationToken = null) + { + return Task.FromResult(this.instance.MissingPartIndicesPerPatch); + } + + public Task>> GetMissingPartIndicesPerTargetFile(CancellationToken? cancellationToken = null) + { + return Task.FromResult(this.instance.MissingPartIndicesPerTargetFile); + } + + public Task> GetSizeMismatchTargetFileIndices(CancellationToken? cancellationToken = null) + { + return Task.FromResult(this.instance.SizeMismatchTargetFileIndices); + } + + public Task SetWorkerProcessPriority(ProcessPriorityClass subprocessPriority, CancellationToken? cancellationToken = null) + { + return Task.CompletedTask; // is a no-op locally + } + + public Task MoveFile(string sourceFile, string targetFile, CancellationToken? cancellationToken = null) + { + var sourceParentDir = new DirectoryInfo(Path.GetDirectoryName(sourceFile)); + var targetParentDir = new DirectoryInfo(Path.GetDirectoryName(targetFile)); + + targetParentDir.Create(); + new FileInfo(sourceFile).MoveTo(targetFile); + + if (!sourceParentDir.GetFileSystemInfos().Any()) + sourceParentDir.Delete(false); + + return Task.CompletedTask; + } + + public Task CreateDirectory(string dir, CancellationToken? cancellationToken = null) + { + new DirectoryInfo(dir).Create(); + return Task.CompletedTask; + } + + public Task RemoveDirectory(string dir, bool recursive = false, CancellationToken? cancellationToken = null) + { + new DirectoryInfo(dir).Delete(recursive); + return Task.CompletedTask; + } + } +} diff --git a/src/XIVLauncher2.Common/Patching/IndexedZiPatch/IndexedZiPatchIndexRemoteInstaller.cs b/src/XIVLauncher2.Common/Patching/IndexedZiPatch/IndexedZiPatchIndexRemoteInstaller.cs new file mode 100644 index 0000000..e7c91c0 --- /dev/null +++ b/src/XIVLauncher2.Common/Patching/IndexedZiPatch/IndexedZiPatchIndexRemoteInstaller.cs @@ -0,0 +1,701 @@ +using Serilog; +using SharedMemory; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.IO.Compression; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace XIVLauncher2.Common.Patching.IndexedZiPatch +{ + public class IndexedZiPatchIndexRemoteInstaller : IIndexedZiPatchIndexInstaller + { + private readonly Process workerProcess; + private readonly RpcBuffer subprocessBuffer; + private int cancellationTokenCounter = 1; + private long lastProgressUpdateCounter = 0; + private bool isDisposed = false; + + public event IndexedZiPatchInstaller.OnInstallProgressDelegate OnInstallProgress; + public event IndexedZiPatchInstaller.OnVerifyProgressDelegate OnVerifyProgress; + + public IndexedZiPatchIndexRemoteInstaller(string workerExecutablePath, bool asAdmin) + { + var rpcChannelName = "RemoteZiPatchIndexInstaller" + Guid.NewGuid().ToString(); + this.subprocessBuffer = new RpcBuffer(rpcChannelName, RpcResponseHandler); + + if (workerExecutablePath != null) + { + this.workerProcess = new(); + this.workerProcess.StartInfo.FileName = workerExecutablePath; + this.workerProcess.StartInfo.UseShellExecute = true; + this.workerProcess.StartInfo.Verb = asAdmin ? "runas" : "open"; + this.workerProcess.StartInfo.Arguments = $"index-rpc {Process.GetCurrentProcess().Id} {rpcChannelName}"; + this.workerProcess.Start(); + } + else + { + this.workerProcess = null; + Task.Run(() => new WorkerSubprocessBody(Process.GetCurrentProcess().Id, rpcChannelName).RunToDisposeSelf()); + } + } + + public void Dispose() + { + if (this.isDisposed) + throw new ObjectDisposedException(GetType().FullName); + + try + { + this.subprocessBuffer.RemoteRequest(((MemoryStream)GetRequestCreator(WorkerInboundOpcode.DisposeAndExit, null).BaseStream).ToArray(), 100); + } + catch (Exception) + { + // ignore any exception + } + + if (this.workerProcess != null && !this.workerProcess.HasExited) + { + this.workerProcess.WaitForExit(1000); + try + { + this.workerProcess.Kill(); + } + catch (Exception) + { + if (!this.workerProcess.HasExited) + throw; + } + } + this.subprocessBuffer.Dispose(); + this.isDisposed = true; + } + + private void RpcResponseHandler(ulong _, byte[] data) + { + using var reader = new BinaryReader(new MemoryStream(data)); + var type = (WorkerOutboundOpcode)reader.ReadInt32(); + switch (type) + { + case WorkerOutboundOpcode.UpdateInstallProgress: + OnReceiveInstallProgressUpdate(reader); + break; + + case WorkerOutboundOpcode.UpdateVerifyProgress: + OnReceiveVerifyProgressUpdate(reader); + break; + + default: + throw new ArgumentException("Unknown recv opc"); + } + } + + private void OnReceiveInstallProgressUpdate(BinaryReader reader) + { + var progressUpdateCounter = reader.ReadInt64(); + if (progressUpdateCounter < this.lastProgressUpdateCounter) + return; + + this.lastProgressUpdateCounter = progressUpdateCounter; + var index = reader.ReadInt32(); + var progress = reader.ReadInt64(); + var max = reader.ReadInt64(); + var state = (IndexedZiPatchInstaller.InstallTaskState)reader.ReadInt32(); + + OnInstallProgress?.Invoke(index, progress, max, state); + } + + private void OnReceiveVerifyProgressUpdate(BinaryReader reader) + { + var progressUpdateCounter = reader.ReadInt64(); + if (progressUpdateCounter < this.lastProgressUpdateCounter) + return; + + this.lastProgressUpdateCounter = progressUpdateCounter; + var index = reader.ReadInt32(); + var progress = reader.ReadInt64(); + var max = reader.ReadInt64(); + + OnVerifyProgress?.Invoke(index, progress, max); + } + + private BinaryWriter GetRequestCreator(WorkerInboundOpcode opcode, CancellationToken? cancellationToken) + { + var ms = new MemoryStream(); + var writer = new BinaryWriter(ms); + var tokenId = -1; + if (cancellationToken.HasValue) + { + tokenId = this.cancellationTokenCounter++; + cancellationToken.Value.Register(async () => await CancelRemoteTask(tokenId)); + } + writer.Write(tokenId); + writer.Write((int)opcode); + return writer; + } + + private async Task WaitForResult(BinaryWriter req, CancellationToken? cancellationToken, int timeoutMs = 30000, bool autoDispose = true) + { + var requestData = ((MemoryStream)req.BaseStream).ToArray(); + RpcResponse response; + if (cancellationToken.HasValue) + response = await this.subprocessBuffer.RemoteRequestAsync(requestData, timeoutMs, cancellationToken.Value); + else + response = await this.subprocessBuffer.RemoteRequestAsync(requestData, timeoutMs); + if (cancellationToken.HasValue) + cancellationToken.Value.ThrowIfCancellationRequested(); + + if (this.isDisposed) + throw new OperationCanceledException(); + var reader = new BinaryReader(new MemoryStream(response.Data)); + try + { + var result = (WorkerResultCode)reader.ReadInt32(); + return result switch + { + WorkerResultCode.Pass => reader, + WorkerResultCode.Cancelled => throw new TaskCanceledException(), + WorkerResultCode.Error => throw new Exception(reader.ReadString()), + _ => throw new InvalidOperationException("Invalid WorkerResultCodes"), + }; + } + finally + { + if (autoDispose) + reader.Dispose(); + } + } + + private async Task CancelRemoteTask(int tokenId) + { + if (this.isDisposed) + return; + + try + { + var writer = GetRequestCreator(WorkerInboundOpcode.CancelTask, null); + writer.Write(tokenId); + await WaitForResult(writer, null); + } + catch (OperationCanceledException) + { + // ignore + } + } + + public async Task ConstructFromPatchFile(IndexedZiPatchIndex patchIndex, int progressReportInterval = 250) + { + var writer = GetRequestCreator(WorkerInboundOpcode.Construct, null); + patchIndex.WriteTo(writer); + writer.Write(progressReportInterval); + await WaitForResult(writer, null); + } + + public async Task VerifyFiles(bool refine = false, int concurrentCount = 8, CancellationToken? cancellationToken = null) + { + var writer = GetRequestCreator(WorkerInboundOpcode.VerifyFiles, cancellationToken); + writer.Write(refine); + writer.Write(concurrentCount); + await WaitForResult(writer, cancellationToken, 864000000); + } + + public async Task MarkFileAsMissing(int targetIndex, CancellationToken? cancellationToken = null) + { + var writer = GetRequestCreator(WorkerInboundOpcode.MarkFileAsMissing, cancellationToken); + writer.Write(targetIndex); + await WaitForResult(writer, cancellationToken); + } + + public async Task SetTargetStreamFromPathReadOnly(int targetIndex, string path, CancellationToken? cancellationToken = null) + { + var writer = GetRequestCreator(WorkerInboundOpcode.SetTargetStreamFromPathReadOnly, cancellationToken); + writer.Write(targetIndex); + writer.Write(path); + await WaitForResult(writer, cancellationToken); + } + + public async Task SetTargetStreamFromPathReadWrite(int targetIndex, string path, CancellationToken? cancellationToken = null) + { + var writer = GetRequestCreator(WorkerInboundOpcode.SetTargetStreamFromPathReadWrite, cancellationToken); + writer.Write(targetIndex); + writer.Write(path); + await WaitForResult(writer, cancellationToken); + } + + public async Task SetTargetStreamsFromPathReadOnly(string rootPath, CancellationToken? cancellationToken = null) + { + var writer = GetRequestCreator(WorkerInboundOpcode.SetTargetStreamsFromPathReadOnly, cancellationToken); + writer.Write(rootPath); + await WaitForResult(writer, cancellationToken); + } + + public async Task SetTargetStreamsFromPathReadWriteForMissingFiles(string rootPath, CancellationToken? cancellationToken = null) + { + var writer = GetRequestCreator(WorkerInboundOpcode.SetTargetStreamsFromPathReadWriteForMissingFiles, cancellationToken); + writer.Write(rootPath); + await WaitForResult(writer, cancellationToken); + } + + public async Task RepairNonPatchData(CancellationToken? cancellationToken = null) => await WaitForResult(GetRequestCreator(WorkerInboundOpcode.RepairNonPatchData, cancellationToken), cancellationToken); + + public async Task WriteVersionFiles(string rootPath, CancellationToken? cancellationToken = null) + { + var writer = GetRequestCreator(WorkerInboundOpcode.WriteVersionFiles, cancellationToken); + writer.Write(rootPath); + await WaitForResult(writer, cancellationToken); + } + + public async Task QueueInstall(int sourceIndex, Uri sourceUrl, string sid, int splitBy = 8, CancellationToken? cancellationToken = null) + { + var writer = GetRequestCreator(WorkerInboundOpcode.QueueInstallFromUrl, cancellationToken); + writer.Write(sourceIndex); + writer.Write(sourceUrl.OriginalString); + writer.Write(sid ?? ""); + writer.Write(splitBy); + await WaitForResult(writer, cancellationToken); + } + + public async Task QueueInstall(int sourceIndex, FileInfo sourceFile, int splitBy = 8, CancellationToken? cancellationToken = null) + { + var writer = GetRequestCreator(WorkerInboundOpcode.QueueInstallFromLocalFile, cancellationToken); + writer.Write(sourceIndex); + writer.Write(sourceFile.FullName); + writer.Write(splitBy); + await WaitForResult(writer, cancellationToken); + } + + public async Task Install(int concurrentCount, CancellationToken? cancellationToken = null) + { + var writer = GetRequestCreator(WorkerInboundOpcode.Install, cancellationToken); + writer.Write(concurrentCount); + await WaitForResult(writer, cancellationToken, 864000000); + } + + public async Task>>> GetMissingPartIndicesPerPatch(CancellationToken? cancellationToken = null) + { + using var reader = await WaitForResult(GetRequestCreator(WorkerInboundOpcode.GetMissingPartIndicesPerPatch, cancellationToken), cancellationToken, 30000, false); + List>> result = new(); + for (int i = 0, iReadLength = reader.ReadInt32(); i < iReadLength; i++) + { + SortedSet> e1 = new(); + for (int j = 0, jReadLength = reader.ReadInt32(); j < jReadLength; j++) + e1.Add(Tuple.Create(reader.ReadInt32(), reader.ReadInt32())); + result.Add(e1); + } + return result; + } + + public async Task>> GetMissingPartIndicesPerTargetFile(CancellationToken? cancellationToken = null) + { + using var reader = await WaitForResult(GetRequestCreator(WorkerInboundOpcode.GetMissingPartIndicesPerTargetFile, cancellationToken), cancellationToken, 30000, false); + List> result = new(); + for (int i = 0, iReadLength = reader.ReadInt32(); i < iReadLength; i++) + { + SortedSet e1 = new(); + for (int j = 0, jReadLength = reader.ReadInt32(); j < jReadLength; j++) + e1.Add(reader.ReadInt32()); + result.Add(e1); + } + return result; + } + + public async Task> GetSizeMismatchTargetFileIndices(CancellationToken? cancellationToken = null) + { + using var reader = await WaitForResult(GetRequestCreator(WorkerInboundOpcode.GetSizeMismatchTargetFileIndices, cancellationToken), cancellationToken, 30000, false); + SortedSet result = new(); + for (int i = 0, readIndex = reader.ReadInt32(); i < readIndex; i++) + result.Add(reader.ReadInt32()); + return result; + } + + public async Task SetWorkerProcessPriority(ProcessPriorityClass subprocessPriority, CancellationToken? cancellationToken = null) + { + var writer = GetRequestCreator(WorkerInboundOpcode.SetWorkerProcessPriority, cancellationToken); + writer.Write((int)subprocessPriority); + await WaitForResult(writer, cancellationToken); + } + + public async Task MoveFile(string sourceFile, string targetFile, CancellationToken? cancellationToken = null) { + var writer = GetRequestCreator(WorkerInboundOpcode.MoveFile, cancellationToken); + writer.Write(sourceFile); + writer.Write(targetFile); + await WaitForResult(writer, cancellationToken); + } + + public async Task CreateDirectory(string dir, CancellationToken? cancellationToken = null) { + var writer = GetRequestCreator(WorkerInboundOpcode.CreateDirectory, cancellationToken); + writer.Write(dir); + await WaitForResult(writer, cancellationToken); + } + + public async Task RemoveDirectory(string dir, bool recursive = false, CancellationToken? cancellationToken = null) { + var writer = GetRequestCreator(WorkerInboundOpcode.RemoveDirectory, cancellationToken); + writer.Write(dir); + writer.Write(recursive); + await WaitForResult(writer, cancellationToken); + } + + public class WorkerSubprocessBody : IDisposable + { + private readonly object progressUpdateSync = new(); + private readonly Process parentProcess; + private readonly RpcBuffer subprocessBuffer; + private readonly Dictionary cancellationTokenSources = new(); + private IndexedZiPatchInstaller instance = null; + private long progressUpdateCounter = 0; + + public WorkerSubprocessBody(int monitorProcessId, string channelName) + { + this.parentProcess = Process.GetProcessById(monitorProcessId); + this.subprocessBuffer = new RpcBuffer(channelName, async (ulong _, byte[] data) => + { + using var reader = new BinaryReader(new MemoryStream(data)); + var cancelSourceId = reader.ReadInt32(); + CancellationToken? cancelToken = null; + if (cancelSourceId != -1) + { + this.cancellationTokenSources[cancelSourceId] = new CancellationTokenSource(); + cancelToken = this.cancellationTokenSources[cancelSourceId].Token; + } + var method = (WorkerInboundOpcode)reader.ReadInt32(); + + var ms = new MemoryStream(); + var writer = new BinaryWriter(ms); + writer.Write(0); + + try + { + switch (method) + { + case WorkerInboundOpcode.CancelTask: + lock (this.cancellationTokenSources) + { + if (this.cancellationTokenSources.TryGetValue(reader.ReadInt32(), out var cts)) + cts.Cancel(); + } + break; + + case WorkerInboundOpcode.Construct: + this.instance?.Dispose(); + this.instance = new(new IndexedZiPatchIndex(reader, false)) + { + ProgressReportInterval = reader.ReadInt32(), + }; + this.instance.OnInstallProgress += OnInstallProgressUpdate; + this.instance.OnVerifyProgress += OnVerifyProgressUpdate; + break; + + case WorkerInboundOpcode.DisposeAndExit: + this.instance?.Dispose(); + this.instance = null; + Environment.Exit(0); + break; + + case WorkerInboundOpcode.VerifyFiles: + await this.instance.VerifyFiles(reader.ReadBoolean(), reader.ReadInt32(), cancelToken); + break; + + case WorkerInboundOpcode.MarkFileAsMissing: + this.instance.MarkFileAsMissing(reader.ReadInt32()); + break; + + case WorkerInboundOpcode.SetTargetStreamFromPathReadOnly: + this.instance.SetTargetStreamForRead(reader.ReadInt32(), new FileStream(reader.ReadString(), FileMode.Open, FileAccess.Read)); + break; + + case WorkerInboundOpcode.SetTargetStreamFromPathReadWrite: + this.instance.SetTargetStreamForWriteFromFile(reader.ReadInt32(), new FileInfo(reader.ReadString())); + break; + + case WorkerInboundOpcode.SetTargetStreamsFromPathReadOnly: + this.instance.SetTargetStreamsFromPathReadOnly(reader.ReadString()); + break; + + case WorkerInboundOpcode.SetTargetStreamsFromPathReadWriteForMissingFiles: + this.instance.SetTargetStreamsFromPathReadWriteForMissingFiles(reader.ReadString()); + break; + + case WorkerInboundOpcode.RepairNonPatchData: + await this.instance.RepairNonPatchData(cancelToken); + break; + + case WorkerInboundOpcode.WriteVersionFiles: + this.instance.WriteVersionFiles(reader.ReadString()); + break; + + case WorkerInboundOpcode.QueueInstallFromUrl: + this.instance.QueueInstall(reader.ReadInt32(), reader.ReadString(), reader.ReadString(), reader.ReadInt32()); + break; + + case WorkerInboundOpcode.QueueInstallFromLocalFile: + this.instance.QueueInstall(reader.ReadInt32(), new FileInfo(reader.ReadString()), reader.ReadInt32()); + break; + + case WorkerInboundOpcode.Install: + await this.instance.Install(reader.ReadInt32(), cancelToken); + break; + + case WorkerInboundOpcode.GetMissingPartIndicesPerPatch: + writer.Write(this.instance.MissingPartIndicesPerPatch.Count); + foreach (var e1 in this.instance.MissingPartIndicesPerPatch) + { + writer.Write(e1.Count); + foreach (var e2 in e1) + { + writer.Write(e2.Item1); + writer.Write(e2.Item2); + } + } + break; + + case WorkerInboundOpcode.GetMissingPartIndicesPerTargetFile: + writer.Write(this.instance.MissingPartIndicesPerTargetFile.Count); + foreach (var e1 in this.instance.MissingPartIndicesPerTargetFile) + { + writer.Write(e1.Count); + foreach (var e2 in e1) + writer.Write(e2); + } + break; + + case WorkerInboundOpcode.GetSizeMismatchTargetFileIndices: + writer.Write(this.instance.SizeMismatchTargetFileIndices.Count); + foreach (var e1 in this.instance.SizeMismatchTargetFileIndices) + writer.Write(e1); + break; + + case WorkerInboundOpcode.SetWorkerProcessPriority: + Process.GetCurrentProcess().PriorityClass = (ProcessPriorityClass)reader.ReadInt32(); + break; + + case WorkerInboundOpcode.MoveFile: + { + var sourceFileName = reader.ReadString(); + var targetFileName = reader.ReadString(); + + var sourceParentDir = new DirectoryInfo(Path.GetDirectoryName(sourceFileName)); + var targetParentDir = new DirectoryInfo(Path.GetDirectoryName(targetFileName)); + + targetParentDir.Create(); + new FileInfo(sourceFileName).MoveTo(targetFileName); + + if (!sourceParentDir.GetFileSystemInfos().Any()) + sourceParentDir.Delete(false); + break; + } + + case WorkerInboundOpcode.CreateDirectory: + new DirectoryInfo(reader.ReadString()).Create(); + break; + + case WorkerInboundOpcode.RemoveDirectory: + { + var dir = new DirectoryInfo(reader.ReadString()); + var recursive = reader.ReadBoolean(); + dir.Delete(recursive); + break; + } + + default: + throw new InvalidOperationException("Invalid WorkerInboundOpcode"); + } + + writer.Seek(0, SeekOrigin.Begin); + writer.Write((int)WorkerResultCode.Pass); + } + catch (Exception ex) + { + writer.Seek(0, SeekOrigin.Begin); + if (ex is OperationCanceledException) + writer.Write((int)WorkerResultCode.Cancelled); + else + { + writer.Write((int)WorkerResultCode.Error); + writer.Write(ex.ToString()); + } + } + finally + { + if (cancelSourceId != -1) + this.cancellationTokenSources.Remove(cancelSourceId); + } + return ms.ToArray(); + }); + } + + private void OnInstallProgressUpdate(int index, long progress, long max, IndexedZiPatchInstaller.InstallTaskState state) + { + lock (this.progressUpdateSync) + { + var ms = new MemoryStream(); + var writer = new BinaryWriter(ms); + writer.Write((int)WorkerOutboundOpcode.UpdateInstallProgress); + writer.Write(this.progressUpdateCounter); + writer.Write(index); + writer.Write(progress); + writer.Write(max); + writer.Write((int)state); + this.progressUpdateCounter += 1; + this.subprocessBuffer.RemoteRequest(ms.ToArray()); + } + } + + private void OnVerifyProgressUpdate(int index, long progress, long max) + { + lock (this.progressUpdateSync) + { + var ms = new MemoryStream(); + var writer = new BinaryWriter(ms); + writer.Write((int)WorkerOutboundOpcode.UpdateVerifyProgress); + writer.Write(this.progressUpdateCounter); + writer.Write(index); + writer.Write(progress); + writer.Write(max); + this.progressUpdateCounter += 1; + this.subprocessBuffer.RemoteRequest(ms.ToArray()); + } + } + + public void Dispose() + { + this.subprocessBuffer.Dispose(); + this.instance?.Dispose(); + } + + public void Run() + { + this.parentProcess.WaitForExit(); + } + + public void RunToDisposeSelf() + { + try + { + Run(); + } + catch (OperationCanceledException) + { + // pass + } + finally + { + Dispose(); + } + } + } + + private enum WorkerResultCode : int + { + Pass, + Cancelled, + Error, + } + + private enum WorkerOutboundOpcode : int + { + UpdateInstallProgress, + UpdateVerifyProgress, + } + + private enum WorkerInboundOpcode : int + { + CancelTask, + Construct, + DisposeAndExit, + VerifyFiles, + MarkFileAsMissing, + SetTargetStreamFromPathReadOnly, + SetTargetStreamFromPathReadWrite, + SetTargetStreamsFromPathReadOnly, + SetTargetStreamsFromPathReadWriteForMissingFiles, + RepairNonPatchData, + WriteVersionFiles, + QueueInstallFromUrl, + QueueInstallFromLocalFile, + Install, + GetMissingPartIndicesPerPatch, + GetMissingPartIndicesPerTargetFile, + GetSizeMismatchTargetFileIndices, + SetWorkerProcessPriority, + MoveFile, + CreateDirectory, + RemoveDirectory, + } + + public static void Test() + { + Task.Run(async () => + { + // Cancel in 15 secs + var cancellationTokenSource = new CancellationTokenSource(); + var cancellationToken = cancellationTokenSource.Token; + + var availableSourceUrls = new Dictionary() { + {"boot:D2013.06.18.0000.0000.patch", "http://patch-dl.ffxiv.com/boot/2b5cbc63/D2013.06.18.0000.0000.patch"}, + {"boot:D2021.11.16.0000.0001.patch", "http://patch-dl.ffxiv.com/boot/2b5cbc63/D2021.11.16.0000.0001.patch"}, + }; + var maxConcurrentConnectionsForPatchSet = 1; + + var baseDir = @"Z:\tgame"; + var rootAndPatchPairs = new List>() { + Tuple.Create(@$"{baseDir}\boot", @"Z:\patch-dl.ffxiv.com\boot\2b5cbc63\D2021.11.16.0000.0001.patch.index"), + }; + + // Run verifier as subprocess + // using var verifier = new IndexedZiPatchIndexRemoteInstaller(System.Reflection.Assembly.GetExecutingAssembly().Location, true); + // Run verifier as another thread + using var verifier = new IndexedZiPatchIndexRemoteInstaller(null, true); + + foreach (var (gameRootPath, patchIndexFilePath) in rootAndPatchPairs) + { + var patchIndex = new IndexedZiPatchIndex(new BinaryReader(new DeflateStream(new FileStream(patchIndexFilePath, FileMode.Open, FileAccess.Read), CompressionMode.Decompress))); + + await verifier.ConstructFromPatchFile(patchIndex, 1000); + + void ReportCheckProgress(int index, long progress, long max) + { + Log.Information("[{0}/{1}] Checking file {2}... {3:0.00}/{4:0.00}MB ({5:00.00}%)", index + 1, patchIndex.Length, patchIndex[Math.Min(index, patchIndex.Length - 1)].RelativePath, progress / 1048576.0, max / 1048576.0, 100.0 * progress / max); + } + + void ReportInstallProgress(int index, long progress, long max, IndexedZiPatchInstaller.InstallTaskState state) + { + Log.Information("[{0}/{1}] {2} {3}... {4:0.00}/{5:0.00}MB ({6:00.00}%)", index + 1, patchIndex.Sources.Count, state, patchIndex.Sources[Math.Min(index, patchIndex.Sources.Count - 1)], progress / 1048576.0, max / 1048576.0, 100.0 * progress / max); + } + + verifier.OnVerifyProgress += ReportCheckProgress; + verifier.OnInstallProgress += ReportInstallProgress; + + for (var attemptIndex = 0; attemptIndex < 5; attemptIndex++) + { + await verifier.SetTargetStreamsFromPathReadOnly(gameRootPath); + // TODO: check one at a time if random access is slow? + await verifier.VerifyFiles(attemptIndex > 0, Environment.ProcessorCount, cancellationToken); + + var missingPartIndicesPerTargetFile = await verifier.GetMissingPartIndicesPerTargetFile(); + if (missingPartIndicesPerTargetFile.All(x => !x.Any())) + break; + + var missingPartIndicesPerPatch = await verifier.GetMissingPartIndicesPerPatch(); + await verifier.SetTargetStreamsFromPathReadWriteForMissingFiles(gameRootPath); + var prefix = patchIndex.ExpacVersion == IndexedZiPatchIndex.EXPAC_VERSION_BOOT ? "boot:" : $"ex{patchIndex.ExpacVersion}:"; + for (var i = 0; i < patchIndex.Sources.Count; i++) + { + if (!missingPartIndicesPerPatch[i].Any()) + continue; + + await verifier.QueueInstall(i, new Uri(availableSourceUrls[prefix + patchIndex.Sources[i]]), null, maxConcurrentConnectionsForPatchSet); + // await verifier.QueueInstall(i, new FileInfo(availableSourceUrls[prefix + patchIndex.Sources[i]].Replace("http:/", "Z:"))); + } + await verifier.Install(maxConcurrentConnectionsForPatchSet, cancellationToken); + await verifier.WriteVersionFiles(gameRootPath); + } + verifier.OnVerifyProgress -= ReportCheckProgress; + verifier.OnInstallProgress -= ReportInstallProgress; + } + }).Wait(); + } + } +} diff --git a/src/XIVLauncher2.Common/Patching/IndexedZiPatch/IndexedZiPatchInstaller.cs b/src/XIVLauncher2.Common/Patching/IndexedZiPatch/IndexedZiPatchInstaller.cs new file mode 100644 index 0000000..2563d97 --- /dev/null +++ b/src/XIVLauncher2.Common/Patching/IndexedZiPatch/IndexedZiPatchInstaller.cs @@ -0,0 +1,793 @@ +using Serilog; +using System; +using System.Collections.Generic; + +#if WIN32 +using System.ComponentModel; +using System.Diagnostics; +using System.Runtime.InteropServices; +#endif + +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using XIVLauncher2.Common.Patching.Util; + +namespace XIVLauncher2.Common.Patching.IndexedZiPatch +{ + + public class IndexedZiPatchInstaller : IDisposable + { + public readonly IndexedZiPatchIndex Index; + public readonly List>> MissingPartIndicesPerPatch = new(); + public readonly List> MissingPartIndicesPerTargetFile = new(); + public readonly SortedSet SizeMismatchTargetFileIndices = new(); + + public int ProgressReportInterval = 250; + private readonly List targetStreams = new(); + private readonly List targetLocks = new(); + + public enum InstallTaskState + { + NotStarted, + WaitingForReattempt, + Connecting, + Working, + Finishing, + Done, + Error, + } + + public delegate void OnCorruptionFoundDelegate(IndexedZiPatchPartLocator part, IndexedZiPatchPartLocator.VerifyDataResult result); + public delegate void OnVerifyProgressDelegate(int targetIndex, long progress, long max); + public delegate void OnInstallProgressDelegate(int sourceIndex, long progress, long max, InstallTaskState state); + + public event OnCorruptionFoundDelegate OnCorruptionFound; + public event OnVerifyProgressDelegate OnVerifyProgress; + public event OnInstallProgressDelegate OnInstallProgress; + + // Definitions taken from PInvoke.net (with some changes) + // ReSharper disable InconsistentNaming + +#if WIN32 + private static class PInvoke + { + #region Constants + + public const UInt32 TOKEN_QUERY = 0x0008; + public const UInt32 TOKEN_ADJUST_PRIVILEGES = 0x0020; + + public const UInt32 SE_PRIVILEGE_ENABLED = 0x00000002; + + public const UInt32 ERROR_NOT_ALL_ASSIGNED = 0x514; + + #endregion + + #region Structures + + [StructLayout(LayoutKind.Sequential)] + public struct LUID + { + public UInt32 LowPart; + public Int32 HighPart; + } + + public struct LUID_AND_ATTRIBUTES + { + public LUID Luid; + public UInt32 Attributes; + } + + [StructLayout(LayoutKind.Sequential)] + public struct TOKEN_PRIVILEGES + { + public UInt32 PrivilegeCount; + + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 1)] + public LUID_AND_ATTRIBUTES[] Privileges; + } + + #endregion + + #region Methods + + [DllImport("kernel32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + public static extern bool SetFileValidData(IntPtr hFile, long ValidDataLength); + + [DllImport("kernel32.dll", SetLastError = true)] + public static extern bool CloseHandle(IntPtr hObject); + + [DllImport("advapi32.dll", SetLastError = true)] + public static extern bool OpenProcessToken( + IntPtr ProcessHandle, + UInt32 DesiredAccess, + out IntPtr TokenHandle); + + [DllImport("advapi32.dll", SetLastError = true)] + public static extern bool LookupPrivilegeValue(string lpSystemName, string lpName, ref LUID lpLuid); + + [DllImport("advapi32.dll", SetLastError = true)] + public static extern bool AdjustTokenPrivileges( + IntPtr TokenHandle, + bool DisableAllPrivileges, + ref TOKEN_PRIVILEGES NewState, + int BufferLengthInBytes, + IntPtr PreviousState, + IntPtr ReturnLengthInBytes); + + #endregion + + #region Utilities + + // https://docs.microsoft.com/en-us/windows/win32/secauthz/enabling-and-disabling-privileges-in-c-- + public static void SetPrivilege(IntPtr hToken, string lpszPrivilege, bool bEnablePrivilege) + { + LUID luid = new(); + if (!LookupPrivilegeValue(null, lpszPrivilege, ref luid)) + throw new Win32Exception(Marshal.GetLastWin32Error(), "LookupPrivilegeValue failed."); + + TOKEN_PRIVILEGES tp = new() + { + PrivilegeCount = 1, + Privileges = new LUID_AND_ATTRIBUTES[] { + new LUID_AND_ATTRIBUTES{ + Luid = luid, + Attributes = bEnablePrivilege ? SE_PRIVILEGE_ENABLED : 0, + } + }, + }; + if (!AdjustTokenPrivileges(hToken, false, ref tp, Marshal.SizeOf(tp), IntPtr.Zero, IntPtr.Zero)) + throw new Win32Exception(Marshal.GetLastWin32Error(), "AdjustTokenPrivileges failed."); + + if (Marshal.GetLastWin32Error() == ERROR_NOT_ALL_ASSIGNED) + throw new Win32Exception(Marshal.GetLastWin32Error(), "The token does not have the specified privilege."); + } + + public static void SetCurrentPrivilege(string lpszPrivilege, bool bEnablePrivilege) + { + if (!OpenProcessToken(Process.GetCurrentProcess().SafeHandle.DangerousGetHandle(), TOKEN_QUERY | TOKEN_ADJUST_PRIVILEGES, out var hToken)) + throw new Win32Exception(Marshal.GetLastWin32Error()); + + try + { + SetPrivilege(hToken, lpszPrivilege, bEnablePrivilege); + } + finally + { + CloseHandle(hToken); + } + } + + #endregion + } + // ReSharper restore once InconsistentNaming +#endif + + public IndexedZiPatchInstaller(IndexedZiPatchIndex def) + { + Index = def; + foreach (var _ in def.Targets) + { + MissingPartIndicesPerTargetFile.Add(new()); + this.targetStreams.Add(null); + this.targetLocks.Add(new()); + } + foreach (var _ in def.Sources) + MissingPartIndicesPerPatch.Add(new()); + } + + public async Task VerifyFiles(bool refine = false, int concurrentCount = 8, CancellationToken? cancellationToken = null) + { + CancellationTokenSource localCancelSource = new(); + + if (cancellationToken.HasValue) + cancellationToken.Value.Register(() => localCancelSource?.Cancel()); + + SizeMismatchTargetFileIndices.Clear(); + foreach (var l in MissingPartIndicesPerPatch) + l.Clear(); + + List verifyTasks = new(); + try + { + long progressCounter = 0; + long progressMax = refine ? MissingPartIndicesPerTargetFile.Select((x, i) => x.Select(y => Index[i][y].TargetSize).Sum()).Sum() : Index.Targets.Select((x, i) => this.targetStreams[i] == null ? 0 : x.FileSize).Sum(); + + Queue pendingTargetIndices = new(); + for (int i = 0; i < Index.Length; i++) + pendingTargetIndices.Enqueue(i); + + Task progressReportTask = null; + while (verifyTasks.Any() || pendingTargetIndices.Any()) + { + localCancelSource.Token.ThrowIfCancellationRequested(); + + while (pendingTargetIndices.Any() && verifyTasks.Count < concurrentCount) + { + var targetIndex = pendingTargetIndices.Dequeue(); + var stream = this.targetStreams[targetIndex]; + if (stream == null) + continue; + + var file = Index[targetIndex]; + if (stream.Length != file.FileSize) + SizeMismatchTargetFileIndices.Add(targetIndex); + + verifyTasks.Add(Task.Run(() => + { + List targetPartIndicesToCheck; + if (refine) + { + targetPartIndicesToCheck = MissingPartIndicesPerTargetFile[targetIndex].ToList(); + MissingPartIndicesPerTargetFile[targetIndex].Clear(); + } + else + { + targetPartIndicesToCheck = new(); + for (var partIndex = 0; partIndex < file.Count; ++partIndex) + targetPartIndicesToCheck.Add(partIndex); + } + foreach (var partIndex in targetPartIndicesToCheck) + { + localCancelSource.Token.ThrowIfCancellationRequested(); + + var verifyResult = file[partIndex].Verify(stream); + lock (verifyTasks) + { + progressCounter += file[partIndex].TargetSize; + switch (verifyResult) + { + case IndexedZiPatchPartLocator.VerifyDataResult.Pass: + break; + + case IndexedZiPatchPartLocator.VerifyDataResult.FailUnverifiable: + throw new Exception($"{file.RelativePath}:{file[partIndex].TargetOffset}:{file[partIndex].TargetEnd}: Should not happen; unverifiable due to insufficient source data"); + + case IndexedZiPatchPartLocator.VerifyDataResult.FailNotEnoughData: + case IndexedZiPatchPartLocator.VerifyDataResult.FailBadData: + MissingPartIndicesPerTargetFile[file[partIndex].TargetIndex].Add(partIndex); + OnCorruptionFound?.Invoke(file[partIndex], verifyResult); + break; + } + } + } + })); + } + + if (progressReportTask == null || progressReportTask.IsCompleted) + { + progressReportTask = Task.Delay(ProgressReportInterval, localCancelSource.Token); + OnVerifyProgress?.Invoke(Math.Max(0, Index.Length - pendingTargetIndices.Count - verifyTasks.Count - 1), progressCounter, progressMax); + } + + verifyTasks.Add(progressReportTask); + await Task.WhenAny(verifyTasks); + verifyTasks.RemoveAt(verifyTasks.Count - 1); + if (verifyTasks.FirstOrDefault(x => x.IsFaulted) is Task x) + throw x.Exception; + verifyTasks.RemoveAll(x => x.IsCompleted); + } + + for (var targetIndex = 0; targetIndex < Index.Length; targetIndex++) + { + foreach (var partIndex in MissingPartIndicesPerTargetFile[targetIndex]) + { + var part = Index[targetIndex][partIndex]; + if (part.IsFromSourceFile) + MissingPartIndicesPerPatch[part.SourceIndex].Add(Tuple.Create(targetIndex, partIndex)); + } + } + } + finally + { + localCancelSource.Cancel(); + foreach (var task in verifyTasks) + { + if (task.IsCompleted) + continue; + try + { + await task; + } + catch (Exception) + { + // ignore + } + } + localCancelSource.Dispose(); + localCancelSource = null; + } + } + + public void MarkFileAsMissing(int targetIndex) + { + var file = Index[targetIndex]; + for (var i = 0; i < file.Count; ++i) + MissingPartIndicesPerTargetFile[targetIndex].Add(i); + } + + public void SetTargetStreamForRead(int targetIndex, Stream targetStream) + { + if (!targetStream.CanRead || !targetStream.CanSeek) + throw new ArgumentException("Target stream must be readable and seekable."); + + this.targetStreams[targetIndex] = targetStream; + } + + public void SetTargetStreamForWriteFromFile(int targetIndex, FileInfo fileInfo, bool useSetFileValidData = false) + { + var file = Index[targetIndex]; + fileInfo.Directory.Create(); + var stream = fileInfo.Open(FileMode.OpenOrCreate, FileAccess.ReadWrite); + + if (stream.Length != file.FileSize) + { + stream.Seek(file.FileSize, SeekOrigin.Begin); + stream.SetLength(file.FileSize); + +#if WIN32 + if (useSetFileValidData && !PInvoke.SetFileValidData(stream.SafeFileHandle.DangerousGetHandle(), file.FileSize)) + Log.Information($"Unable to apply SetFileValidData on file {fileInfo.FullName} (error code {Marshal.GetLastWin32Error()})"); +#endif + } + + this.targetStreams[targetIndex] = stream; + } + + public void SetTargetStreamsFromPathReadOnly(string rootPath) + { + Dispose(); + for (var i = 0; i < Index.Length; i++) + { + var file = Index[i]; + var fileInfo = new FileInfo(Path.Combine(rootPath, file.RelativePath)); + if (fileInfo.Exists) + SetTargetStreamForRead(i, new FileStream(Path.Combine(rootPath, file.RelativePath), FileMode.Open, FileAccess.Read)); + else + MarkFileAsMissing(i); + } + } + + public void SetTargetStreamsFromPathReadWriteForMissingFiles(string rootPath) + { + Dispose(); + +#if WIN32 + var useSetFileValidData = true; + try + { + PInvoke.SetCurrentPrivilege("SeManageVolumePrivilege", true); + } + catch (Win32Exception e) + { + Log.Information(e, "Unable to obtain SeManageVolumePrivilege; not using SetFileValidData."); + useSetFileValidData = false; + } +#else + var useSetFileValidData = false; +#endif + + for (var i = 0; i < Index.Length; i++) + { + if (MissingPartIndicesPerTargetFile[i].Count == 0 && !SizeMismatchTargetFileIndices.Contains(i)) + continue; + + var file = Index[i]; + var fileInfo = new FileInfo(Path.Combine(rootPath, file.RelativePath)); + SetTargetStreamForWriteFromFile(i, fileInfo, useSetFileValidData); + } + } + + private void WriteToTarget(int targetIndex, long targetOffset, byte[] buffer, int offset, int count) + { + var target = this.targetStreams[targetIndex]; + if (target == null) + return; + + lock (this.targetLocks[targetIndex]) + { + target.Seek(targetOffset, SeekOrigin.Begin); + target.Write(buffer, offset, count); + target.Flush(); + } + } + + public async Task RepairNonPatchData(CancellationToken? cancellationToken = null) + { + await Task.Run(() => + { + for (int i = 0, length = Index.Length; i < length; i++) + { + if (cancellationToken.HasValue) + cancellationToken.Value.ThrowIfCancellationRequested(); + + var file = Index[i]; + foreach (var partIndex in MissingPartIndicesPerTargetFile[i]) + { + if (cancellationToken.HasValue) + cancellationToken.Value.ThrowIfCancellationRequested(); + + var part = file[partIndex]; + if (part.IsFromSourceFile) + continue; + + using var buffer = ReusableByteBufferManager.GetBuffer(part.TargetSize); + part.ReconstructWithoutSourceData(buffer.Buffer); + WriteToTarget(i, part.TargetOffset, buffer.Buffer, 0, (int)part.TargetSize); + } + } + }); + } + + public void WriteVersionFiles(string localRootPath) + { + Directory.CreateDirectory(Path.GetDirectoryName(Path.Combine(localRootPath, Index.VersionFileVer))); + using (var writer = new StreamWriter(new FileStream(Path.Combine(localRootPath, Index.VersionFileVer), FileMode.Create, FileAccess.Write))) + writer.Write(Index.VersionName); + using (var writer = new StreamWriter(new FileStream(Path.Combine(localRootPath, Index.VersionFileBck), FileMode.Create, FileAccess.Write))) + writer.Write(Index.VersionName); + } + + public abstract class InstallTaskConfig : IDisposable + { + public long ProgressMax { get; protected set; } + public long ProgressValue { get; protected set; } + public readonly IndexedZiPatchIndex Index; + public readonly IndexedZiPatchInstaller Installer; + public readonly int SourceIndex; + public readonly List> TargetPartIndices; + public readonly List> CompletedTargetPartIndices = new(); + public InstallTaskState State { get; protected set; } = InstallTaskState.NotStarted; + + public InstallTaskConfig(IndexedZiPatchInstaller installer, int sourceIndex, IEnumerable> targetPartIndices) + { + Index = installer.Index; + Installer = installer; + SourceIndex = sourceIndex; + TargetPartIndices = targetPartIndices.ToList(); + } + + public abstract Task Repair(CancellationToken cancellationToken); + + public virtual void Dispose() { } + } + + public class HttpInstallTaskConfig : InstallTaskConfig + { + private static readonly int[] ReattemptWait = new int[] { 0, 500, 1000, 2000, 3000, 5000, 10000, 15000, 20000, 25000, 30000, 45000, 60000 }; + private const int MERGED_GAP_DOWNLOAD = 512; + + public readonly string SourceUrl; + private readonly HttpClient client = new(); + private readonly List targetPartOffsets; + private readonly string sid; + + public HttpInstallTaskConfig(IndexedZiPatchInstaller installer, int sourceIndex, IEnumerable> targetPartIndices, string sourceUrl, string sid) + : base(installer, sourceIndex, targetPartIndices) + { + SourceUrl = sourceUrl; + this.sid = sid; + TargetPartIndices.Sort((x, y) => Index[x.Item1][x.Item2].SourceOffset.CompareTo(Index[y.Item1][y.Item2].SourceOffset)); + this.targetPartOffsets = TargetPartIndices.Select(x => Index[x.Item1][x.Item2].SourceOffset).ToList(); + + foreach (var (targetIndex, partIndex) in TargetPartIndices) + ProgressMax += Index[targetIndex][partIndex].TargetSize; + } + + private MultipartResponseHandler multipartResponse = null; + + private async Task GetNextStream(CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + + if (this.multipartResponse != null) + { + var stream1 = await this.multipartResponse.NextPart(cancellationToken); + if (stream1 != null) + return stream1; + + this.multipartResponse?.Dispose(); + this.multipartResponse = null; + } + + var offsets = new List>(); + offsets.Clear(); + foreach (var (targetIndex, partIndex) in TargetPartIndices) + offsets.Add(Tuple.Create(Index[targetIndex][partIndex].SourceOffset, Math.Min(Index.GetSourceLastPtr(SourceIndex), Index[targetIndex][partIndex].MaxSourceEnd))); + offsets.Sort(); + + for (int i = 1; i < offsets.Count; i++) + { + if (offsets[i].Item1 - offsets[i - 1].Item2 >= MERGED_GAP_DOWNLOAD) + continue; + offsets[i - 1] = Tuple.Create(offsets[i - 1].Item1, Math.Max(offsets[i - 1].Item2, offsets[i].Item2)); + offsets.RemoveAt(i); + i -= 1; + } + + using HttpRequestMessage req = new(HttpMethod.Get, SourceUrl); + req.Headers.Range = new(); + req.Headers.Range.Unit = "bytes"; + foreach (var (rangeFrom, rangeToExclusive) in offsets) + req.Headers.Range.Ranges.Add(new(rangeFrom, rangeToExclusive + 1)); + if (this.sid != null) + req.Headers.Add("X-Patch-Unique-Id", this.sid); + req.Headers.Add("User-Agent", Constants.PatcherUserAgent); + req.Headers.Add("Connection", "Keep-Alive"); + + try + { + var resp = await this.client.SendAsync(req, HttpCompletionOption.ResponseHeadersRead, cancellationToken); + this.multipartResponse = new MultipartResponseHandler(resp); + } + catch (HttpRequestException e) + { + throw new IOException($"Failed to send request to {SourceUrl} with {offsets.Count} range element(s).", e); + } + + var stream2 = await this.multipartResponse.NextPart(cancellationToken); + if (stream2 == null) + throw new EndOfStreamException("Encountered premature end of stream"); + return stream2; + } + + public override async Task Repair(CancellationToken cancellationToken) + { + for (int failedCount = 0; TargetPartIndices.Any() && failedCount < ReattemptWait.Length;) + { + try + { + cancellationToken.ThrowIfCancellationRequested(); + + State = InstallTaskState.WaitingForReattempt; + await Task.Delay(ReattemptWait[failedCount], cancellationToken); + + State = InstallTaskState.Connecting; + var stream = await GetNextStream(cancellationToken); + + State = InstallTaskState.Working; + while (this.targetPartOffsets.Any()) + { + cancellationToken.ThrowIfCancellationRequested(); + + var (targetIndex, partIndex) = TargetPartIndices.First(); + var part = Index[targetIndex][partIndex]; + + if (Math.Min(part.MaxSourceEnd, Index.GetSourceLastPtr(SourceIndex)) > stream.OriginEnd) + break; + + using var targetBuffer = ReusableByteBufferManager.GetBuffer(part.TargetSize); + part.Reconstruct(stream, targetBuffer.Buffer); + Installer.WriteToTarget(part.TargetIndex, part.TargetOffset, targetBuffer.Buffer, 0, (int)part.TargetSize); + failedCount = 0; + + ProgressValue += part.TargetSize; + CompletedTargetPartIndices.Add(TargetPartIndices.First()); + TargetPartIndices.RemoveAt(0); + this.targetPartOffsets.RemoveAt(0); + } + } + catch (IOException ex) + { + if (failedCount >= 8) + Log.Error(ex, "HttpInstallTask failed"); + else + Log.Warning(ex, "HttpInstallTask reattempting"); + + failedCount++; + if (failedCount == ReattemptWait.Length) + { + State = InstallTaskState.Error; + throw; + } + } + catch (Exception) + { + State = InstallTaskState.Error; + throw; + } + } + + State = InstallTaskState.Done; + } + + public override void Dispose() + { + this.multipartResponse?.Dispose(); + this.client.Dispose(); + base.Dispose(); + } + } + + public class StreamInstallTaskConfig : InstallTaskConfig + { + public readonly Stream SourceStream; + public readonly IList> SourceOffsets; + + public StreamInstallTaskConfig(IndexedZiPatchInstaller installer, int sourceIndex, IEnumerable> targetPartIndices, Stream sourceStream) + : base(installer, sourceIndex, targetPartIndices) + { + SourceStream = sourceStream; + long totalTargetSize = 0; + foreach (var (targetIndex, partIndex) in TargetPartIndices) + totalTargetSize += Index[targetIndex][partIndex].TargetSize; + ProgressMax = totalTargetSize; + } + + public override async Task Repair(CancellationToken cancellationToken) + { + State = InstallTaskState.Working; + try + { + await Task.Run(() => + { + while (TargetPartIndices.Any()) + { + cancellationToken.ThrowIfCancellationRequested(); + + var (targetIndex, partIndex) = TargetPartIndices.First(); + var part = Index[targetIndex][partIndex]; + + using var buffer = ReusableByteBufferManager.GetBuffer(part.TargetSize); + part.Reconstruct(SourceStream, buffer.Buffer); + Installer.WriteToTarget(part.TargetIndex, part.TargetOffset, buffer.Buffer, 0, (int)part.TargetSize); + + ProgressValue += part.TargetSize; + CompletedTargetPartIndices.Add(TargetPartIndices.First()); + TargetPartIndices.RemoveAt(0); + } + }); + State = InstallTaskState.Done; + } + catch (Exception) + { + State = InstallTaskState.Error; + } + } + + public override void Dispose() + { + SourceStream.Dispose(); + base.Dispose(); + } + } + + private readonly List installTaskConfigs = new(); + + public void QueueInstall(int sourceIndex, string sourceUrl, string sid, ISet> targetPartIndices) + { + if (targetPartIndices.Any()) + this.installTaskConfigs.Add(new HttpInstallTaskConfig(this, sourceIndex, targetPartIndices, sourceUrl, sid == "" ? null : sid)); + } + + public void QueueInstall(int sourceIndex, string sourceUrl, string sid, int splitBy = 8) + { + const int MAX_DOWNLOAD_PER_REQUEST = 256 * 1024 * 1024; + + var indices = MissingPartIndicesPerPatch[sourceIndex].ToList(); + var indicesPerRequest = (int)Math.Ceiling(1.0 * indices.Count / splitBy); + for (int j = 0; j < indices.Count;) + { + SortedSet> targetPartIndices = new(); + long size = 0; + for (; j < indices.Count && targetPartIndices.Count < indicesPerRequest && size < MAX_DOWNLOAD_PER_REQUEST; ++j) + { + targetPartIndices.Add(indices[j]); + size += Index[indices[j].Item1][indices[j].Item2].MaxSourceSize; + } + QueueInstall(sourceIndex, sourceUrl, sid, targetPartIndices); + } + } + + public void QueueInstall(int sourceIndex, Stream stream, ISet> targetPartIndices) + { + if (targetPartIndices.Any()) + this.installTaskConfigs.Add(new StreamInstallTaskConfig(this, sourceIndex, targetPartIndices, stream)); + } + + public void QueueInstall(int sourceIndex, FileInfo file, ISet> targetPartIndices) + { + if (targetPartIndices.Any()) + QueueInstall(sourceIndex, file.OpenRead(), targetPartIndices); + } + + public void QueueInstall(int sourceIndex, FileInfo file, int splitBy = 8) + { + var indices = MissingPartIndicesPerPatch[sourceIndex]; + var indicesPerRequest = (int)Math.Ceiling(1.0 * indices.Count / splitBy); + for (int j = 0; j < indices.Count; j += indicesPerRequest) + QueueInstall(sourceIndex, file, new HashSet>(indices.Skip(j).Take(Math.Min(indicesPerRequest, indices.Count - j)))); // This was .ToHashSet(), but .NET Standard 2.0 doesn't have it + } + + public async Task Install(int concurrentCount, CancellationToken? cancellationToken = null) + { + if (!this.installTaskConfigs.Any()) + { + await RepairNonPatchData(); + return; + } + + long progressMax = this.installTaskConfigs.Select(x => x.ProgressMax).Sum(); + + CancellationTokenSource localCancelSource = new(); + + if (cancellationToken.HasValue) + cancellationToken.Value.Register(() => localCancelSource?.Cancel()); + + Task progressReportTask = null; + Queue pendingTaskConfigs = new(); + foreach (var x in this.installTaskConfigs) + pendingTaskConfigs.Enqueue(x); + + List runningTasks = new(); + + try + { + while (pendingTaskConfigs.Any() || runningTasks.Any()) + { + localCancelSource.Token.ThrowIfCancellationRequested(); + + while (pendingTaskConfigs.Any() && runningTasks.Count < concurrentCount) + runningTasks.Add(pendingTaskConfigs.Dequeue().Repair(localCancelSource.Token)); + + OnInstallProgress?.Invoke( + this.installTaskConfigs[Math.Max(0, this.installTaskConfigs.Count - pendingTaskConfigs.Count - runningTasks.Count - 1)].SourceIndex, + this.installTaskConfigs.Select(x => x.ProgressValue).Sum(), + progressMax, + this.installTaskConfigs.Where(x => x.State < InstallTaskState.Finishing).Select(x => x.State).Max() + ); + + if (progressReportTask == null || progressReportTask.IsCompleted) + progressReportTask = Task.Delay(ProgressReportInterval, localCancelSource.Token); + runningTasks.Add(progressReportTask); + await Task.WhenAny(runningTasks); + runningTasks.RemoveAt(runningTasks.Count - 1); + + if (runningTasks.FirstOrDefault(x => x.IsFaulted) is Task x) + throw x.Exception; + runningTasks.RemoveAll(x => x.IsCompleted); + } + + OnInstallProgress?.Invoke(this.installTaskConfigs.Last().SourceIndex, progressMax, progressMax, InstallTaskState.Finishing); + await RepairNonPatchData(); + } + finally + { + localCancelSource.Cancel(); + foreach (var task in runningTasks) + { + if (task.IsCompleted) + continue; + try + { + await task; + } + catch (Exception) + { + // ignore + } + } + localCancelSource.Dispose(); + localCancelSource = null; + } + } + + public void Dispose() + { + for (var i = 0; i < this.targetStreams.Count; i++) + { + if (this.targetStreams[i] != null) + { + this.targetStreams[i].Dispose(); + this.targetStreams[i] = null; + } + } + foreach (var item in this.installTaskConfigs) + item.Dispose(); + this.installTaskConfigs.Clear(); + } + } +} diff --git a/src/XIVLauncher2.Common/Patching/IndexedZiPatch/IndexedZiPatchOperations.cs b/src/XIVLauncher2.Common/Patching/IndexedZiPatch/IndexedZiPatchOperations.cs new file mode 100644 index 0000000..ccce5b1 --- /dev/null +++ b/src/XIVLauncher2.Common/Patching/IndexedZiPatch/IndexedZiPatchOperations.cs @@ -0,0 +1,174 @@ +using Serilog; +using System; +using System.Collections.Generic; +using System.IO; +using System.IO.Compression; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using XIVLauncher2.Common.Patching.ZiPatch; + +namespace XIVLauncher2.Common.Patching.IndexedZiPatch +{ + public class IndexedZiPatchOperations + { + public static async Task CreateZiPatchIndices(int expacVersion, IList patchFilePaths, CancellationToken? cancellationToken = null) + { + var sources = new List(); + var patchFiles = new List(); + var patchIndex = new IndexedZiPatchIndex(expacVersion); + try + { + var firstPatchFileIndex = patchFilePaths.Count - 1; + while (firstPatchFileIndex > 0) + { + if (File.Exists(patchFilePaths[firstPatchFileIndex] + ".index")) + break; + firstPatchFileIndex--; + } + for (var i = 0; i < patchFilePaths.Count; ++i) + { + if (cancellationToken.HasValue) + cancellationToken.Value.ThrowIfCancellationRequested(); + + var patchFilePath = patchFilePaths[i]; + sources.Add(new FileStream(patchFilePath, FileMode.Open, FileAccess.Read)); + patchFiles.Add(new ZiPatchFile(sources[sources.Count - 1])); + + if (i < firstPatchFileIndex) + continue; + + if (File.Exists(patchFilePath + ".index")) + { + Log.Information("Reading patch index file {0}...", patchFilePath); + patchIndex = new(new BinaryReader(new DeflateStream(new FileStream(patchFilePath + ".index", FileMode.Open, FileAccess.Read), CompressionMode.Decompress))); + continue; + } + + Log.Information("Indexing patch file {0}...", patchFilePath); + await patchIndex.ApplyZiPatch(Path.GetFileName(patchFilePath), patchFiles[patchFiles.Count - 1], cancellationToken); + + Log.Information("Calculating CRC32 for files resulted from patch file {0}...", patchFilePath); + await patchIndex.CalculateCrc32(sources, cancellationToken); + + using (var writer = new BinaryWriter(new DeflateStream(new FileStream(patchFilePath + ".index.tmp", FileMode.Create), CompressionLevel.Optimal))) + patchIndex.WriteTo(writer); + + File.Move(patchFilePath + ".index.tmp", patchFilePath + ".index"); + } + + return patchIndex; + } + finally + { + foreach (var source in sources) + source.Dispose(); + } + } + + public static async Task VerifyFromZiPatchIndex(string patchIndexFilePath, string gameRootPath, CancellationToken? cancellationToken = null) => await VerifyFromZiPatchIndex(new IndexedZiPatchIndex(new BinaryReader(new DeflateStream(new FileStream(patchIndexFilePath, FileMode.Open, FileAccess.Read), CompressionMode.Decompress))), gameRootPath, cancellationToken); + + public static async Task VerifyFromZiPatchIndex(IndexedZiPatchIndex patchIndex, string gameRootPath, CancellationToken? cancellationToken = null) + { + using var verifier = new IndexedZiPatchInstaller(patchIndex) + { + ProgressReportInterval = 1000 + }; + + var remainingErrorMessagesToShow = 8; + void OnVerifyProgressCallback(int index, long progress, long max) => Log.Information("[{0}/{1}] Checking file {2}... {3:0.00}/{4:0.00}MB ({5:00.00}%)", index + 1, patchIndex.Length, patchIndex[Math.Min(index, patchIndex.Length - 1)].RelativePath, progress / 1048576.0, max / 1048576.0, 100.0 * progress / max); ; + void OnCorruptionFoundCallback(IndexedZiPatchPartLocator part, IndexedZiPatchPartLocator.VerifyDataResult result) + { + switch (result) + { + case IndexedZiPatchPartLocator.VerifyDataResult.FailNotEnoughData: + if (remainingErrorMessagesToShow > 0) + { + Log.Error("{0}:{1}:{2}: Premature EOF detected", patchIndex[part.TargetIndex].RelativePath, part.TargetOffset, patchIndex[part.TargetIndex].FileSize); + remainingErrorMessagesToShow = 0; + } + break; + + case IndexedZiPatchPartLocator.VerifyDataResult.FailBadData: + if (remainingErrorMessagesToShow > 0) + { + if (--remainingErrorMessagesToShow == 0) + Log.Warning("{0}:{1}:{2}: Corrupt data; suppressing further corruption warnings for this file.", patchIndex[part.TargetIndex].RelativePath, part.TargetOffset, part.TargetEnd); + else + Log.Warning("{0}:{1}:{2}: Corrupt data", patchIndex[part.TargetIndex].RelativePath, part.TargetOffset, part.TargetEnd); + } + break; + } + }; + + verifier.OnVerifyProgress += OnVerifyProgressCallback; + verifier.OnCorruptionFound += OnCorruptionFoundCallback; + + try + { + verifier.SetTargetStreamsFromPathReadOnly(gameRootPath); + await verifier.VerifyFiles(false, 8, cancellationToken); + } + finally + { + verifier.OnVerifyProgress -= OnVerifyProgressCallback; + verifier.OnCorruptionFound -= OnCorruptionFoundCallback; + } + + return verifier; + } + + public static async Task RepairFromPatchFileIndexFromFile(IndexedZiPatchIndex patchIndex, string gameRootPath, string patchFileRootDir, int concurrentCount, CancellationToken? cancellationToken = null) + { + using var verifier = await VerifyFromZiPatchIndex(patchIndex, gameRootPath, cancellationToken); + verifier.SetTargetStreamsFromPathReadWriteForMissingFiles(gameRootPath); + for (var i = 0; i < patchIndex.Sources.Count; i++) + verifier.QueueInstall(i, new FileInfo(Path.Combine(patchFileRootDir, patchIndex.Sources[i]))); + await verifier.Install(concurrentCount, cancellationToken); + } + + public static async Task RepairFromPatchFileIndexFromFile(string patchIndexFilePath, string gameRootPath, string patchFileRootDir, int concurrentCount, CancellationToken? cancellationToken = null) => await RepairFromPatchFileIndexFromFile(new IndexedZiPatchIndex(new BinaryReader(new DeflateStream(new FileStream(patchIndexFilePath, FileMode.Open, FileAccess.Read), CompressionMode.Decompress))), gameRootPath, patchFileRootDir, concurrentCount, cancellationToken); + + public static async Task RepairFromPatchFileIndexFromUri(IndexedZiPatchIndex patchIndex, string gameRootPath, string baseUri, int concurrentCount, CancellationToken? cancellationToken = null) + { + using var verifier = await VerifyFromZiPatchIndex(patchIndex, gameRootPath, cancellationToken); + verifier.SetTargetStreamsFromPathReadWriteForMissingFiles(gameRootPath); + for (var i = 0; i < patchIndex.Sources.Count; i++) + verifier.QueueInstall(i, baseUri + patchIndex.Sources[i], null, concurrentCount); + + void OnInstallProgressCallback(int index, long progress, long max, IndexedZiPatchInstaller.InstallTaskState state) => Log.Information("[{0}/{1}] {2} {3}... {4:0.00}/{5:0.00}MB ({6:00.00}%)", index, patchIndex.Sources.Count, state, patchIndex.Sources[Math.Min(index, patchIndex.Sources.Count - 1)], progress / 1048576.0, max / 1048576.0, 100.0 * progress / max); + verifier.OnInstallProgress += OnInstallProgressCallback; + try + { + await verifier.Install(concurrentCount, cancellationToken); + verifier.WriteVersionFiles(gameRootPath); + } + finally + { + verifier.OnInstallProgress -= OnInstallProgressCallback; + } + } + + public static async Task RepairFromPatchFileIndexFromUri(string patchIndexFilePath, string gameRootPath, string baseUri, int concurrentCount, CancellationToken? cancellationToken = null) => await RepairFromPatchFileIndexFromUri(new IndexedZiPatchIndex(new BinaryReader(new DeflateStream(new FileStream(patchIndexFilePath, FileMode.Open, FileAccess.Read), CompressionMode.Decompress))), gameRootPath, baseUri, concurrentCount, cancellationToken); + + private static async Task Test_Single(int expacVersion, string patchFilesPath, string rootPath, string baseUri, CancellationToken? cancellationToken = null) + { + var patchFiles = Directory.GetFiles(Directory.GetDirectories(patchFilesPath).Where(x => Path.GetFileName(x).Length == 8).First(), "*.patch").ToList(); + patchFiles.Sort((x, y) => Path.GetFileName(x).Substring(1).CompareTo(Path.GetFileName(y).Substring(1))); + var patchIndex = await CreateZiPatchIndices(expacVersion, patchFiles, cancellationToken); + await RepairFromPatchFileIndexFromUri(patchIndex, rootPath, baseUri, 8, cancellationToken); + } + + public static void Test() + { + CancellationTokenSource source = new(); + string[] patchFileBaseUrls = new string[] { + "http://patch-dl.ffxiv.com/boot/2b5cbc63/", + }; + // source.Cancel(); + Task.WaitAll(new Task[] { + Test_Single(IndexedZiPatchIndex.EXPAC_VERSION_BOOT, @"Z:\patch-dl.ffxiv.com\boot", @"Z:\tgame\boot", patchFileBaseUrls[0], source.Token), + }); + } + } +} diff --git a/src/XIVLauncher2.Common/Patching/IndexedZiPatch/IndexedZiPatchPartLocator.cs b/src/XIVLauncher2.Common/Patching/IndexedZiPatch/IndexedZiPatchPartLocator.cs new file mode 100644 index 0000000..a3ba4b8 --- /dev/null +++ b/src/XIVLauncher2.Common/Patching/IndexedZiPatch/IndexedZiPatchPartLocator.cs @@ -0,0 +1,349 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.IO.Compression; +using System.Linq; +using System.Runtime.InteropServices; +using XIVLauncher2.Common.Patching.Util; + +namespace XIVLauncher2.Common.Patching.IndexedZiPatch +{ + [StructLayout(LayoutKind.Sequential)] + [Serializable] + public struct IndexedZiPatchPartLocator : IComparable + { + public const byte SOURCE_INDEX_ZEROS = byte.MaxValue - 0; + public const byte SOURCE_INDEX_EMPTY_BLOCK = byte.MaxValue - 1; + public const byte SOURCE_INDEX_UNAVAILABLE = byte.MaxValue - 2; + public const byte SOURCE_INDEX_MAX_VALID = byte.MaxValue - 3; + + private const uint TARGET_SIZE_AND_FLAG_MASK_IS_DEFLATED_BLOCK_DATA = 0x80000000; + private const uint TARGET_SIZE_AND_FLAG_MASK_IS_VALID_CRC32_VALUE = 0x40000000; + private const uint TARGET_SIZE_AND_FLAG_MASK_TARGET_SIZE = 0x3FFFFFFF; + + private uint TargetOffsetUint; // up to 35 bits, using only 32 bits (28 bits for locator + lsh 7; odd values exist), but currently .dat# files are delimited at 1.9GB + private uint SourceOffsetUint; // up to 31 bits (patch files were delimited at 1.5GB-ish; odd values exist) + private uint TargetSizeAndFlags; // 2 flag bits + up to 31 size bits, using only 30 bits (same with above) + public uint Crc32OrPlaceholderEntryDataUnits; // fixed 32 bits + private ushort SplitDecodedSourceFromUshort; // up to 14 bits (max value 15999) + private byte TargetIndexByte; // using only 8 bits for now + private byte SourceIndexByte; // using only 8 bits for now + + public long TargetOffset + { + get => TargetOffsetUint; + set => TargetOffsetUint = CheckedCastToUint(value); + } + + public long SourceOffset + { + get => SourceOffsetUint; + set => SourceOffsetUint = CheckedCastToUint(value); + } + + public long TargetSize + { + get => TargetSizeAndFlags & TARGET_SIZE_AND_FLAG_MASK_TARGET_SIZE; + set => TargetSizeAndFlags = CheckedCastToUint((TargetSizeAndFlags & ~TARGET_SIZE_AND_FLAG_MASK_TARGET_SIZE) | value, TARGET_SIZE_AND_FLAG_MASK_TARGET_SIZE); + } + + public long SplitDecodedSourceFrom + { + get => SplitDecodedSourceFromUshort; + set => SplitDecodedSourceFromUshort = CheckedCastToUshort(value); + } + + public int TargetIndex + { + get => TargetIndexByte; + set => TargetIndexByte = CheckedCastToByte(value); + } + + public int SourceIndex + { + get => SourceIndexByte; + set => SourceIndexByte = CheckedCastToByte(value); + } + + public long MaxSourceSize => IsDeflatedBlockData ? 16384 : TargetSize; + public long MaxSourceEnd => SourceOffset + MaxSourceSize; + public long TargetEnd => TargetOffset + TargetSize; + + public bool IsDeflatedBlockData + { + get => 0 != (TargetSizeAndFlags & TARGET_SIZE_AND_FLAG_MASK_IS_DEFLATED_BLOCK_DATA); + set => TargetSizeAndFlags = (TargetSizeAndFlags & ~TARGET_SIZE_AND_FLAG_MASK_IS_DEFLATED_BLOCK_DATA) | (value ? TARGET_SIZE_AND_FLAG_MASK_IS_DEFLATED_BLOCK_DATA : 0u); + } + + public bool IsValidCrc32Value + { + get => 0 != (TargetSizeAndFlags & TARGET_SIZE_AND_FLAG_MASK_IS_VALID_CRC32_VALUE); + set => TargetSizeAndFlags = (TargetSizeAndFlags & ~TARGET_SIZE_AND_FLAG_MASK_IS_VALID_CRC32_VALUE) | (value ? TARGET_SIZE_AND_FLAG_MASK_IS_VALID_CRC32_VALUE : 0u); + } + + public bool IsAllZeros => SourceIndex == SOURCE_INDEX_ZEROS; + public bool IsEmptyBlock => SourceIndex == SOURCE_INDEX_EMPTY_BLOCK; + public bool IsUnavailable => SourceIndex == SOURCE_INDEX_UNAVAILABLE; + public bool IsFromSourceFile => !IsAllZeros && !IsEmptyBlock && !IsUnavailable; + + public void WriteTo(BinaryWriter writer) + { + writer.Write(this.TargetOffsetUint); + writer.Write(this.SourceOffsetUint); + writer.Write(this.TargetSizeAndFlags); + writer.Write(this.Crc32OrPlaceholderEntryDataUnits); + writer.Write(this.SplitDecodedSourceFromUshort); + writer.Write(this.TargetIndexByte); + writer.Write(this.SourceIndexByte); + } + + public void ReadFrom(BinaryReader reader) + { + this.TargetOffsetUint = reader.ReadUInt32(); + this.SourceOffsetUint = reader.ReadUInt32(); + this.TargetSizeAndFlags = reader.ReadUInt32(); + this.Crc32OrPlaceholderEntryDataUnits = reader.ReadUInt32(); + this.SplitDecodedSourceFromUshort = reader.ReadUInt16(); + this.TargetIndexByte = reader.ReadByte(); + this.SourceIndexByte = reader.ReadByte(); + } + + public int CompareTo(IndexedZiPatchPartLocator other) + { + var x = TargetOffset - other.TargetOffset; + return x < 0 ? -1 : x > 0 ? 1 : 0; + } + + public enum VerifyDataResult + { + Pass, + FailUnverifiable, + FailNotEnoughData, + FailBadData, + } + + public VerifyDataResult Verify(byte[] buf, int offset, int length) + { + if (length != TargetSize) + return VerifyDataResult.FailNotEnoughData; + + if (IsValidCrc32Value) + return Crc32.Calculate(buf, offset, length) == Crc32OrPlaceholderEntryDataUnits ? VerifyDataResult.Pass : VerifyDataResult.FailBadData; + + if (IsAllZeros) + return buf.Skip(offset).Take(length).All(x => x == 0) ? VerifyDataResult.Pass : VerifyDataResult.FailBadData; + + if (IsEmptyBlock) + { + return BitConverter.ToInt32(buf, offset + 0) == 1 << 7 + && BitConverter.ToInt32(buf, offset + 4) == 0 + && BitConverter.ToInt32(buf, offset + 8) == 0 + && BitConverter.ToInt32(buf, offset + 12) == this.Crc32OrPlaceholderEntryDataUnits + && BitConverter.ToInt32(buf, offset + 16) == 0 + && BitConverter.ToInt32(buf, offset + 20) == 0 + && buf.Skip(offset + 24).Take(length - 24).All(x => x == 0) + ? VerifyDataResult.Pass + : VerifyDataResult.FailBadData; + } + + return VerifyDataResult.FailUnverifiable; + } + + public VerifyDataResult Verify(Stream stream, bool seek = true) + { + using var buffer = ReusableByteBufferManager.GetBuffer(); + if (seek) + stream.Seek(TargetOffset, SeekOrigin.Begin); + + if (IsValidCrc32Value) + { + Crc32 crc32 = new(); + + for (var remaining = TargetSize; remaining > 0; remaining -= buffer.Buffer.Length) + { + var readSize = (int)Math.Min(remaining, buffer.Buffer.Length); + if (readSize != stream.Read(buffer.Buffer, 0, readSize)) + return VerifyDataResult.FailNotEnoughData; + + crc32.Update(buffer.Buffer, 0, readSize); + } + + if (crc32.Checksum != Crc32OrPlaceholderEntryDataUnits) + return VerifyDataResult.FailBadData; + + return VerifyDataResult.Pass; + } + else if (IsAllZeros) + { + for (var remaining = TargetSize; remaining > 0; remaining -= buffer.Buffer.Length) + { + var readSize = (int)Math.Min(remaining, buffer.Buffer.Length); + if (readSize != stream.Read(buffer.Buffer, 0, readSize)) + return VerifyDataResult.FailNotEnoughData; + if (!buffer.Buffer.Take(readSize).All(x => x == 0)) + return VerifyDataResult.FailBadData; + } + + return VerifyDataResult.Pass; + } + else if (IsEmptyBlock) + { + var readSize = Math.Min(1 << 7, buffer.Buffer.Length); + if (readSize != stream.Read(buffer.Buffer, 0, readSize)) + return VerifyDataResult.FailNotEnoughData; + + // File entry header for placeholder + if (BitConverter.ToInt32(buffer.Buffer, 0) != 1 << 7 + || BitConverter.ToInt32(buffer.Buffer, 4) != 0 + || BitConverter.ToInt32(buffer.Buffer, 8) != 0 + || BitConverter.ToInt32(buffer.Buffer, 12) != Crc32OrPlaceholderEntryDataUnits + || BitConverter.ToInt32(buffer.Buffer, 16) != 0 + || BitConverter.ToInt32(buffer.Buffer, 20) != 0 + || !buffer.Buffer.Skip(24).Take(readSize - 24).All(x => x == 0)) + return VerifyDataResult.FailBadData; + + return VerifyDataResult.Pass; + } + + return VerifyDataResult.FailUnverifiable; + } + + public int Reconstruct(IList sources, byte[] buffer, int bufferOffset = 0, int bufferSize = -1, int relativeOffset = 0, bool verify = true) + { + if (IsFromSourceFile) + return Reconstruct(sources[SourceIndex], buffer, bufferOffset, bufferSize, relativeOffset, verify); + + return Reconstruct(null, 0, 0, buffer, bufferOffset, bufferSize, relativeOffset, verify); + } + + private int FilterBufferSize(byte[] buffer, int bufferOffset, int bufferSize, int relativeOffset) + { + if (bufferSize == -1) + return (int)Math.Max(0, Math.Min(TargetSize - relativeOffset, buffer.Length - bufferOffset)); + else if (bufferSize > TargetSize - relativeOffset) + return (int)Math.Max(0, TargetSize - relativeOffset); + else if (bufferSize < 0) + throw new ArgumentException("Length cannot be less than zero."); + else + return bufferSize; + } + + public int ReconstructWithoutSourceData(byte[] buffer, int bufferOffset = 0, int bufferSize = -1, int relativeOffset = 0) + { + bufferSize = FilterBufferSize(buffer, bufferOffset, bufferSize, relativeOffset); + if (bufferSize == 0) + return 0; + + if (IsUnavailable) + throw new InvalidOperationException("Unavailable part read attempt"); + else if (IsAllZeros) + Array.Clear(buffer, bufferOffset, bufferSize); + else if (IsEmptyBlock) + { + Array.Clear(buffer, bufferOffset, bufferSize); + + if (relativeOffset < 16) + { + using var buffer2 = ReusableByteBufferManager.GetBuffer(); + buffer2.Writer.Write(1 << 7); + buffer2.Writer.Write(0); + buffer2.Writer.Write(0); + buffer2.Writer.Write((int)Crc32OrPlaceholderEntryDataUnits); + buffer2.Writer.Write(0); + buffer2.Writer.Write(0); + Array.Copy(buffer2.Buffer, relativeOffset, buffer, bufferOffset, Math.Min(bufferSize, 24 - relativeOffset)); + } + } + else + throw new InvalidOperationException("This part requires source data."); + + return bufferSize; + } + + public int Reconstruct(byte[] sourceSegment, int sourceSegmentOffset, int sourceSegmentLength, byte[] buffer, int bufferOffset = 0, int bufferSize = -1, int relativeOffset = 0, bool verify = true) + { + if (!IsFromSourceFile) + return ReconstructWithoutSourceData(buffer, bufferOffset, bufferSize, relativeOffset); + + bufferSize = FilterBufferSize(buffer, bufferOffset, bufferSize, relativeOffset); + if (bufferSize == 0) + return 0; + + + if (IsDeflatedBlockData) + { + using var inflatedBuffer = ReusableByteBufferManager.GetBuffer(MaxSourceSize); + using (var stream = new DeflateStream(new MemoryStream(sourceSegment, sourceSegmentOffset, sourceSegmentLength - sourceSegmentOffset), CompressionMode.Decompress, true)) + stream.FullRead(inflatedBuffer.Buffer, 0, inflatedBuffer.Buffer.Length); + if (verify && VerifyDataResult.Pass != Verify(inflatedBuffer.Buffer, (int)SplitDecodedSourceFrom, (int)TargetSize)) + throw new IOException("Verify failed on reconstruct (inflate)"); + + Array.Copy(inflatedBuffer.Buffer, SplitDecodedSourceFrom + relativeOffset, buffer, bufferOffset, bufferSize); + } + else + { + if (sourceSegmentLength - sourceSegmentOffset < TargetSize) + throw new IOException("Insufficient source data"); + if (verify && VerifyDataResult.Pass != Verify(sourceSegment, (int)(sourceSegmentOffset + SplitDecodedSourceFrom), (int)TargetSize)) + throw new IOException("Verify failed on reconstruct"); + + Array.Copy(sourceSegment, sourceSegmentOffset + SplitDecodedSourceFrom + relativeOffset, buffer, bufferOffset, bufferSize); + } + + return bufferSize; + } + + public int Reconstruct(Stream source, byte[] buffer, int bufferOffset = 0, int bufferSize = -1, int relativeOffset = 0, bool verify = true) + { + if (!IsFromSourceFile) + return ReconstructWithoutSourceData(buffer, bufferOffset, bufferSize, relativeOffset); + + bufferSize = FilterBufferSize(buffer, bufferOffset, bufferSize, relativeOffset); + if (bufferSize == 0) + return 0; + + source.Seek(SourceOffset, SeekOrigin.Begin); + var readSize = (int)(IsDeflatedBlockData ? 16384 : TargetSize); + using var readBuffer = ReusableByteBufferManager.GetBuffer(readSize); + var read = source.Read(readBuffer.Buffer, 0, readSize); + return Reconstruct(readBuffer.Buffer, 0, read, buffer, bufferOffset, bufferSize, relativeOffset, verify); + } + + public static void CalculateCrc32(ref IndexedZiPatchPartLocator part, Stream source) + { + if (part.IsValidCrc32Value) + return; + + using var buffer = ReusableByteBufferManager.GetBuffer(part.TargetSize); + if (part.TargetSize != part.Reconstruct(source, buffer.Buffer, 0, (int)part.TargetSize, 0, false)) + throw new EndOfStreamException("Encountered premature end of file while trying to read the source stream."); + + part.Crc32OrPlaceholderEntryDataUnits = Crc32.Calculate(buffer.Buffer, 0, (int)part.TargetSize); + part.IsValidCrc32Value = true; + } + + private static uint CheckedCastToUint(long v, long maxValue = uint.MaxValue) + { + if (v > maxValue) + throw new ArgumentException("Value too big"); + + return (uint)v; + } + + private static ushort CheckedCastToUshort(long v, long maxValue = ushort.MaxValue) + { + if (v > maxValue) + throw new ArgumentException("Value too big"); + + return (ushort)v; + } + + private static byte CheckedCastToByte(long v, long maxValue = byte.MaxValue) + { + if (v > maxValue) + throw new ArgumentException("Value too big"); + + return (byte)v; + } + } +} diff --git a/src/XIVLauncher2.Common/Patching/IndexedZiPatch/IndexedZiPatchTargetFile.cs b/src/XIVLauncher2.Common/Patching/IndexedZiPatch/IndexedZiPatchTargetFile.cs new file mode 100644 index 0000000..e4f7511 --- /dev/null +++ b/src/XIVLauncher2.Common/Patching/IndexedZiPatch/IndexedZiPatchTargetFile.cs @@ -0,0 +1,232 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace XIVLauncher2.Common.Patching.IndexedZiPatch +{ + public partial class IndexedZiPatchTargetFile : IList + { + public string RelativePath = ""; + private readonly List underlying = new(); + + public IndexedZiPatchTargetFile() : base() { } + + public IndexedZiPatchTargetFile(string fileName) : base() { + RelativePath = fileName; + } + + public IndexedZiPatchTargetFile(BinaryReader reader, bool disposeReader = true) : base() + { + try + { + ReadFrom(reader); + } + finally + { + if (disposeReader) + reader.Dispose(); + } + } + + public IndexedZiPatchPartLocator this[int index] { get => this.underlying[index]; set => this.underlying[index] = value; } + + public int Count => this.underlying.Count; + + public bool IsReadOnly => false; + + public void Add(IndexedZiPatchPartLocator item) => this.underlying.Add(item); + + public void Clear() => this.underlying.Clear(); + + public bool Contains(IndexedZiPatchPartLocator item) => this.underlying.Contains(item); + + public void CopyTo(IndexedZiPatchPartLocator[] array, int arrayIndex) => this.underlying.CopyTo(array, arrayIndex); + + public IEnumerator GetEnumerator() => this.underlying.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => this.underlying.GetEnumerator(); + + public int IndexOf(IndexedZiPatchPartLocator item) => this.underlying.IndexOf(item); + + public void Insert(int index, IndexedZiPatchPartLocator item) => this.underlying.Insert(index, item); + + public bool Remove(IndexedZiPatchPartLocator item) => this.underlying.Remove(item); + + public void RemoveAt(int index) => this.underlying.RemoveAt(index); + + public long FileSize => this.underlying.Count > 0 ? this.underlying.Last().TargetEnd : 0; + + public int BinarySearchByTargetOffset(long targetOffset) + { + return this.underlying.BinarySearch(new IndexedZiPatchPartLocator { TargetOffset = targetOffset }); ; + } + + public void SplitAt(long offset, int targetFileIndex) + { + var i = BinarySearchByTargetOffset(offset); + if (i >= 0) + { + // Already split at given offset + return; + } + + i = ~i; + if (i == 0 && offset == 0) + { + // Do nothing; split at 0 is a given + } + else if (i == 0 && this.underlying.Count == 0) + { + this.underlying.Add(new IndexedZiPatchPartLocator + { + TargetSize = offset, + TargetIndex = targetFileIndex, + SourceIndex = IndexedZiPatchPartLocator.SOURCE_INDEX_ZEROS, + }); + } + else if (i == this.underlying.Count && this.underlying[i - 1].TargetEnd == offset) + { + // Do nothing; split at TargetEnd of last part is give + } + else if (i == this.underlying.Count && this.underlying[i - 1].TargetEnd < offset) + { + this.underlying.Add(new IndexedZiPatchPartLocator + { + TargetOffset = this.underlying[i - 1].TargetEnd, + TargetSize = offset - this.underlying[i - 1].TargetEnd, + TargetIndex = targetFileIndex, + SourceIndex = IndexedZiPatchPartLocator.SOURCE_INDEX_ZEROS, + }); + } + else + { + i -= 1; + var part = this.underlying[i]; + + if (part.IsDeflatedBlockData || part.IsEmptyBlock) + { + this.underlying[i] = new IndexedZiPatchPartLocator + { + TargetOffset = part.TargetOffset, + TargetSize = offset - part.TargetOffset, + TargetIndex = targetFileIndex, + SourceIndex = part.SourceIndex, + SourceOffset = part.SourceOffset, + SplitDecodedSourceFrom = part.SplitDecodedSourceFrom, + Crc32OrPlaceholderEntryDataUnits = part.Crc32OrPlaceholderEntryDataUnits, + IsDeflatedBlockData = part.IsDeflatedBlockData, + }; + this.underlying.Insert(i + 1, new IndexedZiPatchPartLocator + { + TargetOffset = offset, + TargetSize = part.TargetEnd - offset, + TargetIndex = targetFileIndex, + SourceIndex = part.SourceIndex, + SourceOffset = part.SourceOffset, + SplitDecodedSourceFrom = part.SplitDecodedSourceFrom + offset - part.TargetOffset, + Crc32OrPlaceholderEntryDataUnits = part.Crc32OrPlaceholderEntryDataUnits, + IsDeflatedBlockData = part.IsDeflatedBlockData, + }); + } + else + { + if (part.SplitDecodedSourceFrom != 0) + throw new ArgumentException("Not deflated but SplitDecodeSourceFrom is given"); + + this.underlying[i] = new IndexedZiPatchPartLocator + { + TargetOffset = part.TargetOffset, + TargetSize = offset - part.TargetOffset, + TargetIndex = targetFileIndex, + SourceIndex = part.SourceIndex, + SourceOffset = part.SourceOffset, + Crc32OrPlaceholderEntryDataUnits = part.Crc32OrPlaceholderEntryDataUnits, + }; + this.underlying.Insert(i + 1, new IndexedZiPatchPartLocator + { + TargetOffset = offset, + TargetSize = part.TargetEnd - offset, + TargetIndex = targetFileIndex, + SourceIndex = part.SourceIndex, + SourceOffset = part.SourceOffset + offset - part.TargetOffset, + Crc32OrPlaceholderEntryDataUnits = part.Crc32OrPlaceholderEntryDataUnits, + }); + } + } + } + + public void Update(IndexedZiPatchPartLocator part) + { + if (part.TargetSize == 0) + return; + + SplitAt(part.TargetOffset, part.TargetIndex); + SplitAt(part.TargetEnd, part.TargetIndex); + + var left = BinarySearchByTargetOffset(part.TargetOffset); + if (left < 0) + left = ~left; + + if (left == this.underlying.Count) + { + this.underlying.Add(part); + return; + } + + var right = BinarySearchByTargetOffset(part.TargetEnd); + if (right < 0) + right = ~right; + + if (right - left - 1 < 0) + Debugger.Break(); + + this.underlying[left] = part; + this.underlying.RemoveRange(left + 1, right - left - 1); + } + + public async Task CalculateCrc32(List sources, CancellationToken? cancellationToken = null) + { + await Task.Run(() => + { + var list = this.underlying.ToArray(); + for (var i = 0; i < list.Length; ++i) + { + if (cancellationToken.HasValue) + cancellationToken.Value.ThrowIfCancellationRequested(); + if (list[i].IsFromSourceFile) + IndexedZiPatchPartLocator.CalculateCrc32(ref list[i], sources[list[i].SourceIndex]); + } + this.underlying.Clear(); + this.underlying.AddRange(list); + }); + } + + public Stream ToStream(List sources) + { + return new IndexedZiPatchTargetViewStream(sources, this); + } + + public void WriteTo(BinaryWriter writer) + { + writer.Write(RelativePath); + writer.Write(this.underlying.Count); + foreach (var item in this.underlying) + item.WriteTo(writer); + } + + public void ReadFrom(BinaryReader reader) + { + RelativePath = reader.ReadString(); + var dest = new IndexedZiPatchPartLocator[reader.ReadInt32()]; + for (var i = 0; i < dest.Length; ++i) + dest[i].ReadFrom(reader); + this.underlying.Clear(); + this.underlying.AddRange(dest); + } + } +} diff --git a/src/XIVLauncher2.Common/Patching/IndexedZiPatch/IndexedZiPatchTargetViewStream.cs b/src/XIVLauncher2.Common/Patching/IndexedZiPatch/IndexedZiPatchTargetViewStream.cs new file mode 100644 index 0000000..f06045d --- /dev/null +++ b/src/XIVLauncher2.Common/Patching/IndexedZiPatch/IndexedZiPatchTargetViewStream.cs @@ -0,0 +1,80 @@ +using System; +using System.Collections.Generic; +using System.IO; + +namespace XIVLauncher2.Common.Patching.IndexedZiPatch +{ + public class IndexedZiPatchTargetViewStream : Stream + { + private readonly List sources; + private readonly IndexedZiPatchTargetFile partList; + + internal IndexedZiPatchTargetViewStream(List sources, IndexedZiPatchTargetFile partList) + { + this.sources = sources; + this.partList = partList; + } + + public override bool CanRead => true; + + public override bool CanSeek => true; + + public override bool CanWrite => false; + + public override long Length => this.partList.FileSize; + + public override long Position { get; set; } + + public override int Read(byte[] buffer, int offset, int count) + { + var beginOffset = offset; + while (count > 0 && Position < Length) + { + var i = this.partList.BinarySearchByTargetOffset(Position); + if (i < 0) + i = ~i - 1; + + var reconstructedLength = this.partList[i].Reconstruct(this.sources, buffer, offset, count, (int)(Position - this.partList[i].TargetOffset)); + offset += reconstructedLength; + count -= reconstructedLength; + Position += reconstructedLength; + } + return offset - beginOffset; + } + + public override long Seek(long offset, SeekOrigin origin) + { + var position = Position; + switch (origin) + { + case SeekOrigin.Begin: + position = offset; + break; + + case SeekOrigin.Current: + position += offset; + break; + + case SeekOrigin.End: + position = Length - offset; + break; + + default: + throw new NotImplementedException(); + } + + if (position < 0) + throw new ArgumentException("Seeking is attempted before the beginning of the stream."); + + Position = Math.Min(position, Length); + + return Position; + } + + public override void Flush() => throw new NotSupportedException(); + + public override void SetLength(long value) => throw new NotSupportedException(); + + public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException(); + } +} diff --git a/src/XIVLauncher2.Common/Patching/RemotePatchInstaller.cs b/src/XIVLauncher2.Common/Patching/RemotePatchInstaller.cs new file mode 100644 index 0000000..caca568 --- /dev/null +++ b/src/XIVLauncher2.Common/Patching/RemotePatchInstaller.cs @@ -0,0 +1,215 @@ +using System; +using System.Collections.Concurrent; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Serilog; +using XIVLauncher2.Common.PatcherIpc; +using XIVLauncher2.Common.Patching.Rpc; +using XIVLauncher2.Common.Patching.ZiPatch; +using XIVLauncher2.Common.Patching.ZiPatch.Util; + +namespace XIVLauncher2.Common.Patching; + +public class RemotePatchInstaller +{ + private readonly IRpc rpc; + private readonly ConcurrentQueue queuedInstalls = new(); + private readonly Thread patcherThread; + private readonly CancellationTokenSource patcherCancelToken = new(); + + public bool IsDone { get; private set; } + + public bool IsFailed { get; private set; } + + public bool HasQueuedInstalls => !this.queuedInstalls.IsEmpty; + + public RemotePatchInstaller(IRpc rpc) + { + this.rpc = rpc; + this.rpc.MessageReceived += RemoteCallHandler; + + Log.Information("[PATCHER] IPC connected"); + + rpc.SendMessage(new PatcherIpcEnvelope + { + OpCode = PatcherIpcOpCode.Hello, + Timestamp = DateTime.Now + }); + + Log.Information("[PATCHER] sent hello"); + + this.patcherThread = new Thread(this.ProcessPatches); + } + + public void Start() + { + this.patcherThread.Start(); + } + + private void ProcessPatches() + { + try + { + while (!this.patcherCancelToken.IsCancellationRequested) + { + if (!RunInstallQueue()) + { + IsFailed = true; + return; + } + + Thread.Sleep(1000); + } + } + catch (Exception ex) + { + Log.Error(ex, "[PATCHER] RemotePatchInstaller loop encountered an error"); + this.rpc.SendMessage(new PatcherIpcEnvelope + { + OpCode = PatcherIpcOpCode.InstallFailed + }); + } + } + + private void RemoteCallHandler(PatcherIpcEnvelope envelope) + { + switch (envelope.OpCode) + { + case PatcherIpcOpCode.Bye: + Task.Run(() => + { + Thread.Sleep(3000); + IsDone = true; + }); + break; + + case PatcherIpcOpCode.StartInstall: + + var installData = envelope.StartInstallInfo; + this.queuedInstalls.Enqueue(installData); + break; + + case PatcherIpcOpCode.Finish: + var path = envelope.GameDirectory; + + try + { + VerToBck(path); + Log.Information("VerToBck done"); + } + catch (Exception ex) + { + Log.Error(ex, "VerToBck failed"); + this.rpc.SendMessage(new PatcherIpcEnvelope + { + OpCode = PatcherIpcOpCode.InstallFailed + }); + } + + break; + } + } + + private bool RunInstallQueue() + { + if (this.queuedInstalls.TryDequeue(out var installData)) + { + // Ensure that subdirs exist + if (!installData.GameDirectory.Exists) + installData.GameDirectory.Create(); + + installData.GameDirectory.CreateSubdirectory("game"); + installData.GameDirectory.CreateSubdirectory("boot"); + + try + { + InstallPatch(installData.PatchFile.FullName, + Path.Combine(installData.GameDirectory.FullName, + installData.Repo == Repository.Boot ? "boot" : "game")); + + try + { + installData.Repo.SetVer(installData.GameDirectory, installData.VersionId); + this.rpc.SendMessage(new PatcherIpcEnvelope + { + OpCode = PatcherIpcOpCode.InstallOk + }); + + try + { + if (!installData.KeepPatch) + installData.PatchFile.Delete(); + } + catch (Exception exception) + { + Log.Error(exception, "Could not delete patch file"); + } + } + catch (Exception ex) + { + Log.Error(ex, "Could not set ver file"); + this.rpc.SendMessage(new PatcherIpcEnvelope + { + OpCode = PatcherIpcOpCode.InstallFailed + }); + + return false; + } + } + catch (Exception ex) + { + Log.Error(ex, "[PATCHER] Patch install failed"); + this.rpc.SendMessage(new PatcherIpcEnvelope + { + OpCode = PatcherIpcOpCode.InstallFailed + }); + + return false; + } + } + + return true; + } + + public static void InstallPatch(string patchPath, string gamePath) + { + Log.Information("[PATCHER] Installing {0} to {1}", patchPath, gamePath); + + using var patchFile = ZiPatchFile.FromFileName(patchPath); + + using (var store = new SqexFileStreamStore()) + { + var config = new ZiPatchConfig(gamePath) { Store = store }; + + foreach (var chunk in patchFile.GetChunks()) + chunk.ApplyChunk(config); + } + + Log.Information("[PATCHER] Patch {0} installed", patchPath); + } + + private static void VerToBck(DirectoryInfo gamePath) + { + Thread.Sleep(500); + + foreach (var repository in Enum.GetValues(typeof(Repository)).Cast()) + { + // Overwrite the old BCK with the new game version + var ver = repository.GetVer(gamePath); + + try + { + repository.SetVer(gamePath, ver, true); + } + catch (Exception ex) + { + Log.Error(ex, "[PATCHER] Could not copy to BCK"); + + if (ver != Constants.BASE_GAME_VERSION) + throw; + } + } + } +} diff --git a/src/XIVLauncher2.Common/Patching/Rpc/IRpc.cs b/src/XIVLauncher2.Common/Patching/Rpc/IRpc.cs new file mode 100644 index 0000000..0d1f7f8 --- /dev/null +++ b/src/XIVLauncher2.Common/Patching/Rpc/IRpc.cs @@ -0,0 +1,11 @@ +using System; +using XIVLauncher2.Common.PatcherIpc; + +namespace XIVLauncher2.Common.Patching.Rpc; + +public interface IRpc +{ + public void SendMessage(PatcherIpcEnvelope envelope); + + public event Action MessageReceived; +} diff --git a/src/XIVLauncher2.Common/Patching/Rpc/Implementations/InProcessRpc.cs b/src/XIVLauncher2.Common/Patching/Rpc/Implementations/InProcessRpc.cs new file mode 100644 index 0000000..1aacd81 --- /dev/null +++ b/src/XIVLauncher2.Common/Patching/Rpc/Implementations/InProcessRpc.cs @@ -0,0 +1,52 @@ +using System; +using System.Collections.Generic; +using XIVLauncher2.Common.PatcherIpc; + +namespace XIVLauncher2.Common.Patching.Rpc.Implementations; + +public class InProcessRpc : IRpc, IDisposable +{ + private readonly string channelName; + + private static readonly Dictionary> instanceMapping = new(); + + public InProcessRpc(string channelName) + { + this.channelName = channelName; + + if (!instanceMapping.TryGetValue(channelName, out var instanceList)) + { + instanceList = new List(); + instanceMapping.Add(channelName, instanceList); + } + + instanceList.Add(this); + } + + public void SendMessage(PatcherIpcEnvelope envelope) + { + var list = instanceMapping[this.channelName]; + + for (var i = 0; i < list.Count; i++) + { + var otherInstance = list[i]; + + if (otherInstance == this) + continue; + + otherInstance.Dispatch(envelope); + } + } + + private void Dispatch(PatcherIpcEnvelope envelope) + { + this.MessageReceived?.Invoke(envelope); + } + + public event Action MessageReceived; + + public void Dispose() + { + instanceMapping[this.channelName].Remove(this); + } +} diff --git a/src/XIVLauncher2.Common/Patching/Rpc/Implementations/SharedMemoryRpc.cs b/src/XIVLauncher2.Common/Patching/Rpc/Implementations/SharedMemoryRpc.cs new file mode 100644 index 0000000..e3afc00 --- /dev/null +++ b/src/XIVLauncher2.Common/Patching/Rpc/Implementations/SharedMemoryRpc.cs @@ -0,0 +1,41 @@ +using System; +using System.Text; +using System.Text.Json; +using System.Text.Json.Serialization; +using Serilog; +using SharedMemory; +using XIVLauncher2.Common.PatcherIpc; + +namespace XIVLauncher2.Common.Patching.Rpc.Implementations; + +public class SharedMemoryRpc : IRpc, IDisposable +{ + private readonly RpcBuffer rpcBuffer; + + public SharedMemoryRpc(string channelName) + { + this.rpcBuffer = new RpcBuffer(channelName, RemoteCallHandler); + } + + private void RemoteCallHandler(ulong msgId, byte[] payload) + { + var json = IpcHelpers.Base64Decode(Encoding.ASCII.GetString(payload)); + Log.Information("[SHMEMRPC] IPC({0}): {1}", msgId, json); + + var msg = JsonSerializer.Deserialize(json); + MessageReceived?.Invoke(msg); + } + + public void SendMessage(PatcherIpcEnvelope envelope) + { + var json = IpcHelpers.Base64Encode(JsonSerializer.Serialize(envelope)); + this.rpcBuffer.RemoteRequest(Encoding.ASCII.GetBytes(json)); + } + + public event Action MessageReceived; + + public void Dispose() + { + rpcBuffer?.Dispose(); + } +} diff --git a/src/XIVLauncher2.Common/Patching/Rpc/IpcHelpers.cs b/src/XIVLauncher2.Common/Patching/Rpc/IpcHelpers.cs new file mode 100644 index 0000000..9192abd --- /dev/null +++ b/src/XIVLauncher2.Common/Patching/Rpc/IpcHelpers.cs @@ -0,0 +1,16 @@ +namespace XIVLauncher2.Common.PatcherIpc; + +public static class IpcHelpers +{ + public static string Base64Encode(string plainText) + { + var plainTextBytes = System.Text.Encoding.UTF8.GetBytes(plainText); + return System.Convert.ToBase64String(plainTextBytes); + } + + public static string Base64Decode(string base64EncodedData) + { + var base64EncodedBytes = System.Convert.FromBase64String(base64EncodedData); + return System.Text.Encoding.UTF8.GetString(base64EncodedBytes); + } +} diff --git a/src/XIVLauncher2.Common/Patching/Rpc/PatcherIpcEnvelope.cs b/src/XIVLauncher2.Common/Patching/Rpc/PatcherIpcEnvelope.cs new file mode 100644 index 0000000..0558021 --- /dev/null +++ b/src/XIVLauncher2.Common/Patching/Rpc/PatcherIpcEnvelope.cs @@ -0,0 +1,22 @@ +#nullable enable +using System; +using System.IO; +using System.Text.Json.Serialization; + +namespace XIVLauncher2.Common.PatcherIpc +{ + public class PatcherIpcEnvelope + { + public PatcherIpcOpCode OpCode { get; set; } + public DateTime? Timestamp { get; set; } + public PatcherIpcStartInstall? StartInstallInfo { get; set; } + public string? GameDirectoryPath { get; set; } + + [JsonIgnore] + public DirectoryInfo? GameDirectory + { + get => new(GameDirectoryPath!); + set => GameDirectoryPath = value?.FullName; + } + } +} diff --git a/src/XIVLauncher2.Common/Patching/Rpc/PatcherIpcOpCode.cs b/src/XIVLauncher2.Common/Patching/Rpc/PatcherIpcOpCode.cs new file mode 100644 index 0000000..383e0db --- /dev/null +++ b/src/XIVLauncher2.Common/Patching/Rpc/PatcherIpcOpCode.cs @@ -0,0 +1,13 @@ +namespace XIVLauncher2.Common.PatcherIpc +{ + public enum PatcherIpcOpCode + { + Hello, + Bye, + StartInstall, + InstallRunning, + InstallOk, + InstallFailed, + Finish + } +} diff --git a/src/XIVLauncher2.Common/Patching/Rpc/PatcherIpcStartInstall.cs b/src/XIVLauncher2.Common/Patching/Rpc/PatcherIpcStartInstall.cs new file mode 100644 index 0000000..46bbeaa --- /dev/null +++ b/src/XIVLauncher2.Common/Patching/Rpc/PatcherIpcStartInstall.cs @@ -0,0 +1,29 @@ +#nullable enable +using System.IO; +using System.Text.Json.Serialization; + +namespace XIVLauncher2.Common.PatcherIpc +{ + public class PatcherIpcStartInstall + { + public Repository Repo { get; set; } + public string VersionId { get; set; } = null!; + public bool KeepPatch { get; set; } + public string? PatchFilePath { get; set; } + public string? GameDirectoryPath { get; set; } + + [JsonIgnore] + public FileInfo? PatchFile + { + get => new(PatchFilePath!); + set => PatchFilePath = value?.FullName; + } + + [JsonIgnore] + public DirectoryInfo? GameDirectory + { + get => new(GameDirectoryPath!); + set => GameDirectoryPath = value?.FullName; + } + } +} diff --git a/src/XIVLauncher2.Common/Patching/Util/BinaryReaderHelpers.cs b/src/XIVLauncher2.Common/Patching/Util/BinaryReaderHelpers.cs new file mode 100644 index 0000000..9897c34 --- /dev/null +++ b/src/XIVLauncher2.Common/Patching/Util/BinaryReaderHelpers.cs @@ -0,0 +1,126 @@ +using System; +using System.Diagnostics; +using System.IO; +using System.Text; +// ReSharper disable InconsistentNaming + +namespace XIVLauncher2.Common.Patching.Util +{ + // https://stackoverflow.com/a/15274591 + static class BinaryReaderHelpers + { + + public static string ReadFixedLengthString(this BinaryReader reader, uint length) + { + return Encoding.ASCII.GetString(reader.ReadBytesRequired((int)length)).TrimEnd((char)0); + } + + // Note this MODIFIES THE GIVEN ARRAY then returns a reference to the modified array. + public static byte[] Reverse(this byte[] b) + { + Array.Reverse(b); + return b; + } + + public static UInt16 ReadUInt16BE(this BinaryReader binRdr) + { + return BitConverter.ToUInt16(binRdr.ReadBytesRequired(sizeof(UInt16)).Reverse(), 0); + } + + public static Int16 ReadInt16BE(this BinaryReader binRdr) + { + return BitConverter.ToInt16(binRdr.ReadBytesRequired(sizeof(Int16)).Reverse(), 0); + } + + public static UInt32 ReadUInt32BE(this BinaryReader binRdr) + { + return BitConverter.ToUInt32(binRdr.ReadBytesRequired(sizeof(UInt32)).Reverse(), 0); + } + + public static Int32 ReadInt32BE(this BinaryReader binRdr) + { + return BitConverter.ToInt32(binRdr.ReadBytesRequired(sizeof(Int32)).Reverse(), 0); + } + + public static UInt64 ReadUInt64BE(this BinaryReader binRdr) + { + return BitConverter.ToUInt64(binRdr.ReadBytesRequired(sizeof(UInt64)).Reverse(), 0); + } + + public static Int64 ReadInt64BE(this BinaryReader binRdr) + { + return BitConverter.ToInt64(binRdr.ReadBytesRequired(sizeof(Int64)).Reverse(), 0); + } + + public static byte[] ReadBytesRequired(this BinaryReader binRdr, int byteCount) + { + var result = binRdr.ReadBytes(byteCount); + + if (result.Length != byteCount) + throw new EndOfStreamException($"{byteCount} bytes required from stream, but only {result.Length} returned."); + + return result; + } + + public static void Dump(this byte[] bytes, int offset = 0, int bytesPerLine = 16) + { + var hexChars = "0123456789ABCDEF".ToCharArray(); + + var offsetBlock = 8 + 3; + var byteBlock = offsetBlock + bytesPerLine * 3 + (bytesPerLine - 1) / 8 + 2; + var lineLength = byteBlock + bytesPerLine + Environment.NewLine.Length; + + var line = (new string(' ', lineLength - Environment.NewLine.Length) + Environment.NewLine).ToCharArray(); + var numLines = (bytes.Length + bytesPerLine - 1) / bytesPerLine; + + var sb = new StringBuilder(numLines * lineLength); + sb.Append('\n'); + + for (var i = 0; i < bytes.Length; i += bytesPerLine) + { + var h = i + offset; + + line[0] = hexChars[(h >> 28) & 0xF]; + line[1] = hexChars[(h >> 24) & 0xF]; + line[2] = hexChars[(h >> 20) & 0xF]; + line[3] = hexChars[(h >> 16) & 0xF]; + line[4] = hexChars[(h >> 12) & 0xF]; + line[5] = hexChars[(h >> 8) & 0xF]; + line[6] = hexChars[(h >> 4) & 0xF]; + line[7] = hexChars[(h >> 0) & 0xF]; + + var hexColumn = offsetBlock; + var charColumn = byteBlock; + + for (var j = 0; j < bytesPerLine; j++) + { + if (j > 0 && (j & 7) == 0) + { + hexColumn++; + } + + if (i + j >= bytes.Length) + { + line[hexColumn] = ' '; + line[hexColumn + 1] = ' '; + line[charColumn] = ' '; + } + else + { + var by = bytes[i + j]; + line[hexColumn] = hexChars[(by >> 4) & 0xF]; + line[hexColumn + 1] = hexChars[by & 0xF]; + line[charColumn] = by < 32 ? '.' : (char)by; + } + + hexColumn += 3; + charColumn++; + } + + sb.Append(line); + } + + Debug.WriteLine(sb.ToString().TrimEnd(Environment.NewLine.ToCharArray())); + } + } +} diff --git a/src/XIVLauncher2.Common/Patching/Util/ChecksumBinaryReader.cs b/src/XIVLauncher2.Common/Patching/Util/ChecksumBinaryReader.cs new file mode 100644 index 0000000..cf1d0c4 --- /dev/null +++ b/src/XIVLauncher2.Common/Patching/Util/ChecksumBinaryReader.cs @@ -0,0 +1,53 @@ +using System; +using System.IO; + +namespace XIVLauncher2.Common.Patching.Util +{ + public class ChecksumBinaryReader : BinaryReader + { + private readonly Crc32 _crc32 = new Crc32(); + + public ChecksumBinaryReader(Stream input) : base(input) {} + + + public void InitCrc32() + { + _crc32.Init(); + } + + public uint GetCrc32() + { + return _crc32.Checksum; + } + + public override byte[] ReadBytes(int count) + { + var result = base.ReadBytes(count); + + _crc32.Update(result); + + return result; + } + + public override byte ReadByte() + { + var result = base.ReadByte(); + + _crc32.Update(result); + + return result; + } + + public override sbyte ReadSByte() => (sbyte)ReadByte(); + public override bool ReadBoolean() => ReadByte() != 0; + public override char ReadChar() => (char)ReadByte(); + public override short ReadInt16() => BitConverter.ToInt16(ReadBytes(sizeof(short)), 0); + public override ushort ReadUInt16() => BitConverter.ToUInt16(ReadBytes(sizeof(ushort)), 0); + public override int ReadInt32() => BitConverter.ToInt32(ReadBytes(sizeof(int)), 0); + public override uint ReadUInt32() => BitConverter.ToUInt32(ReadBytes(sizeof(uint)), 0); + public override long ReadInt64() => BitConverter.ToInt64(ReadBytes(sizeof(long)), 0); + public override ulong ReadUInt64() => BitConverter.ToUInt64(ReadBytes(sizeof(ulong)), 0); + public override float ReadSingle() => BitConverter.ToSingle(ReadBytes(sizeof(float)), 0); + public override double ReadDouble() => BitConverter.ToDouble(ReadBytes(sizeof(float)), 0); + } +} diff --git a/src/XIVLauncher2.Common/Patching/Util/CircularMemoryStream.cs b/src/XIVLauncher2.Common/Patching/Util/CircularMemoryStream.cs new file mode 100644 index 0000000..b658482 --- /dev/null +++ b/src/XIVLauncher2.Common/Patching/Util/CircularMemoryStream.cs @@ -0,0 +1,268 @@ +using System; +using System.IO; + +namespace XIVLauncher2.Common.Patching.Util +{ + public class CircularMemoryStream : Stream + { + public enum FeedOverflowMode + { + ExtendCapacity, + DiscardOldest, + Throw, + } + + private readonly FeedOverflowMode overflowMode; + private ReusableByteBufferManager.Allocation reusableBuffer; + private int bufferValidTo = 0; + private int bufferValidFrom = 0; + private int length = 0; + private int externalPosition = 0; + + public CircularMemoryStream(int baseCapacity = 0, FeedOverflowMode feedOverflowMode = FeedOverflowMode.ExtendCapacity) + { + this.overflowMode = feedOverflowMode; + if (feedOverflowMode == FeedOverflowMode.ExtendCapacity && baseCapacity == 0) + this.reusableBuffer = ReusableByteBufferManager.GetBuffer(); + else + this.reusableBuffer = ReusableByteBufferManager.GetBuffer(baseCapacity); + } + + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + this.reusableBuffer?.Dispose(); + } + + public void Reserve(long capacity) + { + if (capacity <= Capacity) + return; + + var newBuffer = ReusableByteBufferManager.GetBuffer(capacity); + if (this.length > 0) + { + if (this.bufferValidFrom < this.bufferValidTo) + Array.Copy(this.reusableBuffer.Buffer, this.bufferValidFrom, newBuffer.Buffer, 0, this.length); + else + { + Array.Copy(this.reusableBuffer.Buffer, this.bufferValidFrom, newBuffer.Buffer, 0, Capacity - this.bufferValidFrom); + Array.Copy(this.reusableBuffer.Buffer, 0, newBuffer.Buffer, Capacity - this.bufferValidFrom, this.bufferValidTo); + } + } + + this.reusableBuffer.Dispose(); + this.reusableBuffer = newBuffer; + + this.bufferValidFrom = 0; + this.bufferValidTo = this.length; + } + + public void Feed(byte[] buffer, int offset, int count) + { + if (count == 0) + return; + + if (this.length + count > Capacity) + { + switch (this.overflowMode) + { + case FeedOverflowMode.ExtendCapacity: + Reserve(Length + count); + break; + + case FeedOverflowMode.DiscardOldest: + if (count >= Capacity) + { + this.bufferValidFrom = 0; + this.bufferValidTo = 0; + Array.Copy(buffer, offset + count - Capacity, this.reusableBuffer.Buffer, 0, Capacity); + this.externalPosition = 0; + this.length = Capacity; + return; + } + Consume(null, 0, this.length + count - Capacity); + break; + + case FeedOverflowMode.Throw: + throw new InvalidOperationException($"Cannot feed {count} bytes (length={Length}, capacity={Capacity})"); + } + } + + if (this.bufferValidFrom < this.bufferValidTo) + { + var rightLength = Capacity - this.bufferValidTo; + if (rightLength >= count) + Buffer.BlockCopy(buffer, offset, this.reusableBuffer.Buffer, this.bufferValidTo, count); + else + { + Buffer.BlockCopy(buffer, offset, this.reusableBuffer.Buffer, this.bufferValidTo, rightLength); + Buffer.BlockCopy(buffer, offset + rightLength, this.reusableBuffer.Buffer, 0, count - rightLength); + } + } + else + Buffer.BlockCopy(buffer, offset, this.reusableBuffer.Buffer, this.bufferValidTo, count); + + this.bufferValidTo = (this.bufferValidTo + count) % Capacity; + this.length += count; + } + + public int Consume(byte[] buffer, int offset, int count, bool peek = false) + { + count = Math.Min(count, this.length); + if (buffer != null && count > 0) + { + if (this.bufferValidFrom < this.bufferValidTo) + Buffer.BlockCopy(this.reusableBuffer.Buffer, this.bufferValidFrom, buffer, offset, count); + else + { + int rightLength = Capacity - this.bufferValidFrom; + if (rightLength >= count) + Buffer.BlockCopy(this.reusableBuffer.Buffer, this.bufferValidFrom, buffer, offset, count); + else + { + Buffer.BlockCopy(this.reusableBuffer.Buffer, this.bufferValidFrom, buffer, offset, rightLength); + Buffer.BlockCopy(this.reusableBuffer.Buffer, 0, buffer, offset + rightLength, count - rightLength); + } + } + } + if (!peek) + { + this.length -= count; + if (this.length == 0) + this.bufferValidFrom = this.bufferValidTo = 0; + else + this.bufferValidFrom = (this.bufferValidFrom + count) % Capacity; + this.externalPosition = Math.Max(0, this.externalPosition - count); + } + return count; + } + + public byte this[long i] + { + get + { + if (i < 0 || i >= Length) + throw new ArgumentOutOfRangeException(nameof(i)); + return this.reusableBuffer.Buffer[(this.bufferValidFrom + i) % Capacity]; + } + set + { + if (i < 0 || i >= Length) + throw new ArgumentOutOfRangeException(nameof(i)); + this.reusableBuffer.Buffer[(this.bufferValidFrom + i) % Capacity] = value; + } + } + + public int Capacity => this.reusableBuffer.Buffer.Length; + + public override bool CanRead => true; + public override bool CanSeek => true; + public override bool CanWrite => true; + public override long Length => this.length; + public override long Position + { + get => this.externalPosition; + set => Seek(value, SeekOrigin.Begin); + } + + public override void Flush() { } + + public override void SetLength(long value) + { + if (value > int.MaxValue) + throw new ArgumentOutOfRangeException("Length can be up to int.MaxValue"); + if (value == 0) + { + this.bufferValidFrom = this.bufferValidTo = this.length = 0; + return; + } + + var intValue = (int)value; + if (intValue > Capacity) + Reserve(intValue); + else if (intValue > Length) + { + var extendLength = (int)(intValue - Length); + var newValidTo = (this.bufferValidTo + extendLength) % Capacity; + + if (this.bufferValidTo < newValidTo) + Array.Clear(this.reusableBuffer.Buffer, this.bufferValidTo, newValidTo - this.bufferValidTo); + else + { + Array.Clear(this.reusableBuffer.Buffer, this.bufferValidTo, Capacity - this.bufferValidTo); + Array.Clear(this.reusableBuffer.Buffer, 0, newValidTo); + } + + this.bufferValidTo = newValidTo; + } + else if (intValue < Length) + this.bufferValidTo = (this.bufferValidFrom + intValue) % Capacity; + this.length = (int)value; + } + + public override int Read(byte[] buffer, int offset, int count) + { + count = Math.Min(count, this.length - this.externalPosition); + + var adjValidFrom = (this.bufferValidFrom + this.externalPosition) % Capacity; + if (adjValidFrom < this.bufferValidTo) + Buffer.BlockCopy(this.reusableBuffer.Buffer, adjValidFrom, buffer, offset, count); + else + { + int rightLength = Capacity - adjValidFrom; + if (rightLength >= count) + Buffer.BlockCopy(this.reusableBuffer.Buffer, adjValidFrom, buffer, offset, count); + else + { + Buffer.BlockCopy(this.reusableBuffer.Buffer, adjValidFrom, buffer, offset, rightLength); + Buffer.BlockCopy(this.reusableBuffer.Buffer, 0, buffer, offset + rightLength, count - rightLength); + } + } + + this.externalPosition += count; + return count; + } + + public override long Seek(long offset, SeekOrigin origin) + { + long newPosition = this.externalPosition; + switch (origin) + { + case SeekOrigin.Begin: + newPosition = offset; + break; + case SeekOrigin.Current: + newPosition += offset; + break; + case SeekOrigin.End: + newPosition = Length - offset; + break; + } + if (newPosition < 0) + throw new ArgumentException("Seeking is attempted before the beginning of the stream."); + if (newPosition > this.length) + newPosition = this.length; + this.externalPosition = (int)newPosition; + return newPosition; + } + + public override void Write(byte[] buffer, int offset, int count) + { + if (Length + count > Capacity) + Reserve((int)(Length + count)); + + var writeOffset = (this.bufferValidFrom + this.externalPosition) % Capacity; + if (writeOffset + count <= Capacity) + Array.Copy(buffer, offset, this.reusableBuffer.Buffer, writeOffset, count); + else + { + var writeCount1 = Capacity - writeOffset; + var writeCount2 = count - writeCount1; + Array.Copy(buffer, offset, this.reusableBuffer.Buffer, writeOffset, writeCount1); + Array.Copy(buffer, offset + writeCount1, this.reusableBuffer.Buffer, 0, writeCount2); + } + this.externalPosition += count; + } + } +} diff --git a/src/XIVLauncher2.Common/Patching/Util/Crc32.cs b/src/XIVLauncher2.Common/Patching/Util/Crc32.cs new file mode 100644 index 0000000..16a07a1 --- /dev/null +++ b/src/XIVLauncher2.Common/Patching/Util/Crc32.cs @@ -0,0 +1,67 @@ +using System.Linq; + +namespace XIVLauncher2.Common.Patching.Util +{ + /// + /// Performs the 32-bit reversed variant of the cyclic redundancy check algorithm + /// + public class Crc32 + { + private const uint POLY = 0xedb88320; + + private static readonly uint[] CrcArray = + Enumerable.Range(0, 256).Select(i => + { + var k = (uint)i; + for (var j = 0; j < 8; j++) + k = (k & 1) != 0 ? + (k >> 1) ^ POLY : + k >> 1; + + return k; + }).ToArray(); + + public uint Checksum => ~_crc32; + + private uint _crc32 = 0xFFFFFFFF; + + /// + /// Initializes Crc32's state + /// + public void Init() + { + _crc32 = 0xFFFFFFFF; + } + + /// + /// Updates Crc32's state with new data + /// + /// Data to calculate the new CRC from + public void Update(byte[] data) + { + foreach (var b in data) + Update(b); + } + + public void Update(byte[] data, int offset, int length) + { + for (int i = offset, readIndex = offset + length; i < readIndex; i++) + Update(data[i]); + } + + public void Update(byte b) + { + _crc32 = CrcArray[(_crc32 ^ b) & 0xFF] ^ + ((_crc32 >> 8) & 0x00FFFFFF); + } + + public static uint Calculate(byte[] data, int offset, int length) + { + uint v = 0xFFFFFFFF; + for (int i = offset, readIndex = offset + length; i < readIndex; i++) + v = CrcArray[(v ^ data[i]) & 0xFF] ^ + ((v >> 8) & 0x00FFFFFF); + return ~v; + } + } +} diff --git a/src/XIVLauncher2.Common/Patching/Util/FullDeflateStreamReader.cs b/src/XIVLauncher2.Common/Patching/Util/FullDeflateStreamReader.cs new file mode 100644 index 0000000..ba3b4b9 --- /dev/null +++ b/src/XIVLauncher2.Common/Patching/Util/FullDeflateStreamReader.cs @@ -0,0 +1,22 @@ +using System; +using System.IO; +using System.IO.Compression; +// ReSharper disable InconsistentNaming + +namespace XIVLauncher2.Common.Patching.Util +{ + // works around https://docs.microsoft.com/en-us/dotnet/core/compatibility/core-libraries/6.0/partial-byte-reads-in-streams + static class FullDeflateStreamReader + { + public static void FullRead(this DeflateStream stream, byte[] array, int offset, int count) + { + int totalRead = 0; + while (totalRead < count) + { + int bytesRead = stream.Read(array, offset + totalRead, count - totalRead); + if (bytesRead == 0) break; + totalRead += bytesRead; + } + } + } +} diff --git a/src/XIVLauncher2.Common/Patching/Util/MultipartResponseHandler.cs b/src/XIVLauncher2.Common/Patching/Util/MultipartResponseHandler.cs new file mode 100644 index 0000000..42a70c2 --- /dev/null +++ b/src/XIVLauncher2.Common/Patching/Util/MultipartResponseHandler.cs @@ -0,0 +1,376 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace XIVLauncher2.Common.Patching.Util +{ + public class MultipartResponseHandler : IDisposable + { + private readonly HttpResponseMessage response; + private bool noMoreParts = false; + private Stream baseStream; + public string MultipartBoundary; + private string multipartEndBoundary; + private CircularMemoryStream multipartBufferStream; + private List multipartHeaderLines; + + public MultipartResponseHandler(HttpResponseMessage responseMessage) + { + this.response = responseMessage; + } + + public async Task NextPart(CancellationToken? cancellationToken = null) + { + if (this.noMoreParts) + return null; + + if (this.baseStream == null) + this.baseStream = new BufferedStream(await this.response.Content.ReadAsStreamAsync(), 16384); + + if (MultipartBoundary == null) + { + switch (this.response.StatusCode) + { + case System.Net.HttpStatusCode.OK: + { + this.noMoreParts = true; + var stream = new MultipartPartStream(this.response.Content.Headers.ContentLength.Value, 0, this.response.Content.Headers.ContentLength.Value); + stream.AppendBaseStream(new ReadLengthLimitingStream(this.baseStream, this.response.Content.Headers.ContentLength.Value)); + return stream; + } + + case System.Net.HttpStatusCode.PartialContent: + if (this.response.Content.Headers.ContentType.MediaType.ToLowerInvariant() != "multipart/byteranges") + { + this.noMoreParts = true; + var rangeHeader = this.response.Content.Headers.ContentRange; + var rangeLength = rangeHeader.To.Value + 1 - rangeHeader.From.Value; + var stream = new MultipartPartStream(rangeHeader.Length.Value, rangeHeader.From.Value, rangeLength); + stream.AppendBaseStream(new ReadLengthLimitingStream(this.baseStream, rangeLength)); + return stream; + } + else + { + MultipartBoundary = "--" + this.response.Content.Headers.ContentType.Parameters.Where(p => p.Name.ToLowerInvariant() == "boundary").First().Value; + this.multipartEndBoundary = MultipartBoundary + "--"; + this.multipartBufferStream = new(); + this.multipartHeaderLines = new(); + } + break; + + default: + this.response.EnsureSuccessStatusCode(); + throw new EndOfStreamException($"Unhandled success status code {this.response.StatusCode}"); + } + } + + while (true) + { + if (cancellationToken.HasValue) + cancellationToken.Value.ThrowIfCancellationRequested(); + + var eof = false; + using (var buffer = ReusableByteBufferManager.GetBuffer()) + { + int readSize; + if (cancellationToken == null) + readSize = await this.baseStream.ReadAsync(buffer.Buffer, 0, buffer.Buffer.Length); + else + readSize = await this.baseStream.ReadAsync(buffer.Buffer, 0, buffer.Buffer.Length, (CancellationToken)cancellationToken); + + if (readSize == 0) + eof = true; + else + this.multipartBufferStream.Feed(buffer.Buffer, 0, readSize); + } + + for (int i = 0; i < this.multipartBufferStream.Length - 1; ++i) + { + if (this.multipartBufferStream[i + 0] != '\r' || this.multipartBufferStream[i + 1] != '\n') + continue; + + var isEmptyLine = i == 0; + + if (isEmptyLine) + this.multipartBufferStream.Consume(null, 0, 2); + else + { + using var buffer = ReusableByteBufferManager.GetBuffer(); + if (i > buffer.Buffer.Length) + throw new IOException($"Multipart header line is too long ({i} bytes)"); + + this.multipartBufferStream.Consume(buffer.Buffer, 0, i + 2); + this.multipartHeaderLines.Add(Encoding.UTF8.GetString(buffer.Buffer, 0, i)); + } + i = -1; + + if (this.multipartHeaderLines.Count == 0) + continue; + if (this.multipartHeaderLines.Last() == this.multipartEndBoundary) + { + this.noMoreParts = true; + return null; + } + if (!isEmptyLine) + continue; + + ContentRangeHeaderValue rangeHeader = null; + foreach (var headerLine in this.multipartHeaderLines) + { + var kvs = headerLine.Split(new char[] { ':' }, 2); + if (kvs.Length != 2) + continue; + if (kvs[0].ToLowerInvariant() != "content-range") + continue; + if (ContentRangeHeaderValue.TryParse(kvs[1], out rangeHeader)) + break; + } + if (rangeHeader == null) + throw new IOException("Content-Range not found in multipart part"); + + this.multipartHeaderLines.Clear(); + var rangeFrom = rangeHeader.From.Value; + var rangeLength = rangeHeader.To.Value - rangeFrom + 1; + var stream = new MultipartPartStream(rangeHeader.Length.Value, rangeFrom, rangeLength); + stream.AppendBaseStream(new ConsumeLengthLimitingStream(this.multipartBufferStream, Math.Min(rangeLength, this.multipartBufferStream.Length))); + stream.AppendBaseStream(new ReadLengthLimitingStream(this.baseStream, stream.UnfulfilledBaseStreamLength)); + return stream; + } + + if (eof && !this.noMoreParts) + throw new EndOfStreamException("Reached premature EOF"); + } + } + + public void Dispose() + { + this.multipartBufferStream?.Dispose(); + this.baseStream?.Dispose(); + this.response?.Dispose(); + } + + private class ReadLengthLimitingStream : Stream + { + private readonly Stream baseStream; + private readonly long limitedLength; + private long limitedPointer = 0; + + public ReadLengthLimitingStream(Stream stream, long length) + { + this.baseStream = stream; + this.limitedLength = length; + } + + public override int Read(byte[] buffer, int offset, int count) + { + count = (int)Math.Min(count, this.limitedLength - this.limitedPointer); + if (count == 0) + return 0; + + var read = this.baseStream.Read(buffer, offset, count); + if (read == 0) + throw new EndOfStreamException("Premature end of stream detected"); + this.limitedPointer += read; + return read; + } + + public override long Length => this.limitedLength; + + public override bool CanRead => true; + + public override bool CanSeek => false; + + public override bool CanWrite => false; + + public override long Position { get => this.limitedPointer; set => throw new NotSupportedException(); } + + public override void Flush() => throw new NotSupportedException(); + + public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException(); + + public override void SetLength(long value) => throw new NotSupportedException(); + + public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException(); + } + + private class ConsumeLengthLimitingStream : Stream + { + private readonly CircularMemoryStream baseStream; + private readonly long limitedLength; + private long limitedPointer = 0; + + public ConsumeLengthLimitingStream(CircularMemoryStream stream, long length) + { + this.baseStream = stream; + this.limitedLength = length; + } + + public override int Read(byte[] buffer, int offset, int count) + { + count = (int)Math.Min(count, this.limitedLength - this.limitedPointer); + if (count == 0) + return 0; + + var read = this.baseStream.Consume(buffer, offset, count); + if (read == 0) + throw new EndOfStreamException("Premature end of stream detected"); + this.limitedPointer += read; + return read; + } + + public override long Length => this.limitedLength; + + public override bool CanRead => true; + + public override bool CanSeek => false; + + public override bool CanWrite => false; + + public override long Position { get => this.limitedPointer; set => throw new NotSupportedException(); } + + public override void Flush() => throw new NotSupportedException(); + + public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException(); + + public override void SetLength(long value) => throw new NotSupportedException(); + + public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException(); + } + + public class MultipartPartStream : Stream + { + private readonly CircularMemoryStream loopStream = new(16384, CircularMemoryStream.FeedOverflowMode.DiscardOldest); + private readonly List baseStreams = new(); + private int baseStreamIndex = 0; + public readonly long OriginTotalLength; + public readonly long OriginOffset; + public readonly long OriginLength; + public long OriginEnd => OriginOffset + OriginLength; + private long positionInternal; + + internal MultipartPartStream(long originTotalLength, long originOffset, long originLength) + { + OriginTotalLength = originTotalLength; + OriginOffset = originOffset; + OriginLength = originLength; + this.positionInternal = originOffset; + } + + internal void AppendBaseStream(Stream stream) + { + if (stream.Length == 0) + return; + if (UnfulfilledBaseStreamLength < stream.Length) + throw new ArgumentException("Total length of given streams exceed OriginTotalLength."); + this.baseStreams.Add(stream); + } + + internal long UnfulfilledBaseStreamLength => OriginLength - this.baseStreams.Select(x => x.Length).Sum(); + + public void CaptureBackwards(long captureCapacity) + { + this.loopStream.Reserve(captureCapacity); + } + + public override int Read(byte[] buffer, int offset, int count) + { + int totalRead = 0; + while (count > 0 && this.loopStream.Position < this.loopStream.Length) + { + var read1 = (int)Math.Min(count, this.loopStream.Length - this.loopStream.Position); + var read2 = this.loopStream.Read(buffer, offset, read1); + if (read2 == 0) + throw new EndOfStreamException("MultipartPartStream.Read:1"); + + totalRead += read2; + this.positionInternal += read2; + count -= read2; + offset += read2; + } + + while (count > 0 && this.baseStreamIndex < this.baseStreams.Count) + { + var stream = this.baseStreams[this.baseStreamIndex]; + var read1 = (int)Math.Min(count, stream.Length - stream.Position); + var read2 = stream.Read(buffer, offset, read1); + if (read2 == 0) + throw new EndOfStreamException("MultipartPartStream.Read:2"); + + this.loopStream.Feed(buffer, offset, read2); + this.loopStream.Position = this.loopStream.Length; + + totalRead += read2; + this.positionInternal += read2; + count -= read2; + offset += read2; + + if (stream.Position == stream.Length) + this.baseStreamIndex++; + } + + return totalRead; + } + + public override long Seek(long offset, SeekOrigin origin) + { + switch (origin) + { + case SeekOrigin.Begin: + offset -= this.positionInternal; + break; + case SeekOrigin.End: + offset = OriginTotalLength - offset - this.positionInternal; + break; + } + + var finalPosition = this.positionInternal + offset; + + if (finalPosition > OriginOffset + OriginLength) + throw new ArgumentException("Tried to seek after the end of the segment."); + else if (finalPosition < OriginOffset) + throw new ArgumentException("Tried to seek behind the beginning of the segment."); + + var backwards = this.loopStream.Length - this.loopStream.Position; + var backwardAdjustment = Math.Min(backwards, offset); + this.loopStream.Position += backwardAdjustment; // This will throw if there are not enough old data available + offset -= backwardAdjustment; + this.positionInternal += backwardAdjustment; + + if (offset > 0) + { + using var buf = ReusableByteBufferManager.GetBuffer(); + for (var i = 0; i < offset; i += buf.Buffer.Length) + if (0 == Read(buf.Buffer, 0, (int)Math.Min(offset - i, buf.Buffer.Length))) + throw new EndOfStreamException("MultipartPartStream.Read:3"); + } + + if (this.positionInternal != finalPosition) + throw new IOException("Failed to seek properly."); + + return this.positionInternal; + } + + public override bool CanRead => true; + + public override bool CanSeek => true; + + public override bool CanWrite => false; + + public override long Length => OriginTotalLength; + + public override long Position { get => this.positionInternal; set => Seek(value, SeekOrigin.Begin); } + + public override void Flush() => throw new NotSupportedException(); + + public override void SetLength(long value) => throw new NotSupportedException(); + + public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException(); + } + } +} diff --git a/src/XIVLauncher2.Common/Patching/Util/ReusableByteBufferManager.cs b/src/XIVLauncher2.Common/Patching/Util/ReusableByteBufferManager.cs new file mode 100644 index 0000000..7ac0a4d --- /dev/null +++ b/src/XIVLauncher2.Common/Patching/Util/ReusableByteBufferManager.cs @@ -0,0 +1,106 @@ +using System; +using System.IO; +using System.Linq; + +namespace XIVLauncher2.Common.Patching.Util +{ + public class ReusableByteBufferManager + { + private static readonly int[] ArraySizes = new int[] { 1 << 14, 1 << 16, 1 << 18, 1 << 20, 1 << 22 }; + private static readonly ReusableByteBufferManager[] Instances = ArraySizes.Select(x => new ReusableByteBufferManager(x, 2 * Environment.ProcessorCount)).ToArray(); + + public class Allocation : IDisposable + { + public readonly ReusableByteBufferManager BufferManager; + public readonly byte[] Buffer; + public readonly MemoryStream Stream; + public readonly BinaryWriter Writer; + + internal Allocation(ReusableByteBufferManager b, long size) + { + BufferManager = b; + Buffer = new byte[size]; + Stream = new MemoryStream(Buffer); + Writer = new BinaryWriter(Stream); + } + + public void ResetState() + { + Stream.SetLength(0); + Stream.Seek(0, SeekOrigin.Begin); + } + + public void Clear() => Array.Clear(Buffer, 0, Buffer.Length); + + public void Dispose() => BufferManager?.Return(this); + } + + private readonly int arraySize; + private readonly Allocation[] buffers; + + public ReusableByteBufferManager(int arraySize, int maxBuffers) + { + this.arraySize = arraySize; + this.buffers = new Allocation[maxBuffers]; + } + + public Allocation Allocate(bool clear = false) + { + Allocation res = null; + + for (int i = 0; i < this.buffers.Length; i++) + { + if (this.buffers[i] == null) + continue; + + lock (this.buffers.SyncRoot) + { + if (this.buffers[i] == null) + continue; + + res = this.buffers[i]; + this.buffers[i] = null; + break; + } + } + if (res == null) + res = new Allocation(this, this.arraySize); + else if (clear) + res.Clear(); + res.ResetState(); + return res; + } + + internal void Return(Allocation buf) + { + for (int i = 0; i < this.buffers.Length; i++) + { + if (this.buffers[i] != null) + continue; + + lock (this.buffers.SyncRoot) + { + if (this.buffers[i] != null) + continue; + + this.buffers[i] = buf; + return; + } + } + } + + public static Allocation GetBuffer(bool clear = false) + { + return Instances[0].Allocate(clear); + } + + public static Allocation GetBuffer(long minSize, bool clear = false) + { + for (int i = 0; i < ArraySizes.Length; i++) + if (ArraySizes[i] >= minSize) + return Instances[i].Allocate(clear); + + return new Allocation(null, minSize); + } + } +} diff --git a/src/XIVLauncher2.Common/Patching/ZiPatch/Chunk/AddDirectoryChunk.cs b/src/XIVLauncher2.Common/Patching/ZiPatch/Chunk/AddDirectoryChunk.cs new file mode 100644 index 0000000..2f722cb --- /dev/null +++ b/src/XIVLauncher2.Common/Patching/ZiPatch/Chunk/AddDirectoryChunk.cs @@ -0,0 +1,36 @@ +using System.IO; +using XIVLauncher2.Common.Patching.Util; + +namespace XIVLauncher2.Common.Patching.ZiPatch.Chunk +{ + public class AddDirectoryChunk : ZiPatchChunk + { + public new static string Type = "ADIR"; + + public string DirName { get; protected set; } + + protected override void ReadChunk() + { + var start = this.Reader.BaseStream.Position; + + var dirNameLen = this.Reader.ReadUInt32BE(); + + DirName = this.Reader.ReadFixedLengthString(dirNameLen); + + this.Reader.ReadBytes(Size - (int)(this.Reader.BaseStream.Position - start)); + } + + + public AddDirectoryChunk(ChecksumBinaryReader reader, int offset, int size) : base(reader, offset, size) {} + + public override void ApplyChunk(ZiPatchConfig config) + { + Directory.CreateDirectory(config.GamePath + DirName); + } + + public override string ToString() + { + return $"{Type}:{DirName}"; + } + } +} diff --git a/src/XIVLauncher2.Common/Patching/ZiPatch/Chunk/ApplyFreeSpaceChunk.cs b/src/XIVLauncher2.Common/Patching/ZiPatch/Chunk/ApplyFreeSpaceChunk.cs new file mode 100644 index 0000000..b715e47 --- /dev/null +++ b/src/XIVLauncher2.Common/Patching/ZiPatch/Chunk/ApplyFreeSpaceChunk.cs @@ -0,0 +1,31 @@ +using XIVLauncher2.Common.Patching.Util; + +namespace XIVLauncher2.Common.Patching.ZiPatch.Chunk +{ + public class ApplyFreeSpaceChunk : ZiPatchChunk + { + // This is a NOP on recent patcher versions, so I don't think we'll be seeing it. + public new static string Type = "APFS"; + + // TODO: No samples of this were found, so these fields are theoretical + public long UnknownFieldA { get; protected set; } + public long UnknownFieldB { get; protected set; } + + protected override void ReadChunk() + { + var start = this.Reader.BaseStream.Position; + + UnknownFieldA = this.Reader.ReadInt64BE(); + UnknownFieldB = this.Reader.ReadInt64BE(); + + this.Reader.ReadBytes(Size - (int)(this.Reader.BaseStream.Position - start)); + } + + public ApplyFreeSpaceChunk(ChecksumBinaryReader reader, int offset, int size) : base(reader, offset, size) {} + + public override string ToString() + { + return $"{Type}:{UnknownFieldA}:{UnknownFieldB}"; + } + } +} diff --git a/src/XIVLauncher2.Common/Patching/ZiPatch/Chunk/ApplyOptionChunk.cs b/src/XIVLauncher2.Common/Patching/ZiPatch/Chunk/ApplyOptionChunk.cs new file mode 100644 index 0000000..36a43ca --- /dev/null +++ b/src/XIVLauncher2.Common/Patching/ZiPatch/Chunk/ApplyOptionChunk.cs @@ -0,0 +1,61 @@ +using XIVLauncher2.Common.Patching.Util; + +namespace XIVLauncher2.Common.Patching.ZiPatch.Chunk +{ + public class ApplyOptionChunk : ZiPatchChunk + { + public new static string Type = "APLY"; + + public enum ApplyOptionKind : uint + { + IgnoreMissing = 1, + IgnoreOldMismatch = 2 + } + + // These are both false on all files seen + public ApplyOptionKind OptionKind { get; protected set; } + + public bool OptionValue { get; protected set; } + + public ApplyOptionChunk(ChecksumBinaryReader reader, int offset, int size) : base(reader, offset, size) {} + + protected override void ReadChunk() + { + var start = this.Reader.BaseStream.Position; + + OptionKind = (ApplyOptionKind)this.Reader.ReadUInt32BE(); + + // Discarded padding, always 0x0000_0004 as far as observed + this.Reader.ReadBytes(4); + + var value = this.Reader.ReadUInt32BE() != 0; + + if (OptionKind == ApplyOptionKind.IgnoreMissing || + OptionKind == ApplyOptionKind.IgnoreOldMismatch) + OptionValue = value; + else + OptionValue = false; // defaults to false if OptionKind isn't valid + + this.Reader.ReadBytes(Size - (int)(this.Reader.BaseStream.Position - start)); + } + + public override void ApplyChunk(ZiPatchConfig config) + { + switch (OptionKind) + { + case ApplyOptionKind.IgnoreMissing: + config.IgnoreMissing = OptionValue; + break; + + case ApplyOptionKind.IgnoreOldMismatch: + config.IgnoreOldMismatch = OptionValue; + break; + } + } + + public override string ToString() + { + return $"{Type}:{OptionKind}:{OptionValue}"; + } + } +} diff --git a/src/XIVLauncher2.Common/Patching/ZiPatch/Chunk/DeleteDirectoryChunk.cs b/src/XIVLauncher2.Common/Patching/ZiPatch/Chunk/DeleteDirectoryChunk.cs new file mode 100644 index 0000000..76528a2 --- /dev/null +++ b/src/XIVLauncher2.Common/Patching/ZiPatch/Chunk/DeleteDirectoryChunk.cs @@ -0,0 +1,45 @@ +using System; +using System.IO; +using Serilog; +using XIVLauncher2.Common.Patching.Util; + +namespace XIVLauncher2.Common.Patching.ZiPatch.Chunk +{ + public class DeleteDirectoryChunk : ZiPatchChunk + { + public new static string Type = "DELD"; + + public string DirName { get; protected set; } + + public DeleteDirectoryChunk(ChecksumBinaryReader reader, int offset, int size) : base(reader, offset, size) {} + + protected override void ReadChunk() + { + var start = this.Reader.BaseStream.Position; + + var dirNameLen = this.Reader.ReadUInt32BE(); + + DirName = this.Reader.ReadFixedLengthString(dirNameLen); + + this.Reader.ReadBytes(Size - (int)(this.Reader.BaseStream.Position - start)); + } + + public override void ApplyChunk(ZiPatchConfig config) + { + try + { + Directory.Delete(config.GamePath + DirName); + } + catch (Exception e) + { + Log.Debug(e, "Ran into {This}, failed at deleting the dir", this); + throw; + } + } + + public override string ToString() + { + return $"{Type}:{DirName}"; + } + } +} diff --git a/src/XIVLauncher2.Common/Patching/ZiPatch/Chunk/EndOfFileChunk.cs b/src/XIVLauncher2.Common/Patching/ZiPatch/Chunk/EndOfFileChunk.cs new file mode 100644 index 0000000..631f036 --- /dev/null +++ b/src/XIVLauncher2.Common/Patching/ZiPatch/Chunk/EndOfFileChunk.cs @@ -0,0 +1,23 @@ +using XIVLauncher2.Common.Patching.Util; + +namespace XIVLauncher2.Common.Patching.ZiPatch.Chunk +{ + public class EndOfFileChunk : ZiPatchChunk + { + public new static string Type = "EOF_"; + + protected override void ReadChunk() + { + var start = this.Reader.BaseStream.Position; + + this.Reader.ReadBytes(Size - (int)(this.Reader.BaseStream.Position - start)); + } + + public EndOfFileChunk(ChecksumBinaryReader reader, int offset, int size) : base(reader, offset, size) {} + + public override string ToString() + { + return Type; + } + } +} diff --git a/src/XIVLauncher2.Common/Patching/ZiPatch/Chunk/FileHeaderChunk.cs b/src/XIVLauncher2.Common/Patching/ZiPatch/Chunk/FileHeaderChunk.cs new file mode 100644 index 0000000..30b8205 --- /dev/null +++ b/src/XIVLauncher2.Common/Patching/ZiPatch/Chunk/FileHeaderChunk.cs @@ -0,0 +1,62 @@ +using XIVLauncher2.Common.Patching.Util; + +namespace XIVLauncher2.Common.Patching.ZiPatch.Chunk +{ + public class FileHeaderChunk : ZiPatchChunk + { + public new static string Type = "FHDR"; + + // V1?/2 + public byte Version { get; protected set; } + public string PatchType { get; protected set; } + public uint EntryFiles { get; protected set; } + + // V3 + public uint AddDirectories { get; protected set; } + public uint DeleteDirectories { get; protected set; } + public long DeleteDataSize { get; protected set; } // Split in 2 DWORD; Low, High + public uint MinorVersion { get; protected set; } + public uint RepositoryName { get; protected set; } + public uint Commands { get; protected set; } + public uint SqpkAddCommands { get; protected set; } + public uint SqpkDeleteCommands { get; protected set; } + public uint SqpkExpandCommands { get; protected set; } + public uint SqpkHeaderCommands { get; protected set; } + public uint SqpkFileCommands { get; protected set; } + + public FileHeaderChunk(ChecksumBinaryReader reader, int offset, int size) : base(reader, offset, size) {} + + protected override void ReadChunk() + { + var start = this.Reader.BaseStream.Position; + + Version = (byte)(this.Reader.ReadUInt32() >> 16); + PatchType = this.Reader.ReadFixedLengthString(4u); + EntryFiles = this.Reader.ReadUInt32BE(); + + if (Version == 3) + { + AddDirectories = this.Reader.ReadUInt32BE(); + DeleteDirectories = this.Reader.ReadUInt32BE(); + DeleteDataSize = this.Reader.ReadUInt32BE() | ((long)this.Reader.ReadUInt32BE() << 32); + MinorVersion = this.Reader.ReadUInt32BE(); + RepositoryName = this.Reader.ReadUInt32BE(); + Commands = this.Reader.ReadUInt32BE(); + SqpkAddCommands = this.Reader.ReadUInt32BE(); + SqpkDeleteCommands = this.Reader.ReadUInt32BE(); + SqpkExpandCommands = this.Reader.ReadUInt32BE(); + SqpkHeaderCommands = this.Reader.ReadUInt32BE(); + SqpkFileCommands = this.Reader.ReadUInt32BE(); + } + + // 0xB8 of unknown data for V3, 0x08 of 0x00 for V2 + // ... Probably irrelevant. + this.Reader.ReadBytes(Size - (int)(this.Reader.BaseStream.Position - start)); + } + + public override string ToString() + { + return $"{Type}:V{Version}:{RepositoryName}"; + } + } +} diff --git a/src/XIVLauncher2.Common/Patching/ZiPatch/Chunk/SqpkChunk.cs b/src/XIVLauncher2.Common/Patching/ZiPatch/Chunk/SqpkChunk.cs new file mode 100644 index 0000000..16f1684 --- /dev/null +++ b/src/XIVLauncher2.Common/Patching/ZiPatch/Chunk/SqpkChunk.cs @@ -0,0 +1,66 @@ +using System; +using System.Collections.Generic; +using System.IO; +using XIVLauncher2.Common.Patching.Util; +using XIVLauncher2.Common.Patching.ZiPatch.Chunk.SqpkCommand; + +namespace XIVLauncher2.Common.Patching.ZiPatch.Chunk +{ + public abstract class SqpkChunk : ZiPatchChunk + { + public new static string Type = "SQPK"; + public static string Command { get; protected set; } + + + private static readonly Dictionary> CommandTypes = + new Dictionary> { + { SqpkAddData.Command, (reader, offset, size) => new SqpkAddData(reader, offset, size) }, + { SqpkDeleteData.Command, (reader, offset, size) => new SqpkDeleteData(reader, offset, size) }, + { SqpkHeader.Command, (reader, offset, size) => new SqpkHeader(reader, offset, size) }, + { SqpkTargetInfo.Command, (reader, offset, size) => new SqpkTargetInfo(reader, offset, size) }, + { SqpkExpandData.Command, (reader, offset, size) => new SqpkExpandData(reader, offset, size) }, + { SqpkIndex.Command, (reader, offset, size) => new SqpkIndex(reader, offset, size) }, + { SqpkFile.Command, (reader, offset, size) => new SqpkFile(reader, offset, size) }, + { SqpkPatchInfo.Command, (reader, offset, size) => new SqpkPatchInfo(reader, offset, size) } + }; + + public static ZiPatchChunk GetCommand(ChecksumBinaryReader reader, int offset, int size) + { + try + { + // Have not seen this differ from size + var innerSize = reader.ReadInt32BE(); + if (size != innerSize) + throw new ZiPatchException(); + + var command = reader.ReadFixedLengthString(1u); + if (!CommandTypes.TryGetValue(command, out var constructor)) + throw new ZiPatchException(); + + var chunk = constructor(reader, offset, innerSize - 5); + + return chunk; + } + catch (EndOfStreamException e) + { + throw new ZiPatchException("Could not get command", e); + } + } + + + protected override void ReadChunk() + { + var start = this.Reader.BaseStream.Position; + + this.Reader.ReadBytes(Size - (int)(this.Reader.BaseStream.Position - start)); + } + + protected SqpkChunk(ChecksumBinaryReader reader, int offset, int size) : base(reader, offset, size) + { } + + public override string ToString() + { + return Type; + } + } +} diff --git a/src/XIVLauncher2.Common/Patching/ZiPatch/Chunk/SqpkCommand/SqpkAddData.cs b/src/XIVLauncher2.Common/Patching/ZiPatch/Chunk/SqpkCommand/SqpkAddData.cs new file mode 100644 index 0000000..d277ca7 --- /dev/null +++ b/src/XIVLauncher2.Common/Patching/ZiPatch/Chunk/SqpkCommand/SqpkAddData.cs @@ -0,0 +1,58 @@ +using System.IO; +using XIVLauncher2.Common.Patching.Util; +using XIVLauncher2.Common.Patching.ZiPatch.Util; + +namespace XIVLauncher2.Common.Patching.ZiPatch.Chunk.SqpkCommand +{ + class SqpkAddData : SqpkChunk + { + public new static string Command = "A"; + + + public SqpackDatFile TargetFile { get; protected set; } + public int BlockOffset { get; protected set; } + public int BlockNumber { get; protected set; } + public int BlockDeleteNumber { get; protected set; } + + public byte[] BlockData { get; protected set; } + public long BlockDataSourceOffset { get; protected set; } + + + public SqpkAddData(ChecksumBinaryReader reader, int offset, int size) : base(reader, offset, size) {} + + protected override void ReadChunk() + { + var start = this.Reader.BaseStream.Position; + + this.Reader.ReadBytes(3); // Alignment + + TargetFile = new SqpackDatFile(this.Reader); + + BlockOffset = this.Reader.ReadInt32BE() << 7; + BlockNumber = this.Reader.ReadInt32BE() << 7; + BlockDeleteNumber = this.Reader.ReadInt32BE() << 7; + + BlockDataSourceOffset = Offset + this.Reader.BaseStream.Position; + BlockData = this.Reader.ReadBytes((int)BlockNumber); + + this.Reader.ReadBytes(Size - (int)(this.Reader.BaseStream.Position - start)); + } + + public override void ApplyChunk(ZiPatchConfig config) + { + TargetFile.ResolvePath(config.Platform); + + var file = config.Store == null ? + TargetFile.OpenStream(config.GamePath, FileMode.OpenOrCreate) : + TargetFile.OpenStream(config.Store, config.GamePath, FileMode.OpenOrCreate); + + file.WriteFromOffset(BlockData, BlockOffset); + file.Wipe(BlockDeleteNumber); + } + + public override string ToString() + { + return $"{Type}:{Command}:{TargetFile}:{BlockOffset}:{BlockNumber}:{BlockDeleteNumber}"; + } + } +} diff --git a/src/XIVLauncher2.Common/Patching/ZiPatch/Chunk/SqpkCommand/SqpkDeleteData.cs b/src/XIVLauncher2.Common/Patching/ZiPatch/Chunk/SqpkCommand/SqpkDeleteData.cs new file mode 100644 index 0000000..c734b4a --- /dev/null +++ b/src/XIVLauncher2.Common/Patching/ZiPatch/Chunk/SqpkCommand/SqpkDeleteData.cs @@ -0,0 +1,51 @@ +using System.IO; +using XIVLauncher2.Common.Patching.Util; +using XIVLauncher2.Common.Patching.ZiPatch.Util; + +namespace XIVLauncher2.Common.Patching.ZiPatch.Chunk.SqpkCommand +{ + class SqpkDeleteData : SqpkChunk + { + public new static string Command = "D"; + + + public SqpackDatFile TargetFile { get; protected set; } + public int BlockOffset { get; protected set; } + public int BlockNumber { get; protected set; } + + + public SqpkDeleteData(ChecksumBinaryReader reader, int offset, int size) : base(reader, offset, size) {} + + protected override void ReadChunk() + { + var start = this.Reader.BaseStream.Position; + + this.Reader.ReadBytes(3); // Alignment + + TargetFile = new SqpackDatFile(this.Reader); + + BlockOffset = this.Reader.ReadInt32BE() << 7; + BlockNumber = this.Reader.ReadInt32BE(); + + this.Reader.ReadUInt32(); // Reserved + + this.Reader.ReadBytes(Size - (int)(this.Reader.BaseStream.Position - start)); + } + + public override void ApplyChunk(ZiPatchConfig config) + { + TargetFile.ResolvePath(config.Platform); + + var file = config.Store == null ? + TargetFile.OpenStream(config.GamePath, FileMode.OpenOrCreate) : + TargetFile.OpenStream(config.Store, config.GamePath, FileMode.OpenOrCreate); + + SqpackDatFile.WriteEmptyFileBlockAt(file, BlockOffset, BlockNumber); + } + + public override string ToString() + { + return $"{Type}:{Command}:{TargetFile}:{BlockOffset}:{BlockNumber}"; + } + } +} diff --git a/src/XIVLauncher2.Common/Patching/ZiPatch/Chunk/SqpkCommand/SqpkExpandData.cs b/src/XIVLauncher2.Common/Patching/ZiPatch/Chunk/SqpkCommand/SqpkExpandData.cs new file mode 100644 index 0000000..4c1490b --- /dev/null +++ b/src/XIVLauncher2.Common/Patching/ZiPatch/Chunk/SqpkCommand/SqpkExpandData.cs @@ -0,0 +1,51 @@ +using System.IO; +using XIVLauncher2.Common.Patching.Util; +using XIVLauncher2.Common.Patching.ZiPatch.Util; + +namespace XIVLauncher2.Common.Patching.ZiPatch.Chunk.SqpkCommand +{ + class SqpkExpandData : SqpkChunk + { + public new static string Command = "E"; + + + public SqpackDatFile TargetFile { get; protected set; } + public int BlockOffset { get; protected set; } + public int BlockNumber { get; protected set; } + + + public SqpkExpandData(ChecksumBinaryReader reader, int offset, int size) : base(reader, offset, size) {} + + protected override void ReadChunk() + { + var start = this.Reader.BaseStream.Position; + + this.Reader.ReadBytes(3); + + TargetFile = new SqpackDatFile(this.Reader); + + BlockOffset = this.Reader.ReadInt32BE() << 7; + BlockNumber = this.Reader.ReadInt32BE(); + + this.Reader.ReadUInt32(); // Reserved + + this.Reader.ReadBytes(Size - (int)(this.Reader.BaseStream.Position - start)); + } + + public override void ApplyChunk(ZiPatchConfig config) + { + TargetFile.ResolvePath(config.Platform); + + var file = config.Store == null ? + TargetFile.OpenStream(config.GamePath, FileMode.OpenOrCreate) : + TargetFile.OpenStream(config.Store, config.GamePath, FileMode.OpenOrCreate); + + SqpackDatFile.WriteEmptyFileBlockAt(file, BlockOffset, BlockNumber); + } + + public override string ToString() + { + return $"{Type}:{Command}:{BlockOffset}:{BlockNumber}"; + } + } +} diff --git a/src/XIVLauncher2.Common/Patching/ZiPatch/Chunk/SqpkCommand/SqpkFile.cs b/src/XIVLauncher2.Common/Patching/ZiPatch/Chunk/SqpkCommand/SqpkFile.cs new file mode 100644 index 0000000..28ff288 --- /dev/null +++ b/src/XIVLauncher2.Common/Patching/ZiPatch/Chunk/SqpkCommand/SqpkFile.cs @@ -0,0 +1,111 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; +using XIVLauncher2.Common.Patching.Util; +using XIVLauncher2.Common.Patching.ZiPatch.Util; + +namespace XIVLauncher2.Common.Patching.ZiPatch.Chunk.SqpkCommand +{ + internal class SqpkFile : SqpkChunk + { + public new static string Command = "F"; + + public enum OperationKind : byte + { + AddFile = (byte)'A', + RemoveAll = (byte)'R', + + // I've seen no cases in the wild of these two + DeleteFile = (byte)'D', + MakeDirTree = (byte)'M' + } + + public OperationKind Operation { get; protected set; } + public long FileOffset { get; protected set; } + public ulong FileSize { get; protected set; } + public ushort ExpansionId { get; protected set; } + public SqexFile TargetFile { get; protected set; } + + public List CompressedDataSourceOffsets { get; protected set; } + public List CompressedData { get; protected set; } + + public SqpkFile(ChecksumBinaryReader reader, int offset, int size) : base(reader, offset, size) {} + + protected override void ReadChunk() + { + var start = this.Reader.BaseStream.Position; + + Operation = (OperationKind)this.Reader.ReadByte(); + this.Reader.ReadBytes(2); // Alignment + + FileOffset = this.Reader.ReadInt64BE(); + FileSize = this.Reader.ReadUInt64BE(); + + var pathLen = this.Reader.ReadUInt32BE(); + + ExpansionId = this.Reader.ReadUInt16BE(); + this.Reader.ReadBytes(2); + + TargetFile = new SqexFile(this.Reader.ReadFixedLengthString(pathLen)); + + if (Operation == OperationKind.AddFile) + { + CompressedDataSourceOffsets = new(); + CompressedData = new List(); + + while (Size - this.Reader.BaseStream.Position + start > 0) + { + CompressedDataSourceOffsets.Add(Offset + this.Reader.BaseStream.Position); + CompressedData.Add(new SqpkCompressedBlock(this.Reader)); + CompressedDataSourceOffsets[CompressedDataSourceOffsets.Count - 1] += CompressedData[CompressedData.Count - 1].HeaderSize; + } + } + + this.Reader.ReadBytes(Size - (int)(this.Reader.BaseStream.Position - start)); + } + + private static bool RemoveAllFilter(string filePath) => + !new[] { ".var", "00000.bk2", "00001.bk2", "00002.bk2", "00003.bk2" }.Any(filePath.EndsWith); + + public override void ApplyChunk(ZiPatchConfig config) + { + switch (Operation) + { + // Default behaviour falls through to AddFile, though this shouldn't happen + case OperationKind.AddFile: + default: + // TODO: Check this. I *think* boot usually creates all the folders like sqpack, movie, etc., so this might be kind of a hack + TargetFile.CreateDirectoryTree(config.GamePath); + + var fileStream = config.Store == null ? TargetFile.OpenStream(config.GamePath, FileMode.OpenOrCreate) : TargetFile.OpenStream(config.Store, config.GamePath, FileMode.OpenOrCreate); + + if (FileOffset == 0) + fileStream.SetLength(0); + + fileStream.Seek(FileOffset, SeekOrigin.Begin); + foreach (var block in CompressedData) + block.DecompressInto(fileStream); + + break; + + case OperationKind.RemoveAll: + foreach (var file in SqexFile.GetAllExpansionFiles(config.GamePath, ExpansionId).Where(RemoveAllFilter)) + File.Delete(file); + break; + + case OperationKind.DeleteFile: + File.Delete(config.GamePath + "/" + TargetFile.RelativePath); + break; + + case OperationKind.MakeDirTree: + Directory.CreateDirectory(config.GamePath + "/" + TargetFile.RelativePath); + break; + } + } + + public override string ToString() + { + return $"{Type}:{Command}:{Operation}:{FileOffset}:{FileSize}:{ExpansionId}:{TargetFile}"; + } + } +} diff --git a/src/XIVLauncher2.Common/Patching/ZiPatch/Chunk/SqpkCommand/SqpkHeader.cs b/src/XIVLauncher2.Common/Patching/ZiPatch/Chunk/SqpkCommand/SqpkHeader.cs new file mode 100644 index 0000000..5bf1103 --- /dev/null +++ b/src/XIVLauncher2.Common/Patching/ZiPatch/Chunk/SqpkCommand/SqpkHeader.cs @@ -0,0 +1,69 @@ +using System.IO; +using XIVLauncher2.Common.Patching.Util; +using XIVLauncher2.Common.Patching.ZiPatch.Util; + +namespace XIVLauncher2.Common.Patching.ZiPatch.Chunk.SqpkCommand +{ + class SqpkHeader : SqpkChunk + { + public new static string Command = "H"; + + public enum TargetFileKind : byte + { + Dat = (byte)'D', + Index = (byte)'I' + } + public enum TargetHeaderKind : byte + { + Version = (byte)'V', + Index = (byte)'I', + Data = (byte)'D' + } + + public const int HEADER_SIZE = 1024; + + public TargetFileKind FileKind { get; protected set; } + public TargetHeaderKind HeaderKind { get; protected set; } + public SqpackFile TargetFile { get; protected set; } + + public byte[] HeaderData { get; protected set; } + public long HeaderDataSourceOffset { get; protected set; } + + public SqpkHeader(ChecksumBinaryReader reader, int offset, int size) : base(reader, offset, size) {} + + protected override void ReadChunk() + { + var start = this.Reader.BaseStream.Position; + + FileKind = (TargetFileKind)this.Reader.ReadByte(); + HeaderKind = (TargetHeaderKind)this.Reader.ReadByte(); + this.Reader.ReadByte(); // Alignment + + if (FileKind == TargetFileKind.Dat) + TargetFile = new SqpackDatFile(this.Reader); + else + TargetFile = new SqpackIndexFile(this.Reader); + + HeaderDataSourceOffset = Offset + this.Reader.BaseStream.Position; + HeaderData = this.Reader.ReadBytes(HEADER_SIZE); + + this.Reader.ReadBytes(Size - (int)(this.Reader.BaseStream.Position - start)); + } + + public override void ApplyChunk(ZiPatchConfig config) + { + TargetFile.ResolvePath(config.Platform); + + var file = config.Store == null ? + TargetFile.OpenStream(config.GamePath, FileMode.OpenOrCreate) : + TargetFile.OpenStream(config.Store, config.GamePath, FileMode.OpenOrCreate); + + file.WriteFromOffset(HeaderData, HeaderKind == TargetHeaderKind.Version ? 0 : HEADER_SIZE); + } + + public override string ToString() + { + return $"{Type}:{Command}:{FileKind}:{HeaderKind}:{TargetFile}"; + } + } +} diff --git a/src/XIVLauncher2.Common/Patching/ZiPatch/Chunk/SqpkCommand/SqpkIndex.cs b/src/XIVLauncher2.Common/Patching/ZiPatch/Chunk/SqpkCommand/SqpkIndex.cs new file mode 100644 index 0000000..87be07e --- /dev/null +++ b/src/XIVLauncher2.Common/Patching/ZiPatch/Chunk/SqpkCommand/SqpkIndex.cs @@ -0,0 +1,53 @@ +using XIVLauncher2.Common.Patching.Util; +using XIVLauncher2.Common.Patching.ZiPatch.Util; + +namespace XIVLauncher2.Common.Patching.ZiPatch.Chunk.SqpkCommand +{ + class SqpkIndex : SqpkChunk + { + // This is a NOP on recent patcher versions. + public new static string Command = "I"; + + public enum IndexCommandKind : byte + { + Add = (byte)'A', + Delete = (byte)'D' + } + + public IndexCommandKind IndexCommand { get; protected set; } + public bool IsSynonym { get; protected set; } + public SqpackIndexFile TargetFile { get; protected set; } + public ulong FileHash { get; protected set; } + public uint BlockOffset { get; protected set; } + + // TODO: Figure out what this is used for + public uint BlockNumber { get; protected set; } + + + + public SqpkIndex(ChecksumBinaryReader reader, int offset, int size) : base(reader, offset, size) {} + + protected override void ReadChunk() + { + var start = this.Reader.BaseStream.Position; + + IndexCommand = (IndexCommandKind)this.Reader.ReadByte(); + IsSynonym = this.Reader.ReadBoolean(); + this.Reader.ReadByte(); // Alignment + + TargetFile = new SqpackIndexFile(this.Reader); + + FileHash = this.Reader.ReadUInt64BE(); + + BlockOffset = this.Reader.ReadUInt32BE(); + BlockNumber = this.Reader.ReadUInt32BE(); + + this.Reader.ReadBytes(Size - (int)(this.Reader.BaseStream.Position - start)); + } + + public override string ToString() + { + return $"{Type}:{Command}:{IndexCommand}:{IsSynonym}:{TargetFile}:{FileHash:X8}:{BlockOffset}:{BlockNumber}"; + } + } +} diff --git a/src/XIVLauncher2.Common/Patching/ZiPatch/Chunk/SqpkCommand/SqpkPatchInfo.cs b/src/XIVLauncher2.Common/Patching/ZiPatch/Chunk/SqpkCommand/SqpkPatchInfo.cs new file mode 100644 index 0000000..e43823f --- /dev/null +++ b/src/XIVLauncher2.Common/Patching/ZiPatch/Chunk/SqpkCommand/SqpkPatchInfo.cs @@ -0,0 +1,35 @@ +using XIVLauncher2.Common.Patching.Util; + +namespace XIVLauncher2.Common.Patching.ZiPatch.Chunk.SqpkCommand +{ + internal class SqpkPatchInfo : SqpkChunk + { + // This is a NOP on recent patcher versions + public new static string Command = "X"; + + // Don't know what this stuff is for + public byte Status { get; protected set; } + public byte Version { get; protected set; } + public ulong InstallSize { get; protected set; } + + public SqpkPatchInfo(ChecksumBinaryReader reader, int offset, int size) : base(reader, offset, size) {} + + protected override void ReadChunk() + { + var start = this.Reader.BaseStream.Position; + + Status = this.Reader.ReadByte(); + Version = this.Reader.ReadByte(); + this.Reader.ReadByte(); // Alignment + + InstallSize = this.Reader.ReadUInt64BE(); + + this.Reader.ReadBytes(Size - (int)(this.Reader.BaseStream.Position - start)); + } + + public override string ToString() + { + return $"{Type}:{Command}:{Status}:{Version}:{InstallSize}"; + } + } +} diff --git a/src/XIVLauncher2.Common/Patching/ZiPatch/Chunk/SqpkCommand/SqpkTargetInfo.cs b/src/XIVLauncher2.Common/Patching/ZiPatch/Chunk/SqpkCommand/SqpkTargetInfo.cs new file mode 100644 index 0000000..4fe99c6 --- /dev/null +++ b/src/XIVLauncher2.Common/Patching/ZiPatch/Chunk/SqpkCommand/SqpkTargetInfo.cs @@ -0,0 +1,55 @@ +using XIVLauncher2.Common.Patching.Util; + +namespace XIVLauncher2.Common.Patching.ZiPatch.Chunk.SqpkCommand +{ + internal class SqpkTargetInfo : SqpkChunk + { + // Only Platform is used on recent patcher versions + public new static string Command = "T"; + + // US/EU/JP are Global + // ZH seems to also be Global + // KR is unknown + public enum RegionId : short + { + Global = -1 + } + + public ZiPatchConfig.PlatformId Platform { get; protected set; } + public RegionId Region { get; protected set; } + public bool IsDebug { get; protected set; } + public ushort Version { get; protected set; } + public ulong DeletedDataSize { get; protected set; } + public ulong SeekCount { get; protected set; } + + public SqpkTargetInfo(ChecksumBinaryReader reader, int offset, int size) : base(reader, offset, size) {} + + protected override void ReadChunk() + { + var start = this.Reader.BaseStream.Position; + + // Reserved + this.Reader.ReadBytes(3); + + Platform = (ZiPatchConfig.PlatformId)this.Reader.ReadUInt16BE(); + Region = (RegionId)this.Reader.ReadInt16BE(); + IsDebug = this.Reader.ReadInt16BE() != 0; + Version = this.Reader.ReadUInt16BE(); + DeletedDataSize = this.Reader.ReadUInt64(); + SeekCount = this.Reader.ReadUInt64(); + + // Empty 32 + 64 bytes + this.Reader.ReadBytes(Size - (int)(this.Reader.BaseStream.Position - start)); + } + + public override void ApplyChunk(ZiPatchConfig config) + { + config.Platform = Platform; + } + + public override string ToString() + { + return $"{Type}:{Command}:{Platform}:{Region}:{IsDebug}:{Version}:{DeletedDataSize}:{SeekCount}"; + } + } +} diff --git a/src/XIVLauncher2.Common/Patching/ZiPatch/Chunk/XXXXChunk.cs b/src/XIVLauncher2.Common/Patching/ZiPatch/Chunk/XXXXChunk.cs new file mode 100644 index 0000000..f80e7bc --- /dev/null +++ b/src/XIVLauncher2.Common/Patching/ZiPatch/Chunk/XXXXChunk.cs @@ -0,0 +1,25 @@ +using XIVLauncher2.Common.Patching.Util; + +namespace XIVLauncher2.Common.Patching.ZiPatch.Chunk +{ + // ReSharper disable once InconsistentNaming + public class XXXXChunk : ZiPatchChunk + { + // TODO: This... Never happens. + public new static string Type = "XXXX"; + + protected override void ReadChunk() + { + var start = this.Reader.BaseStream.Position; + + this.Reader.ReadBytes(Size - (int)(this.Reader.BaseStream.Position - start)); + } + + public XXXXChunk(ChecksumBinaryReader reader, int offset, int size) : base(reader, offset, size) {} + + public override string ToString() + { + return Type; + } + } +} diff --git a/src/XIVLauncher2.Common/Patching/ZiPatch/Chunk/ZiPatchChunk.cs b/src/XIVLauncher2.Common/Patching/ZiPatch/Chunk/ZiPatchChunk.cs new file mode 100644 index 0000000..f70dc1c --- /dev/null +++ b/src/XIVLauncher2.Common/Patching/ZiPatch/Chunk/ZiPatchChunk.cs @@ -0,0 +1,113 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Reflection; +using System.Threading; +using XIVLauncher2.Common.Patching.Util; + +namespace XIVLauncher2.Common.Patching.ZiPatch.Chunk +{ + public abstract class ZiPatchChunk + { + public static string Type { get; protected set; } + // Hack: C# doesn't let you get static fields from instances. + public virtual string ChunkType => (string) GetType() + .GetField("Type", BindingFlags.Static | BindingFlags.FlattenHierarchy | BindingFlags.Public) + ?.GetValue(null); + + public int Offset { get; protected set; } + public int Size { get; protected set; } + public uint Checksum { get; protected set; } + public uint CalculatedChecksum { get; protected set; } + + protected readonly ChecksumBinaryReader Reader; + + private static readonly AsyncLocal localMemoryStream = new AsyncLocal(); + + + // Only FileHeader, ApplyOption, Sqpk, and EOF have been observed in XIVARR+ patches + // AddDirectory and DeleteDirectory can theoretically happen, so they're implemented + // ApplyFreeSpace doesn't seem to show up anymore, and EntryFile will just error out + private static readonly Dictionary> ChunkTypes = + new Dictionary> { + { FileHeaderChunk.Type, (reader, offset, size) => new FileHeaderChunk(reader, offset, size) }, + { ApplyOptionChunk.Type, (reader, offset, size) => new ApplyOptionChunk(reader, offset, size) }, + { ApplyFreeSpaceChunk.Type, (reader, offset, size) => new ApplyFreeSpaceChunk(reader, offset, size) }, + { AddDirectoryChunk.Type, (reader, offset, size) => new AddDirectoryChunk(reader, offset, size) }, + { DeleteDirectoryChunk.Type, (reader, offset, size) => new DeleteDirectoryChunk(reader, offset, size) }, + { SqpkChunk.Type, SqpkChunk.GetCommand }, + { EndOfFileChunk.Type, (reader, offset, size) => new EndOfFileChunk(reader, offset, size) }, + { XXXXChunk.Type, (reader, offset, size) => new XXXXChunk(reader, offset, size) } + }; + + + public static ZiPatchChunk GetChunk(Stream stream) + { + localMemoryStream.Value = localMemoryStream.Value ?? new MemoryStream(); + + var memoryStream = localMemoryStream.Value; + try + { + var reader = new BinaryReader(stream); + var size = reader.ReadInt32BE(); + var baseOffset = (int)stream.Position; + + // size of chunk + header + checksum + var readSize = size + 4 + 4; + + // Enlarge MemoryStream if necessary, or set length at capacity + var maxLen = Math.Max(readSize, memoryStream.Capacity); + if (memoryStream.Length < maxLen) + memoryStream.SetLength(maxLen); + + // Read into MemoryStream's inner buffer + reader.BaseStream.Read(memoryStream.GetBuffer(), 0, readSize); + + var binaryReader = new ChecksumBinaryReader(memoryStream); + binaryReader.InitCrc32(); + + var type = binaryReader.ReadFixedLengthString(4u); + if (!ChunkTypes.TryGetValue(type, out var constructor)) + throw new ZiPatchException(); + + + var chunk = constructor(binaryReader, baseOffset, size); + + chunk.ReadChunk(); + chunk.ReadChecksum(); + return chunk; + } + catch (EndOfStreamException e) + { + throw new ZiPatchException("Could not get chunk", e); + } + finally + { + memoryStream.Position = 0; + } + } + + protected ZiPatchChunk(ChecksumBinaryReader reader, int offset, int size) + { + this.Reader = reader; + + Offset = offset; + Size = size; + } + + protected virtual void ReadChunk() + { + this.Reader.ReadBytes(Size); + } + + public virtual void ApplyChunk(ZiPatchConfig config) {} + + protected void ReadChecksum() + { + CalculatedChecksum = this.Reader.GetCrc32(); + Checksum = this.Reader.ReadUInt32BE(); + } + + public bool IsChecksumValid => CalculatedChecksum == Checksum; + } +} diff --git a/src/XIVLauncher2.Common/Patching/ZiPatch/Util/SqexFile.cs b/src/XIVLauncher2.Common/Patching/ZiPatch/Util/SqexFile.cs new file mode 100644 index 0000000..75da350 --- /dev/null +++ b/src/XIVLauncher2.Common/Patching/ZiPatch/Util/SqexFile.cs @@ -0,0 +1,55 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; + +namespace XIVLauncher2.Common.Patching.ZiPatch.Util +{ + public class SqexFile + { + public string RelativePath { get; set; } + + protected SqexFile() {} + + public SqexFile(string relativePath) + { + RelativePath = relativePath; + } + + public SqexFileStream? OpenStream(string basePath, FileMode mode, int tries = 5, int sleeptime = 1) => + SqexFileStream.WaitForStream($@"{basePath}/{RelativePath}", mode, tries, sleeptime); + + public SqexFileStream OpenStream(SqexFileStreamStore store, string basePath, FileMode mode, + int tries = 5, int sleeptime = 1) => + store.GetStream($@"{basePath}/{RelativePath}", mode, tries, sleeptime); + + public void CreateDirectoryTree(string basePath) + { + var dirName = Path.GetDirectoryName($@"{basePath}/{RelativePath}"); + if (dirName != null) + Directory.CreateDirectory(dirName); + } + + public override string ToString() => RelativePath; + + public static string GetExpansionFolder(byte expansionId) => + expansionId == 0 ? "ffxiv" : $"ex{expansionId}"; + + public static IEnumerable GetAllExpansionFiles(string fullPath, ushort expansionId) + { + var xpacPath = GetExpansionFolder((byte)expansionId); + + var sqpack = $@"{fullPath}\sqpack\{xpacPath}"; + var movie = $@"{fullPath}\movie\{xpacPath}"; + + var files = Enumerable.Empty(); + + if (Directory.Exists(sqpack)) + files = files.Concat(Directory.GetFiles(sqpack)); + + if (Directory.Exists(movie)) + files = files.Concat(Directory.GetFiles(movie)); + + return files; + } + } +} diff --git a/src/XIVLauncher2.Common/Patching/ZiPatch/Util/SqexFileStream.cs b/src/XIVLauncher2.Common/Patching/ZiPatch/Util/SqexFileStream.cs new file mode 100644 index 0000000..01b2aa1 --- /dev/null +++ b/src/XIVLauncher2.Common/Patching/ZiPatch/Util/SqexFileStream.cs @@ -0,0 +1,55 @@ +using System; +using System.IO; +using System.Threading; + +namespace XIVLauncher2.Common.Patching.ZiPatch.Util +{ + public class SqexFileStream : FileStream + { + private static readonly byte[] WipeBuffer = new byte[1 << 16]; + + public SqexFileStream(string path, FileMode mode) : base(path, mode, FileAccess.ReadWrite, FileShare.Read, 1 << 16) + {} + + public static SqexFileStream? WaitForStream(string path, FileMode mode, int tries = 5, int sleeptime = 1) + { + do + { + try + { + return new SqexFileStream(path, mode); + } + catch (IOException) + { + if (tries == 0) + throw; + + Thread.Sleep(sleeptime * 1000); + } + } while (0 < --tries); + + return null; + } + + public void WriteFromOffset(byte[] data, int offset) + { + Seek(offset, SeekOrigin.Begin); + Write(data, 0, data.Length); + } + + public void Wipe(int length) + { + for (int numBytes; length > 0; length -= numBytes) + { + numBytes = Math.Min(WipeBuffer.Length, length); + Write(WipeBuffer, 0, numBytes); + } + } + + public void WipeFromOffset(int length, int offset) + { + Seek(offset, SeekOrigin.Begin); + Wipe(length); + } + } +} diff --git a/src/XIVLauncher2.Common/Patching/ZiPatch/Util/SqexFileStreamStore.cs b/src/XIVLauncher2.Common/Patching/ZiPatch/Util/SqexFileStreamStore.cs new file mode 100644 index 0000000..621738d --- /dev/null +++ b/src/XIVLauncher2.Common/Patching/ZiPatch/Util/SqexFileStreamStore.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Generic; +using System.IO; + +namespace XIVLauncher2.Common.Patching.ZiPatch.Util +{ + public class SqexFileStreamStore : IDisposable + { + private readonly Dictionary _streams = new Dictionary(); + + public SqexFileStream GetStream(string path, FileMode mode, int tries, int sleeptime) + { + // Normalise path + path = Path.GetFullPath(path); + + if (_streams.TryGetValue(path, out var stream)) + return stream; + + stream = SqexFileStream.WaitForStream(path, mode, tries, sleeptime); + _streams.Add(path, stream); + + return stream; + } + + public void Dispose() + { + foreach (var stream in _streams.Values) + stream.Dispose(); + } + } +} diff --git a/src/XIVLauncher2.Common/Patching/ZiPatch/Util/SqpackDatFile.cs b/src/XIVLauncher2.Common/Patching/ZiPatch/Util/SqpackDatFile.cs new file mode 100644 index 0000000..8f588bf --- /dev/null +++ b/src/XIVLauncher2.Common/Patching/ZiPatch/Util/SqpackDatFile.cs @@ -0,0 +1,37 @@ +using System.IO; +using System.Text; + +namespace XIVLauncher2.Common.Patching.ZiPatch.Util +{ + class SqpackDatFile : SqpackFile + { + public SqpackDatFile(BinaryReader reader) : base(reader) {} + + + protected override string GetFileName(ZiPatchConfig.PlatformId platform) => + $"{base.GetFileName(platform)}.dat{FileId}"; + + + public static void WriteEmptyFileBlockAt(SqexFileStream stream, int offset, int blockNumber) + { + stream.WipeFromOffset(blockNumber << 7, offset); + stream.Position = offset; + + using (var file = new BinaryWriter(stream, Encoding.Default, true)) + { + // FileBlockHeader - the 0 writes are technically unnecessary but are in for illustrative purposes + + // Block size + file.Write(1 << 7); + // ???? + file.Write(0); + // File size + file.Write(0); + // Total number of blocks? + file.Write(blockNumber - 1); + // Used number of blocks? + file.Write(0); + } + } + } +} diff --git a/src/XIVLauncher2.Common/Patching/ZiPatch/Util/SqpackFile.cs b/src/XIVLauncher2.Common/Patching/ZiPatch/Util/SqpackFile.cs new file mode 100644 index 0000000..43fb403 --- /dev/null +++ b/src/XIVLauncher2.Common/Patching/ZiPatch/Util/SqpackFile.cs @@ -0,0 +1,38 @@ +using System.IO; +using XIVLauncher2.Common.Patching.Util; + +namespace XIVLauncher2.Common.Patching.ZiPatch.Util +{ + public abstract class SqpackFile : SqexFile + { + protected ushort MainId { get; } + protected ushort SubId { get; } + protected uint FileId { get; } + + protected byte ExpansionId => (byte)(SubId >> 8); + + protected SqpackFile(BinaryReader reader) + { + MainId = reader.ReadUInt16BE(); + SubId = reader.ReadUInt16BE(); + FileId = reader.ReadUInt32BE(); + + RelativePath = GetExpansionPath(); + } + + protected string GetExpansionPath() => + $@"/sqpack/{GetExpansionFolder(ExpansionId)}/"; + + protected virtual string GetFileName(ZiPatchConfig.PlatformId platform) => + $"{GetExpansionPath()}{MainId:x2}{SubId:x4}.{platform.ToString().ToLower()}"; + + public void ResolvePath(ZiPatchConfig.PlatformId platform) => + RelativePath = GetFileName(platform); + + public override string ToString() + { + // Default to Win32 for prints; we're unlikely to run in PS3/PS4 + return GetFileName(ZiPatchConfig.PlatformId.Win32); + } + } +} diff --git a/src/XIVLauncher2.Common/Patching/ZiPatch/Util/SqpackIndexFile.cs b/src/XIVLauncher2.Common/Patching/ZiPatch/Util/SqpackIndexFile.cs new file mode 100644 index 0000000..1494e0a --- /dev/null +++ b/src/XIVLauncher2.Common/Patching/ZiPatch/Util/SqpackIndexFile.cs @@ -0,0 +1,13 @@ +using System.IO; + +namespace XIVLauncher2.Common.Patching.ZiPatch.Util +{ + class SqpackIndexFile : SqpackFile + { + public SqpackIndexFile(BinaryReader reader) : base(reader) {} + + + protected override string GetFileName(ZiPatchConfig.PlatformId platform) => + $"{base.GetFileName(platform)}.index{(FileId == 0 ? string.Empty : FileId.ToString())}"; + } +} diff --git a/src/XIVLauncher2.Common/Patching/ZiPatch/Util/SqpkCompressedBlock.cs b/src/XIVLauncher2.Common/Patching/ZiPatch/Util/SqpkCompressedBlock.cs new file mode 100644 index 0000000..a2446e2 --- /dev/null +++ b/src/XIVLauncher2.Common/Patching/ZiPatch/Util/SqpkCompressedBlock.cs @@ -0,0 +1,45 @@ +using System.IO; +using System.IO.Compression; + +namespace XIVLauncher2.Common.Patching.ZiPatch.Util +{ + class SqpkCompressedBlock + { + public int HeaderSize { get; protected set; } + public int CompressedSize { get; protected set; } + public int DecompressedSize { get; protected set; } + + public bool IsCompressed => CompressedSize != 0x7d00; + public int CompressedBlockLength => (int)(((IsCompressed ? CompressedSize : DecompressedSize) + 143) & 0xFFFF_FF80); + + public byte[] CompressedBlock { get; protected set; } + + public SqpkCompressedBlock(BinaryReader reader) + { + HeaderSize = reader.ReadInt32(); + reader.ReadUInt32(); // Pad + + CompressedSize = reader.ReadInt32(); + DecompressedSize = reader.ReadInt32(); + + if (IsCompressed) + CompressedBlock = reader.ReadBytes(CompressedBlockLength - HeaderSize); + else + { + CompressedBlock = reader.ReadBytes(DecompressedSize); + + reader.ReadBytes(CompressedBlockLength - HeaderSize - DecompressedSize); + } + } + + public void DecompressInto(Stream outStream) + { + if (IsCompressed) + using (var stream = new DeflateStream(new MemoryStream(CompressedBlock), CompressionMode.Decompress)) + stream.CopyTo(outStream); + else + using (var stream = new MemoryStream(CompressedBlock)) + stream.CopyTo(outStream); + } + } +} diff --git a/src/XIVLauncher2.Common/Patching/ZiPatch/ZiPatchConfig.cs b/src/XIVLauncher2.Common/Patching/ZiPatch/ZiPatchConfig.cs new file mode 100644 index 0000000..46b761e --- /dev/null +++ b/src/XIVLauncher2.Common/Patching/ZiPatch/ZiPatchConfig.cs @@ -0,0 +1,27 @@ +using XIVLauncher2.Common.Patching.ZiPatch.Util; + +namespace XIVLauncher2.Common.Patching.ZiPatch +{ + public class ZiPatchConfig + { + public enum PlatformId : ushort + { + Win32 = 0, + Ps3 = 1, + Ps4 = 2, + Unknown = 3 + } + + public string GamePath { get; protected set; } + public PlatformId Platform { get; set; } + public bool IgnoreMissing { get; set; } + public bool IgnoreOldMismatch { get; set; } + public SqexFileStreamStore Store { get; set; } + + + public ZiPatchConfig(string gamePath) + { + GamePath = gamePath; + } + } +} diff --git a/src/XIVLauncher2.Common/Patching/ZiPatch/ZiPatchException.cs b/src/XIVLauncher2.Common/Patching/ZiPatch/ZiPatchException.cs new file mode 100644 index 0000000..9ffe312 --- /dev/null +++ b/src/XIVLauncher2.Common/Patching/ZiPatch/ZiPatchException.cs @@ -0,0 +1,11 @@ +using System; + +namespace XIVLauncher2.Common.Patching.ZiPatch +{ + public class ZiPatchException : Exception + { + public ZiPatchException(string message = "ZiPatch error", Exception? innerException = null) : base(message, innerException) + { + } + } +} diff --git a/src/XIVLauncher2.Common/Patching/ZiPatch/ZiPatchFile.cs b/src/XIVLauncher2.Common/Patching/ZiPatch/ZiPatchFile.cs new file mode 100644 index 0000000..51327f8 --- /dev/null +++ b/src/XIVLauncher2.Common/Patching/ZiPatch/ZiPatchFile.cs @@ -0,0 +1,61 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using XIVLauncher2.Common.Patching.ZiPatch.Chunk; +using XIVLauncher2.Common.Patching.ZiPatch.Util; + +namespace XIVLauncher2.Common.Patching.ZiPatch +{ + public class ZiPatchFile : IDisposable + { + private static readonly uint[] zipatchMagic = + { + 0x50495A91, 0x48435441, 0x0A1A0A0D + }; + + private readonly Stream _stream; + + + /// + /// Instantiates a ZiPatchFile from a Stream + /// + /// Stream to a ZiPatch + public ZiPatchFile(Stream stream) + { + this._stream = stream; + + var reader = new BinaryReader(stream); + if (zipatchMagic.Any(magic => magic != reader.ReadUInt32())) + throw new ZiPatchException(); + } + + /// + /// Instantiates a ZiPatchFile from a file path + /// + /// Path to patch file + public static ZiPatchFile FromFileName(string filepath) + { + var stream = SqexFileStream.WaitForStream(filepath, FileMode.Open); + return new ZiPatchFile(stream); + } + + + + public IEnumerable GetChunks() + { + ZiPatchChunk chunk; + do + { + chunk = ZiPatchChunk.GetChunk(_stream); + + yield return chunk; + } while (chunk.ChunkType != EndOfFileChunk.Type); + } + + public void Dispose() + { + _stream?.Dispose(); + } + } +} diff --git a/src/XIVLauncher2.Common/Paths.cs b/src/XIVLauncher2.Common/Paths.cs new file mode 100644 index 0000000..e083b90 --- /dev/null +++ b/src/XIVLauncher2.Common/Paths.cs @@ -0,0 +1,22 @@ +using System; +using System.IO; + +namespace XIVLauncher2.Common +{ + public class Paths + { + static Paths() + { + RoamingPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "XIVLauncher"); + } + + public static string RoamingPath { get; private set; } + + public static string ResourcesPath => Path.Combine(AppContext.BaseDirectory, "Resources"); + + public static void OverrideRoamingPath(string path) + { + RoamingPath = Environment.ExpandEnvironmentVariables(path); + } + } +} diff --git a/src/XIVLauncher2.Common/Platform.cs b/src/XIVLauncher2.Common/Platform.cs new file mode 100644 index 0000000..9bc9d18 --- /dev/null +++ b/src/XIVLauncher2.Common/Platform.cs @@ -0,0 +1,10 @@ +namespace XIVLauncher2.Common +{ + public enum Platform + { + Win32, + Win32OnLinux, + Linux, + Mac, + } +} diff --git a/src/XIVLauncher2.Common/PlatformAbstractions/IDalamudCompatibilityCheck.cs b/src/XIVLauncher2.Common/PlatformAbstractions/IDalamudCompatibilityCheck.cs new file mode 100644 index 0000000..db3a9ed --- /dev/null +++ b/src/XIVLauncher2.Common/PlatformAbstractions/IDalamudCompatibilityCheck.cs @@ -0,0 +1,20 @@ +using System; + +namespace XIVLauncher2.Common.PlatformAbstractions; + +public interface IDalamudCompatibilityCheck +{ + public void EnsureCompatibility(); + + public class ArchitectureNotSupportedException : Exception + { + public ArchitectureNotSupportedException(string message) + : base(message) + { + } + } + + public class NoRedistsException : Exception + { + } +} diff --git a/src/XIVLauncher2.Common/PlatformAbstractions/IDalamudLoadingOverlay.cs b/src/XIVLauncher2.Common/PlatformAbstractions/IDalamudLoadingOverlay.cs new file mode 100644 index 0000000..12b1b30 --- /dev/null +++ b/src/XIVLauncher2.Common/PlatformAbstractions/IDalamudLoadingOverlay.cs @@ -0,0 +1,21 @@ +namespace XIVLauncher2.Common.PlatformAbstractions; + +public interface IDalamudLoadingOverlay +{ + public enum DalamudUpdateStep + { + Dalamud, + Assets, + Runtime, + Unavailable, + Starting, + } + + public void SetStep(DalamudUpdateStep step); + + public void SetVisible(); + + public void SetInvisible(); + + public void ReportProgress(long? size, long downloaded, double? progress); +} diff --git a/src/XIVLauncher2.Common/PlatformAbstractions/IDalamudRunner.cs b/src/XIVLauncher2.Common/PlatformAbstractions/IDalamudRunner.cs new file mode 100644 index 0000000..f9758a6 --- /dev/null +++ b/src/XIVLauncher2.Common/PlatformAbstractions/IDalamudRunner.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using XIVLauncher2.Common.Dalamud; + +namespace XIVLauncher2.Common.PlatformAbstractions; + +public interface IDalamudRunner +{ + Process? Run(FileInfo runner, bool fakeLogin, bool noPlugins, bool noThirdPlugins, FileInfo gameExe, string gameArgs, IDictionary environment, DalamudLoadMethod loadMethod, DalamudStartInfo startInfo); +} diff --git a/src/XIVLauncher2.Common/PlatformAbstractions/IGameRunner.cs b/src/XIVLauncher2.Common/PlatformAbstractions/IGameRunner.cs new file mode 100644 index 0000000..80837c5 --- /dev/null +++ b/src/XIVLauncher2.Common/PlatformAbstractions/IGameRunner.cs @@ -0,0 +1,9 @@ +using System.Collections.Generic; +using System.Diagnostics; + +namespace XIVLauncher2.Common.PlatformAbstractions; + +public interface IGameRunner +{ + Process? Start(string path, string workingDirectory, string arguments, IDictionary environment, DpiAwareness dpiAwareness); +} diff --git a/src/XIVLauncher2.Common/PlatformAbstractions/ISettings.cs b/src/XIVLauncher2.Common/PlatformAbstractions/ISettings.cs new file mode 100644 index 0000000..f6f3c6e --- /dev/null +++ b/src/XIVLauncher2.Common/PlatformAbstractions/ISettings.cs @@ -0,0 +1,16 @@ +using System.IO; +using XIVLauncher2.Common.Game.Patch.Acquisition; + +namespace XIVLauncher2.Common.PlatformAbstractions; + +public interface ISettings +{ + string AcceptLanguage { get; } + ClientLanguage? ClientLanguage { get; } + bool? KeepPatches { get; } + DirectoryInfo PatchPath { get; } + DirectoryInfo GamePath { get; } + AcquisitionMethod? PatchAcquisitionMethod { get; } + long SpeedLimitBytes { get; } + int DalamudInjectionDelayMs { get; } +} diff --git a/src/XIVLauncher2.Common/PlatformAbstractions/ISteam.cs b/src/XIVLauncher2.Common/PlatformAbstractions/ISteam.cs new file mode 100644 index 0000000..94d3538 --- /dev/null +++ b/src/XIVLauncher2.Common/PlatformAbstractions/ISteam.cs @@ -0,0 +1,32 @@ +using System; +using System.Threading.Tasks; + +namespace XIVLauncher2.Common.PlatformAbstractions; + +public interface ISteam +{ + void Initialize(uint appId); + bool IsValid { get; } + bool BLoggedOn { get; } + bool BOverlayNeedsPresent { get; } + void Shutdown(); + Task GetAuthSessionTicketAsync(); + bool IsAppInstalled(uint appId); + string GetAppInstallDir(uint appId); + bool ShowGamepadTextInput(bool password, bool multiline, string description, int maxChars, string existingText = ""); + string GetEnteredGamepadText(); + bool ShowFloatingGamepadTextInput(EFloatingGamepadTextInputMode mode, int x, int y, int width, int height); + bool IsRunningOnSteamDeck(); + uint GetServerRealTime(); + public void ActivateGameOverlayToWebPage(string url, bool modal = false); + + enum EFloatingGamepadTextInputMode + { + EnterDismisses, + UserDismisses, + Email, + Numeric, + } + + event Action OnGamepadTextInputDismissed; +} diff --git a/src/XIVLauncher2.Common/PlatformAbstractions/IUniqueIdCache.cs b/src/XIVLauncher2.Common/PlatformAbstractions/IUniqueIdCache.cs new file mode 100644 index 0000000..eb5d1d8 --- /dev/null +++ b/src/XIVLauncher2.Common/PlatformAbstractions/IUniqueIdCache.cs @@ -0,0 +1,19 @@ +namespace XIVLauncher2.Common.PlatformAbstractions; + +public interface IUniqueIdCache +{ + bool HasValidCache(string name); + + void Add(string name, string uid, int region, int maxExpansion); + + bool TryGet(string userName, out CachedUid cached); + + void Reset(); + + public struct CachedUid + { + public string UniqueId; + public int Region; + public int MaxExpansion; + } +} diff --git a/src/XIVLauncher2.Common/Repository.cs b/src/XIVLauncher2.Common/Repository.cs new file mode 100644 index 0000000..084cc42 --- /dev/null +++ b/src/XIVLauncher2.Common/Repository.cs @@ -0,0 +1,113 @@ +using System; +using System.IO; +using System.Text; + +namespace XIVLauncher2.Common +{ + public enum Repository + { + Boot, + Ffxiv, + Ex1, + Ex2, + Ex3, + Ex4 + } + + public static class RepoExtensions + { + private static DirectoryInfo GetRepoPath(this Repository repo, DirectoryInfo gamePath) + { + switch (repo) + { + case Repository.Boot: + return new DirectoryInfo(Path.Combine(gamePath.FullName, "boot")); + case Repository.Ffxiv: + return new DirectoryInfo(Path.Combine(gamePath.FullName, "game")); + case Repository.Ex1: + return new DirectoryInfo(Path.Combine(gamePath.FullName, "game", "sqpack", "ex1")); + case Repository.Ex2: + return new DirectoryInfo(Path.Combine(gamePath.FullName, "game", "sqpack", "ex2")); + case Repository.Ex3: + return new DirectoryInfo(Path.Combine(gamePath.FullName, "game", "sqpack", "ex3")); + case Repository.Ex4: + return new DirectoryInfo(Path.Combine(gamePath.FullName, "game", "sqpack", "ex4")); + default: + throw new ArgumentOutOfRangeException(nameof(repo), repo, null); + } + } + + public static FileInfo GetVerFile(this Repository repo, DirectoryInfo gamePath, bool isBck = false) + { + var repoPath = repo.GetRepoPath(gamePath).FullName; + switch (repo) + { + case Repository.Boot: + return new FileInfo(Path.Combine(repoPath, "ffxivboot" + (isBck ? ".bck" : ".ver"))); + case Repository.Ffxiv: + return new FileInfo(Path.Combine(repoPath, "ffxivgame" + (isBck ? ".bck" : ".ver"))); + case Repository.Ex1: + return new FileInfo(Path.Combine(repoPath, "ex1" + (isBck ? ".bck" : ".ver"))); + case Repository.Ex2: + return new FileInfo(Path.Combine(repoPath, "ex2" + (isBck ? ".bck" : ".ver"))); + case Repository.Ex3: + return new FileInfo(Path.Combine(repoPath, "ex3" + (isBck ? ".bck" : ".ver"))); + case Repository.Ex4: + return new FileInfo(Path.Combine(repoPath, "ex4" + (isBck ? ".bck" : ".ver"))); + default: + throw new ArgumentOutOfRangeException(nameof(repo), repo, null); + } + } + + public static string GetVer(this Repository repo, DirectoryInfo gamePath, bool isBck = false) + { + var verFile = repo.GetVerFile(gamePath, isBck); + + if (!verFile.Exists) + return Constants.BASE_GAME_VERSION; + + var ver = File.ReadAllText(verFile.FullName); + return string.IsNullOrWhiteSpace(ver) ? Constants.BASE_GAME_VERSION : ver; + } + + public static void SetVer(this Repository repo, DirectoryInfo gamePath, string newVer, bool isBck = false) + { + var verFile = GetVerFile(repo, gamePath, isBck); + + if (!verFile.Directory.Exists) + verFile.Directory.Create(); + + using var fileStream = verFile.Open(FileMode.Create, FileAccess.Write, FileShare.None); + var buffer = Encoding.ASCII.GetBytes(newVer); + fileStream.Write(buffer, 0, buffer.Length); + fileStream.Flush(); + } + + public static bool IsBaseVer(this Repository repo, DirectoryInfo gamePath) + { + return repo.GetVer(gamePath) == Constants.BASE_GAME_VERSION; + } + + // TODO + public static string GetRepoHash(this Repository repo) + { + switch (repo) + { + case Repository.Boot: + return null; + case Repository.Ffxiv: + return null; + case Repository.Ex1: + return null; + case Repository.Ex2: + return null; + case Repository.Ex3: + return null; + case Repository.Ex4: + return null; + default: + throw new ArgumentOutOfRangeException(nameof(repo), repo, null); + } + } + } +} diff --git a/src/XIVLauncher2.Common/SeVersion.cs b/src/XIVLauncher2.Common/SeVersion.cs new file mode 100644 index 0000000..0ebea05 --- /dev/null +++ b/src/XIVLauncher2.Common/SeVersion.cs @@ -0,0 +1,88 @@ +using System; + +namespace XIVLauncher2.Common +{ + public class SeVersion : IComparable + { + public uint Year { get; set; } + public uint Month { get; set; } + public uint Day { get; set; } + public uint Revision { get; set; } + public uint Part { get; set; } + + public static SeVersion Parse(string input) + { + var parts = input.Split('.'); + return new SeVersion + { + Year = uint.Parse(parts[0]), + Month = uint.Parse(parts[1]), + Day = uint.Parse(parts[2]), + Revision = uint.Parse(parts[3]), + Part = uint.Parse(parts[4]), + }; + } + + public override string ToString() => $"{Year:0000}.{Month:00}.{Day:00}.{Revision:0000}.{Part:0000}"; + + public int CompareTo(object obj) + { + var other = obj as SeVersion; + if (other == null) + return 1; + + if (Year > other.Year) + return 1; + + if (Year < other.Year) + return -1; + + if (Month > other.Month) + return 1; + + if (Month < other.Month) + return -1; + + if (Day > other.Day) + return 1; + + if (Day < other.Day) + return -1; + + if (Revision > other.Revision) + return 1; + + if (Revision < other.Revision) + return -1; + + if (Part > other.Part) + return 1; + + if (Part < other.Part) + return -1; + + return 0; + } + + public static bool operator <(SeVersion x, SeVersion y) => x.CompareTo(y) < 0; + public static bool operator >(SeVersion x, SeVersion y) => x.CompareTo(y) > 0; + public static bool operator <=(SeVersion x, SeVersion y) => x.CompareTo(y) <= 0; + public static bool operator >=(SeVersion x, SeVersion y) => x.CompareTo(y) >= 0; + + public static bool operator ==(SeVersion x, SeVersion y) + { + if (x is null) + return y is null; + + return x.CompareTo(y) == 0; + } + + public static bool operator !=(SeVersion x, SeVersion y) + { + if (x is null) + return y != null; + + return x.CompareTo(y) != 0; + } + } +} diff --git a/src/XIVLauncher2.Common/SettingsAnnotation.cs b/src/XIVLauncher2.Common/SettingsAnnotation.cs new file mode 100644 index 0000000..f234fa0 --- /dev/null +++ b/src/XIVLauncher2.Common/SettingsAnnotation.cs @@ -0,0 +1,17 @@ +using System; + +namespace XIVLauncher2.Common; + +[AttributeUsage(AttributeTargets.Field)] +public class SettingsDescriptionAttribute : Attribute +{ + public string FriendlyName { get; set; } + + public string Description { get; set; } + + public SettingsDescriptionAttribute(string friendlyName, string description) + { + this.FriendlyName = friendlyName; + this.Description = description; + } +} diff --git a/src/XIVLauncher2.Common/Storage.cs b/src/XIVLauncher2.Common/Storage.cs new file mode 100644 index 0000000..154fedd --- /dev/null +++ b/src/XIVLauncher2.Common/Storage.cs @@ -0,0 +1,49 @@ +using System; +using System.IO; + +namespace XIVLauncher2.Common; + +public class Storage +{ + public DirectoryInfo Root { get; } + + public Storage(string appName, string? overridePath = null) + { + if (Environment.OSVersion.Platform == PlatformID.Win32NT) + { + this.Root = new DirectoryInfo(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), appName)); + } + else + { + this.Root = new DirectoryInfo(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), $".{appName}")); + } + + if (!string.IsNullOrEmpty(overridePath)) + { + this.Root = new DirectoryInfo(overridePath); + } + + if (!this.Root.Exists) + this.Root.Create(); + } + + public FileInfo GetFile(string fileName) + { + return new FileInfo(Path.Combine(this.Root.FullName, fileName)); + } + + /// + /// Gets a folder and makes sure that it exists. + /// + /// + /// + public DirectoryInfo GetFolder(string folderName) + { + var folder = new DirectoryInfo(Path.Combine(this.Root.FullName, folderName)); + + if (!folder.Exists) + folder.Create(); + + return folder; + } +} diff --git a/src/XIVLauncher2.Common/Support/LogInit.cs b/src/XIVLauncher2.Common/Support/LogInit.cs new file mode 100644 index 0000000..ba5e456 --- /dev/null +++ b/src/XIVLauncher2.Common/Support/LogInit.cs @@ -0,0 +1,68 @@ +using System.IO; +using System.Linq; +using CommandLine; +using Serilog; + +namespace XIVLauncher2.Common.Support; + +public static class LogInit +{ + // ReSharper disable once ClassNeverInstantiated.Local + private class LogOptions + { + [Option('v', "verbose", Required = false, HelpText = "Set output to verbose messages.")] + public bool Verbose { get; set; } + + [Option("log-file-path", Required = false, HelpText = "Set path for log file.")] + public string? LogPath { get; set; } + } + + public static void Setup(string defaultLogPath, string[] args) + { + ParserResult result = null; + + try + { + var parser = new Parser(c => { c.IgnoreUnknownArguments = true; }); + result = parser.ParseArguments(args); + } + catch + { +#if DEBUG + throw; +#endif + } + + var config = new LoggerConfiguration() + .WriteTo.Sink(SerilogEventSink.Instance); + + var parsed = result?.Value ?? new LogOptions(); + + if (!string.IsNullOrEmpty(parsed.LogPath)) + { + config.WriteTo.Async(a => + { + a.File(parsed.LogPath); + }); + } + else + { + config.WriteTo.Async(a => + { + a.File(defaultLogPath); + }); + } + +#if DEBUG + config.WriteTo.Debug(); + config.MinimumLevel.Verbose(); +#else + config.MinimumLevel.Information(); +#endif + + if (parsed.Verbose) + config.MinimumLevel.Verbose(); + + Log.Logger = config.CreateLogger(); + } +} diff --git a/src/XIVLauncher2.Common/Support/SerilogEventSink.cs b/src/XIVLauncher2.Common/Support/SerilogEventSink.cs new file mode 100644 index 0000000..9c69d42 --- /dev/null +++ b/src/XIVLauncher2.Common/Support/SerilogEventSink.cs @@ -0,0 +1,50 @@ +using System; +using Serilog.Core; +using Serilog.Events; + +namespace XIVLauncher2.Common.Support +{ + /// + /// Serilog event sink. + /// + public class SerilogEventSink : ILogEventSink + { + private static SerilogEventSink instance; + private readonly IFormatProvider formatProvider; + + /// + /// Initializes a new instance of the class. + /// + /// Logging format provider. + private SerilogEventSink(IFormatProvider formatProvider) + { + this.formatProvider = formatProvider; + } + + /// + /// Event on a log line being emitted. + /// + public event EventHandler<(string Line, LogEventLevel Level, DateTimeOffset TimeStamp, Exception? Exception)>? LogLine; + + /// + /// Gets the default instance. + /// + public static SerilogEventSink Instance => instance ??= new SerilogEventSink(null); + + /// + /// Emit a log event. + /// + /// Log event to be emitted. + public void Emit(LogEvent logEvent) + { + var message = logEvent.RenderMessage(this.formatProvider); + + if (logEvent.Exception != null) + { + message += "\n" + logEvent.Exception; + } + + this.LogLine?.Invoke(this, (message, logEvent.Level, logEvent.Timestamp, logEvent.Exception)); + } + } +} diff --git a/src/XIVLauncher2.Common/Util/ApiHelpers.cs b/src/XIVLauncher2.Common/Util/ApiHelpers.cs new file mode 100644 index 0000000..86e5594 --- /dev/null +++ b/src/XIVLauncher2.Common/Util/ApiHelpers.cs @@ -0,0 +1,80 @@ +using System; +using System.Linq; +using System.Net.Http.Headers; + +namespace XIVLauncher2.Common.Util; + +public static class ApiHelpers +{ + public static long GetUnixMillis() + { + return (long)DateTime.UtcNow.Subtract(new DateTime(1970, 1, 1)).TotalMilliseconds; + } + + public static string BytesToString(double byteCount) => BytesToString(Convert.ToInt64(Math.Floor(byteCount))); + + public static string BytesToString(long byteCount) + { + string[] suf = { "B", "KB", "MB", "GB", "TB", "PB", "EB" }; //Longs run out around EB + + if (byteCount == 0) + return "0" + suf[0]; + + var bytes = Math.Abs(byteCount); + var place = Convert.ToInt32(Math.Floor(Math.Log(bytes, 1024))); + var num = Math.Round(bytes / Math.Pow(1024, place), 1); + return $"{(Math.Sign(byteCount) * num):#0.0}{suf[place]}"; + } + + public static string GenerateAcceptLanguage(int asdf = 0) + { + var codes = new string[] { "de-DE", "en-US", "ja" }; + var codesMany = new string[] { "de-DE", "en-US,en", "en-GB,en", "fr-BE,fr", "ja", "fr-FR,fr", "fr-CH,fr" }; + var rng = new Random(asdf); + + var many = rng.Next(10) < 3; + + if (many) + { + var howMany = rng.Next(2, 4); + var deck = codesMany.OrderBy((x) => rng.Next()).Take(howMany).ToArray(); + + var hdr = string.Empty; + + for (int i = 0; i < deck.Count(); i++) + { + hdr += deck.ElementAt(i) + $";q=0.{10 - (i + 1)}"; + + if (i != deck.Length - 1) + hdr += ";"; + } + + return hdr; + } + + return codes[rng.Next(0, codes.Length)]; + } + + public static void AddWithoutValidation(this HttpHeaders headers, string key, string value) + { + var res = headers.TryAddWithoutValidation(key, value); + + if (!res) + throw new InvalidOperationException($"Could not add header - {key}: {value}"); + } + + /// + /// Gets an attribute on an enum. + /// + /// The type of attribute to get. + /// The enum value that has an attached attribute. + /// The attached attribute, if any. + public static TAttribute? GetAttribute(this Enum value) + where TAttribute : Attribute + { + var type = value.GetType(); + var memInfo = type.GetMember(value.ToString()); + var attributes = memInfo[0].GetCustomAttributes(typeof(TAttribute), false); + return (attributes.Length > 0) ? (TAttribute)attributes[0] : null; + } +} diff --git a/src/XIVLauncher2.Common/Util/DebugHelpers.cs b/src/XIVLauncher2.Common/Util/DebugHelpers.cs new file mode 100644 index 0000000..955ae75 --- /dev/null +++ b/src/XIVLauncher2.Common/Util/DebugHelpers.cs @@ -0,0 +1,73 @@ +using System; +using System.Text; + +namespace XIVLauncher2.Common.Util; + +public static class DebugHelpers +{ + /// + /// Create a hexdump of the provided bytes. + /// + /// The bytes to hexdump. + /// The offset in the byte array to start at. + /// The amount of bytes to display per line. + /// The generated hexdump in string form. + public static string ByteArrayToHex(byte[] bytes, int offset = 0, int bytesPerLine = 16) + { + if (bytes == null) return string.Empty; + + var hexChars = "0123456789ABCDEF".ToCharArray(); + + const int OFFSET_BLOCK = 8 + 3; + var byteBlock = OFFSET_BLOCK + (bytesPerLine * 3) + ((bytesPerLine - 1) / 8) + 2; + var lineLength = byteBlock + bytesPerLine + Environment.NewLine.Length; + + var line = (new string(' ', lineLength - Environment.NewLine.Length) + Environment.NewLine).ToCharArray(); + var numLines = (bytes.Length + bytesPerLine - 1) / bytesPerLine; + + var sb = new StringBuilder(numLines * lineLength); + + for (var i = 0; i < bytes.Length; i += bytesPerLine) + { + var h = i + offset; + + line[0] = hexChars[(h >> 28) & 0xF]; + line[1] = hexChars[(h >> 24) & 0xF]; + line[2] = hexChars[(h >> 20) & 0xF]; + line[3] = hexChars[(h >> 16) & 0xF]; + line[4] = hexChars[(h >> 12) & 0xF]; + line[5] = hexChars[(h >> 8) & 0xF]; + line[6] = hexChars[(h >> 4) & 0xF]; + line[7] = hexChars[(h >> 0) & 0xF]; + + var hexColumn = OFFSET_BLOCK; + var charColumn = byteBlock; + + for (var j = 0; j < bytesPerLine; j++) + { + if (j > 0 && (j & 7) == 0) hexColumn++; + + if (i + j >= bytes.Length) + { + line[hexColumn] = ' '; + line[hexColumn + 1] = ' '; + line[charColumn] = ' '; + } + else + { + var by = bytes[i + j]; + line[hexColumn] = hexChars[(by >> 4) & 0xF]; + line[hexColumn + 1] = hexChars[by & 0xF]; + line[charColumn] = by < 32 ? '.' : (char)by; + } + + hexColumn += 3; + charColumn++; + } + + sb.Append(line); + } + + return sb.ToString().TrimEnd(Environment.NewLine.ToCharArray()); + } +} diff --git a/src/XIVLauncher2.Common/Util/GameHelpers.cs b/src/XIVLauncher2.Common/Util/GameHelpers.cs new file mode 100644 index 0000000..1b9942d --- /dev/null +++ b/src/XIVLauncher2.Common/Util/GameHelpers.cs @@ -0,0 +1,105 @@ +using System; +using System.Diagnostics; +using System.Globalization; +using System.IO; +using System.Linq; + +namespace XIVLauncher2.Common.Util; + +public static class GameHelpers +{ + /// + /// Returns if the current system region is set to North America. + /// + public static bool IsRegionNorthAmerica() + { + return RegionInfo.CurrentRegion.TwoLetterISORegionName is "US" or "MX" or "CA"; + } + + public static bool IsValidGamePath(string path) + { + if (string.IsNullOrEmpty(path)) + return false; + + return Directory.Exists(Path.Combine(path, "game")) && Directory.Exists(Path.Combine(path, "boot")); + } + + public static bool CanMightNotBeInternationalClient(string path) + { + if (Directory.Exists(Path.Combine(path, "sdo"))) + return true; + + if (File.Exists(Path.Combine(path, "boot", "FFXIV_Boot.exe"))) + return true; + + return false; + } + + public static bool LetChoosePath(string path) + { + if (string.IsNullOrEmpty(path)) + return true; + + var di = new DirectoryInfo(path); + + if (di.Name == "game") + return false; + + if (di.Name == "boot") + return false; + + if (di.Name == "sqpack") + return false; + + return true; + } + + public static FileInfo GetOfficialLauncherPath(DirectoryInfo gamePath) => new(Path.Combine(gamePath.FullName, "boot", "ffxivboot.exe")); + + public static void StartOfficialLauncher(DirectoryInfo gamePath, bool isSteam, bool isFreeTrial) + { + var args = string.Empty; + + if (isSteam && isFreeTrial) + { + args = "-issteamfreetrial"; + } + else if (isSteam) + { + args = "-issteam"; + } + + Process.Start(GetOfficialLauncherPath(gamePath).FullName, args); + } + + public static bool CheckIsGameOpen() + { +#if DEBUG + return false; +#endif + + var procs = Process.GetProcesses(); + + if (procs.Any(x => x.ProcessName == "ffxiv")) + return true; + + if (procs.Any(x => x.ProcessName == "ffxiv_dx11")) + return true; + + if (procs.Any(x => x.ProcessName == "ffxivboot")) + return true; + + if (procs.Any(x => x.ProcessName == "ffxivlauncher")) + return true; + + return false; + } + + public static string ToMangledSeBase64(byte[] input) + { + return Convert.ToBase64String(input) + .Replace('+', '-') + .Replace('/', '_') + .Replace('=', '*'); + } +} diff --git a/src/XIVLauncher2.Common/Util/HttpClientWithProgress.cs b/src/XIVLauncher2.Common/Util/HttpClientWithProgress.cs new file mode 100644 index 0000000..44a283d --- /dev/null +++ b/src/XIVLauncher2.Common/Util/HttpClientWithProgress.cs @@ -0,0 +1,90 @@ +using System; +using System.IO; +using System.Net.Http; +using System.Threading.Tasks; + +namespace XIVLauncher2.Common.Util; + +public class HttpClientDownloadWithProgress : IDisposable +{ + private readonly string downloadUrl; + private readonly string destinationFilePath; + + private HttpClient httpClient; + + public delegate void ProgressChangedHandler(long? totalFileSize, long totalBytesDownloaded, double? progressPercentage); + + public event ProgressChangedHandler ProgressChanged; + + public HttpClientDownloadWithProgress(string downloadUrl, string destinationFilePath) + { + this.downloadUrl = downloadUrl; + this.destinationFilePath = destinationFilePath; + } + + public async Task Download(TimeSpan? timeout = null) + { + timeout ??= TimeSpan.FromDays(1); + this.httpClient = new HttpClient { Timeout = timeout.Value }; + + using var response = await this.httpClient.GetAsync(this.downloadUrl, HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false); + await this.DownloadFileFromHttpResponseMessage(response).ConfigureAwait(false); + } + + private async Task DownloadFileFromHttpResponseMessage(HttpResponseMessage response) + { + response.EnsureSuccessStatusCode(); + + var totalBytes = response.Content.Headers.ContentLength; + + using var contentStream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false); + await this.ProcessContentStream(totalBytes, contentStream).ConfigureAwait(false); + } + + private async Task ProcessContentStream(long? totalDownloadSize, Stream contentStream) + { + var totalBytesRead = 0L; + var readCount = 0L; + var buffer = new byte[8192]; + var isMoreToRead = true; + + using var fileStream = new FileStream(this.destinationFilePath, FileMode.Create, FileAccess.Write, FileShare.None, 8192, true); + + do + { + var bytesRead = await contentStream.ReadAsync(buffer, 0, buffer.Length).ConfigureAwait(false); + + if (bytesRead == 0) + { + isMoreToRead = false; + this.TriggerProgressChanged(totalDownloadSize, totalBytesRead); + continue; + } + + await fileStream.WriteAsync(buffer, 0, bytesRead).ConfigureAwait(false); + + totalBytesRead += bytesRead; + readCount += 1; + + if (readCount % 100 == 0) + this.TriggerProgressChanged(totalDownloadSize, totalBytesRead); + } while (isMoreToRead); + } + + private void TriggerProgressChanged(long? totalDownloadSize, long totalBytesRead) + { + if (this.ProgressChanged == null) + return; + + double? progressPercentage = null; + if (totalDownloadSize.HasValue) + progressPercentage = Math.Round((double)totalBytesRead / totalDownloadSize.Value * 100, 2); + + this.ProgressChanged(totalDownloadSize, totalBytesRead, progressPercentage); + } + + public void Dispose() + { + this.httpClient?.Dispose(); + } +} diff --git a/src/XIVLauncher2.Common/Util/PlatformHelpers.cs b/src/XIVLauncher2.Common/Util/PlatformHelpers.cs new file mode 100644 index 0000000..3176e8d --- /dev/null +++ b/src/XIVLauncher2.Common/Util/PlatformHelpers.cs @@ -0,0 +1,148 @@ +using System; +using System.Diagnostics; +using System.IO; +using System.Net; +using System.Net.Sockets; +using System.Runtime.InteropServices; +using System.Security.Principal; + +namespace XIVLauncher2.Common.Util; + +public static class PlatformHelpers +{ + public static Platform GetPlatform() + { + if (EnvironmentSettings.IsWine) + return Platform.Win32OnLinux; + else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + return Platform.Linux; + else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + return Platform.Mac; + else + return Platform.Win32; + + // TODO(goat): Add mac here, once it's merged + + + } + + /// + /// Generates a temporary file name. + /// + /// A temporary file name that is almost guaranteed to be unique. + public static string GetTempFileName() + { + // https://stackoverflow.com/a/50413126 + return Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); + } + + public static void OpenBrowser(string url) + { + // https://github.com/dotnet/corefx/issues/10361 + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + url = url.Replace("&", "^&"); + Process.Start(new ProcessStartInfo(url) { UseShellExecute = true }); + } + else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + Process.Start("xdg-open", url); + } + else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + Process.Start("open", url); + } + else + { + throw new NotImplementedException(); + } + } + + [DllImport("libc")] + private static extern uint geteuid(); + + public static bool IsElevated() + { + switch (Environment.OSVersion.Platform) + { + case PlatformID.Win32NT: + return new WindowsPrincipal(WindowsIdentity.GetCurrent()).IsInRole(WindowsBuiltInRole.Administrator); + + case PlatformID.Unix: + return geteuid() == 0; + + default: + return false; + } + } + + public static void Untar(string path, string output) + { + var psi = new ProcessStartInfo("tar") + { + Arguments = $"-xf \"{path}\" -C \"{output}\"" + }; + + var tarProcess = Process.Start(psi); + + if (tarProcess == null) + throw new Exception("Could not start tar."); + + tarProcess.WaitForExit(); + + if (tarProcess.ExitCode != 0) + throw new Exception("Could not untar."); + } + + private static readonly IPEndPoint DefaultLoopbackEndpoint = new(IPAddress.Loopback, port: 0); + + public static int GetAvailablePort() + { + using var socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); + + socket.Bind(DefaultLoopbackEndpoint); + return ((IPEndPoint)socket.LocalEndPoint).Port; + } + +#if WIN32 + /* + * WINE: The APIs DriveInfo uses are buggy on Wine. Let's just use the kernel32 API instead. + */ + + [System.Runtime.InteropServices.DllImport("kernel32.dll", SetLastError = true, CharSet = System.Runtime.InteropServices.CharSet.Unicode)] + [return: System.Runtime.InteropServices.MarshalAs(System.Runtime.InteropServices.UnmanagedType.Bool)] + public static extern bool GetDiskFreeSpaceEx(string lpDirectoryName, + out ulong lpFreeBytesAvailable, + out ulong lpTotalNumberOfBytes, + out ulong lpTotalNumberOfFreeBytes); + + public static long GetDiskFreeSpace(DirectoryInfo info) + { + if (info == null) + { + throw new ArgumentNullException(nameof(info)); + } + + ulong dummy = 0; + + if (!GetDiskFreeSpaceEx(info.Root.FullName, out ulong freeSpace, out dummy, out dummy)) + { + throw new System.ComponentModel.Win32Exception(System.Runtime.InteropServices.Marshal.GetLastWin32Error()); + } + + return (long)freeSpace; + } +#else + public static long GetDiskFreeSpace(DirectoryInfo info) + { + if (info == null) + { + throw new ArgumentNullException(nameof(info)); + } + + DriveInfo drive = new DriveInfo(info.FullName); + + return drive.AvailableFreeSpace; + } +#endif +} diff --git a/src/XIVLauncher2.Common/XIVLauncher2.Common.csproj b/src/XIVLauncher2.Common/XIVLauncher2.Common.csproj index 9d130cd..a62d000 100644 --- a/src/XIVLauncher2.Common/XIVLauncher2.Common.csproj +++ b/src/XIVLauncher2.Common/XIVLauncher2.Common.csproj @@ -1,7 +1,57 @@ - + - net7.0 - enable - enable + XIVLauncher2.Common + XIVLauncher2.Common + Common components for XIVLauncher2. + 2.0.0 + disable + + true + + true + true + true + + + $(DefineConstants);$(ExtraDefineConstants) + + + + $(DefineConstants);WIN32 + + + $(DefineConstants);OSX + + + $(DefineConstants);LINUX + + + + $(MSBuildProjectDirectory)\ + $(AppOutputBase)=C:\goatsoft\xl\XIVLauncher.Common\ + + + + Library + net7.0 + latest + true + true + + + + + + + + + + + + + + + + diff --git a/src/XIVLauncher2.sln b/src/XIVLauncher2.sln index 14fb6e5..8fa7613 100644 --- a/src/XIVLauncher2.sln +++ b/src/XIVLauncher2.sln @@ -2,7 +2,11 @@ Microsoft Visual Studio Solution File, Format Version 12.00 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "XIVLauncher2", "XIVLauncher2\XIVLauncher2.csproj", "{92F61103-FC83-48D6-8678-B1DDD1B8BEEA}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "XIVLauncher2.Common", "XIVLauncher2.Common\XIVLauncher2.Common.csproj", "{C7DC3CBB-CFFA-4FFA-93CB-16ED892430EF}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "XIVLauncher2.Common", "XIVLauncher2.Common\XIVLauncher2.Common.csproj", "{A19943FC-450A-4F99-AB0D-B43EB910EFB9}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "XIVLauncher2.Common.Unix", "XIVLauncher2.Common.Unix\XIVLauncher2.Common.Unix.csproj", "{74160ED3-9F5A-486A-9F1A-4B1F21BCE0AD}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "XIVLauncher2.Common.Windows", "XIVLauncher2.Common.Windows\XIVLauncher2.Common.Windows.csproj", "{A3DC19C7-CE2C-489F-9602-50E41DC4FEA7}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -14,9 +18,17 @@ Global {92F61103-FC83-48D6-8678-B1DDD1B8BEEA}.Debug|Any CPU.Build.0 = Debug|Any CPU {92F61103-FC83-48D6-8678-B1DDD1B8BEEA}.Release|Any CPU.ActiveCfg = Release|Any CPU {92F61103-FC83-48D6-8678-B1DDD1B8BEEA}.Release|Any CPU.Build.0 = Release|Any CPU - {C7DC3CBB-CFFA-4FFA-93CB-16ED892430EF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {C7DC3CBB-CFFA-4FFA-93CB-16ED892430EF}.Debug|Any CPU.Build.0 = Debug|Any CPU - {C7DC3CBB-CFFA-4FFA-93CB-16ED892430EF}.Release|Any CPU.ActiveCfg = Release|Any CPU - {C7DC3CBB-CFFA-4FFA-93CB-16ED892430EF}.Release|Any CPU.Build.0 = Release|Any CPU + {A19943FC-450A-4F99-AB0D-B43EB910EFB9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A19943FC-450A-4F99-AB0D-B43EB910EFB9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A19943FC-450A-4F99-AB0D-B43EB910EFB9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A19943FC-450A-4F99-AB0D-B43EB910EFB9}.Release|Any CPU.Build.0 = Release|Any CPU + {74160ED3-9F5A-486A-9F1A-4B1F21BCE0AD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {74160ED3-9F5A-486A-9F1A-4B1F21BCE0AD}.Debug|Any CPU.Build.0 = Debug|Any CPU + {74160ED3-9F5A-486A-9F1A-4B1F21BCE0AD}.Release|Any CPU.ActiveCfg = Release|Any CPU + {74160ED3-9F5A-486A-9F1A-4B1F21BCE0AD}.Release|Any CPU.Build.0 = Release|Any CPU + {A3DC19C7-CE2C-489F-9602-50E41DC4FEA7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A3DC19C7-CE2C-489F-9602-50E41DC4FEA7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A3DC19C7-CE2C-489F-9602-50E41DC4FEA7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A3DC19C7-CE2C-489F-9602-50E41DC4FEA7}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection EndGlobal