From eef7d672830c84d1e7b66c2a4297d24139748709 Mon Sep 17 00:00:00 2001 From: Sewer 56 Date: Thu, 24 Mar 2022 22:15:59 +0000 Subject: [PATCH] Changed: Modernize Project --- Publish.ps1 | 406 +++++++++++++++-- PublishAll.ps1 | 9 + .../Configuration/Config.cs | 31 +- .../Configuration/Configurable.cs | 4 +- .../Configuration/Configurator.cs | 50 +- SonicHeroes.Utils.OneRedirector/Constants.cs | 17 +- .../ModConfig.json | 48 +- .../Native/Native.cs | 429 +++++++++--------- .../One/VirtualFile.cs | 63 ++- .../One/VirtualOne.cs | 66 ++- .../One/VirtualOneBuilder.cs | 105 +++-- .../OneBuilderCollection.cs | 101 ++--- .../OneFileTracker.cs | 367 ++++++++------- SonicHeroes.Utils.OneRedirector/OneHook.cs | 191 ++++---- SonicHeroes.Utils.OneRedirector/Program.cs | 80 ++-- .../Robust.Trimming.targets | 122 +++++ .../SonicHeroes.Utils.OneRedirector.csproj | 21 +- .../Structs/FileInfo.cs | 33 +- .../Structs/NativeFunctions.cs | 60 ++- .../Structs/Utilities.cs | 75 ++- 20 files changed, 1422 insertions(+), 856 deletions(-) create mode 100644 PublishAll.ps1 create mode 100644 SonicHeroes.Utils.OneRedirector/Robust.Trimming.targets diff --git a/Publish.ps1 b/Publish.ps1 index 496376c..b330241 100644 --- a/Publish.ps1 +++ b/Publish.ps1 @@ -1,35 +1,377 @@ -# Project Output Paths -$modOutputPath = "Release" -$solutionName = "SonicHeroes.Utils.OneRedirector/SonicHeroes.Utils.OneRedirector.csproj" -$publishName = "sonicheroes.utils.oneredirector.zip" -$publishDirectory = "Publish" +<# +.SYNOPSIS + Builds and Publishes a Reloaded II Mod +.DESCRIPTION + Windows script to Build and Publish a Reloaded Mod. + By default, published items will be output to a directory called `Publish/ToUpload`. + If you acquired this script by creating a new Reloaded Mod in VS. Then most likely everything + (aside from delta updates) should be preconfigured here. + +.PARAMETER ProjectPath + Path to the project to be built. + Useful if using this script from another script for the purpose of building multiple mods. + +.PARAMETER PackageName + Name of the package to be built. + Affects the name of the output files of the publish. + +.PARAMETER PublishOutputDir + Default: "Publish/ToUpload" + Declares the directory for placing the output files. + +.PARAMETER BuildR2R + Default: $False + + Builds the mod using an optimisation called `Ready to Run`, which sacrifices file size for potentially + faster startup time. This is only worth enabling on mods with a lot of code, usually it is best left disabled. + + For more details see: https://docs.microsoft.com/en-us/dotnet/core/deploying/ready-to-run + +.PARAMETER ChangelogPath + Full or relative path to a file containing the changelog for the mod. + The changelog should be written in Markdown format. + +.PARAMETER IsPrerelease + Default: $False + + If set to true, the version downloaded for delta package generation will be the latest pre-release + as opposed to the latest stable version. + +.PARAMETER MakeDelta + Default: $False + + Set to true to create Delta packages. + Usually this is true in a CI/CD environment when creating a release, else false in development. + + If this is true, you should set UseGitHubDelta, UseGameBananaDelta, UseNuGetDelta or equivalent to true. + +.PARAMETER MetadataFileName + Default: Sewer56.Update.ReleaseMetadata.json + Name of the release metadata file used to download the delta package. + +.PARAMETER UseGitHubDelta + Default: $False + If true, sources the last version of the package to publish from GitHub. + +.PARAMETER UseGameBananaDelta + Default: $False + If true, sources the last version of the package to publish from GameBanana. + +.PARAMETER UseNuGetDelta + Default: $False + If true, sources the last version of the package to publish from NuGet. + +.PARAMETER GitHubUserName + [Use if UseGitHubDelta is true] + Sets the username used for obtaining Deltas from GitHub. + +.PARAMETER GitHubRepoName + [Use if UseGitHubDelta is true] + Sets the repository used for obtaining Deltas from GitHub. + +.PARAMETER GitHubFallbackPattern + [Use if UseGitHubDelta is true] + Allows you to specify a Wildcard pattern (e.g. *Update.zip) for the file to be downloaded. + This is a fallback used in cases no Release Metadata file can be found. + +.PARAMETER GitHubInheritVersionFromTag + [Use if UseGitHubDelta is true] + Uses version determined from release tag (in GitHub Releases) as opposed to the + Release Metadata file in latest release. + +.PARAMETER GameBananaItemId + [Use if UseGameBananaDelta is true] + Example: 150118 + + Unique identifier for the individual mod. This is the last number of a GameBanana Mod Page URL + e.g. https://gamebanana.com/mods/150118 -> 150118 + +.PARAMETER NuGetPackageId + [Use if UseNuGetDelta is true] + Example: reloaded.sharedlib.hooks + + The ID of the package to use as delta. + +.PARAMETER NuGetFeedUrl + [Use if UseNuGetDelta is true] + Example: http://packages.sewer56.moe:5000/v3/index.json + + The URL of the NuGet feed to download the delta from. + +.PARAMETER NuGetAllowUnlisted + [Use if UseNuGetDelta is true] + Default: $False + + Allows for the downloading of unlisted packages. + +.PARAMETER PublishGeneric + Default: $True + + Publishes a generic package that can be uploaded to any other website. + +.PARAMETER PublishNuGet + Default: $True + + Publishes a package that can be uploaded to any NuGet Source. + +.PARAMETER PublishGameBanana + Default: $True + + Publishes a package that can be uploaded to GameBanana. + +.PARAMETER Build + Default: $True + + Whether the project should be built. + Setting this to false lets you use the publish part of the script standalone in a non .NET environment. + +.PARAMETER RemoveExe + Default: $True + + Removes executables from build output. Useful when performing R2R Optimisation. + +.EXAMPLE + .\Publish.ps1 -ProjectPath "Reloaded.Hooks.ReloadedII/Reloaded.Hooks.ReloadedII.csproj" -PackageName "Reloaded.Hooks.ReloadedII" -PublishOutputDir "Publish/ToUpload" + +.EXAMPLE + .\Publish.ps1 -MakeDelta true -BuildR2R true -UseGitHubDelta True + +.EXAMPLE + .\Publish.ps1 -BuildR2R true + +#> +[cmdletbinding()] +param ( + $IsPrerelease=$False, + $MakeDelta=$False, + $ChangelogPath="", + $Build=$True, + $BuildR2R=$False, + $RemoveExe = $True, + + ## => User Config <= ## + $ProjectPath = "Reloaded.Hooks.ReloadedII/Reloaded.Hooks.ReloadedII.csproj", + $PackageName = "Reloaded.Hooks.ReloadedII", + $PublishOutputDir = "Publish/ToUpload", + + ## => User: Delta Config + # Pick one and configure settings below. + $MetadataFileName = "Sewer56.Update.ReleaseMetadata.json", + $UseGitHubDelta = $True, + $UseGameBananaDelta = $False, + $UseNuGetDelta = $False, + + $GitHubUserName = "Sewer56", + $GitHubRepoName = "Reloaded.SharedLib.Hooks.ReloadedII", + $GitHubFallbackPattern = "reloaded.sharedlib.hooks.zip", # For migrating from legacy. + $GitHubInheritVersionFromTag = $True, # Uses version determined from release tag as opposed to metadata file in latest release. + + $GameBananaItemId = 333681, # From mod page URL. + + $NuGetPackageId = "reloaded.sharedlib.hooks", + $NuGetFeedUrl = "http://packages.sewer56.moe:5000/v3/index.json", + $NuGetAllowUnlisted = $False, + + ## => User: Publish Config + $PublishGeneric = $True, + $PublishNuGet = $True, + $PublishGameBanana = $True +) + +## => User: Publish Output +$publishBuildDirectory = "Publish/Builds/CurrentVersion" # Build directory for current version of the mod. +$deltaDirectory = "Publish/Builds/LastVersion" # Path to last version of the mod. + +$PublishGenericDirectory = "$PublishOutputDir/Generic" # Publish files for any target not listed below. +$PublishNuGetDirectory = "$PublishOutputDir/NuGet" # Publish files for NuGet +$PublishGameBananaDirectory = "$PublishOutputDir/GameBanana" # Publish files for GameBanana + +## => User Config <= ## +# Tools +$reloadedToolsPath = "./Publish/Tools/Reloaded-Tools" # Used to check if tools are installed. +$updateToolsPath = "./Publish/Tools/Update-Tools" # Used to check if update tools are installed. +$reloadedToolPath = "$reloadedToolsPath/Reloaded.Publisher.exe" # Path to Reloaded publishing tool. +$updateToolPath = "$updateToolsPath/Sewer56.Update.Tool.dll" # Path to Update tool. + +## => Script <= ## +# Set Working Directory +Split-Path $MyInvocation.MyCommand.Path | Push-Location [Environment]::CurrentDirectory = $PWD -# Clean anything in existing Release directory. -Remove-Item $modOutputPath -Recurse -Remove-Item $publishDirectory -Recurse -New-Item $modOutputPath -ItemType Directory -New-Item $publishDirectory -ItemType Directory - -# Build -dotnet restore $solutionName -dotnet clean $solutionName -dotnet publish $solutionName -c Release -r win-x86 --self-contained false -o "$modOutputPath/x86" /p:PublishReadyToRun=true -dotnet publish $solutionName -c Release -r win-x64 --self-contained false -o "$modOutputPath/x64" /p:PublishReadyToRun=true - -# Remove Redundant Files -Move-Item -Path "$modOutputPath/x86/ModConfig.json" -Destination "$modOutputPath/ModConfig.json" -Move-Item -Path "$modOutputPath/x86/Preview.png" -Destination "$modOutputPath/Preview.png" - -Remove-Item "$modOutputPath/x64/ModConfig.json" -Remove-Item "$modOutputPath/x64/Preview.png" - -# Cleanup Unnecessary Files -Get-ChildItem $modOutputPath -Include *.exe -Recurse | Remove-Item -Force -Recurse -Get-ChildItem $modOutputPath -Include *.pdb -Recurse | Remove-Item -Force -Recurse -Get-ChildItem $modOutputPath -Include *.xml -Recurse | Remove-Item -Force -Recurse - -# Compress -Add-Type -A System.IO.Compression.FileSystem -[IO.Compression.ZipFile]::CreateFromDirectory($modOutputPath, "$publishDirectory/$publishName") \ No newline at end of file +# Convert Booleans +$IsPrerelease = [bool]::Parse($IsPrerelease) +$MakeDelta = [bool]::Parse($MakeDelta) +$Build = [bool]::Parse($Build) +$BuildR2R = [bool]::Parse($BuildR2R) +$RemoveExe = [bool]::Parse($RemoveExe) +$UseGitHubDelta = [bool]::Parse($UseGitHubDelta) +$UseGameBananaDelta = [bool]::Parse($UseGameBananaDelta) +$UseNuGetDelta = [bool]::Parse($UseNuGetDelta) +$NuGetAllowUnlisted = [bool]::Parse($NuGetAllowUnlisted) +$PublishGeneric = [bool]::Parse($PublishGeneric) +$PublishNuGet = [bool]::Parse($PublishNuGet) +$PublishGameBanana = [bool]::Parse($PublishGameBanana) +$GitHubInheritVersionFromTag = [bool]::Parse($GitHubInheritVersionFromTag) + +function Get-Tools { + # Download Tools (if needed) + $ProgressPreference = 'SilentlyContinue' + if (-not(Test-Path -Path $reloadedToolsPath -PathType Any)) { + Write-Host "Downloading Reloaded Tools" + Invoke-WebRequest -Uri "https://github.com/Reloaded-Project/Reloaded-II/releases/latest/download/Tools.zip" -OutFile "$env:TEMP/Tools.zip" + Expand-Archive -LiteralPath "$env:TEMP/Tools.zip" -DestinationPath $reloadedToolsPath + + # Remove Items + Remove-Item "$env:TEMP/Tools.zip" -ErrorAction SilentlyContinue + } + + if ($MakeDelta -and -not(Test-Path -Path $updateToolsPath -PathType Any)) { + Write-Host "Downloading Update Library Tools" + Invoke-WebRequest -Uri "https://github.com/Sewer56/Update/releases/latest/download/Sewer56.Update.Tool.zip" -OutFile "$env:TEMP/Sewer56.Update.Tool.zip" + Expand-Archive -LiteralPath "$env:TEMP/Sewer56.Update.Tool.zip" -DestinationPath $updateToolsPath + + # Remove Items + Remove-Item "$env:TEMP/Sewer56.Update.Tool.zip" -ErrorAction SilentlyContinue + } +} + +# Publish for targets +function Build { + # Clean anything in existing Release directory. + Remove-Item $publishBuildDirectory -Recurse -ErrorAction SilentlyContinue + New-Item $publishBuildDirectory -ItemType Directory -ErrorAction SilentlyContinue + + # Build + dotnet restore $ProjectPath + dotnet clean $ProjectPath + + if ($BuildR2R) { + dotnet publish $ProjectPath -c Release -r win-x86 --self-contained false -o "$publishBuildDirectory/x86" /p:PublishReadyToRun=true + dotnet publish $ProjectPath -c Release -r win-x64 --self-contained false -o "$publishBuildDirectory/x64" /p:PublishReadyToRun=true + + # Remove Redundant Files + Move-Item -Path "$publishBuildDirectory/x86/ModConfig.json" -Destination "$publishBuildDirectory/ModConfig.json" -ErrorAction SilentlyContinue + Move-Item -Path "$publishBuildDirectory/x86/Preview.png" -Destination "$publishBuildDirectory/Preview.png" -ErrorAction SilentlyContinue + Remove-Item "$publishBuildDirectory/x64/Preview.png" -ErrorAction SilentlyContinue + Remove-Item "$publishBuildDirectory/x64/ModConfig.json" -ErrorAction SilentlyContinue + } + else { + dotnet publish $ProjectPath -c Release --self-contained false -o "$publishBuildDirectory" + } + + # Cleanup Unnecessary Files + if ($RemoveExe) { + Get-ChildItem $publishBuildDirectory -Include *.exe -Recurse | Remove-Item -Force -Recurse + } + + Get-ChildItem $publishBuildDirectory -Include *.pdb -Recurse | Remove-Item -Force -Recurse + Get-ChildItem $publishBuildDirectory -Include *.xml -Recurse | Remove-Item -Force -Recurse +} + +function Get-Last-Version { + + Remove-Item $deltaDirectory -Recurse -ErrorAction SilentlyContinue + New-Item $deltaDirectory -ItemType Directory -ErrorAction SilentlyContinue + $arguments = "DownloadPackage --extract --outputpath `"$deltaDirectory`" --allowprereleases `"$IsPrerelease`" --metadatafilename `"$MetadataFileName`"" + + if ($UseGitHubDelta) { + $arguments += " --source GitHub --githubusername `"$GitHubUserName`" --githubrepositoryname `"$GitHubRepoName`" --githublegacyfallbackpattern `"$GitHubFallbackPattern`" --githubinheritversionfromtag `"$GitHubInheritVersionFromTag`"" + } + elseif ($UseNuGetDelta) { + $arguments += " --source NuGet --nugetpackageid `"$NuGetPackageId`" --nugetfeedurl `"$NuGetFeedUrl`" --nugetallowunlisted `"$NuGetAllowUnlisted`"" + } + elseif ($UseGameBananaDelta) { + $arguments += " --source GameBanana --gamebananaitemid `"$GameBananaItemId`"" + } + + Invoke-Expression "dotnet `"$updateToolPath`" $arguments" +} + +function Get-Common-Publish-Args { + + param ( + $AllowDeltas=$True + ) + + $arguments = "--modfolder `"$publishBuildDirectory`" --packagename `"$PackageName`"" + if ($ChangelogPath) { + $arguments += " --changelogpath `"$ChangelogPath`"" + } + + if ($AllowDeltas -and $MakeDelta) { + $arguments += " --olderversionfolders `"$deltaDirectory`"" + } + + return $arguments +} + +function Publish-Common { + + param ( + $Directory="", + $AllowDeltas=$True, + $PublishTarget="" + ) + + Remove-Item $Directory -Recurse -ErrorAction SilentlyContinue + New-Item $Directory -ItemType Directory -ErrorAction SilentlyContinue + $arguments = "$(Get-Common-Publish-Args -AllowDeltas $AllowDeltas) --outputfolder `"$Directory`" --publishtarget $PublishTarget" + $command = "$reloadedToolPath $arguments" + Write-Host "$command`r`n`r`n" + Invoke-Expression $command +} + +function Publish-GameBanana { + Publish-Common -Directory $PublishGameBananaDirectory -PublishTarget GameBanana +} + +function Publish-NuGet { + Publish-Common -Directory $PublishNuGetDirectory -PublishTarget NuGet -AllowDeltas $False +} + +function Publish-Generic { + Publish-Common -Directory $PublishGenericDirectory -PublishTarget Default +} + +function Cleanup { + Remove-Item $PublishOutputDir -Recurse -ErrorAction SilentlyContinue + Remove-Item $PublishNuGetDirectory -Recurse -ErrorAction SilentlyContinue + Remove-Item $PublishGenericDirectory -Recurse -ErrorAction SilentlyContinue + Remove-Item $publishBuildDirectory -Recurse -ErrorAction SilentlyContinue + Remove-Item $deltaDirectory -Recurse -ErrorAction SilentlyContinue +} + +# Build & Publish +Cleanup +Get-Tools + +if ($MakeDelta) { + Write-Host "Downloading Delta (Last Version)" + Get-Last-Version +} + +if ($Build) { + Write-Host "Building Mod" + Build +} + +if ($PublishGeneric) { + Write-Host "Publishing Mod for Default Target" + Publish-Generic +} + +if ($PublishNuGet) { + Write-Host "Publishing Mod for NuGet Target" + Publish-NuGet +} + +if ($PublishGameBanana) { + Write-Host "Publishing Mod for GameBanana Target" + Publish-GameBanana +} + +# Restore Working Directory +Write-Host "Done." +Write-Host "Upload the files in folder `"$PublishOutputDir`" to respective location or website." +Pop-Location \ No newline at end of file diff --git a/PublishAll.ps1 b/PublishAll.ps1 new file mode 100644 index 0000000..ef77ca1 --- /dev/null +++ b/PublishAll.ps1 @@ -0,0 +1,9 @@ + +# Set Working Directory +Split-Path $MyInvocation.MyCommand.Path | Push-Location +[Environment]::CurrentDirectory = $PWD + +./Publish.ps1 -ProjectPath "SonicHeroes.Utils.OneRedirector/SonicHeroes.Utils.OneRedirector.csproj" ` + -PackageName "SonicHeroes.Utils.OneRedirector" ` + +Pop-Location \ No newline at end of file diff --git a/SonicHeroes.Utils.OneRedirector/Configuration/Config.cs b/SonicHeroes.Utils.OneRedirector/Configuration/Config.cs index 8bf0283..1564c69 100644 --- a/SonicHeroes.Utils.OneRedirector/Configuration/Config.cs +++ b/SonicHeroes.Utils.OneRedirector/Configuration/Config.cs @@ -1,21 +1,20 @@ using System.ComponentModel; -namespace SonicHeroes.Utils.OneRedirector.Configuration +namespace SonicHeroes.Utils.OneRedirector.Configuration; + +public class Config : Configurable { - public class Config : Configurable - { - /* - User Properties: - - Please put all of your configurable properties here. - - Tip: Consider using the various available attributes https://stackoverflow.com/a/15051390/11106111 - - By default, configuration saves as "Config.json" in mod folder. - Need more config files/classes? See Configuration.cs - */ + /* + User Properties: + - Please put all of your configurable properties here. + - Tip: Consider using the various available attributes https://stackoverflow.com/a/15051390/11106111 + + By default, configuration saves as "Config.json" in mod folder. + Need more config files/classes? See Configuration.cs + */ - [DisplayName("Always Build Archives")] - [Description("Rebuilds the .ONE archive every time the game opens the files. This allows you to swap out files while the game is running at the expense or more time taken to load the file(s).")] - public bool AlwaysBuildArchive { get; set; } = false; - } -} + [DisplayName("Always Build Archives")] + [Description("Rebuilds the .ONE archive every time the game opens the files. This allows you to swap out files while the game is running at the expense or more time taken to load the file(s).")] + public bool AlwaysBuildArchive { get; set; } = false; +} \ No newline at end of file diff --git a/SonicHeroes.Utils.OneRedirector/Configuration/Configurable.cs b/SonicHeroes.Utils.OneRedirector/Configuration/Configurable.cs index 22372e2..6fb4156 100644 --- a/SonicHeroes.Utils.OneRedirector/Configuration/Configurable.cs +++ b/SonicHeroes.Utils.OneRedirector/Configuration/Configurable.cs @@ -14,7 +14,7 @@ namespace SonicHeroes.Utils.OneRedirector.Configuration { // Default Serialization Options // If you wish to change the serializer used, refer to Reloaded.Messaging documentation: https://github.com/Reloaded-Project/Reloaded.Messaging - public static JsonSerializerOptions SerializerOptions { get; } = new JsonSerializerOptions() + public static JsonSerializerOptions SerializerOptions { get; } = new() { Converters = { new JsonStringEnumConverter() }, WriteIndented = true @@ -90,7 +90,7 @@ public void DisposeEvents() /// Safety lock for when changed event gets raised twice on file save. /// [Browsable(false)] - private static object _readLock = new object(); + private static readonly object _readLock = new(); /// /// Loads a specified configuration from the hard disk, or creates a default if it does not exist. diff --git a/SonicHeroes.Utils.OneRedirector/Configuration/Configurator.cs b/SonicHeroes.Utils.OneRedirector/Configuration/Configurator.cs index 912dbf0..269645d 100644 --- a/SonicHeroes.Utils.OneRedirector/Configuration/Configurator.cs +++ b/SonicHeroes.Utils.OneRedirector/Configuration/Configurator.cs @@ -1,20 +1,23 @@ -using System.IO; +using System; +using System.IO; using Reloaded.Mod.Interfaces; namespace SonicHeroes.Utils.OneRedirector.Configuration { - public class Configurator : IConfigurator + public class Configurator : IConfiguratorV2 { - /* For latest documentation: - - See the interface! (Go To Definition) or if not available - - Google the Source Code! - */ + private const string ConfigFileName = "Config.json"; /// - /// Full path to the mod folder. + /// The folder where the modification files are stored. /// public string ModFolder { get; private set; } + /// + /// Full path to the config folder. + /// + public string ConfigFolder { get; private set; } + /// /// Returns a list of configurations. /// @@ -26,7 +29,7 @@ private IUpdatableConfigurable[] MakeConfigurations() _configurations = new IUpdatableConfigurable[] { // Add more configurations here if needed. - Configurable.FromFile(Path.Combine(ModFolder, "Config.json"), "Default Config") + Configurable.FromFile(Path.Combine(ConfigFolder, ConfigFileName), "ONE Redirector Options") }; // Add self-updating to configurations. @@ -43,9 +46,27 @@ private IUpdatableConfigurable[] MakeConfigurations() } public Configurator() { } - public Configurator(string modDirectory) : this() + public Configurator(string configDirectory) : this() { - ModFolder = modDirectory; + ConfigFolder = configDirectory; + } + + /* Configurator V2 */ + + /// + /// Migrates from the old config location to the newer config location. + /// + /// Old directory containing the mod configs. + /// New directory pointing to user config folder. + public void Migrate(string oldDirectory, string newDirectory) + { + TryMoveFile(ConfigFileName); + + void TryMoveFile(string fileName) + { + try { File.Move(Path.Combine(oldDirectory, fileName), Path.Combine(newDirectory, fileName), true); } + catch (Exception) { /* Ignored */ } + } } /* Configurator */ @@ -58,9 +79,9 @@ public Configurator(string modDirectory) : this() /* IConfigurator. */ /// - /// Sets the mod directory for the Configurator. + /// Sets the config directory for the Configurator. /// - public void SetModDirectory(string modDirectory) => ModFolder = modDirectory; + public void SetConfigDirectory(string configDirectory) => ConfigFolder = configDirectory; /// /// Returns a list of user configurations. @@ -72,5 +93,10 @@ public Configurator(string modDirectory) : this() /// If you have your own configuration program/code, run that code here and return true, else return false. /// public bool TryRunCustomConfiguration() => false; + + /// + /// Sets the mod directory for the Configurator. + /// + public void SetModDirectory(string modDirectory) { ModFolder = modDirectory; } } } diff --git a/SonicHeroes.Utils.OneRedirector/Constants.cs b/SonicHeroes.Utils.OneRedirector/Constants.cs index be9ce05..3239196 100644 --- a/SonicHeroes.Utils.OneRedirector/Constants.cs +++ b/SonicHeroes.Utils.OneRedirector/Constants.cs @@ -1,10 +1,9 @@ -namespace SonicHeroes.Utils.OneRedirector +namespace SonicHeroes.Utils.OneRedirector; + +public static class Constants { - public static class Constants - { - public const string OneExtension = ".one"; - public const string PrsExtension = ".prs"; - public const string DeleteExtension = ".del"; - public const string RedirectorFolderName = "OneRedirector"; - } -} + public const string OneExtension = ".one"; + public const string PrsExtension = ".prs"; + public const string DeleteExtension = ".del"; + public const string RedirectorFolderName = "OneRedirector"; +} \ No newline at end of file diff --git a/SonicHeroes.Utils.OneRedirector/ModConfig.json b/SonicHeroes.Utils.OneRedirector/ModConfig.json index 88843d2..40dd7b7 100644 --- a/SonicHeroes.Utils.OneRedirector/ModConfig.json +++ b/SonicHeroes.Utils.OneRedirector/ModConfig.json @@ -1,8 +1,8 @@ -{ +{ "ModId": "sonicheroes.utils.oneredirector", "ModName": "Generic Heroes ONE Archive Redirector", "ModAuthor": "Sewer56", - "ModVersion": "1.0.0", + "ModVersion": "1.0.1", "ModDescription": "Experimental Windows FileSystem level hook allowing redirection of Sonic Heroes ONE archive contents.", "ModDll": "SonicHeroes.Utils.OneRedirector.dll", "ModIcon": "Preview.png", @@ -10,7 +10,43 @@ "ModR2RManagedDll64": "x64/SonicHeroes.Utils.OneRedirector.dll", "ModNativeDll32": "", "ModNativeDll64": "", - "ModDependencies": [ "reloaded.sharedlib.hooks", "reloaded.sharedlib.csharp.prs" ], - "OptionalDependencies": [ "reloaded.universal.redirector" ], - "SupportedAppId": [ "tsonic_win.exe" ] -} \ No newline at end of file + "IsLibrary": false, + "ReleaseMetadataFileName": "Sewer56.Update.ReleaseMetadata.json", + "PluginData": { + "GitHubDependencies": { + "IdToConfigMap": { + "reloaded.sharedlib.hooks": { + "Config": { + "UserName": "Reloaded.SharedLib.Hooks.ReloadedII", + "RepositoryName": "Sewer56", + "UseReleaseTag": true, + "AssetFileName": "reloaded.sharedlib.hooks.zip" + } + } + } + }, + "GitHubRelease": { + "UserName": "Sewer56", + "RepositoryName": "Heroes.Utils.OneRedirector.ReloadedII", + "UseReleaseTag": true, + "AssetFileName": "Mod.zip" + }, + "NuGet": { + "AllowUpdateFromAnyRepository": false, + "DefaultRepositoryUrls": [ + "http://packages.sewer56.moe:5000/v3/index.json" + ] + } + }, + "IsUniversalMod": false, + "ModDependencies": [ + "reloaded.sharedlib.hooks", + "reloaded.sharedlib.csharp.prs" + ], + "OptionalDependencies": [ + "reloaded.universal.redirector" + ], + "SupportedAppId": [ + "tsonic_win.exe" + ] +} diff --git a/SonicHeroes.Utils.OneRedirector/Native/Native.cs b/SonicHeroes.Utils.OneRedirector/Native/Native.cs index 00986f6..6e96ded 100644 --- a/SonicHeroes.Utils.OneRedirector/Native/Native.cs +++ b/SonicHeroes.Utils.OneRedirector/Native/Native.cs @@ -4,262 +4,261 @@ using System.Security; using System.Text; -namespace SonicHeroes.Utils.OneRedirector.Native +namespace SonicHeroes.Utils.OneRedirector.Native; + +public unsafe class Native { - public unsafe class Native - { - /// - /// Creates a new file or directory, or opens an existing file, device, directory, or volume. - /// (The description here is a partial, lazy copy from MSDN) - /// - [UnmanagedFunctionPointer(CallingConvention.Cdecl)] - [Reloaded.Hooks.Definitions.X64.Function(Reloaded.Hooks.Definitions.X64.CallingConventions.Microsoft)] - [Reloaded.Hooks.Definitions.X86.Function(Reloaded.Hooks.Definitions.X86.CallingConventions.Stdcall)] - public delegate int NtCreateFile(out IntPtr handle, FileAccess access, ref OBJECT_ATTRIBUTES objectAttributes, - ref IO_STATUS_BLOCK ioStatus, ref long allocSize, uint fileAttributes, FileShare share, uint createDisposition, uint createOptions, - IntPtr eaBuffer, uint eaLength); + /// + /// Creates a new file or directory, or opens an existing file, device, directory, or volume. + /// (The description here is a partial, lazy copy from MSDN) + /// + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + [Reloaded.Hooks.Definitions.X64.Function(Reloaded.Hooks.Definitions.X64.CallingConventions.Microsoft)] + [Reloaded.Hooks.Definitions.X86.Function(Reloaded.Hooks.Definitions.X86.CallingConventions.Stdcall)] + public delegate int NtCreateFile(out IntPtr handle, FileAccess access, ref OBJECT_ATTRIBUTES objectAttributes, + ref IO_STATUS_BLOCK ioStatus, ref long allocSize, uint fileAttributes, FileShare share, uint createDisposition, uint createOptions, + IntPtr eaBuffer, uint eaLength); - [UnmanagedFunctionPointer(CallingConvention.Cdecl)] - [Reloaded.Hooks.Definitions.X64.Function(Reloaded.Hooks.Definitions.X64.CallingConventions.Microsoft)] - [Reloaded.Hooks.Definitions.X86.Function(Reloaded.Hooks.Definitions.X86.CallingConventions.Stdcall)] - public delegate int NtReadFile(IntPtr handle, IntPtr hEvent, IntPtr* apcRoutine, IntPtr* apcContext, - ref IO_STATUS_BLOCK ioStatus, byte* buffer, uint length, long* byteOffset, IntPtr key); + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + [Reloaded.Hooks.Definitions.X64.Function(Reloaded.Hooks.Definitions.X64.CallingConventions.Microsoft)] + [Reloaded.Hooks.Definitions.X86.Function(Reloaded.Hooks.Definitions.X86.CallingConventions.Stdcall)] + public delegate int NtReadFile(IntPtr handle, IntPtr hEvent, IntPtr* apcRoutine, IntPtr* apcContext, + ref IO_STATUS_BLOCK ioStatus, byte* buffer, uint length, long* byteOffset, IntPtr key); - [UnmanagedFunctionPointer(CallingConvention.Cdecl)] - [Reloaded.Hooks.Definitions.X64.Function(Reloaded.Hooks.Definitions.X64.CallingConventions.Microsoft)] - [Reloaded.Hooks.Definitions.X86.Function(Reloaded.Hooks.Definitions.X86.CallingConventions.Stdcall)] - public delegate int SetFilePointer(IntPtr hFile, int liDistanceToMove, IntPtr lpNewFilePointer, uint dwMoveMethod); + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + [Reloaded.Hooks.Definitions.X64.Function(Reloaded.Hooks.Definitions.X64.CallingConventions.Microsoft)] + [Reloaded.Hooks.Definitions.X86.Function(Reloaded.Hooks.Definitions.X86.CallingConventions.Stdcall)] + public delegate int SetFilePointer(IntPtr hFile, int liDistanceToMove, IntPtr lpNewFilePointer, uint dwMoveMethod); - [UnmanagedFunctionPointer(CallingConvention.Cdecl)] - [Reloaded.Hooks.Definitions.X64.Function(Reloaded.Hooks.Definitions.X64.CallingConventions.Microsoft)] - [Reloaded.Hooks.Definitions.X86.Function(Reloaded.Hooks.Definitions.X86.CallingConventions.Stdcall)] - public delegate int NtSetInformationFile(IntPtr hFile, out IO_STATUS_BLOCK ioStatusBlock, void* fileInformation, uint length, FileInformationClass fileInformationClass); + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + [Reloaded.Hooks.Definitions.X64.Function(Reloaded.Hooks.Definitions.X64.CallingConventions.Microsoft)] + [Reloaded.Hooks.Definitions.X86.Function(Reloaded.Hooks.Definitions.X86.CallingConventions.Stdcall)] + public delegate int NtSetInformationFile(IntPtr hFile, out IO_STATUS_BLOCK ioStatusBlock, void* fileInformation, uint length, FileInformationClass fileInformationClass); - [UnmanagedFunctionPointer(CallingConvention.Cdecl)] - [Reloaded.Hooks.Definitions.X64.Function(Reloaded.Hooks.Definitions.X64.CallingConventions.Microsoft)] - [Reloaded.Hooks.Definitions.X86.Function(Reloaded.Hooks.Definitions.X86.CallingConventions.Stdcall)] - public delegate int NtQueryInformationFile(IntPtr hFile, out IO_STATUS_BLOCK ioStatusBlock, void* fileInformation, uint length, FileInformationClass fileInformationClass); + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + [Reloaded.Hooks.Definitions.X64.Function(Reloaded.Hooks.Definitions.X64.CallingConventions.Microsoft)] + [Reloaded.Hooks.Definitions.X86.Function(Reloaded.Hooks.Definitions.X86.CallingConventions.Stdcall)] + public delegate int NtQueryInformationFile(IntPtr hFile, out IO_STATUS_BLOCK ioStatusBlock, void* fileInformation, uint length, FileInformationClass fileInformationClass); - [StructLayout(LayoutKind.Sequential)] - public struct FILE_STANDARD_INFORMATION - { - public long AllocationSize; - public long EndOfFile; - public int NumberOfLinks; - public byte DeletePending; - public byte Directory; - }; + [StructLayout(LayoutKind.Sequential)] + public struct FILE_STANDARD_INFORMATION + { + public long AllocationSize; + public long EndOfFile; + public int NumberOfLinks; + public byte DeletePending; + public byte Directory; + }; + /// + /// A driver sets an IRP's I/O status block to indicate the final status of an I/O request, before calling IoCompleteRequest for the IRP. + /// + [StructLayout(LayoutKind.Sequential)] + public struct IO_STATUS_BLOCK + { + public UInt32 Status; + public IntPtr Information; + } + + /// + /// The OBJECT_ATTRIBUTES structure specifies attributes that can be applied to objects or object + /// handles by routines that create objects and/or return handles to objects. + /// + [StructLayout(LayoutKind.Sequential)] + public struct OBJECT_ATTRIBUTES : IDisposable + { /// - /// A driver sets an IRP's I/O status block to indicate the final status of an I/O request, before calling IoCompleteRequest for the IRP. + /// Lengthm of this structure. /// - [StructLayout(LayoutKind.Sequential)] - public struct IO_STATUS_BLOCK - { - public UInt32 Status; - public IntPtr Information; - } + public int Length; /// - /// The OBJECT_ATTRIBUTES structure specifies attributes that can be applied to objects or object - /// handles by routines that create objects and/or return handles to objects. + /// Optional handle to the root object directory for the path name specified by the ObjectName member. + /// If RootDirectory is NULL, ObjectName must point to a fully qualified object name that includes the full path to the target object. + /// If RootDirectory is non-NULL, ObjectName specifies an object name relative to the RootDirectory directory. + /// The RootDirectory handle can refer to a file system directory or an object directory in the object manager namespace. /// - [StructLayout(LayoutKind.Sequential)] - public struct OBJECT_ATTRIBUTES : IDisposable - { - /// - /// Lengthm of this structure. - /// - public int Length; + public IntPtr RootDirectory; - /// - /// Optional handle to the root object directory for the path name specified by the ObjectName member. - /// If RootDirectory is NULL, ObjectName must point to a fully qualified object name that includes the full path to the target object. - /// If RootDirectory is non-NULL, ObjectName specifies an object name relative to the RootDirectory directory. - /// The RootDirectory handle can refer to a file system directory or an object directory in the object manager namespace. - /// - public IntPtr RootDirectory; + /// + /// Pointer to a Unicode string that contains the name of the object for which a handle is to be opened. + /// This must either be a fully qualified object name, or a relative path name to the directory specified by the RootDirectory member. + /// + private IntPtr objectName; - /// - /// Pointer to a Unicode string that contains the name of the object for which a handle is to be opened. - /// This must either be a fully qualified object name, or a relative path name to the directory specified by the RootDirectory member. - /// - private IntPtr objectName; + /// + /// Bitmask of flags that specify object handle attributes. This member can contain one or more of the flags in the following table (See MSDN) + /// + public uint Attributes; - /// - /// Bitmask of flags that specify object handle attributes. This member can contain one or more of the flags in the following table (See MSDN) - /// - public uint Attributes; + /// + /// Specifies a security descriptor (SECURITY_DESCRIPTOR) for the object when the object is created. + /// If this member is NULL, the object will receive default security settings. + /// + public IntPtr SecurityDescriptor; - /// - /// Specifies a security descriptor (SECURITY_DESCRIPTOR) for the object when the object is created. - /// If this member is NULL, the object will receive default security settings. - /// - public IntPtr SecurityDescriptor; + /// + /// Optional quality of service to be applied to the object when it is created. + /// Used to indicate the security impersonation level and context tracking mode (dynamic or static). + /// Currently, the InitializeObjectAttributes macro sets this member to NULL. + /// + public IntPtr SecurityQualityOfService; - /// - /// Optional quality of service to be applied to the object when it is created. - /// Used to indicate the security impersonation level and context tracking mode (dynamic or static). - /// Currently, the InitializeObjectAttributes macro sets this member to NULL. - /// - public IntPtr SecurityQualityOfService; + /// + /// Gets or sets the file path of the files loaded in or out. + /// + public unsafe UNICODE_STRING ObjectName + { + get => *(UNICODE_STRING*)objectName; - /// - /// Gets or sets the file path of the files loaded in or out. - /// - public unsafe UNICODE_STRING ObjectName + set { - get => *(UNICODE_STRING*)objectName; - - set - { - // Check if we need to delete old memory. - bool fDeleteOld = objectName != IntPtr.Zero; + // Check if we need to delete old memory. + bool fDeleteOld = objectName != IntPtr.Zero; - // Allocates the necessary bytes for the string. - if (!fDeleteOld) - objectName = Marshal.AllocHGlobal(Marshal.SizeOf(value)); + // Allocates the necessary bytes for the string. + if (!fDeleteOld) + objectName = Marshal.AllocHGlobal(Marshal.SizeOf(value)); - // Deallocate old string while writing the new one. - Marshal.StructureToPtr(value, objectName, fDeleteOld); - } - } - - /// - /// Disposes of the actual object name (file name) in question. - /// - public void Dispose() - { - if (objectName != IntPtr.Zero) - { - Marshal.DestroyStructure(objectName, typeof(UNICODE_STRING)); - Marshal.FreeHGlobal(objectName); - objectName = IntPtr.Zero; - } + // Deallocate old string while writing the new one. + Marshal.StructureToPtr(value, objectName, fDeleteOld); } } - /// - /// Does this really need to be explained to you? + /// Disposes of the actual object name (file name) in question. /// - [StructLayout(LayoutKind.Sequential)] - public struct UNICODE_STRING : IDisposable + public void Dispose() { - public ushort Length; - public ushort MaximumLength; - private IntPtr buffer; - - public UNICODE_STRING(string s) + if (objectName != IntPtr.Zero) { - Length = (ushort)(s.Length * 2); - MaximumLength = (ushort)(Length + 2); - buffer = Marshal.StringToHGlobalUni(s); + Marshal.DestroyStructure(objectName, typeof(UNICODE_STRING)); + Marshal.FreeHGlobal(objectName); + objectName = IntPtr.Zero; } + } + } - /// - /// Disposes of the current file name assigned to this Unicode String. - /// - public void Dispose() - { - Marshal.FreeHGlobal(buffer); - buffer = IntPtr.Zero; - } - /// - /// Returns a string with the contents - /// - /// - public override string ToString() - { - try - { - if (buffer != IntPtr.Zero) - { - Reloaded.Memory.Sources.Memory.CurrentProcess.ReadRaw(buffer, out byte[] uniString, Length); - return Encoding.Unicode.GetString(uniString); - } + /// + /// Does this really need to be explained to you? + /// + [StructLayout(LayoutKind.Sequential)] + public struct UNICODE_STRING : IDisposable + { + public ushort Length; + public ushort MaximumLength; + private IntPtr buffer; - return ""; - } - catch { return ""; } - } + public UNICODE_STRING(string s) + { + Length = (ushort)(s.Length * 2); + MaximumLength = (ushort)(Length + 2); + buffer = Marshal.StringToHGlobalUni(s); } + /// + /// Disposes of the current file name assigned to this Unicode String. + /// + public void Dispose() + { + Marshal.FreeHGlobal(buffer); + buffer = IntPtr.Zero; + } /// - /// Enumeration of the various file information classes. - /// See wdm.h. + /// Returns a string with the contents /// - public enum FileInformationClass + /// + public override string ToString() { - None = 0, - FileDirectoryInformation = 1, - FileFullDirectoryInformation, // 2 - FileBothDirectoryInformation, // 3 - FileBasicInformation, // 4 - FileStandardInformation, // 5 - FileInternalInformation, // 6 - FileEaInformation, // 7 - FileAccessInformation, // 8 - FileNameInformation, // 9 - FileRenameInformation, // 10 - FileLinkInformation, // 11 - FileNamesInformation, // 12 - FileDispositionInformation, // 13 - FilePositionInformation, // 14 - FileFullEaInformation, // 15 - FileModeInformation, // 16 - FileAlignmentInformation, // 17 - FileAllInformation, // 18 - FileAllocationInformation, // 19 - FileEndOfFileInformation, // 20 - FileAlternateNameInformation, // 21 - FileStreamInformation, // 22 - FilePipeInformation, // 23 - FilePipeLocalInformation, // 24 - FilePipeRemoteInformation, // 25 - FileMailslotQueryInformation, // 26 - FileMailslotSetInformation, // 27 - FileCompressionInformation, // 28 - FileObjectIdInformation, // 29 - FileCompletionInformation, // 30 - FileMoveClusterInformation, // 31 - FileQuotaInformation, // 32 - FileReparsePointInformation, // 33 - FileNetworkOpenInformation, // 34 - FileAttributeTagInformation, // 35 - FileTrackingInformation, // 36 - FileIdBothDirectoryInformation, // 37 - FileIdFullDirectoryInformation, // 38 - FileValidDataLengthInformation, // 39 - FileShortNameInformation, // 40 - FileIoCompletionNotificationInformation, // 41 - FileIoStatusBlockRangeInformation, // 42 - FileIoPriorityHintInformation, // 43 - FileSfioReserveInformation, // 44 - FileSfioVolumeInformation, // 45 - FileHardLinkInformation, // 46 - FileProcessIdsUsingFileInformation, // 47 - FileNormalizedNameInformation, // 48 - FileNetworkPhysicalNameInformation, // 49 - FileIdGlobalTxDirectoryInformation, // 50 - FileIsRemoteDeviceInformation, // 51 - FileAttributeCacheInformation, // 52 - FileNumaNodeInformation, // 53 - FileStandardLinkInformation, // 54 - FileRemoteProtocolInformation, // 55 - FileMaximumInformation, + try + { + if (buffer != IntPtr.Zero) + { + Reloaded.Memory.Sources.Memory.CurrentProcess.ReadRaw(buffer, out byte[] uniString, Length); + return Encoding.Unicode.GetString(uniString); + } + + return ""; + } + catch { return ""; } } + } - [SuppressUnmanagedCodeSecurity] - [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)] - public static extern IntPtr LoadLibraryW([MarshalAs(UnmanagedType.LPWStr)] string lpFileName); - [SuppressUnmanagedCodeSecurity] - [DllImport("kernel32.dll", CharSet = CharSet.Ansi, ExactSpelling = true, SetLastError = true)] - public static extern IntPtr GetProcAddress(IntPtr hModule, string procName); + /// + /// Enumeration of the various file information classes. + /// See wdm.h. + /// + public enum FileInformationClass + { + None = 0, + FileDirectoryInformation = 1, + FileFullDirectoryInformation, // 2 + FileBothDirectoryInformation, // 3 + FileBasicInformation, // 4 + FileStandardInformation, // 5 + FileInternalInformation, // 6 + FileEaInformation, // 7 + FileAccessInformation, // 8 + FileNameInformation, // 9 + FileRenameInformation, // 10 + FileLinkInformation, // 11 + FileNamesInformation, // 12 + FileDispositionInformation, // 13 + FilePositionInformation, // 14 + FileFullEaInformation, // 15 + FileModeInformation, // 16 + FileAlignmentInformation, // 17 + FileAllInformation, // 18 + FileAllocationInformation, // 19 + FileEndOfFileInformation, // 20 + FileAlternateNameInformation, // 21 + FileStreamInformation, // 22 + FilePipeInformation, // 23 + FilePipeLocalInformation, // 24 + FilePipeRemoteInformation, // 25 + FileMailslotQueryInformation, // 26 + FileMailslotSetInformation, // 27 + FileCompressionInformation, // 28 + FileObjectIdInformation, // 29 + FileCompletionInformation, // 30 + FileMoveClusterInformation, // 31 + FileQuotaInformation, // 32 + FileReparsePointInformation, // 33 + FileNetworkOpenInformation, // 34 + FileAttributeTagInformation, // 35 + FileTrackingInformation, // 36 + FileIdBothDirectoryInformation, // 37 + FileIdFullDirectoryInformation, // 38 + FileValidDataLengthInformation, // 39 + FileShortNameInformation, // 40 + FileIoCompletionNotificationInformation, // 41 + FileIoStatusBlockRangeInformation, // 42 + FileIoPriorityHintInformation, // 43 + FileSfioReserveInformation, // 44 + FileSfioVolumeInformation, // 45 + FileHardLinkInformation, // 46 + FileProcessIdsUsingFileInformation, // 47 + FileNormalizedNameInformation, // 48 + FileNetworkPhysicalNameInformation, // 49 + FileIdGlobalTxDirectoryInformation, // 50 + FileIsRemoteDeviceInformation, // 51 + FileAttributeCacheInformation, // 52 + FileNumaNodeInformation, // 53 + FileStandardLinkInformation, // 54 + FileRemoteProtocolInformation, // 55 + FileMaximumInformation, } -} + + [SuppressUnmanagedCodeSecurity] + [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)] + public static extern IntPtr LoadLibraryW([MarshalAs(UnmanagedType.LPWStr)] string lpFileName); + + [SuppressUnmanagedCodeSecurity] + [DllImport("kernel32.dll", CharSet = CharSet.Ansi, ExactSpelling = true, SetLastError = true)] + public static extern IntPtr GetProcAddress(IntPtr hModule, string procName); +} \ No newline at end of file diff --git a/SonicHeroes.Utils.OneRedirector/One/VirtualFile.cs b/SonicHeroes.Utils.OneRedirector/One/VirtualFile.cs index fae3c56..b251f13 100644 --- a/SonicHeroes.Utils.OneRedirector/One/VirtualFile.cs +++ b/SonicHeroes.Utils.OneRedirector/One/VirtualFile.cs @@ -1,41 +1,38 @@ -using System; -using System.IO; -using System.Runtime.InteropServices; +using System.IO; using Heroes.SDK; -namespace SonicHeroes.Utils.OneRedirector.One +namespace SonicHeroes.Utils.OneRedirector.One; + +public class VirtualFile { - public class VirtualFile - { - /// - /// Path to the file on the hard disk. - /// - public string FilePath { get; private set; } + /// + /// Path to the file on the hard disk. + /// + public string FilePath { get; private set; } - /// - /// True if the file is compressed, else false. - /// - public bool IsCompressed { get; private set; } + /// + /// True if the file is compressed, else false. + /// + public bool IsCompressed { get; private set; } - public VirtualFile(string filePath, bool isCompressed) - { - FilePath = filePath; - IsCompressed = isCompressed; - } + public VirtualFile(string filePath, bool isCompressed) + { + FilePath = filePath; + IsCompressed = isCompressed; + } - /// - /// Reads the file from the hard disk. - /// - public byte[] GetData() => File.ReadAllBytes(FilePath); + /// + /// Reads the file from the hard disk. + /// + public byte[] GetData() => File.ReadAllBytes(FilePath); - /// - /// Reads the file from the hard disk and compresses it if necessary. - /// - /// Size of the search buffer used for compression, between 1 - 8191 - public byte[] GetDataCompressed(int searchBufferSize = 256) - { - var data = GetData(); - return !IsCompressed ? SDK.Prs.Compress(data, searchBufferSize) : data; - } + /// + /// Reads the file from the hard disk and compresses it if necessary. + /// + /// Size of the search buffer used for compression, between 1 - 8191 + public byte[] GetDataCompressed(int searchBufferSize = 256) + { + var data = GetData(); + return !IsCompressed ? SDK.Prs.Compress(data, searchBufferSize) : data; } -} +} \ No newline at end of file diff --git a/SonicHeroes.Utils.OneRedirector/One/VirtualOne.cs b/SonicHeroes.Utils.OneRedirector/One/VirtualOne.cs index 794750a..197fe5b 100644 --- a/SonicHeroes.Utils.OneRedirector/One/VirtualOne.cs +++ b/SonicHeroes.Utils.OneRedirector/One/VirtualOne.cs @@ -1,45 +1,41 @@ using System; -using System.Collections.Generic; using System.Runtime.InteropServices; -using SonicHeroes.Utils.OneRedirector.Structs; -using static SonicHeroes.Utils.OneRedirector.Structs.Utilities; -namespace SonicHeroes.Utils.OneRedirector.One +namespace SonicHeroes.Utils.OneRedirector.One; + +public unsafe class VirtualOne : IDisposable { - public unsafe class VirtualOne : IDisposable - { - /// - /// Contains the entire ONE file. - /// - public byte[] File { get; private set; } + /// + /// Contains the entire ONE file. + /// + public byte[] File { get; private set; } - /// - /// A pointer to the ONE file. - /// - public byte* FilePtr { get; private set; } + /// + /// A pointer to the ONE file. + /// + public byte* FilePtr { get; private set; } - private GCHandle? _virtualOneHandle; + private readonly GCHandle? _virtualOneHandle; - /// - /// Creates a Virtual ONE given the name of the file and the header of an ONE file. - /// - /// The bytes corresponding to the new ONE header. - public VirtualOne(byte[] oneFile) - { - File = oneFile; - _virtualOneHandle = GCHandle.Alloc(File, GCHandleType.Pinned); - FilePtr = (byte*) _virtualOneHandle.Value.AddrOfPinnedObject(); - } + /// + /// Creates a Virtual ONE given the name of the file and the header of an ONE file. + /// + /// The bytes corresponding to the new ONE header. + public VirtualOne(byte[] oneFile) + { + File = oneFile; + _virtualOneHandle = GCHandle.Alloc(File, GCHandleType.Pinned); + FilePtr = (byte*) _virtualOneHandle.Value.AddrOfPinnedObject(); + } - ~VirtualOne() - { - Dispose(); - } + ~VirtualOne() + { + Dispose(); + } - public void Dispose() - { - _virtualOneHandle?.Free(); - GC.SuppressFinalize(this); - } + public void Dispose() + { + _virtualOneHandle?.Free(); + GC.SuppressFinalize(this); } -} +} \ No newline at end of file diff --git a/SonicHeroes.Utils.OneRedirector/One/VirtualOneBuilder.cs b/SonicHeroes.Utils.OneRedirector/One/VirtualOneBuilder.cs index c926405..a0f8c78 100644 --- a/SonicHeroes.Utils.OneRedirector/One/VirtualOneBuilder.cs +++ b/SonicHeroes.Utils.OneRedirector/One/VirtualOneBuilder.cs @@ -4,73 +4,70 @@ using System.Linq; using Heroes.SDK.Definitions.Structures.Archive.OneFile.Custom; using Heroes.SDK.Parsers; -using Reloaded.Memory; -using SonicHeroes.Utils.OneRedirector.Structs; using static SonicHeroes.Utils.OneRedirector.Program; -namespace SonicHeroes.Utils.OneRedirector.One +namespace SonicHeroes.Utils.OneRedirector.One; + +/// +/// Stores the information required to build a "Virtual ONE" file. +/// +public unsafe class VirtualOneBuilder { + private static bool _warningGiven = false; + + private readonly Dictionary _customFiles = new(); + private readonly HashSet _deletedFiles = new(); + /// - /// Stores the information required to build a "Virtual ONE" file. + /// Adds a file to the Virtual ONE builder. /// - public unsafe class VirtualOneBuilder + public void AddOrReplaceFile(string fileName, string filePath) { - private static bool _warningGiven = false; + bool isCompressed = fileName.EndsWith(Constants.PrsExtension); + if (isCompressed) + fileName = fileName.Substring(0, fileName.Length - Constants.PrsExtension.Length); - private Dictionary _customFiles = new Dictionary(); - private HashSet _deletedFiles = new HashSet(); - - /// - /// Adds a file to the Virtual ONE builder. - /// - public void AddOrReplaceFile(string fileName, string filePath) +#if RELEASE + if (!isCompressed && !_warningGiven) { - bool isCompressed = fileName.EndsWith(Constants.PrsExtension); - if (isCompressed) - fileName = fileName.Substring(0, fileName.Length - Constants.PrsExtension.Length); - - #if RELEASE - if (!isCompressed && !_warningGiven) - { - Logger.WriteLine($"[ONE Redirector] Uncompressed file(s) found. This is only meant to be used for testing/creating mods. " + - $"If you are an end user, who downloaded this mod from the internet, please ask the mod author to compress their files. " + - $"Here's a file path to the offending file: {filePath}'", Logger.ColorYellowLight); - _warningGiven = true; - } - #endif - - _customFiles[fileName] = new VirtualFile(filePath, isCompressed); + Logger.WriteLine($"[ONE Redirector] Uncompressed file(s) found. This is only meant to be used for testing/creating mods. " + + $"If you are an end user, who downloaded this mod from the internet, please ask the mod author to compress their files. " + + $"Here's a file path to the offending file: {filePath}'", Logger.ColorYellowLight); + _warningGiven = true; } +#endif - /// - /// Removes a file from the resulting ONE builder. - /// - public void RemoveFile(string fileName) - { - if (fileName.EndsWith(Constants.DeleteExtension)) - fileName = fileName.Substring(0, fileName.Length - Constants.DeleteExtension.Length); + _customFiles[fileName] = new VirtualFile(filePath, isCompressed); + } - _deletedFiles.Add(fileName); - } + /// + /// Removes a file from the resulting ONE builder. + /// + public void RemoveFile(string fileName) + { + if (fileName.EndsWith(Constants.DeleteExtension)) + fileName = fileName.Substring(0, fileName.Length - Constants.DeleteExtension.Length); - /// - /// Builds a virtual ONE based upon a supplied base ONE file. - /// - public VirtualOne Build(string filePath) - { - var originalFile = new OneArchive(File.ReadAllBytes(filePath)); - var filesDictionary = new Dictionary(StringComparer.OrdinalIgnoreCase); - foreach (var file in originalFile.GetFiles()) - filesDictionary[file.Name] = file; + _deletedFiles.Add(fileName); + } - foreach (var deleteFile in _deletedFiles) - filesDictionary.Remove(deleteFile); + /// + /// Builds a virtual ONE based upon a supplied base ONE file. + /// + public VirtualOne Build(string filePath) + { + var originalFile = new OneArchive(File.ReadAllBytes(filePath)); + var filesDictionary = new Dictionary(StringComparer.OrdinalIgnoreCase); + foreach (var file in originalFile.GetFiles()) + filesDictionary[file.Name] = file; - foreach (var customFile in _customFiles) - filesDictionary[customFile.Key] = new ManagedOneFile(customFile.Key, customFile.Value.GetDataCompressed(), true, originalFile.Header->RenderWareVersion); + foreach (var deleteFile in _deletedFiles) + filesDictionary.Remove(deleteFile); - var files = filesDictionary.Values.ToArray(); - return new VirtualOne(OneArchive.FromFiles(files)); - } + foreach (var customFile in _customFiles) + filesDictionary[customFile.Key] = new ManagedOneFile(customFile.Key, customFile.Value.GetDataCompressed(), true, originalFile.Header->RenderWareVersion); + + var files = filesDictionary.Values.ToArray(); + return new VirtualOne(OneArchive.FromFiles(files)); } -} +} \ No newline at end of file diff --git a/SonicHeroes.Utils.OneRedirector/OneBuilderCollection.cs b/SonicHeroes.Utils.OneRedirector/OneBuilderCollection.cs index 1d9d11f..07333c7 100644 --- a/SonicHeroes.Utils.OneRedirector/OneBuilderCollection.cs +++ b/SonicHeroes.Utils.OneRedirector/OneBuilderCollection.cs @@ -4,66 +4,65 @@ using System.Linq; using SonicHeroes.Utils.OneRedirector.One; -namespace SonicHeroes.Utils.OneRedirector +namespace SonicHeroes.Utils.OneRedirector; + +/// +/// Encapsulates a group of Builders, keyed by file names. +/// +public class OneBuilderCollection { + private readonly Dictionary _builders = new(StringComparer.OrdinalIgnoreCase); + /// - /// Encapsulates a group of Builders, keyed by file names. + /// Returns all of the builders held by this collection, mapped string to builder. /// - public class OneBuilderCollection - { - private Dictionary _builders = new Dictionary(StringComparer.OrdinalIgnoreCase); + /// + public KeyValuePair[] GetAllBuilders() => _builders.ToArray(); - /// - /// Returns all of the builders held by this collection, mapped string to builder. - /// - /// - public KeyValuePair[] GetAllBuilders() => _builders.ToArray(); - - /// - /// Adds files to the collection of builders given a directory containing directories - /// corresponding to ONE archives. - /// - /// Folder containing folders named after ONE archives. - public void AddFromFolders(string folder) + /// + /// Adds files to the collection of builders given a directory containing directories + /// corresponding to ONE archives. + /// + /// Folder containing folders named after ONE archives. + public void AddFromFolders(string folder) + { + foreach (var directory in Directory.GetDirectories(folder)) { - foreach (var directory in Directory.GetDirectories(folder)) - { - if (!directory.EndsWith(Constants.OneExtension, StringComparison.OrdinalIgnoreCase)) - continue; + if (!directory.EndsWith(Constants.OneExtension, StringComparison.OrdinalIgnoreCase)) + continue; - var builder = GetBuilder(Path.GetFileName(directory)); - foreach (var filePath in Directory.GetFiles(directory)) - { - var fileName = Path.GetFileName(filePath); - if (fileName.EndsWith(Constants.DeleteExtension)) - builder.RemoveFile(fileName); - else - builder.AddOrReplaceFile(fileName, filePath); - } + var builder = GetBuilder(Path.GetFileName(directory)); + foreach (var filePath in Directory.GetFiles(directory)) + { + var fileName = Path.GetFileName(filePath); + if (fileName.EndsWith(Constants.DeleteExtension)) + builder.RemoveFile(fileName); + else + builder.AddOrReplaceFile(fileName, filePath); } } + } - /// - /// Tries to get a for the given file name. - /// Returns true on success, false on failure. - /// - /// The file name of the ONE, including extension. Case insensitive. - public bool TryGetBuilder(string fileName, out VirtualOneBuilder builder) - { - builder = _builders.ContainsKey(fileName) ? _builders[fileName] : null; - return builder != null; - } + /// + /// Tries to get a for the given file name. + /// Returns true on success, false on failure. + /// + /// The file name of the ONE, including extension. Case insensitive. + public bool TryGetBuilder(string fileName, out VirtualOneBuilder builder) + { + builder = _builders.ContainsKey(fileName) ? _builders[fileName] : null; + return builder != null; + } - /// - /// Gets a for the given name or creates one if one does not exist. - /// - /// The file name of the ONE, including extension. Case insensitive. - public VirtualOneBuilder GetBuilder(string fileName) - { - if (!_builders.ContainsKey(fileName)) - _builders[fileName] = new VirtualOneBuilder(); + /// + /// Gets a for the given name or creates one if one does not exist. + /// + /// The file name of the ONE, including extension. Case insensitive. + public VirtualOneBuilder GetBuilder(string fileName) + { + if (!_builders.ContainsKey(fileName)) + _builders[fileName] = new VirtualOneBuilder(); - return _builders[fileName]; - } + return _builders[fileName]; } -} +} \ No newline at end of file diff --git a/SonicHeroes.Utils.OneRedirector/OneFileTracker.cs b/SonicHeroes.Utils.OneRedirector/OneFileTracker.cs index 02e0579..d3a910f 100644 --- a/SonicHeroes.Utils.OneRedirector/OneFileTracker.cs +++ b/SonicHeroes.Utils.OneRedirector/OneFileTracker.cs @@ -8,245 +8,240 @@ using SonicHeroes.Utils.OneRedirector.Structs; using FileInfo = SonicHeroes.Utils.OneRedirector.Structs.FileInfo; -namespace SonicHeroes.Utils.OneRedirector +namespace SonicHeroes.Utils.OneRedirector; + +public unsafe class OneFileTracker { - public unsafe class OneFileTracker + /// + /// Executed when a handle to a file is opened. + /// + public event HandleOpened OnOneHandleOpened = (path, handle) => { }; + + /// + /// Executed when application queries for the file size + /// + public event GetFileSize OnGetFileSize = (handle) => -1; + + /// + /// Executed after data is read from a file. + /// + public event DataRead OnOneReadData = (IntPtr handle, byte* buffer, uint length, long offset, out int numReadBytes) => { - /// - /// Executed when a handle to a file is opened. - /// - public event HandleOpened OnOneHandleOpened = (path, handle) => { }; - - /// - /// Executed when application queries for the file size - /// - public event GetFileSize OnGetFileSize = (handle) => -1; - - /// - /// Executed after data is read from a file. - /// - public event DataRead OnOneReadData = (IntPtr handle, byte* buffer, uint length, long offset, out int numReadBytes) => - { - numReadBytes = 0; - return false; - }; - - /// - /// Maps file handles to file paths. - /// - private ConcurrentDictionary _handleToInfoMap = new ConcurrentDictionary(); - private Dictionary _isOneFileCache = new Dictionary(StringComparer.OrdinalIgnoreCase); - - private IHook _createFileHook; - private IHook _readFileHook; - private IHook _setFilePointerHook; - private IHook _getFileSizeHook; - - private object _createLock = new object(); - private object _readLock = new object(); - private object _setInfoLock = new object(); - private object _getInfoLock = new object(); - - public OneFileTracker(NativeFunctions functions) - { - _createFileHook = functions.NtCreateFile.Hook(NtCreateFileImpl).Activate(); - _readFileHook = functions.NtReadFile.Hook(NtReadFileImpl).Activate(); - _setFilePointerHook = functions.NtSetInformationFile.Hook(SetInformationFileImpl).Activate(); - _getFileSizeHook = functions.NtQueryInformationFile.Hook(QueryInformationFileImpl).Activate(); - - // TODO: Hook NtClose - // Problem: Native->Managed Transition hits NtClose in .NET Core, so our hook code is never hit. - // Problem: NtClose needs synchronization. - // Solution: Write custom ASM to solve the problem, see NtClose branch. - } + numReadBytes = 0; + return false; + }; + + /// + /// Maps file handles to file paths. + /// + private readonly ConcurrentDictionary _handleToInfoMap = new(); + private readonly Dictionary _isOneFileCache = new(StringComparer.OrdinalIgnoreCase); + + private readonly IHook _createFileHook; + private readonly IHook _readFileHook; + private readonly IHook _setFilePointerHook; + private readonly IHook _getFileSizeHook; + + private readonly object _createLock = new(); + private readonly object _readLock = new(); + private readonly object _setInfoLock = new(); + private readonly object _getInfoLock = new(); + + public OneFileTracker(NativeFunctions functions) + { + _createFileHook = functions.NtCreateFile.Hook(NtCreateFileImpl).Activate(); + _readFileHook = functions.NtReadFile.Hook(NtReadFileImpl).Activate(); + _setFilePointerHook = functions.NtSetInformationFile.Hook(SetInformationFileImpl).Activate(); + _getFileSizeHook = functions.NtQueryInformationFile.Hook(QueryInformationFileImpl).Activate(); + + // TODO: Hook NtClose + // Problem: Native->Managed Transition hits NtClose in .NET Core, so our hook code is never hit. + // Problem: NtClose needs synchronization. + // Solution: Write custom ASM to solve the problem, see NtClose branch. + } - /// - /// Tries to get the information for a file behind a handle. - /// - public bool TryGetInfoForHandle(IntPtr handle, out FileInfo info) - { - info = null; + /// + /// Tries to get the information for a file behind a handle. + /// + public bool TryGetInfoForHandle(IntPtr handle, out FileInfo info) + { + info = null; - if (!_handleToInfoMap.ContainsKey(handle)) - return false; + if (!_handleToInfoMap.ContainsKey(handle)) + return false; - info = _handleToInfoMap[handle]; - return true; - } + info = _handleToInfoMap[handle]; + return true; + } - private int QueryInformationFileImpl(IntPtr hfile, out Native.Native.IO_STATUS_BLOCK ioStatusBlock, void* fileInformation, uint length, Native.Native.FileInformationClass fileInformationClass) + private int QueryInformationFileImpl(IntPtr hfile, out Native.Native.IO_STATUS_BLOCK ioStatusBlock, void* fileInformation, uint length, Native.Native.FileInformationClass fileInformationClass) + { + lock (_getInfoLock) { - lock (_getInfoLock) + if (_handleToInfoMap.ContainsKey(hfile) && fileInformationClass == Native.Native.FileInformationClass.FileStandardInformation) { - if (_handleToInfoMap.ContainsKey(hfile) && fileInformationClass == Native.Native.FileInformationClass.FileStandardInformation) + var result = _getFileSizeHook.OriginalFunction(hfile, out ioStatusBlock, fileInformation, length, fileInformationClass); + var information = (Native.Native.FILE_STANDARD_INFORMATION*)fileInformation; + var newFileSize = OnGetFileSize(hfile); + if (newFileSize != -1) { - var result = _getFileSizeHook.OriginalFunction(hfile, out ioStatusBlock, fileInformation, length, fileInformationClass); - var information = (Native.Native.FILE_STANDARD_INFORMATION*)fileInformation; - var newFileSize = OnGetFileSize(hfile); - if (newFileSize != -1) - { - information->EndOfFile = newFileSize; - information->AllocationSize = Utilities.RoundUp(newFileSize, 4096); - } + information->EndOfFile = newFileSize; + information->AllocationSize = Utilities.RoundUp(newFileSize, 4096); + } #if DEBUG - Console.WriteLine($"[ONEHook] QueryInformationFile: Alloc Size: {information->AllocationSize}, EndOfFile: {information->EndOfFile}"); + Console.WriteLine($"[ONEHook] QueryInformationFile: Alloc Size: {information->AllocationSize}, EndOfFile: {information->EndOfFile}"); #endif - return result; - } - - return _getFileSizeHook.OriginalFunction(hfile, out ioStatusBlock, fileInformation, length, fileInformationClass); + return result; } + + return _getFileSizeHook.OriginalFunction(hfile, out ioStatusBlock, fileInformation, length, fileInformationClass); } + } - private int SetInformationFileImpl(IntPtr hfile, out Native.Native.IO_STATUS_BLOCK ioStatusBlock, void* fileInformation, uint length, Native.Native.FileInformationClass fileInformationClass) + private int SetInformationFileImpl(IntPtr hfile, out Native.Native.IO_STATUS_BLOCK ioStatusBlock, void* fileInformation, uint length, Native.Native.FileInformationClass fileInformationClass) + { + lock (_setInfoLock) { - lock (_setInfoLock) + if (_handleToInfoMap.ContainsKey(hfile) && fileInformationClass == Native.Native.FileInformationClass.FilePositionInformation) { - if (_handleToInfoMap.ContainsKey(hfile) && fileInformationClass == Native.Native.FileInformationClass.FilePositionInformation) - { - var pointer = *(long*)fileInformation; - _handleToInfoMap[hfile].FilePointer = pointer; - } + var pointer = *(long*)fileInformation; + _handleToInfoMap[hfile].FilePointer = pointer; + } - return _setFilePointerHook.OriginalFunction(hfile, out ioStatusBlock, fileInformation, length, fileInformationClass); + return _setFilePointerHook.OriginalFunction(hfile, out ioStatusBlock, fileInformation, length, fileInformationClass); - } } + } - private unsafe int NtReadFileImpl(IntPtr handle, IntPtr hEvent, IntPtr* apcRoutine, IntPtr* apcContext, ref Native.Native.IO_STATUS_BLOCK ioStatus, byte* buffer, uint length, long* byteOffset, IntPtr key) + private unsafe int NtReadFileImpl(IntPtr handle, IntPtr hEvent, IntPtr* apcRoutine, IntPtr* apcContext, ref Native.Native.IO_STATUS_BLOCK ioStatus, byte* buffer, uint length, long* byteOffset, IntPtr key) + { + lock (_readLock) { - lock (_readLock) + if (_handleToInfoMap.ContainsKey(handle)) { - if (_handleToInfoMap.ContainsKey(handle)) - { - long offset = _handleToInfoMap[handle].FilePointer; - long requestedOffset = byteOffset != (void*) 0 ? *byteOffset : -1; + long offset = _handleToInfoMap[handle].FilePointer; + long requestedOffset = byteOffset != (void*) 0 ? *byteOffset : -1; #if DEBUG - Console.WriteLine($"[ONEHook] Read Request, Buffer: {(long)buffer:X}, Length: {length}, Offset (via SetInformationFile): {offset}, Requested Offset (Optional): {requestedOffset}"); + Console.WriteLine($"[ONEHook] Read Request, Buffer: {(long)buffer:X}, Length: {length}, Offset (via SetInformationFile): {offset}, Requested Offset (Optional): {requestedOffset}"); #endif - DisableRedirectionHooks(); - bool result; - int numReadBytes; - result = requestedOffset != -1 ? OnOneReadData(handle, buffer, length, requestedOffset, out numReadBytes) - : OnOneReadData(handle, buffer, length, offset, out numReadBytes); - EnableRedirectionHooks(); - - if (result) - { - offset += length; - SetInformationFileImpl(handle, out _, &offset, sizeof(long), Native.Native.FileInformationClass.FilePositionInformation); - - // Set number of read bytes. - ioStatus.Status = 0; - ioStatus.Information = new IntPtr(numReadBytes); - return 0; - } - } + DisableRedirectionHooks(); + bool result; + int numReadBytes; + result = requestedOffset != -1 ? OnOneReadData(handle, buffer, length, requestedOffset, out numReadBytes) + : OnOneReadData(handle, buffer, length, offset, out numReadBytes); + EnableRedirectionHooks(); + + if (result) + { + offset += length; + SetInformationFileImpl(handle, out _, &offset, sizeof(long), Native.Native.FileInformationClass.FilePositionInformation); - return _readFileHook.OriginalFunction(handle, hEvent, apcRoutine, apcContext, ref ioStatus, buffer, length, byteOffset, key); + // Set number of read bytes. + ioStatus.Status = 0; + ioStatus.Information = new IntPtr(numReadBytes); + return 0; + } } + + return _readFileHook.OriginalFunction(handle, hEvent, apcRoutine, apcContext, ref ioStatus, buffer, length, byteOffset, key); } + } - private int NtCreateFileImpl(out IntPtr handle, FileAccess access, ref Native.Native.OBJECT_ATTRIBUTES objectAttributes, ref Native.Native.IO_STATUS_BLOCK ioStatus, ref long allocSize, uint fileAttributes, FileShare share, uint createDisposition, uint createOptions, IntPtr eaBuffer, uint eaLength) + private int NtCreateFileImpl(out IntPtr handle, FileAccess access, ref Native.Native.OBJECT_ATTRIBUTES objectAttributes, ref Native.Native.IO_STATUS_BLOCK ioStatus, ref long allocSize, uint fileAttributes, FileShare share, uint createDisposition, uint createOptions, IntPtr eaBuffer, uint eaLength) + { + lock (_createLock) { - lock (_createLock) - { - string oldFileName = objectAttributes.ObjectName.ToString(); - if (!TryGetFullPath(oldFileName, out var newFilePath)) - return _createFileHook.OriginalFunction(out handle, access, ref objectAttributes, ref ioStatus, ref allocSize, fileAttributes, share, createDisposition, createOptions, eaBuffer, eaLength); + string oldFileName = objectAttributes.ObjectName.ToString(); + if (!TryGetFullPath(oldFileName, out var newFilePath)) + return _createFileHook.OriginalFunction(out handle, access, ref objectAttributes, ref ioStatus, ref allocSize, fileAttributes, share, createDisposition, createOptions, eaBuffer, eaLength); - // Check if ONE file and register if it is. - if (newFilePath.Contains(Constants.OneExtension, StringComparison.OrdinalIgnoreCase)) + // Check if ONE file and register if it is. + if (newFilePath.Contains(Constants.OneExtension, StringComparison.OrdinalIgnoreCase)) + { + var result = _createFileHook.OriginalFunction(out handle, access, ref objectAttributes, ref ioStatus, ref allocSize, fileAttributes, share, createDisposition, createOptions, eaBuffer, eaLength); + DisableRedirectionHooks(); + if (IsOneFile(newFilePath)) { - var result = _createFileHook.OriginalFunction(out handle, access, ref objectAttributes, ref ioStatus, ref allocSize, fileAttributes, share, createDisposition, createOptions, eaBuffer, eaLength); - DisableRedirectionHooks(); - if (IsOneFile(newFilePath)) - { #if DEBUG - Console.WriteLine($"[ONEHook] ONE Handle Opened: {handle}, File: {newFilePath}"); + Console.WriteLine($"[ONEHook] ONE Handle Opened: {handle}, File: {newFilePath}"); #endif - _handleToInfoMap[handle] = new FileInfo(newFilePath, 0); - OnOneHandleOpened(handle, newFilePath); - } - EnableRedirectionHooks(); - return result; + _handleToInfoMap[handle] = new FileInfo(newFilePath, 0); + OnOneHandleOpened(handle, newFilePath); } + EnableRedirectionHooks(); + return result; + } - var ntStatus = _createFileHook.OriginalFunction(out handle, access, ref objectAttributes, ref ioStatus, ref allocSize, fileAttributes, share, createDisposition, createOptions, eaBuffer, eaLength); + var ntStatus = _createFileHook.OriginalFunction(out handle, access, ref objectAttributes, ref ioStatus, ref allocSize, fileAttributes, share, createDisposition, createOptions, eaBuffer, eaLength); - // Invalidate Duplicate Handles (until we implement NtClose hook). - if (_handleToInfoMap.ContainsKey(handle)) - { - _handleToInfoMap.TryRemove(handle, out var value); + // Invalidate Duplicate Handles (until we implement NtClose hook). + if (_handleToInfoMap.ContainsKey(handle)) + { + _handleToInfoMap.TryRemove(handle, out var value); #if DEBUG - Console.WriteLine($"[ONEHook] Removed old disposed handle: {handle}, File: {value.FilePath}"); + Console.WriteLine($"[ONEHook] Removed old disposed handle: {handle}, File: {value.FilePath}"); #endif - } - - return ntStatus; } - } - - private void DisableRedirectionHooks() - { - _createFileHook.Disable(); - _readFileHook.Disable(); - } - private void EnableRedirectionHooks() - { - _readFileHook.Enable(); - _createFileHook.Enable(); + return ntStatus; } + } - /// - /// Checks if a file at a specified path is an ONE archive. - /// - private bool IsOneFile(string filePath) - { - if (_isOneFileCache.ContainsKey(filePath)) - return _isOneFileCache[filePath]; + private void DisableRedirectionHooks() + { + _createFileHook.Disable(); + _readFileHook.Disable(); + } - if (!File.Exists(filePath)) - return false; + private void EnableRedirectionHooks() + { + _readFileHook.Enable(); + _createFileHook.Enable(); + } - using FileStream stream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, sizeof(OneArchiveHeader)); + /// + /// Checks if a file at a specified path is an ONE archive. + /// + private bool IsOneFile(string filePath) + { + if (_isOneFileCache.ContainsKey(filePath)) + return _isOneFileCache[filePath]; - var data = new byte[sizeof(OneArchiveHeader)]; - var dataSpan = data.AsSpan(); - stream.Read(dataSpan); - Struct.FromArray(dataSpan, out OneArchiveHeader header); + if (!File.Exists(filePath)) + return false; - var sizeOfFile = stream.Seek(0, SeekOrigin.End); - bool isOneFile = header.FileSize + sizeof(OneArchiveHeader) == sizeOfFile; - _isOneFileCache[filePath] = isOneFile; - return isOneFile; - } + using FileStream stream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, sizeof(OneArchiveHeader)); + stream.TryReadSafe(out OneArchiveHeader header); - /// - /// Tries to resolve a given file path from NtCreateFile to a full file path. - /// - private bool TryGetFullPath(string oldFilePath, out string newFilePath) - { - if (oldFilePath.StartsWith("\\??\\", StringComparison.InvariantCultureIgnoreCase)) - oldFilePath = oldFilePath.Replace("\\??\\", ""); + var sizeOfFile = stream.Seek(0, SeekOrigin.End); + bool isOneFile = header.FileSize + sizeof(OneArchiveHeader) == sizeOfFile; + _isOneFileCache[filePath] = isOneFile; + return isOneFile; + } - if (!String.IsNullOrEmpty(oldFilePath)) - { - newFilePath = Path.GetFullPath(oldFilePath); - return true; - } + /// + /// Tries to resolve a given file path from NtCreateFile to a full file path. + /// + private bool TryGetFullPath(string oldFilePath, out string newFilePath) + { + if (oldFilePath.StartsWith("\\??\\", StringComparison.InvariantCultureIgnoreCase)) + oldFilePath = oldFilePath.Replace("\\??\\", ""); - newFilePath = oldFilePath; - return false; + if (!String.IsNullOrEmpty(oldFilePath)) + { + newFilePath = Path.GetFullPath(oldFilePath); + return true; } - public delegate int GetFileSize(IntPtr handle); - public delegate void HandleOpened(IntPtr handle, string filePath); - public delegate bool DataRead(IntPtr handle, byte* buffer, uint length, long offset, out int numReadBytes); + newFilePath = oldFilePath; + return false; } -} + + public delegate int GetFileSize(IntPtr handle); + public delegate void HandleOpened(IntPtr handle, string filePath); + public delegate bool DataRead(IntPtr handle, byte* buffer, uint length, long offset, out int numReadBytes); +} \ No newline at end of file diff --git a/SonicHeroes.Utils.OneRedirector/OneHook.cs b/SonicHeroes.Utils.OneRedirector/OneHook.cs index 0f0b4b3..7c252b0 100644 --- a/SonicHeroes.Utils.OneRedirector/OneHook.cs +++ b/SonicHeroes.Utils.OneRedirector/OneHook.cs @@ -7,123 +7,122 @@ using SonicHeroes.Utils.OneRedirector.One; using SonicHeroes.Utils.OneRedirector.Structs; -namespace SonicHeroes.Utils.OneRedirector +namespace SonicHeroes.Utils.OneRedirector; + +/// +/// FileSystem hook that redirects accesses to ONE file. +/// +public unsafe class OneHook { - /// - /// FileSystem hook that redirects accesses to ONE file. - /// - public unsafe class OneHook - { - private OneFileTracker _oneFileTracker; + private readonly OneFileTracker _oneFileTracker; - private ILogger _logger; - private Config _configuration; - private OneBuilderCollection _builderCollection; - private Dictionary _virtualOneFiles = new Dictionary(StringComparer.OrdinalIgnoreCase); - private HashSet _modFolders = new HashSet(); + private readonly ILogger _logger; + private Config _configuration; + private OneBuilderCollection _builderCollection; + private readonly Dictionary _virtualOneFiles = new(StringComparer.OrdinalIgnoreCase); + private readonly HashSet _modFolders = new(); - public OneHook(ILogger logger, Config configuration, NativeFunctions functions) - { - _logger = logger; - _configuration = configuration; - _configuration.ConfigurationUpdated += OnConfigurationUpdated; - _oneFileTracker = new OneFileTracker(functions); - _oneFileTracker.OnOneHandleOpened += OnOneHandleOpened; - _oneFileTracker.OnOneReadData += OnOneReadData; - _oneFileTracker.OnGetFileSize += OnGetFileSize; - } + public OneHook(ILogger logger, Config configuration, NativeFunctions functions) + { + _logger = logger; + _configuration = configuration; + _configuration.ConfigurationUpdated += OnConfigurationUpdated; + _oneFileTracker = new OneFileTracker(functions); + _oneFileTracker.OnOneHandleOpened += OnOneHandleOpened; + _oneFileTracker.OnOneReadData += OnOneReadData; + _oneFileTracker.OnGetFileSize += OnGetFileSize; + } - private void OnConfigurationUpdated(IConfigurable obj) - { - _configuration = (Config)obj; - _logger.WriteLine($"[ONE Redirector] Config Updated"); - } + private void OnConfigurationUpdated(IConfigurable obj) + { + _configuration = (Config)obj; + _logger.WriteLine($"[ONE Redirector] Config Updated"); + } - private int OnGetFileSize(IntPtr handle) - { - if (!_oneFileTracker.TryGetInfoForHandle(handle, out var info)) - return -1; + private int OnGetFileSize(IntPtr handle) + { + if (!_oneFileTracker.TryGetInfoForHandle(handle, out var info)) + return -1; - if (_virtualOneFiles.ContainsKey(info.FilePath)) - return _virtualOneFiles[info.FilePath].File.Length; + if (_virtualOneFiles.ContainsKey(info.FilePath)) + return _virtualOneFiles[info.FilePath].File.Length; - return -1; - } + return -1; + } - /// - /// The evil one. Commits hard drive reading fraud! - /// - private bool OnOneReadData(IntPtr handle, byte* buffer, uint length, long offset, out int numReadBytes) + /// + /// The evil one. Commits hard drive reading fraud! + /// + private bool OnOneReadData(IntPtr handle, byte* buffer, uint length, long offset, out int numReadBytes) + { + if (_oneFileTracker.TryGetInfoForHandle(handle, out var info)) { - if (_oneFileTracker.TryGetInfoForHandle(handle, out var info)) + if (!_virtualOneFiles.ContainsKey(info.FilePath)) { - if (!_virtualOneFiles.ContainsKey(info.FilePath)) - { - numReadBytes = 0; - return false; - } + numReadBytes = 0; + return false; + } - var oneFile = _virtualOneFiles[info.FilePath]; - var bufferSpan = new Span(buffer, (int)length); - var oneFileSpan = new Span(oneFile.FilePtr, oneFile.File.Length); + var oneFile = _virtualOneFiles[info.FilePath]; + var bufferSpan = new Span(buffer, (int)length); + var oneFileSpan = new Span(oneFile.FilePtr, oneFile.File.Length); - var endOfReadOffset = offset + length; - if (endOfReadOffset > oneFileSpan.Length) - length -= (uint)(endOfReadOffset - oneFileSpan.Length); + var endOfReadOffset = offset + length; + if (endOfReadOffset > oneFileSpan.Length) + length -= (uint)(endOfReadOffset - oneFileSpan.Length); - var slice = oneFileSpan.Slice((int)offset, (int)length); - slice.CopyTo(bufferSpan); + var slice = oneFileSpan.Slice((int)offset, (int)length); + slice.CopyTo(bufferSpan); - numReadBytes = slice.Length; - return true; - } - - numReadBytes = 0; - return false; + numReadBytes = slice.Length; + return true; } - /// - /// When an ONE file is found, associate it with an existing virtual file. - /// - private void OnOneHandleOpened(IntPtr handle, string filepath) - { - if (!_configuration.AlwaysBuildArchive && _virtualOneFiles.ContainsKey(filepath)) - return; + numReadBytes = 0; + return false; + } - // Note: This is a bit inefficient, but necessary for `Always Build Archive` - // allowing modifications without restarting game. - _builderCollection = new OneBuilderCollection(); - foreach (var modFolder in _modFolders) - _builderCollection.AddFromFolders(GetRedirectPath(modFolder)); + /// + /// When an ONE file is found, associate it with an existing virtual file. + /// + private void OnOneHandleOpened(IntPtr handle, string filepath) + { + if (!_configuration.AlwaysBuildArchive && _virtualOneFiles.ContainsKey(filepath)) + return; - string fileName = Path.GetFileName(filepath); - if (_builderCollection.TryGetBuilder(fileName, out var builder)) - { - #if DEBUG - Console.WriteLine("------------ BUILDING ONE ------------"); - Stopwatch watch = new Stopwatch(); - watch.Start(); - #endif - _virtualOneFiles[filepath] = builder.Build(filepath); - #if DEBUG - Console.WriteLine($"------------ COMPLETE {watch.ElapsedMilliseconds}ms ------------"); - #endif - } - } + // Note: This is a bit inefficient, but necessary for `Always Build Archive` + // allowing modifications without restarting game. + _builderCollection = new OneBuilderCollection(); + foreach (var modFolder in _modFolders) + _builderCollection.AddFromFolders(GetRedirectPath(modFolder)); - /// The full path to the mod. - public void OnModLoading(string modDirectory) + string fileName = Path.GetFileName(filepath); + if (_builderCollection.TryGetBuilder(fileName, out var builder)) { - if (Directory.Exists(GetRedirectPath(modDirectory))) - _modFolders.Add(modDirectory); +#if DEBUG + Console.WriteLine("------------ BUILDING ONE ------------"); + Stopwatch watch = new Stopwatch(); + watch.Start(); +#endif + _virtualOneFiles[filepath] = builder.Build(filepath); +#if DEBUG + Console.WriteLine($"------------ COMPLETE {watch.ElapsedMilliseconds}ms ------------"); +#endif } + } - /// The full path to the mod. - public void OnModUnloading(string modDirectory) - { - _modFolders.Remove(modDirectory); - } + /// The full path to the mod. + public void OnModLoading(string modDirectory) + { + if (Directory.Exists(GetRedirectPath(modDirectory))) + _modFolders.Add(modDirectory); + } - private string GetRedirectPath(string modFolder) => $"{modFolder}/{Constants.RedirectorFolderName}"; + /// The full path to the mod. + public void OnModUnloading(string modDirectory) + { + _modFolders.Remove(modDirectory); } -} + + private string GetRedirectPath(string modFolder) => $"{modFolder}/{Constants.RedirectorFolderName}"; +} \ No newline at end of file diff --git a/SonicHeroes.Utils.OneRedirector/Program.cs b/SonicHeroes.Utils.OneRedirector/Program.cs index 6b9d141..2c52c95 100644 --- a/SonicHeroes.Utils.OneRedirector/Program.cs +++ b/SonicHeroes.Utils.OneRedirector/Program.cs @@ -1,5 +1,4 @@ using System; -using System.Diagnostics; using csharp_prs_interfaces; using Heroes.SDK; using Reloaded.Mod.Interfaces; @@ -8,44 +7,47 @@ using SonicHeroes.Utils.OneRedirector.Structs; using IReloadedHooks = Reloaded.Hooks.ReloadedII.Interfaces.IReloadedHooks; -namespace SonicHeroes.Utils.OneRedirector +namespace SonicHeroes.Utils.OneRedirector; + +public unsafe class Program : IMod { - public unsafe class Program : IMod + public static ILogger Logger { get; private set; } + + private IModLoader _modLoader; + private OneHook _oneHook; + + public void StartEx(IModLoaderV1 loader, IModConfigV1 config) { - public static ILogger Logger { get; private set; } - - private IModLoader _modLoader; - private OneHook _oneHook; - - public static void Main(string[] args) { } - public void Start(IModLoaderV1 loader) - { - _modLoader = (IModLoader)loader; - _modLoader.GetController().TryGetTarget(out var hooks); - _modLoader.GetController().TryGetTarget(out var prsInstance); - Logger = (ILogger) _modLoader.GetLogger(); - SDK.Init(hooks, prsInstance); - - /* Your mod code starts here. */ - var configurator = new Configurator(_modLoader.GetDirectoryForModId("sonicheroes.utils.oneredirector")); - var config = configurator.GetConfiguration(0); - - _oneHook = new OneHook(Logger, config, NativeFunctions.GetInstance(hooks)); - _modLoader.ModLoading += OnModLoading; - _modLoader.ModUnloading += OnModUnloading; - } - - private void OnModUnloading(IModV1 modInstance, IModConfigV1 modConfig) => _oneHook.OnModUnloading(_modLoader.GetDirectoryForModId(modConfig.ModId)); - private void OnModLoading(IModV1 modInstance, IModConfigV1 modConfig) => _oneHook.OnModLoading(_modLoader.GetDirectoryForModId(modConfig.ModId)); - - /* Mod loader actions. */ - public void Suspend() { } - public void Resume() { } - public void Unload() { } - public bool CanUnload() => false; - public bool CanSuspend() => false; - - /* Automatically called by the mod loader when the mod is about to be unloaded. */ - public Action Disposing { get; } + _modLoader = (IModLoader)loader; + _modLoader.GetController().TryGetTarget(out var hooks); + _modLoader.GetController().TryGetTarget(out var prsInstance); + Logger = (ILogger)_modLoader.GetLogger(); + SDK.Init(hooks, prsInstance); + + /* Your mod code starts here. */ + var modFolder = _modLoader.GetDirectoryForModId(config.ModId); + var configFolder = _modLoader.GetModConfigDirectory(config.ModId); + var configurator = new Configurator(configFolder); + configurator.Migrate(modFolder, configFolder); + + var modConfig = configurator.GetConfiguration(0); + _oneHook = new OneHook(Logger, modConfig, NativeFunctions.GetInstance(hooks)); + _modLoader.ModLoading += OnModLoading; + _modLoader.ModUnloading += OnModUnloading; } -} + + private void OnModUnloading(IModV1 modInstance, IModConfigV1 modConfig) => _oneHook.OnModUnloading(_modLoader.GetDirectoryForModId(modConfig.ModId)); + private void OnModLoading(IModV1 modInstance, IModConfigV1 modConfig) => _oneHook.OnModLoading(_modLoader.GetDirectoryForModId(modConfig.ModId)); + + /* Mod loader actions. */ + public void Suspend() { } + public void Resume() { } + public void Unload() { } + public bool CanUnload() => false; + public bool CanSuspend() => false; + + /* Automatically called by the mod loader when the mod is about to be unloaded. */ + public Action Disposing { get; } + + public static void Main(string[] args) { } +} \ No newline at end of file diff --git a/SonicHeroes.Utils.OneRedirector/Robust.Trimming.targets b/SonicHeroes.Utils.OneRedirector/Robust.Trimming.targets new file mode 100644 index 0000000..b416b50 --- /dev/null +++ b/SonicHeroes.Utils.OneRedirector/Robust.Trimming.targets @@ -0,0 +1,122 @@ + + + + + + + + false + + + false + + + + + + + + + + + + + + + + + + + link + copy + <_ExtraTrimmerArgs>--skip-unresolved true $(_ExtraTrimmerArgs) + $(TreatWarningsAsErrors) + true + + + + + Copy + + + Link + + + + + + <__PDBToLink Include="@(ResolvedFileToPublish)" Exclude="@(RobustAssemblyToLink->'%(RelativeDir)%(Filename).pdb')" /> + <_PDBToLink Include="@(ResolvedFileToPublish)" Exclude="@(__PDBToLink)" /> + + + + <_LinkedResolvedFileToPublishCandidate Include="@(RobustAssemblyToLink->'$(IntermediateLinkDir)%(Filename)%(Extension)')" /> + <_LinkedResolvedFileToPublishCandidate Include="@(_PDBToLink->'$(IntermediateLinkDir)%(Filename)%(Extension)')" /> + + + + + + <_TrimmerFeatureSettings Include="@(RuntimeHostConfigurationOption)" + Condition="'%(RuntimeHostConfigurationOption.Trim)' == 'true'" /> + + + + + + + + + + + <_LinkedResolvedFileToPublish Include="@(_LinkedResolvedFileToPublishCandidate)" Condition="Exists('%(Identity)')" /> + + + + + + + \ No newline at end of file diff --git a/SonicHeroes.Utils.OneRedirector/SonicHeroes.Utils.OneRedirector.csproj b/SonicHeroes.Utils.OneRedirector/SonicHeroes.Utils.OneRedirector.csproj index 871c37c..5428705 100644 --- a/SonicHeroes.Utils.OneRedirector/SonicHeroes.Utils.OneRedirector.csproj +++ b/SonicHeroes.Utils.OneRedirector/SonicHeroes.Utils.OneRedirector.csproj @@ -1,7 +1,7 @@  - netcoreapp3.0 + net5.0 false true SonicHeroes.Utils.OneRedirector @@ -10,8 +10,19 @@ WinExe + preview + $(RELOADEDIIMODS)\sonicheroes.utils.oneredirector + true + + + + + + + + @@ -27,10 +38,10 @@ - - - - + + + + diff --git a/SonicHeroes.Utils.OneRedirector/Structs/FileInfo.cs b/SonicHeroes.Utils.OneRedirector/Structs/FileInfo.cs index 8200648..7c5ea01 100644 --- a/SonicHeroes.Utils.OneRedirector/Structs/FileInfo.cs +++ b/SonicHeroes.Utils.OneRedirector/Structs/FileInfo.cs @@ -1,21 +1,20 @@ -namespace SonicHeroes.Utils.OneRedirector.Structs +namespace SonicHeroes.Utils.OneRedirector.Structs; + +public class FileInfo { - public class FileInfo - { - /// - /// Contains the absolute file path to the file. - /// - public string FilePath { get; set; } + /// + /// Contains the absolute file path to the file. + /// + public string FilePath { get; set; } - /// - /// Current read pointer for the file. - /// - public long FilePointer { get; set; } + /// + /// Current read pointer for the file. + /// + public long FilePointer { get; set; } - public FileInfo(string filePath, long filePointer) - { - FilePath = filePath; - FilePointer = filePointer; - } + public FileInfo(string filePath, long filePointer) + { + FilePath = filePath; + FilePointer = filePointer; } -} +} \ No newline at end of file diff --git a/SonicHeroes.Utils.OneRedirector/Structs/NativeFunctions.cs b/SonicHeroes.Utils.OneRedirector/Structs/NativeFunctions.cs index 10d8dc8..b409243 100644 --- a/SonicHeroes.Utils.OneRedirector/Structs/NativeFunctions.cs +++ b/SonicHeroes.Utils.OneRedirector/Structs/NativeFunctions.cs @@ -1,42 +1,40 @@ -using System; -using Reloaded.Hooks.Definitions; +using Reloaded.Hooks.Definitions; using static SonicHeroes.Utils.OneRedirector.Native.Native; -namespace SonicHeroes.Utils.OneRedirector.Structs +namespace SonicHeroes.Utils.OneRedirector.Structs; + +public struct NativeFunctions { - public struct NativeFunctions - { - private static bool _instanceMade; - private static NativeFunctions _instance; + private static bool _instanceMade; + private static NativeFunctions _instance; - public IFunction NtCreateFile; - public IFunction NtReadFile; - public IFunction NtSetInformationFile; - public IFunction NtQueryInformationFile; + public IFunction NtCreateFile; + public IFunction NtReadFile; + public IFunction NtSetInformationFile; + public IFunction NtQueryInformationFile; - public static NativeFunctions GetInstance(IReloadedHooks hooks) - { - if (_instanceMade) - return _instance; + public static NativeFunctions GetInstance(IReloadedHooks hooks) + { + if (_instanceMade) + return _instance; - var ntdllHandle = LoadLibraryW("ntdll"); - var ntCreateFilePointer = GetProcAddress(ntdllHandle, "NtCreateFile"); - var ntReadFilePointer = GetProcAddress(ntdllHandle, "NtReadFile"); - var ntSetInformationFile = GetProcAddress(ntdllHandle, "NtSetInformationFile"); - var ntQueryInformationFile = GetProcAddress(ntdllHandle, "NtQueryInformationFile"); + var ntdllHandle = LoadLibraryW("ntdll"); + var ntCreateFilePointer = GetProcAddress(ntdllHandle, "NtCreateFile"); + var ntReadFilePointer = GetProcAddress(ntdllHandle, "NtReadFile"); + var ntSetInformationFile = GetProcAddress(ntdllHandle, "NtSetInformationFile"); + var ntQueryInformationFile = GetProcAddress(ntdllHandle, "NtQueryInformationFile"); - _instance = new NativeFunctions() - { - NtCreateFile = hooks.CreateFunction((long) ntCreateFilePointer), - NtReadFile = hooks.CreateFunction((long)ntReadFilePointer), - NtSetInformationFile = hooks.CreateFunction((long)ntSetInformationFile), - NtQueryInformationFile = hooks.CreateFunction((long)ntQueryInformationFile), - }; + _instance = new NativeFunctions() + { + NtCreateFile = hooks.CreateFunction((long) ntCreateFilePointer), + NtReadFile = hooks.CreateFunction((long)ntReadFilePointer), + NtSetInformationFile = hooks.CreateFunction((long)ntSetInformationFile), + NtQueryInformationFile = hooks.CreateFunction((long)ntQueryInformationFile), + }; - _instanceMade = true; + _instanceMade = true; - return _instance; - } + return _instance; } -} +} \ No newline at end of file diff --git a/SonicHeroes.Utils.OneRedirector/Structs/Utilities.cs b/SonicHeroes.Utils.OneRedirector/Structs/Utilities.cs index ca5295a..b997840 100644 --- a/SonicHeroes.Utils.OneRedirector/Structs/Utilities.cs +++ b/SonicHeroes.Utils.OneRedirector/Structs/Utilities.cs @@ -1,23 +1,64 @@ -namespace SonicHeroes.Utils.OneRedirector.Structs +using System; +using System.IO; +using System.Runtime.CompilerServices; + +namespace SonicHeroes.Utils.OneRedirector.Structs; + +internal static class Utilities { - internal static class Utilities + /// + /// Rounds up a specified number to the next multiple of X. + /// + /// The number to round up. + /// The multiple the number should be rounded to. + /// + public static int RoundUp(int number, int multiple) { - /// - /// Rounds up a specified number to the next multiple of X. - /// - /// The number to round up. - /// The multiple the number should be rounded to. - /// - public static int RoundUp(int number, int multiple) - { - if (multiple == 0) - return number; + if (multiple == 0) + return number; - int remainder = number % multiple; - if (remainder == 0) - return number; + int remainder = number % multiple; + if (remainder == 0) + return number; - return number + multiple - remainder; + return number + multiple - remainder; + } + + /// + /// Reads an unmanaged, generic type from the stream. + /// + /// The stream to read the value from. + /// The value to return. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static unsafe bool TryReadSafe(this Stream stream, out T value) where T : unmanaged + { + value = default; + var valueSpan = new Span(Unsafe.AsPointer(ref value), sizeof(T)); + return TryReadSafe(stream, valueSpan); + } + + /// + /// Reads a given number of bytes from a stream. + /// + /// The stream to read the value from. + /// The buffer to receive the bytes. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static unsafe bool TryReadSafe(this Stream stream, Span result) + { + int numBytesRead = 0; + int numBytesToRead = result.Length; + + do + { + int bytesRead = stream.Read(result.Slice(numBytesRead)); + if (bytesRead <= 0) + return false; + + numBytesRead += bytesRead; + numBytesToRead -= bytesRead; } + while (numBytesRead < numBytesToRead); + + return true; } -} +} \ No newline at end of file