├── .gitattributes ├── .github └── workflows │ ├── cd.yaml │ └── ci.yaml ├── .gitignore ├── Dependencies ├── Common.dll ├── ContentSerialization.dll ├── EasyStorage.dll ├── FEZ.exe ├── FNA.dll ├── FezEngine.dll ├── README.md └── XnaWordWrapCore.dll ├── Docs ├── additional.md ├── createmods.md └── thumbnail.png ├── FEZ.HAT.mm.csproj ├── HAT.sln ├── Helpers ├── DrawingTools.cs └── InputHelper.cs ├── ILRepack.targets ├── Installers ├── AssetManagementInstaller.cs ├── IHatInstaller.cs ├── LoggerModifier.cs └── ModMenuInstaller.cs ├── LICENSE ├── Patches ├── Fez.cs ├── FezLogo.cs ├── Program.cs └── TextPatch.cs ├── Properties └── MonoModRules.cs ├── README.md ├── Source ├── Assets │ ├── Asset.cs │ └── AssetLoaderHelper.cs ├── DependencyResolver.cs ├── FileProxies │ ├── DirectoryFileProxy.cs │ ├── IFileProxy.cs │ └── ZipFileProxy.cs ├── Hat.cs ├── ModDefinition │ ├── Mod.cs │ ├── ModDependency.cs │ ├── ModDependencyInfo.cs │ ├── ModDependencyStatus.cs │ ├── ModIdentityHelper.cs │ └── ModMetadata.cs └── ModsTextListLoader.cs └── scripts ├── hat_install.bat └── hat_install.sh /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.github/workflows/cd.yaml: -------------------------------------------------------------------------------- 1 | name: CD 2 | 3 | on: 4 | push: 5 | tags: 6 | - '*' 7 | 8 | jobs: 9 | build: 10 | runs-on: windows-latest 11 | steps: 12 | - uses: actions/checkout@v4 13 | - uses: actions/setup-dotnet@v4 14 | with: 15 | dotnet-version: '9.x' 16 | 17 | - name: Restore packages 18 | run: dotnet restore FEZ.HAT.mm.csproj 19 | 20 | - name: Build 21 | run: dotnet build FEZ.HAT.mm.csproj -c Release 22 | 23 | - name: Prepare artifact 24 | shell: pwsh 25 | run: | 26 | New-Item -Path artifact -ItemType Directory -Force 27 | Copy-Item -Path 'bin/Release/*' -Destination artifact -Recurse 28 | Copy-Item -Path 'scripts/*' -Destination artifact -Recurse 29 | Compress-Archive -Path artifact/* -DestinationPath HAT.zip 30 | 31 | - name: Upload artifact 32 | uses: actions/upload-artifact@v4 33 | with: 34 | name: HAT 35 | path: HAT.zip 36 | if-no-files-found: error 37 | release: 38 | if: github.repository == 'FEZModding/HAT' 39 | needs: [build] 40 | runs-on: ubuntu-latest 41 | steps: 42 | - name: Download Build 43 | uses: actions/download-artifact@v4 44 | with: 45 | name: HAT 46 | 47 | - name: Create Release 48 | uses: softprops/action-gh-release@v1 49 | with: 50 | body: | 51 | ## Installation 52 | 53 | 1. Download `HAT.zip` from Release tab and unpack it in the game's directory (next to FEZ.exe). 54 | 2. Run `hat_install.bat` (for Windows) or `hat_install.sh` (for Linux, experimental!). This should generate new executable file called `MONOMODDED_FEZ.exe`. 55 | 3. Run `MONOMODDED_FEZ.exe` and enjoy modding! 56 | 57 | ## Changelog 58 | 59 | TODO 60 | files: HAT.zip 61 | fail_on_unmatched_files: true 62 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - '**' 7 | tags-ignore: 8 | - '**' 9 | paths-ignore: 10 | - '.github/*' 11 | - '.github/workflows/**.yml' 12 | - '.gitattributes' 13 | - '.gitignore' 14 | - 'docs/**' 15 | - '**.md' 16 | - 'LICENSE' 17 | 18 | jobs: 19 | build: 20 | runs-on: windows-latest 21 | steps: 22 | - uses: actions/checkout@v4 23 | - uses: actions/setup-dotnet@v4 24 | with: 25 | dotnet-version: '9.x' 26 | 27 | - name: Restore packages 28 | run: dotnet restore FEZ.HAT.mm.csproj 29 | 30 | - name: Build 31 | run: dotnet build FEZ.HAT.mm.csproj -c Debug 32 | 33 | - name: Prepare artifact 34 | shell: pwsh 35 | run: | 36 | New-Item -Path artifact -ItemType Directory -Force 37 | Copy-Item -Path 'bin/Debug/*' -Destination artifact -Recurse 38 | Copy-Item -Path 'scripts/*' -Destination artifact -Recurse 39 | Compress-Archive -Path artifact/* -DestinationPath HAT.zip 40 | 41 | - name: Upload artifact 42 | uses: actions/upload-artifact@v4 43 | with: 44 | name: HAT 45 | path: HAT.zip 46 | if-no-files-found: error 47 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Mono auto generated files 17 | mono_crash.* 18 | 19 | # Build results 20 | [Dd]ebug/ 21 | [Dd]ebugPublic/ 22 | [Rr]elease/ 23 | [Rr]eleases/ 24 | x64/ 25 | x86/ 26 | [Ww][Ii][Nn]32/ 27 | [Aa][Rr][Mm]/ 28 | [Aa][Rr][Mm]64/ 29 | bld/ 30 | [Bb]in/ 31 | [Oo]bj/ 32 | [Ll]og/ 33 | [Ll]ogs/ 34 | 35 | # Visual Studio 2015/2017 cache/options directory 36 | .vs/ 37 | # Uncomment if you have tasks that create the project's static files in wwwroot 38 | #wwwroot/ 39 | 40 | # Visual Studio 2017 auto generated files 41 | Generated\ Files/ 42 | 43 | # MSTest test Results 44 | [Tt]est[Rr]esult*/ 45 | [Bb]uild[Ll]og.* 46 | 47 | # NUnit 48 | *.VisualState.xml 49 | TestResult.xml 50 | nunit-*.xml 51 | 52 | # Build Results of an ATL Project 53 | [Dd]ebugPS/ 54 | [Rr]eleasePS/ 55 | dlldata.c 56 | 57 | # Benchmark Results 58 | BenchmarkDotNet.Artifacts/ 59 | 60 | # .NET Core 61 | project.lock.json 62 | project.fragment.lock.json 63 | artifacts/ 64 | 65 | # ASP.NET Scaffolding 66 | ScaffoldingReadMe.txt 67 | 68 | # StyleCop 69 | StyleCopReport.xml 70 | 71 | # Files built by Visual Studio 72 | *_i.c 73 | *_p.c 74 | *_h.h 75 | *.ilk 76 | *.meta 77 | *.obj 78 | *.iobj 79 | *.pch 80 | *.pdb 81 | *.ipdb 82 | *.pgc 83 | *.pgd 84 | *.rsp 85 | *.sbr 86 | *.tlb 87 | *.tli 88 | *.tlh 89 | *.tmp 90 | *.tmp_proj 91 | *_wpftmp.csproj 92 | *.log 93 | *.tlog 94 | *.vspscc 95 | *.vssscc 96 | .builds 97 | *.pidb 98 | *.svclog 99 | *.scc 100 | 101 | # Chutzpah Test files 102 | _Chutzpah* 103 | 104 | # Visual C++ cache files 105 | ipch/ 106 | *.aps 107 | *.ncb 108 | *.opendb 109 | *.opensdf 110 | *.sdf 111 | *.cachefile 112 | *.VC.db 113 | *.VC.VC.opendb 114 | 115 | # Visual Studio profiler 116 | *.psess 117 | *.vsp 118 | *.vspx 119 | *.sap 120 | 121 | # Visual Studio Trace Files 122 | *.e2e 123 | 124 | # TFS 2012 Local Workspace 125 | $tf/ 126 | 127 | # Guidance Automation Toolkit 128 | *.gpState 129 | 130 | # ReSharper is a .NET coding add-in 131 | _ReSharper*/ 132 | *.[Rr]e[Ss]harper 133 | *.DotSettings.user 134 | 135 | # TeamCity is a build add-in 136 | _TeamCity* 137 | 138 | # DotCover is a Code Coverage Tool 139 | *.dotCover 140 | 141 | # AxoCover is a Code Coverage Tool 142 | .axoCover/* 143 | !.axoCover/settings.json 144 | 145 | # Coverlet is a free, cross platform Code Coverage Tool 146 | coverage*.json 147 | coverage*.xml 148 | coverage*.info 149 | 150 | # Visual Studio code coverage results 151 | *.coverage 152 | *.coveragexml 153 | 154 | # NCrunch 155 | _NCrunch_* 156 | .*crunch*.local.xml 157 | nCrunchTemp_* 158 | 159 | # Web workbench (sass) 160 | .sass-cache/ 161 | 162 | # Installshield output folder 163 | [Ee]xpress/ 164 | 165 | # DocProject is a documentation generator add-in 166 | DocProject/buildhelp/ 167 | DocProject/Help/*.HxT 168 | DocProject/Help/*.HxC 169 | DocProject/Help/*.hhc 170 | DocProject/Help/*.hhk 171 | DocProject/Help/*.hhp 172 | DocProject/Help/Html2 173 | DocProject/Help/html 174 | 175 | # Click-Once directory 176 | publish/ 177 | 178 | # Publish Web Output 179 | *.[Pp]ublish.xml 180 | *.azurePubxml 181 | # Note: Comment the next line if you want to checkin your web deploy settings, 182 | # but database connection strings (with potential passwords) will be unencrypted 183 | *.pubxml 184 | *.publishproj 185 | 186 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 187 | # checkin your Azure Web App publish settings, but sensitive information contained 188 | # in these scripts will be unencrypted 189 | PublishScripts/ 190 | 191 | # NuGet Packages 192 | *.nupkg 193 | # NuGet Symbol Packages 194 | *.snupkg 195 | # The packages folder can be ignored because of Package Restore 196 | **/[Pp]ackages/* 197 | # except build/, which is used as an MSBuild target. 198 | !**/[Pp]ackages/build/ 199 | # Uncomment if necessary however generally it will be regenerated when needed 200 | #!**/[Pp]ackages/repositories.config 201 | # NuGet v3's project.json files produces more ignorable files 202 | *.nuget.props 203 | *.nuget.targets 204 | 205 | # Microsoft Azure Build Output 206 | csx/ 207 | *.build.csdef 208 | 209 | # Microsoft Azure Emulator 210 | ecf/ 211 | rcf/ 212 | 213 | # Windows Store app package directories and files 214 | AppPackages/ 215 | BundleArtifacts/ 216 | Package.StoreAssociation.xml 217 | _pkginfo.txt 218 | *.appx 219 | *.appxbundle 220 | *.appxupload 221 | 222 | # Visual Studio cache files 223 | # files ending in .cache can be ignored 224 | *.[Cc]ache 225 | # but keep track of directories ending in .cache 226 | !?*.[Cc]ache/ 227 | 228 | # Others 229 | ClientBin/ 230 | ~$* 231 | *~ 232 | *.dbmdl 233 | *.dbproj.schemaview 234 | *.jfm 235 | *.pfx 236 | *.publishsettings 237 | orleans.codegen.cs 238 | 239 | # Including strong name files can present a security risk 240 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 241 | #*.snk 242 | 243 | # Since there are multiple workflows, uncomment next line to ignore bower_components 244 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 245 | #bower_components/ 246 | 247 | # RIA/Silverlight projects 248 | Generated_Code/ 249 | 250 | # Backup & report files from converting an old project file 251 | # to a newer Visual Studio version. Backup files are not needed, 252 | # because we have git ;-) 253 | _UpgradeReport_Files/ 254 | Backup*/ 255 | UpgradeLog*.XML 256 | UpgradeLog*.htm 257 | ServiceFabricBackup/ 258 | *.rptproj.bak 259 | 260 | # SQL Server files 261 | *.mdf 262 | *.ldf 263 | *.ndf 264 | 265 | # Business Intelligence projects 266 | *.rdl.data 267 | *.bim.layout 268 | *.bim_*.settings 269 | *.rptproj.rsuser 270 | *- [Bb]ackup.rdl 271 | *- [Bb]ackup ([0-9]).rdl 272 | *- [Bb]ackup ([0-9][0-9]).rdl 273 | 274 | # Microsoft Fakes 275 | FakesAssemblies/ 276 | 277 | # GhostDoc plugin setting file 278 | *.GhostDoc.xml 279 | 280 | # Node.js Tools for Visual Studio 281 | .ntvs_analysis.dat 282 | node_modules/ 283 | 284 | # Visual Studio 6 build log 285 | *.plg 286 | 287 | # Visual Studio 6 workspace options file 288 | *.opt 289 | 290 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 291 | *.vbw 292 | 293 | # Visual Studio 6 auto-generated project file (contains which files were open etc.) 294 | *.vbp 295 | 296 | # Visual Studio 6 workspace and project file (working project files containing files to include in project) 297 | *.dsw 298 | *.dsp 299 | 300 | # Visual Studio 6 technical files 301 | *.ncb 302 | *.aps 303 | 304 | # Visual Studio LightSwitch build output 305 | **/*.HTMLClient/GeneratedArtifacts 306 | **/*.DesktopClient/GeneratedArtifacts 307 | **/*.DesktopClient/ModelManifest.xml 308 | **/*.Server/GeneratedArtifacts 309 | **/*.Server/ModelManifest.xml 310 | _Pvt_Extensions 311 | 312 | # Paket dependency manager 313 | .paket/paket.exe 314 | paket-files/ 315 | 316 | # FAKE - F# Make 317 | .fake/ 318 | 319 | # CodeRush personal settings 320 | .cr/personal 321 | 322 | # Python Tools for Visual Studio (PTVS) 323 | __pycache__/ 324 | *.pyc 325 | 326 | # Cake - Uncomment if you are using it 327 | # tools/** 328 | # !tools/packages.config 329 | 330 | # Tabs Studio 331 | *.tss 332 | 333 | # Telerik's JustMock configuration file 334 | *.jmconfig 335 | 336 | # BizTalk build output 337 | *.btp.cs 338 | *.btm.cs 339 | *.odx.cs 340 | *.xsd.cs 341 | 342 | # OpenCover UI analysis results 343 | OpenCover/ 344 | 345 | # Azure Stream Analytics local run output 346 | ASALocalRun/ 347 | 348 | # MSBuild Binary and Structured Log 349 | *.binlog 350 | 351 | # NVidia Nsight GPU debugger configuration file 352 | *.nvuser 353 | 354 | # MFractors (Xamarin productivity tool) working folder 355 | .mfractor/ 356 | 357 | # Local History for Visual Studio 358 | .localhistory/ 359 | 360 | # Visual Studio History (VSHistory) files 361 | .vshistory/ 362 | 363 | # BeatPulse healthcheck temp database 364 | healthchecksdb 365 | 366 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 367 | MigrationBackup/ 368 | 369 | # Ionide (cross platform F# VS Code tools) working folder 370 | .ionide/ 371 | 372 | # Fody - auto-generated XML schema 373 | FodyWeavers.xsd 374 | 375 | # VS Code files for those working on multiple tools 376 | .vscode/* 377 | !.vscode/settings.json 378 | !.vscode/tasks.json 379 | !.vscode/launch.json 380 | !.vscode/extensions.json 381 | *.code-workspace 382 | 383 | # Local History for Visual Studio Code 384 | .history/ 385 | 386 | # Windows Installer files from build outputs 387 | *.cab 388 | *.msi 389 | *.msix 390 | *.msm 391 | *.msp 392 | 393 | # JetBrains Rider 394 | *.sln.iml 395 | .idea/* -------------------------------------------------------------------------------- /Dependencies/Common.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FEZModding/HAT/c844c902eedd4b451df58e183d041423fa0d5cc8/Dependencies/Common.dll -------------------------------------------------------------------------------- /Dependencies/ContentSerialization.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FEZModding/HAT/c844c902eedd4b451df58e183d041423fa0d5cc8/Dependencies/ContentSerialization.dll -------------------------------------------------------------------------------- /Dependencies/EasyStorage.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FEZModding/HAT/c844c902eedd4b451df58e183d041423fa0d5cc8/Dependencies/EasyStorage.dll -------------------------------------------------------------------------------- /Dependencies/FEZ.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FEZModding/HAT/c844c902eedd4b451df58e183d041423fa0d5cc8/Dependencies/FEZ.exe -------------------------------------------------------------------------------- /Dependencies/FNA.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FEZModding/HAT/c844c902eedd4b451df58e183d041423fa0d5cc8/Dependencies/FNA.dll -------------------------------------------------------------------------------- /Dependencies/FezEngine.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FEZModding/HAT/c844c902eedd4b451df58e183d041423fa0d5cc8/Dependencies/FezEngine.dll -------------------------------------------------------------------------------- /Dependencies/README.md: -------------------------------------------------------------------------------- 1 | This directory contains the stripped binaries of FEZ, generated by [FEZStripGen](https://github.com/FEZModding/FEZStripGen/). 2 | 3 | # DO NOT PUSH THE ORIGINAL VERSION OF THE STRIPPED BINARIES 4 | 5 | However, you're more than free to replace them **locally**. 6 | -------------------------------------------------------------------------------- /Dependencies/XnaWordWrapCore.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FEZModding/HAT/c844c902eedd4b451df58e183d041423fa0d5cc8/Dependencies/XnaWordWrapCore.dll -------------------------------------------------------------------------------- /Docs/additional.md: -------------------------------------------------------------------------------- 1 | # Additional HAT behaviour 2 | 3 | Apart from loading mods, HAT does some additional internal behaviour worth mentioning. Here's a full list: 4 | 5 | - `FezLogo` class has been patched in order to draw HAT logo and mod loader tooltip. 6 | - Several methods in `Logger` class have been hooked to override location of debug log files (they're now stored in `%appdata%/FEZ/Debug Logs` directory) and to show an error with stack trace on fatal error. 7 | - `StaticText` class used to fetch localized text has been patched to return a raw string if it's prefixed by `@`. This is useful when you want to create your own menus where you have limited control over how text is displayed. 8 | - `Menu Base` class' `Initialize` method has a hook which adds an additional `MODS` menu, where you can preview a list of currently installed modifications. 9 | -------------------------------------------------------------------------------- /Docs/createmods.md: -------------------------------------------------------------------------------- 1 | # Create your own HAT modifications 2 | 3 | ## Basic mod architecture 4 | 5 | Start by creating a mod's directory within `FEZ/Mods` directory. You can name it whatever you'd like, as the mod loader doesn't actually use it for mod identification, but it would be nice if it at least contained the actual mod's name to avoid confusion. 6 | 7 | Mod loader expects `Metadata.xml` file in the mod's directory. Create one in a directory you've just made. Its content should look roughly like this: 8 | 9 | ```xml 10 | 11 | YourModName 12 | Short description of your mod. 13 | YourName 14 | 1.0 15 | 16 | 17 | 18 | 19 | 20 | ``` 21 | 22 | `Name` tag is required and is treated as an unique case-sensitive identifier of your mod - mod loader will load only one mod with the same name (it'll choose the one with the most recent version). 23 | 24 | `Version` tag is also required. Mod loader compares two version strings by putting them in an alphanumberical order, however, each number is treated as a separate token, which order is determined by numberical value (this means `1.2beta` will be treated as older version to `1.11`). 25 | 26 | `LibraryName` is used to determine a DLL library with C# assembly the mod loader will load. The library should end with `.dll` extension and should be placed in your mod's directory. This tag is optional, as your mod doesn't have to add any new logic. 27 | 28 | `Dependencies` is a list of `DependencyInfo` tags. If your mod requires a specific version of HAT mod loader or relies on another mod, your can use these tags to prevent mod loader from loading this mod if given dependencies aren't present. It's entirely optional. 29 | 30 | All other fields are purely informational. 31 | 32 | ## Creating asset mod 33 | 34 | If you want to add new assets or override existing ones, create `Assets` directory within your mods directory. All valid files within it will be loaded as game assets with path relative to the `Assets` directory. Currently, the only supported format is `.xnb`, but in the future, a conversion from popular file formats will be implemented, allowing much easier modding process (for isntance, PNG files will be automatically converted to Texture2D assets). As of right now, there isn't really a good way of creating `.xnb` assets and you have to rely on [FEZRepacker](https://github.com/Krzyhau/FEZRepacker). 35 | 36 | As an example, here's an instruction on how to change Gomez's house background plane. 37 | 38 | 1. Use FEZRepacker to unpack game's `Other.pak` archive. 39 | 2. Find `background planes/gomez_house_a.png` file and copy it. 40 | 3. Edit the image however you'd like. 41 | 4. Use FEZRepacker to convert the image into an XNB. 42 | 7. In your mod's `Assets` directory, create `background planes` directory and put your XNB file there. 43 | 8. From now on Gomez's house should have your modified texture. 44 | 45 | A small note regarding music files: since they're normally stored in a separate `.pak` archive (`Music.pak`) and handled by a separate subsystem, music files are organized in a root directory. It is **not** the case for HAT mods, and instead it looks for OGG files (audio format used by music in this game) in `[Your mod]/Assets/Music` directory, then uses a path relative to this directory to identify the music file. For example, in order to replace `villageville\bed` music file, your new music file needs to be located at `[Your mod]/Assets/Music/villageville/bed.ogg`. 46 | 47 | ## Creating custom logic mod 48 | 49 | Mod loader loads library file given in metadata as an assembly, then attempts to create instances of every non-abstract public class extending the `GameComponent` class before initialization (before any services are created). After the game has been initialized (that is, as soon as all necessary services are initiated), it adds created instances into the list of game's components and initializes them, allowing their `Update` and `Draw` (use `DrawableGameComponent`) to be properly executed within the game's loop. 50 | 51 | In order to create a HAT-compatible library, start by creating an empty C# library project. Then, add `FEZ.exe`, `FezEngine.dll` and all other needed game's dependencies as references - make sure to set "Copy Local" to "False" on all of those references, otherwise you will ship your mod with copies of those files. 52 | 53 | Once you have your project done, create a public class inheriting from either `GameComponent` or `DrawableGameComponent` and add your logic there. Once that's done, build it and put it in the mod's directory. 54 | 55 | For help, you can see an example of already functioning custom logic mod: [FEZUG](https://github.com/Krzyhau/FEZUG). 56 | 57 | ## Distributing your mod 58 | 59 | Mod loader is capable of loading ZIP archives the same way directories are loaded. Simply pack all contents of your mod's directory into a ZIP file. In order for other people to use it, they simply need to put the archive in the `FEZ/Mods` directory and it should work right off the bat. 60 | -------------------------------------------------------------------------------- /Docs/thumbnail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FEZModding/HAT/c844c902eedd4b451df58e183d041423fa0d5cc8/Docs/thumbnail.png -------------------------------------------------------------------------------- /FEZ.HAT.mm.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | Library 7 | net48 8 | latest 9 | FEZ.HAT.mm 10 | HatModLoader 11 | 12 | enable 13 | enable 14 | 15 | true 16 | 17 | full 18 | 19 | 20 | 21 | false 22 | false 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | all 53 | runtime; build; native; contentfiles; analyzers; buildtransitive 54 | 55 | 56 | 57 | 58 | all 59 | 60 | 61 | 62 | 63 | 64 | Dependencies\Common.dll 65 | False 66 | 67 | 68 | Dependencies\ContentSerialization.dll 69 | False 70 | 71 | 72 | Dependencies\EasyStorage.dll 73 | False 74 | 75 | 76 | Dependencies\FEZ.exe 77 | False 78 | 79 | 80 | Dependencies\FezEngine.dll 81 | False 82 | 83 | 84 | Dependencies\FNA.dll 85 | False 86 | 87 | 88 | 89 | Dependencies\XnaWordWrapCore.dll 90 | False 91 | 92 | 93 | 94 | -------------------------------------------------------------------------------- /HAT.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.1.32407.343 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FEZ.HAT.mm", "FEZ.HAT.mm.csproj", "{6F13616E-70E1-4FE0-AAC3-FDECCB9D8229}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|x86 = Debug|x86 11 | Release|x86 = Release|x86 12 | EndGlobalSection 13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 14 | {6F13616E-70E1-4FE0-AAC3-FDECCB9D8229}.Debug|x86.ActiveCfg = Debug|Any CPU 15 | {6F13616E-70E1-4FE0-AAC3-FDECCB9D8229}.Debug|x86.Build.0 = Debug|Any CPU 16 | {6F13616E-70E1-4FE0-AAC3-FDECCB9D8229}.Release|x86.ActiveCfg = Release|Any CPU 17 | {6F13616E-70E1-4FE0-AAC3-FDECCB9D8229}.Release|x86.Build.0 = Release|Any CPU 18 | EndGlobalSection 19 | GlobalSection(SolutionProperties) = preSolution 20 | HideSolutionNode = FALSE 21 | EndGlobalSection 22 | GlobalSection(ExtensibilityGlobals) = postSolution 23 | SolutionGuid = {C452BFAA-0F26-4A52-8C6E-C9B5CA1B2E83} 24 | EndGlobalSection 25 | EndGlobal 26 | -------------------------------------------------------------------------------- /Helpers/DrawingTools.cs: -------------------------------------------------------------------------------- 1 |  2 | using FezEngine.Components; 3 | using FezEngine.Tools; 4 | using Microsoft.Xna.Framework; 5 | using Microsoft.Xna.Framework.Graphics; 6 | 7 | namespace HatModLoader.Helpers 8 | { 9 | internal static class DrawingTools 10 | { 11 | public static IFontManager FontManager { get; private set; } 12 | public static GraphicsDevice GraphicsDevice { get; private set; } 13 | public static SpriteBatch Batch { get; private set; } 14 | 15 | private static Texture2D fillTexture; 16 | 17 | public static SpriteFont DefaultFont { get; set; } 18 | public static float DefaultFontSize { get; set; } 19 | 20 | public static void Init() 21 | { 22 | FontManager = ServiceHelper.Get(); 23 | GraphicsDevice = ServiceHelper.Get().GraphicsDevice; 24 | Batch = new SpriteBatch(GraphicsDevice); 25 | DefaultFont = FontManager.Big; 26 | DefaultFontSize = 2.0f; 27 | 28 | fillTexture = new Texture2D(GraphicsDevice, 1, 1, false, SurfaceFormat.Color); 29 | fillTexture.SetData(new[] { new Color(255, 255, 255) }); 30 | } 31 | 32 | public static Viewport GetViewport() 33 | { 34 | return GraphicsDevice.Viewport; 35 | } 36 | 37 | public static void BeginBatch() 38 | { 39 | Batch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, DepthStencilState.None, RasterizerState.CullNone); 40 | } 41 | 42 | public static void EndBatch() 43 | { 44 | Batch.End(); 45 | } 46 | 47 | public static void DrawRect(Rectangle rect, Color color) 48 | { 49 | Batch.Draw(fillTexture, rect, color); 50 | } 51 | 52 | public static void DrawText(string text, Vector2 position) 53 | { 54 | DrawText(text, position, Color.White); 55 | } 56 | 57 | public static void DrawText(string text, Vector2 position, Color color) 58 | { 59 | DrawText(text, position, 0.0f, DefaultFontSize, Vector2.Zero, color); 60 | } 61 | 62 | public static void DrawText(string text, Vector2 position, float rotation, float scale, Color color) 63 | { 64 | DrawText(text, position, rotation, scale, Vector2.Zero, color); 65 | } 66 | 67 | public static void DrawText(string text, Vector2 position, float rotation, float scale, Vector2 origin, Color color) 68 | { 69 | scale *= FontManager.BigFactor / 2f; 70 | Batch.DrawString(DefaultFont, text, position, color, 71 | rotation, origin, scale, SpriteEffects.None, 0f 72 | ); 73 | } 74 | 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /Helpers/InputHelper.cs: -------------------------------------------------------------------------------- 1 | using FezEngine.Services; 2 | using FezEngine.Tools; 3 | using Microsoft.Xna.Framework; 4 | using Microsoft.Xna.Framework.Input; 5 | using System; 6 | using System.Collections.Generic; 7 | using System.Linq; 8 | using System.Text; 9 | using System.Threading.Tasks; 10 | 11 | namespace HatModLoader.Helpers 12 | { 13 | internal static class InputHelper 14 | { 15 | private static Dictionary KeyboardRepeatHeldTimers = new Dictionary(); 16 | private static List KeyboardRepeatedPresses = new List(); 17 | 18 | public static KeyboardState CurrentKeyboardState { get; private set; } 19 | public static KeyboardState PreviousKeyboardState { get; private set; } 20 | 21 | public static double KeyboardRepeatDelay { get; set; } = 0.4; 22 | public static double KeyboardRepeatSpeed { get; set; } = 0.03; 23 | 24 | public static void Update(GameTime gameTime) 25 | { 26 | PreviousKeyboardState = CurrentKeyboardState; 27 | CurrentKeyboardState = Keyboard.GetState(); 28 | 29 | 30 | KeyboardRepeatedPresses.Clear(); 31 | foreach (Keys key in CurrentKeyboardState.GetPressedKeys()) 32 | { 33 | if (IsKeyPressed(key) || !KeyboardRepeatHeldTimers.ContainsKey(key)) 34 | { 35 | KeyboardRepeatHeldTimers[key] = 0.0f; 36 | } 37 | 38 | KeyboardRepeatHeldTimers[key] += gameTime.ElapsedGameTime.TotalSeconds; 39 | if (KeyboardRepeatHeldTimers[key] > KeyboardRepeatDelay + KeyboardRepeatSpeed) 40 | { 41 | KeyboardRepeatHeldTimers[key] = KeyboardRepeatDelay; 42 | KeyboardRepeatedPresses.Add(key); 43 | } 44 | } 45 | } 46 | 47 | public static bool IsKeyPressed(Keys key) 48 | { 49 | return PreviousKeyboardState.IsKeyUp(key) && CurrentKeyboardState.IsKeyDown(key); 50 | } 51 | 52 | public static bool IsKeyHeld(Keys key) 53 | { 54 | return CurrentKeyboardState.IsKeyDown(key); 55 | } 56 | 57 | public static bool IsKeyReleased(Keys key) 58 | { 59 | return PreviousKeyboardState.IsKeyDown(key) && CurrentKeyboardState.IsKeyUp(key); 60 | } 61 | 62 | public static bool IsKeyTyped(Keys key) 63 | { 64 | return IsKeyPressed(key) || KeyboardRepeatedPresses.Contains(key); 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /ILRepack.targets: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 18 | 19 | 23 | 24 | 25 | 26 | 27 | 28 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /Installers/AssetManagementInstaller.cs: -------------------------------------------------------------------------------- 1 | using Common; 2 | using FezEngine.Services; 3 | using FezEngine.Tools; 4 | using HatModLoader.Source; 5 | using Microsoft.Xna.Framework; 6 | using MonoMod.RuntimeDetour; 7 | using System.Reflection; 8 | 9 | namespace HatModLoader.Installers 10 | { 11 | internal class AssetManagementInstaller : IHatInstaller 12 | { 13 | public static Hook CMProviderCtorDetour; 14 | public static Hook SMInitializeLibraryDetour; 15 | 16 | public void Install() 17 | { 18 | CMProviderCtorDetour = new Hook( 19 | typeof(ContentManagerProvider).GetConstructor(BindingFlags.Instance | BindingFlags.Public, null, 20 | CallingConventions.HasThis, new Type[] { typeof(Game) }, null), 21 | new Action, ContentManagerProvider, Game>((orig, self, game) => { 22 | orig(self, game); 23 | InjectAssets(self); 24 | }) 25 | ); 26 | 27 | SMInitializeLibraryDetour = new Hook( 28 | typeof(SoundManager).GetMethod("InitializeLibrary"), 29 | new Action, SoundManager>((orig, self) => { 30 | orig(self); 31 | InjectMusic(self); 32 | }) 33 | ); 34 | } 35 | public void Uninstall() 36 | { 37 | CMProviderCtorDetour.Dispose(); 38 | SMInitializeLibraryDetour.Dispose(); 39 | } 40 | 41 | private static void InjectAssets(ContentManagerProvider CMProvider) 42 | { 43 | var cachedAssetsField = typeof(MemoryContentManager).GetField("cachedAssets", BindingFlags.NonPublic | BindingFlags.Static); 44 | var cachedAssets = cachedAssetsField.GetValue(null) as Dictionary; 45 | 46 | foreach(var asset in Hat.Instance.GetFullAssetList()) 47 | { 48 | if (asset.IsMusicFile) continue; 49 | cachedAssets[asset.AssetPath] = asset.Data; 50 | } 51 | 52 | Logger.Log("HAT", "Asset injection completed!"); 53 | } 54 | 55 | private static void InjectMusic(SoundManager soundManager) 56 | { 57 | var musicCacheField = typeof(SoundManager).GetField("MusicCache", BindingFlags.NonPublic | BindingFlags.Instance); 58 | var musicCache = musicCacheField.GetValue(soundManager) as Dictionary; 59 | 60 | foreach (var asset in Hat.Instance.GetFullAssetList()) 61 | { 62 | if (!asset.IsMusicFile) continue; 63 | musicCache[asset.AssetPath] = asset.Data; 64 | } 65 | 66 | Logger.Log("HAT", "Music injection completed!"); 67 | } 68 | 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /Installers/IHatInstaller.cs: -------------------------------------------------------------------------------- 1 | namespace HatModLoader.Installers 2 | { 3 | internal interface IHatInstaller 4 | { 5 | void Install(); 6 | void Uninstall(); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /Installers/LoggerModifier.cs: -------------------------------------------------------------------------------- 1 | using Common; 2 | using Microsoft.Xna.Framework; 3 | using MonoMod.RuntimeDetour; 4 | using System.Reflection; 5 | 6 | namespace HatModLoader.Installers 7 | { 8 | internal class LoggerModifier : IHatInstaller 9 | { 10 | private static readonly string LogDirectory = "Debug Logs"; 11 | private static string CustomLoggerPath => Path.Combine(Util.LocalSaveFolder, LogDirectory); 12 | 13 | private static readonly int MaximumLogDays = 30; 14 | 15 | public static Hook LogDetour; 16 | 17 | public void Install() 18 | { 19 | LogDetour = new Hook( 20 | typeof(Logger).GetMethod("Log", new Type[] { typeof(string), typeof(LogSeverity), typeof(string) }), 21 | new Action, string, LogSeverity, string>((orig, component, severity, message) => { 22 | orig(component, severity, message); 23 | LogCrashHandler(component, severity, message); 24 | }) 25 | ); 26 | 27 | SetCustomLoggerPath(); 28 | MoveOriginalLogsToCustomLoggerPath(); 29 | RemoveFilesOlderThanDays(MaximumLogDays); 30 | } 31 | 32 | private static string GetTimestampedLogFileName(DateTime date, int index = 0) 33 | { 34 | return 35 | index == 0 36 | ? $"[{date.ToString("yyyy-MM-dd_HH-mm-ss")}] Debug Log.txt" 37 | : $"[{date.ToString("yyyy-MM-dd_HH-mm-ss")}] Debug Log #{index+1}.txt"; 38 | } 39 | 40 | private static string GetUniqueCustomLogFileName(DateTime date) 41 | { 42 | string path; 43 | int i = 0; 44 | do path = Path.Combine(CustomLoggerPath, GetTimestampedLogFileName(date, i++)); 45 | while (File.Exists(path)); 46 | return path; 47 | } 48 | 49 | private static void SetCustomLoggerPath() 50 | { 51 | if (!Directory.Exists(CustomLoggerPath)) 52 | { 53 | Directory.CreateDirectory(CustomLoggerPath); 54 | } 55 | 56 | var logFilePath = GetUniqueCustomLogFileName(DateTime.Now); 57 | 58 | typeof(Logger).GetField("FirstLog", BindingFlags.NonPublic | BindingFlags.Static).SetValue(null, false); 59 | typeof(Logger).GetField("LogFilePath", BindingFlags.NonPublic | BindingFlags.Static).SetValue(null, logFilePath); 60 | } 61 | 62 | private static void MoveOriginalLogsToCustomLoggerPath() 63 | { 64 | foreach(var file in Directory.EnumerateFiles(Util.LocalSaveFolder, "*Debug Log*.txt")) 65 | { 66 | var fileCreationDate = File.GetCreationTime(file); 67 | var newPath = GetUniqueCustomLogFileName(fileCreationDate); 68 | File.Move(file, newPath); 69 | } 70 | } 71 | 72 | private static void RemoveFilesOlderThanDays(int days) 73 | { 74 | foreach (var file in Directory.EnumerateFiles(CustomLoggerPath, "*Debug Log*.txt")) 75 | { 76 | if ((DateTime.UtcNow - File.GetLastWriteTimeUtc(file)).TotalDays > days) 77 | { 78 | File.Delete(file); 79 | } 80 | } 81 | } 82 | 83 | private static void LogCrashHandler(string component, LogSeverity severity, string message) 84 | { 85 | if (severity != LogSeverity.Error) return; 86 | var FNAPlatformType = Assembly.GetAssembly(typeof(Game)).GetType("Microsoft.Xna.Framework.SDL2_FNAPlatform"); 87 | var ShowRuntimeErrorFunc = FNAPlatformType.GetMethod("ShowRuntimeError", BindingFlags.Public | BindingFlags.Static); 88 | ShowRuntimeErrorFunc.Invoke(null, new object[] { $"FEZ [{component}]", message }); 89 | } 90 | 91 | public void Uninstall() 92 | { 93 | LogDetour.Dispose(); 94 | } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /Installers/ModMenuInstaller.cs: -------------------------------------------------------------------------------- 1 | using Common; 2 | using FezGame; 3 | using HatModLoader.Source; 4 | using MonoMod.RuntimeDetour; 5 | using System.Collections; 6 | using System.Reflection; 7 | 8 | namespace HatModLoader.Installers 9 | { 10 | internal class ModMenuInstaller : IHatInstaller 11 | { 12 | 13 | private static Type MenuLevelType; 14 | private static Type MenuItemType; 15 | private static Type MenuBaseType; 16 | private static Type MainMenuType; 17 | 18 | private static int modMenuCurrentIndex; 19 | 20 | private static Hook MenuInitHook; 21 | 22 | public void Install() 23 | { 24 | MenuLevelType = Assembly.GetAssembly(typeof(Fez)).GetType("FezGame.Structure.MenuLevel"); 25 | MenuItemType = Assembly.GetAssembly(typeof(Fez)).GetType("FezGame.Structure.MenuItem"); 26 | MenuBaseType = Assembly.GetAssembly(typeof(Fez)).GetType("FezGame.Components.MenuBase"); 27 | MainMenuType = Assembly.GetAssembly(typeof(Fez)).GetType("FezGame.Components.MainMenu"); 28 | 29 | MenuInitHook = new Hook( 30 | MenuBaseType.GetMethod("Initialize"), 31 | new Action, object>((orig, self) => { 32 | orig(self); 33 | CreateAndAddModLevel(self); 34 | }) 35 | ); 36 | } 37 | 38 | private static void CreateAndAddModLevel(object MenuBase) 39 | { 40 | const BindingFlags privBind = BindingFlags.NonPublic | BindingFlags.Instance; 41 | 42 | // prepare main menu object 43 | object MenuRoot = null; 44 | if (MenuBase.GetType() == MainMenuType) 45 | { 46 | MenuRoot = MainMenuType.GetField("RealMenuRoot", privBind).GetValue(MenuBase); 47 | } 48 | 49 | if(MenuBase.GetType() != MainMenuType || MenuRoot == null) 50 | { 51 | MenuRoot = MenuBaseType.GetField("MenuRoot", privBind).GetValue(MenuBase); 52 | } 53 | 54 | if(MenuRoot == null) 55 | { 56 | Logger.Log("HAT", LogSeverity.Warning, "Unable to create MODS menu!"); 57 | return; 58 | } 59 | 60 | MenuLevelType.GetField("IsDynamic").SetValue(MenuRoot, true); 61 | // create new level 62 | object ModLevel = Activator.CreateInstance(MenuLevelType); 63 | MenuLevelType.GetField("IsDynamic").SetValue(ModLevel, true); 64 | MenuLevelType.GetProperty("Title").SetValue(ModLevel, "@MODS"); 65 | MenuLevelType.GetField("Parent").SetValue(ModLevel, MenuRoot); 66 | MenuLevelType.GetField("Oversized").SetValue(ModLevel, true); 67 | 68 | 69 | 70 | var MenuLevelAddItemGeneric = MenuLevelType.GetMethods().FirstOrDefault(mi => mi.Name == "AddItem" && mi.GetParameters().Length == 5); 71 | var MenuLevelAddItemInt = MenuLevelAddItemGeneric.MakeGenericMethod(new Type[] { typeof(int) }); 72 | 73 | if (Hat.Instance.Mods.Count > 0) 74 | { 75 | var menuIteratorItem = MenuLevelAddItemInt.Invoke(ModLevel, new object[] { 76 | null, (Action)delegate { }, false, 77 | (Func) delegate{ return modMenuCurrentIndex; }, 78 | (Action) delegate(int value, int change) { 79 | modMenuCurrentIndex += change; 80 | if (modMenuCurrentIndex < 0) modMenuCurrentIndex = Hat.Instance.Mods.Count-1; 81 | if (modMenuCurrentIndex >= Hat.Instance.Mods.Count) modMenuCurrentIndex = 0; 82 | } 83 | }); 84 | MenuItemType.GetProperty("SuffixText").SetValue(menuIteratorItem, (Func)delegate 85 | { 86 | return $"{modMenuCurrentIndex + 1} / {Hat.Instance.Mods.Count}"; 87 | }); 88 | } 89 | 90 | Action> AddInactiveStringItem = delegate (string name, Func suffix) 91 | { 92 | var item = MenuLevelType.GetMethod("AddItem", new Type[] { typeof(string) }) 93 | .Invoke(ModLevel, new object[] {name}); 94 | MenuItemType.GetProperty("Selectable").SetValue(item, false); 95 | if(suffix != null) 96 | { 97 | MenuItemType.GetProperty("SuffixText").SetValue(item, suffix); 98 | } 99 | }; 100 | 101 | if (Hat.Instance.Mods.Count == 0) 102 | { 103 | AddInactiveStringItem(null, () => "No HAT Mods Installed"); 104 | } 105 | else 106 | { 107 | AddInactiveStringItem(null, null); 108 | AddInactiveStringItem(null, () => Hat.Instance.Mods[modMenuCurrentIndex].Info.Name); 109 | AddInactiveStringItem(null, () => Hat.Instance.Mods[modMenuCurrentIndex].Info.Description); 110 | AddInactiveStringItem(null, () => $"made by {Hat.Instance.Mods[modMenuCurrentIndex].Info.Author}"); 111 | AddInactiveStringItem(null, () => $"version {Hat.Instance.Mods[modMenuCurrentIndex].Info.Version}"); 112 | } 113 | 114 | // add created menu level to the main menu 115 | int modsIndex = ((IList)MenuLevelType.GetField("Items").GetValue(MenuRoot)).Count - 2; 116 | MenuLevelType.GetMethod("AddItem", new Type[] { typeof(string), typeof(Action), typeof(int) }) 117 | .Invoke(MenuRoot, new object[] { "@MODS", (Action) delegate{ 118 | MenuBaseType.GetMethod("ChangeMenuLevel").Invoke(MenuBase, new object[] { ModLevel, false }); 119 | }, modsIndex}); 120 | 121 | // needed to refresh the menu before the transition to it happens (pause menu) 122 | MenuBaseType.GetMethod("RenderToTexture", privBind).Invoke(MenuBase, new object[] { }); 123 | } 124 | 125 | public void Uninstall() 126 | { 127 | MenuInitHook.Dispose(); 128 | } 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Krzyhau 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Patches/Fez.cs: -------------------------------------------------------------------------------- 1 | using FezEngine.Tools; 2 | using HatModLoader.Helpers; 3 | using HatModLoader.Installers; 4 | using HatModLoader.Source; 5 | using Microsoft.Xna.Framework; 6 | using MonoMod; 7 | using System.Reflection; 8 | 9 | namespace FezGame 10 | { 11 | class patch_Fez : Fez 12 | { 13 | public static Hat HatML; 14 | 15 | public extern void orig_ctor(); 16 | [MonoModConstructor] 17 | public void ctor() 18 | { 19 | // executing IHatInstallers in a constructor so it can be called before everything else 20 | foreach (Type type in Assembly.GetExecutingAssembly().GetTypes() 21 | .Where(t => t.IsClass && typeof(IHatInstaller).IsAssignableFrom(t))) 22 | { 23 | IHatInstaller installer = (IHatInstaller)Activator.CreateInstance(type); 24 | installer.Install(); 25 | } 26 | 27 | orig_ctor(); 28 | } 29 | 30 | protected extern void orig_Initialize(); 31 | protected override void Initialize() 32 | { 33 | HatML = new Hat(this); 34 | HatML.InitalizeAssemblies(); 35 | //HatML.InitializeAssets(musicPass: false); 36 | orig_Initialize(); 37 | DrawingTools.Init(); 38 | //HatML.InitializeAssets(musicPass: true); 39 | } 40 | 41 | internal static extern void orig_LoadComponents(Fez game); 42 | internal static void LoadComponents(Fez game) 43 | { 44 | bool doLoad = !ServiceHelper.FirstLoadDone; 45 | orig_LoadComponents(game); 46 | if (doLoad) { 47 | HatML.InitalizeComponents(); 48 | } 49 | } 50 | 51 | protected extern void orig_Update(GameTime gameTime); 52 | protected override void Update(GameTime gameTime) 53 | { 54 | InputHelper.Update(gameTime); 55 | orig_Update(gameTime); 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Patches/FezLogo.cs: -------------------------------------------------------------------------------- 1 | using FezEngine.Effects; 2 | using FezEngine.Structure; 3 | using FezEngine.Structure.Geometry; 4 | using FezEngine.Tools; 5 | using HatModLoader.Helpers; 6 | using HatModLoader.Source; 7 | using Microsoft.Xna.Framework; 8 | using Microsoft.Xna.Framework.Graphics; 9 | using System.Reflection; 10 | 11 | namespace FezGame.Components 12 | { 13 | public class patch_FezLogo : FezLogo 14 | { 15 | public patch_FezLogo(Game game) : base(game){} 16 | 17 | public extern void orig_Initialize(); 18 | public override void Initialize() 19 | { 20 | orig_Initialize(); 21 | 22 | // custom very cool procedural logo creation code 23 | 24 | var LogoMesh = new Mesh 25 | { 26 | AlwaysOnTop = true, 27 | DepthWrites = false, 28 | Blending = BlendingMode.Alphablending 29 | }; 30 | var WireMesh = new Mesh 31 | { 32 | DepthWrites = false, 33 | AlwaysOnTop = true 34 | }; 35 | 36 | var LogoMap = new string[] 37 | { 38 | "# # ### ###", 39 | "### ### # ", 40 | "### ### # ", 41 | "# # # # # " 42 | }; 43 | 44 | var logoWidth = LogoMap[0].Length; 45 | var logoHeight = LogoMap.Length; 46 | 47 | Func IsFilled = delegate (int x, int y) 48 | { 49 | y = logoHeight - (y + 1); 50 | if (x < 0 || x >= logoWidth || y < 0 || y >= logoHeight) return false; 51 | return LogoMap[y][x] == '#'; 52 | }; 53 | 54 | 55 | var WireMeshVertices = new List(); 56 | var WireMeshIndices = new List(); 57 | 58 | Action AddPoint = delegate (Vector3 pos) 59 | { 60 | int index = WireMeshVertices.IndexOf(pos); 61 | if (index < 0) 62 | { 63 | index = WireMeshVertices.Count; 64 | WireMeshVertices.Add(pos); 65 | } 66 | WireMeshIndices.Add(index); 67 | }; 68 | 69 | Action Line = delegate (float x1, float y1, float x2, float y2) 70 | { 71 | if (x1 == x2 && y1 == y2) 72 | { 73 | AddPoint(new Vector3(x1, y1, 0.0f)); 74 | AddPoint(new Vector3(x1, y1, 1.0f)); 75 | } 76 | else for(float i = 0.0f; i <= 1.0f; i++) 77 | { 78 | AddPoint(new Vector3(x1, y1, i)); 79 | AddPoint(new Vector3(x2, y2, i)); 80 | } 81 | }; 82 | 83 | for (int x = 0; x < logoWidth; x++) 84 | { 85 | for (int y = 0; y < logoHeight; y++) 86 | { 87 | // colored box for LogoMesh 88 | if (!IsFilled(x, y)) continue; 89 | LogoMesh.AddColoredBox(Vector3.One, new Vector3(x, y, 0f), Color.Black, centeredOnOrigin: false); 90 | 91 | // wireframe for WireMesh 92 | bool top = IsFilled(x, y + 1); 93 | bool bottom = IsFilled(x, y - 1); 94 | bool left = IsFilled(x - 1, y); 95 | bool right = IsFilled(x + 1, y); 96 | bool topleft = IsFilled(x - 1, y + 1); 97 | bool topright = IsFilled(x + 1, y + 1); 98 | bool bottomleft = IsFilled(x - 1, y - 1); 99 | bool bottomright = IsFilled(x + 1, y - 1); 100 | 101 | if (!top) Line(x, y + 1, x + 1, y + 1); 102 | if (!bottom) Line(x, y, x + 1, y); 103 | if (!right) Line(x + 1, y, x + 1, y + 1); 104 | if (!left) Line(x, y, x, y + 1); 105 | if ((!top && !left) || (top && left && !topleft)) Line(x, y + 1, x, y + 1); 106 | if ((!top && !right) || (top && right && !topright)) Line(x + 1, y + 1, x + 1, y + 1); 107 | if ((!bottom && !left) || (bottom && left && !bottomleft)) Line(x, y, x, y); 108 | if ((!bottom && !right) || (bottom && right && !bottomright)) Line(x + 1, y, x + 1, y); 109 | } 110 | } 111 | 112 | 113 | IndexedUserPrimitives indexedUserPrimitives = (IndexedUserPrimitives)(WireMesh.AddGroup().Geometry = new IndexedUserPrimitives(PrimitiveType.LineList)); 114 | 115 | indexedUserPrimitives.Vertices = WireMeshVertices.Select(pos => new FezVertexPositionColor(pos, Color.White)).ToArray(); 116 | indexedUserPrimitives.Indices = WireMeshIndices.ToArray(); 117 | 118 | WireMesh.Position = LogoMesh.Position = new Vector3(-logoWidth * 0.5f, -logoHeight * 0.5f, -0.5f); 119 | WireMesh.BakeTransform(); 120 | LogoMesh.BakeTransform(); 121 | LogoMesh.Material.Opacity = 0f; 122 | 123 | var FezEffectField = typeof(FezLogo).GetField("FezEffect", BindingFlags.NonPublic | BindingFlags.Instance); 124 | var LogoMeshField = typeof(FezLogo).GetField("LogoMesh", BindingFlags.NonPublic | BindingFlags.Instance); 125 | var WireMeshField = typeof(FezLogo).GetField("WireMesh", BindingFlags.NonPublic | BindingFlags.Instance); 126 | 127 | DrawActionScheduler.Schedule(delegate 128 | { 129 | WireMesh.Effect = LogoMesh.Effect = (BaseEffect)FezEffectField.GetValue(this); 130 | }); 131 | 132 | LogoMeshField.SetValue(this, LogoMesh); 133 | WireMeshField.SetValue(this, WireMesh); 134 | } 135 | 136 | public extern void orig_Draw(GameTime gameTime); 137 | public override void Draw(GameTime gameTime) 138 | { 139 | orig_Draw(gameTime); 140 | 141 | if (Hat.Instance == null) return; 142 | 143 | float alpha = Math.Max(0, Math.Min(Starfield.Opacity, 1.0f - SinceStarted)); 144 | if (alpha == 0.0f) return; 145 | 146 | Viewport viewport = DrawingTools.GetViewport(); 147 | 148 | int modCount = Hat.Instance.Mods.Count; 149 | string hatText = $"HAT Mod Loader, version {Hat.Version}, {modCount} mod{(modCount != 1 ? "s" : "")} installed"; 150 | if (modCount == 69) hatText += "... nice"; 151 | 152 | Color textColor = Color.Lerp(Color.White, Color.Black, alpha); 153 | Color warningColor = Color.Lerp(Color.White, Color.Red, alpha); 154 | 155 | float lineHeight = DrawingTools.DefaultFont.LineSpacing * DrawingTools.DefaultFontSize; 156 | 157 | int invalidModCount = Hat.Instance.InvalidMods.Count; 158 | string invalidModsText = $"Could not load {invalidModCount} mod{(invalidModCount != 1 ? "s" : "")}. Check logs for more details."; 159 | 160 | DrawingTools.BeginBatch(); 161 | if(invalidModCount == 0) 162 | { 163 | DrawingTools.DrawText(hatText, new Vector2(30, viewport.Height - 80), textColor); 164 | } 165 | else 166 | { 167 | DrawingTools.DrawText(hatText, new Vector2(30, viewport.Height - 80 - lineHeight), textColor); 168 | DrawingTools.DrawText(invalidModsText, new Vector2(30, viewport.Height - 80), warningColor); 169 | } 170 | DrawingTools.EndBatch(); 171 | } 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /Patches/Program.cs: -------------------------------------------------------------------------------- 1 | using Common; 2 | using HatModLoader.Source; 3 | using System.Globalization; 4 | using System.Runtime.InteropServices; 5 | 6 | namespace FezGame 7 | { 8 | internal static class patch_Program 9 | { 10 | private static extern void orig_Main(string[] args); 11 | 12 | private static void Main(string[] args) 13 | { 14 | // Ensuring that dependency resolver is registered as soon as it's possible. 15 | DependencyResolver.Register(); 16 | 17 | // Ensure uniform culture 18 | Thread.CurrentThread.CurrentCulture = CultureInfo.GetCultureInfo("en-GB"); 19 | 20 | // The game is encapsulating the main game component in a Logger-based try-catch. 21 | // However, occasionally, error can occur during HAT initialisation, or when the 22 | // game is shutting down. We want to keep track of it. 23 | 24 | Logger.Try(orig_Main, args); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Patches/TextPatch.cs: -------------------------------------------------------------------------------- 1 | namespace FezGame.Tools 2 | { 3 | internal static class TextPatch 4 | { 5 | public static string GetRawOrDefault(string tag, string defaultText) 6 | { 7 | // returns original text if it's prefixed with @ 8 | // allows easier injection of custom text into in-game UI structures like main menu 9 | 10 | if (tag.StartsWith("@")) return tag.Substring(1); 11 | return defaultText; 12 | } 13 | } 14 | 15 | public static class patch_StaticText 16 | { 17 | public static extern string orig_GetString(string tag); 18 | public static string GetString(string tag) => TextPatch.GetRawOrDefault(tag, orig_GetString(tag)); 19 | } 20 | 21 | public static class patch_GameText 22 | { 23 | public static extern string orig_GetString(string tag); 24 | public static string GetString(string tag) => TextPatch.GetRawOrDefault(tag, orig_GetString(tag)); 25 | 26 | public static extern string orig_GetStringRaw(string tag); 27 | public static string GetStringRaw(string tag) => TextPatch.GetRawOrDefault(tag, orig_GetStringRaw(tag)); 28 | } 29 | 30 | public static class patch_CreditsText 31 | { 32 | public static extern string orig_GetString(string tag); 33 | public static string GetString(string tag) => TextPatch.GetRawOrDefault(tag, orig_GetString(tag)); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Properties/MonoModRules.cs: -------------------------------------------------------------------------------- 1 | namespace MonoMod 2 | { 3 | static class MonoModRules 4 | { 5 | static MonoModRules() 6 | { 7 | 8 | } 9 | } 10 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # HAT - Simple mod loader for FEZ 2 | 3 | ![Thumbnail](Docs/thumbnail.png) 4 | 5 | ## Overview 6 | 7 | **HAT** is a [MonoMod](https://github.com/MonoMod/MonoMod)-based mod loader for FEZ, currently in development. Its main purpose is to make process of FEZ modding slightly easier for end user. 8 | 9 | When patched into the FEZ instance, it can be used to dynamically load game modifications on the game launch. Correctly prepared mods can add/override game assets or inject its own logic through custom-made plugin. 10 | 11 | ## Installing mod loader 12 | 13 | 1. Download latest `HAT.zip` from Release tab and unpack it in the game's directory (next to the `FEZ.exe`). 14 | 2. Run `hat_install.bat` (for Windows) or `hat_install.sh` (for Linux). This should generate new executable file called `MONOMODDED_FEZ.exe`. 15 | 3. Run `MONOMODDED_FEZ.exe` and enjoy modding! 16 | 17 | In the future, this process will be automated by a custom-made installer/mod manager (something like Olympus for Celeste's Everest). 18 | 19 | ## Adding mods 20 | 21 | 1. On first HAT launch, `Mods` directory should be created in the executable's directory. If not, create it. 22 | 2. Download the mod's archive and put it in this directory. 23 | 3. Start the game with `MONOMODDED_FEZ.exe` and enjoy your mod! 24 | 25 | It's that simple! 26 | 27 | ## Building HAT 28 | 29 | HAT is now using stripped game binaries and NuGet packages for building process, so it is not required to configure anything. Building HAT libraries should be as easy as cloning the repository and running the building process within the IDE of your choice (or through dotnet CLI if that's your thing). 30 | 31 | ## "Documentation" 32 | 33 | * [Create your own HAT modifications](/Docs/createmods.md) 34 | * [Additional HAT behaviour](/Docs/additional.md) 35 | 36 | ## Mods created for HAT 37 | 38 | * [FEZUG](https://github.com/Krzyhau/FEZUG) - a power tool for speedrun practicing and messing with the game 39 | * [FezSonezSkin](https://github.com/Krzyhau/FezSonezSkin) - mod replacing Gomez skin with Sonic-like guy seen in Speedrun Mode thumbnail 40 | * [FezMultiplayerMod](https://github.com/FEZModding/FezMultiplayerMod) - mod adding multiplayer functionalities to FEZ 41 | -------------------------------------------------------------------------------- /Source/Assets/Asset.cs: -------------------------------------------------------------------------------- 1 | using FEZRepacker.Core.Conversion; 2 | using FEZRepacker.Core.FileSystem; 3 | using FEZRepacker.Core.XNB; 4 | using System.IO.Compression; 5 | 6 | namespace HatModLoader.Source.Assets 7 | { 8 | public class Asset 9 | { 10 | public string AssetPath { get; private set; } 11 | public string Extension { get; private set; } 12 | public byte[] Data { get; private set; } 13 | public bool IsMusicFile { get; private set; } 14 | 15 | public Asset(string path, string extension) 16 | { 17 | AssetPath = path; 18 | Extension = extension; 19 | 20 | CheckMusicAsset(); 21 | } 22 | 23 | public Asset(string path, string extension, byte[] data) 24 | : this(path, extension) 25 | { 26 | Data = data; 27 | } 28 | 29 | public Asset(string path, string extension, Stream data) 30 | : this(path, extension) 31 | { 32 | Data = new byte[data.Length]; 33 | data.Read(Data, 0, Data.Length); 34 | } 35 | 36 | private void CheckMusicAsset() 37 | { 38 | if (Extension == ".ogg" && AssetPath.StartsWith("music\\")) 39 | { 40 | IsMusicFile = true; 41 | AssetPath = AssetPath.Substring("music\\".Length); 42 | } 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Source/Assets/AssetLoaderHelper.cs: -------------------------------------------------------------------------------- 1 |  2 | using Common; 3 | using FEZRepacker.Core.Conversion; 4 | using FEZRepacker.Core.FileSystem; 5 | using FEZRepacker.Core.XNB; 6 | 7 | namespace HatModLoader.Source.Assets 8 | { 9 | internal static class AssetLoaderHelper 10 | { 11 | private static readonly string[] AllowedRawExtensions = { ".xnb", ".ogg", ".fxc" }; 12 | 13 | public static List GetListFromFileDictionary(Dictionary files) 14 | { 15 | var assets = new List(); 16 | 17 | var bundles = FileBundle.BundleFiles(files); 18 | 19 | foreach (var bundle in bundles) 20 | { 21 | try 22 | { 23 | var deconvertedObject = FormatConversion.Deconvert(bundle)!; 24 | using var xnbData = XnbSerializer.Serialize(deconvertedObject); 25 | 26 | assets.Add(new Asset(bundle.BundlePath, ".xnb", xnbData)); 27 | } 28 | catch(Exception ex) 29 | { 30 | bool savedAnyRawFiles = false; 31 | foreach (var file in bundle.Files) 32 | { 33 | var extension = file.Extension; 34 | if (extension.Length == 0) extension = bundle.MainExtension; 35 | if (!AllowedRawExtensions.Contains(extension)) continue; 36 | 37 | file.Data.Seek(0, SeekOrigin.Begin); 38 | assets.Add(new Asset(bundle.BundlePath, extension, file.Data)); 39 | savedAnyRawFiles = true; 40 | } 41 | 42 | if (!savedAnyRawFiles) 43 | { 44 | Logger.Log("HAT", $"Could not convert asset bundle {bundle.BundlePath}: {ex.Message}\n{ex.StackTrace}"); 45 | } 46 | } 47 | 48 | bundle.Dispose(); 49 | } 50 | 51 | return assets; 52 | } 53 | 54 | public static List LoadPakPackage(Stream stream) 55 | { 56 | var assets = new List(); 57 | 58 | using var pakReader = new PakReader(stream); 59 | foreach(var file in pakReader.ReadFiles()) 60 | { 61 | using var fileData = file.Open(); 62 | assets.Add(new Asset(file.Path, file.FindExtension(), fileData)); 63 | } 64 | 65 | return assets; 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /Source/DependencyResolver.cs: -------------------------------------------------------------------------------- 1 | using Common; 2 | using System.Reflection; 3 | 4 | namespace HatModLoader.Source 5 | { 6 | internal static class DependencyResolver 7 | { 8 | private static readonly string DependencyDirectory = "HATDependencies"; 9 | 10 | private static readonly Dictionary DependencyMap = new(); 11 | private static readonly Dictionary DependencyCache = new(); 12 | 13 | public static void Register() 14 | { 15 | AppDomain.CurrentDomain.AssemblyResolve += ResolveAssembliesEventHandler; 16 | } 17 | 18 | private static Assembly ResolveAssembliesEventHandler(object sender, ResolveEventArgs args) 19 | { 20 | FillInDependencyMap(args); 21 | 22 | Assembly assembly; 23 | if (DependencyCache.TryGetValue(IsolateName(args.Name), out assembly)) return assembly; 24 | if (TryResolveAssemblyFor("MonoMod", args, out assembly)) return assembly; 25 | if (TryResolveAssemblyFor("FEZRepacker.Core", args, out assembly)) return assembly; 26 | if (TryResolveModdedDependency(args, out assembly)) return assembly; 27 | 28 | Logger.Log("HAT", "Could not resolve assembly: \"" + args.Name + "\", required by \"" + args.RequestingAssembly?.FullName ?? "(none)" + "\""); 29 | 30 | return default!; 31 | } 32 | 33 | private static void FillInDependencyMap(ResolveEventArgs args) 34 | { 35 | if (args.RequestingAssembly == null) return; 36 | 37 | var assemblyName = IsolateName(args.Name); 38 | var requestingAssemblyName = IsolateName(args.RequestingAssembly?.FullName ?? ""); 39 | 40 | if (DependencyMap.ContainsKey(requestingAssemblyName)) 41 | { 42 | DependencyMap[assemblyName] = DependencyMap[requestingAssemblyName]; 43 | } 44 | else 45 | { 46 | DependencyMap[assemblyName] = args.RequestingAssembly!; 47 | } 48 | } 49 | 50 | private static bool TryResolveAssemblyFor(string assemblyName, ResolveEventArgs args, out Assembly assembly) 51 | { 52 | if (!ShouldResolveNamedFor(assemblyName, args)) 53 | { 54 | assembly = default!; 55 | return false; 56 | } 57 | 58 | var requiredAssemblyName = args.Name.Split(',')[0]; 59 | var dependencyPath = Path.Combine(DependencyDirectory, assemblyName); 60 | 61 | foreach (var file in Directory.EnumerateFiles(dependencyPath)) 62 | { 63 | var fileName = Path.GetFileNameWithoutExtension(file); 64 | 65 | if (requiredAssemblyName == fileName) 66 | { 67 | assembly = Assembly.Load(File.ReadAllBytes(file)); 68 | DependencyCache[requiredAssemblyName] = assembly; 69 | return true; 70 | } 71 | } 72 | assembly = default!; 73 | return false; 74 | } 75 | 76 | private static bool ShouldResolveNamedFor(string assemblyName, ResolveEventArgs args) 77 | { 78 | var requiredAssemblyName = IsolateName(args.Name); 79 | var requestingAssemblyName = IsolateName(args.RequestingAssembly?.FullName ?? ""); 80 | var mainRequestingAssemblyName = IsolateName(GetMainRequiringAssembly(args)?.FullName ?? ""); 81 | 82 | bool requiredAssemblyValid = requiredAssemblyName.Contains(assemblyName); 83 | bool requestingAssemblyValid = requestingAssemblyName.Contains(assemblyName); 84 | bool mainRequestingAssemblyValid = mainRequestingAssemblyName.Contains(assemblyName); 85 | 86 | return requiredAssemblyValid || requestingAssemblyValid || mainRequestingAssemblyValid; 87 | } 88 | 89 | private static bool TryResolveModdedDependency(ResolveEventArgs args, out Assembly assembly) 90 | { 91 | assembly = default!; 92 | 93 | if (Hat.Instance == null) return false; 94 | 95 | var requestingMainAssembly = GetMainRequiringAssembly(args); 96 | if (requestingMainAssembly == null) return false; 97 | 98 | var matchingAssembliesInMods = Hat.Instance.Mods 99 | .Where(mod => mod.Assembly == requestingMainAssembly); 100 | if (!matchingAssembliesInMods.Any()) return false; 101 | 102 | var requiredAssemblyName = IsolateName(args.Name); 103 | var requiredAssemblyPath = requiredAssemblyName + ".dll"; 104 | var fileProxy = matchingAssembliesInMods.First().FileProxy; 105 | if (!fileProxy.FileExists(requiredAssemblyPath)) return false; 106 | 107 | using var assemblyData = fileProxy.OpenFile(requiredAssemblyPath); 108 | var assemblyBytes = new byte[assemblyData.Length]; 109 | assemblyData.Read(assemblyBytes, 0, assemblyBytes.Length); 110 | assembly = Assembly.Load(assemblyBytes); 111 | DependencyCache[requiredAssemblyName] = assembly; 112 | return true; 113 | } 114 | 115 | private static Assembly GetMainRequiringAssembly(ResolveEventArgs args) 116 | { 117 | var requestingMainAssembly = args.RequestingAssembly; 118 | 119 | if(requestingMainAssembly == null) 120 | { 121 | return default!; 122 | } 123 | 124 | var requestingAssemblyName = IsolateName(requestingMainAssembly.FullName); 125 | 126 | if (DependencyMap.ContainsKey(requestingAssemblyName)) 127 | { 128 | requestingMainAssembly = DependencyMap[requestingAssemblyName]; 129 | } 130 | 131 | return requestingMainAssembly; 132 | } 133 | 134 | private static string IsolateName(string fullAssemblyQualifier) 135 | { 136 | return fullAssemblyQualifier.Split(',')[0]; 137 | } 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /Source/FileProxies/DirectoryFileProxy.cs: -------------------------------------------------------------------------------- 1 | namespace HatModLoader.Source.FileProxies 2 | { 3 | internal class DirectoryFileProxy : IFileProxy 4 | { 5 | private string modDirectory; 6 | 7 | public string RootPath => modDirectory; 8 | public string ContainerName => new DirectoryInfo(modDirectory).Name; 9 | 10 | public DirectoryFileProxy(string directoryPath) 11 | { 12 | modDirectory = directoryPath; 13 | } 14 | 15 | public IEnumerable EnumerateFiles(string localPath) 16 | { 17 | var searchPath = Path.Combine(modDirectory, localPath); 18 | 19 | if(!Directory.Exists(searchPath)) 20 | { 21 | return Enumerable.Empty(); 22 | } 23 | 24 | var localFilePaths = Directory.EnumerateFiles(searchPath, "*", SearchOption.AllDirectories) 25 | .Select(path => path.Substring(modDirectory.Length + 1)); 26 | 27 | return localFilePaths; 28 | } 29 | 30 | public bool FileExists(string localPath) 31 | { 32 | return File.Exists(Path.Combine(modDirectory, localPath)); 33 | } 34 | 35 | public Stream OpenFile(string localPath) 36 | { 37 | return File.OpenRead(Path.Combine(modDirectory, localPath)); 38 | } 39 | 40 | public void Dispose() { } 41 | 42 | 43 | public static IEnumerable EnumerateInDirectory(string directory) 44 | { 45 | return Directory.EnumerateDirectories(directory) 46 | .Select(path => new DirectoryFileProxy(path)); 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /Source/FileProxies/IFileProxy.cs: -------------------------------------------------------------------------------- 1 | namespace HatModLoader.Source.FileProxies 2 | { 3 | public interface IFileProxy : IDisposable 4 | { 5 | public string RootPath { get; } 6 | public string ContainerName { get; } 7 | public IEnumerable EnumerateFiles(string localPath); 8 | public bool FileExists(string localPath); 9 | public Stream OpenFile(string localPath); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Source/FileProxies/ZipFileProxy.cs: -------------------------------------------------------------------------------- 1 | using System.IO.Compression; 2 | 3 | namespace HatModLoader.Source.FileProxies 4 | { 5 | public class ZipFileProxy : IFileProxy 6 | { 7 | private ZipArchive archive; 8 | private string zipPath; 9 | public string RootPath => zipPath; 10 | public string ContainerName => Path.GetFileName(zipPath); 11 | 12 | public ZipFileProxy(string zipPath) 13 | { 14 | this.zipPath = zipPath; 15 | archive = ZipFile.Open(zipPath, ZipArchiveMode.Update); 16 | } 17 | 18 | public IEnumerable EnumerateFiles(string localPath) 19 | { 20 | if (!localPath.EndsWith("/")) localPath += "/"; 21 | 22 | return archive.Entries 23 | .Where(e => e.FullName.StartsWith(localPath)) 24 | .Select(e => e.FullName); 25 | } 26 | 27 | public bool FileExists(string localPath) 28 | { 29 | return archive.Entries.Where(e => e.FullName == localPath).Any(); 30 | } 31 | 32 | public Stream OpenFile(string localPath) 33 | { 34 | return archive.Entries.Where(e => e.FullName == localPath).First().Open(); 35 | } 36 | 37 | public void Dispose() 38 | { 39 | archive.Dispose(); 40 | } 41 | 42 | public static IEnumerable EnumerateInDirectory(string directory) 43 | { 44 | return Directory.EnumerateFiles(directory) 45 | .Where(file => Path.GetExtension(file).Equals(".zip", StringComparison.OrdinalIgnoreCase)) 46 | .Select(file => new ZipFileProxy(file)); 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /Source/Hat.cs: -------------------------------------------------------------------------------- 1 | using Common; 2 | using FezGame; 3 | using HatModLoader.Source.Assets; 4 | using HatModLoader.Source.FileProxies; 5 | using HatModLoader.Source.ModDefinition; 6 | 7 | namespace HatModLoader.Source 8 | { 9 | public class Hat 10 | { 11 | private List ignoredModNames = new(); 12 | private List priorityModNamesList = new(); 13 | 14 | public static Hat Instance; 15 | 16 | public Fez Game; 17 | public List Mods; 18 | public List InvalidMods; 19 | 20 | public static string Version 21 | { 22 | get 23 | { 24 | const string version = "1.2.1"; 25 | #if DEBUG 26 | return $"{version}-dev"; 27 | #else 28 | return $"{version}"; 29 | #endif 30 | } 31 | } 32 | 33 | 34 | public Hat(Fez fez) 35 | { 36 | Instance = this; 37 | Game = fez; 38 | 39 | Mods = new List(); 40 | InvalidMods = new List(); 41 | 42 | Logger.Log("HAT", $"HAT Mod Loader {Version}"); 43 | PrepareMods(); 44 | } 45 | 46 | 47 | private void PrepareMods() 48 | { 49 | LoadMods(); 50 | 51 | if(Mods.Count == 0) 52 | { 53 | Logger.Log("HAT", $"No mods have been found in the directory."); 54 | return; 55 | } 56 | 57 | InitializeIgnoredModsList(); 58 | InitializePriorityList(); 59 | 60 | RemoveBlacklistedMods(); 61 | SortModsByPriority(); 62 | RemoveDuplicates(); 63 | InitializeDependencies(); 64 | FilterOutInvalidMods(); 65 | SortModsBasedOnDependencies(); 66 | 67 | LogLoadedMods(); 68 | } 69 | 70 | private void EnsureModDirectory() 71 | { 72 | if (!Directory.Exists(Mod.GetModsDirectory())) 73 | { 74 | Logger.Log("HAT", LogSeverity.Warning, "Main mods directory not found. Creating and skipping mod loading process..."); 75 | Directory.CreateDirectory(Mod.GetModsDirectory()); 76 | return; 77 | } 78 | } 79 | 80 | private void LoadMods() 81 | { 82 | Mods.Clear(); 83 | 84 | EnsureModDirectory(); 85 | 86 | var modProxies = EnumerateFileProxiesInModsDirectory(); 87 | 88 | foreach (var proxy in modProxies) 89 | { 90 | bool loadingState = Mod.TryLoad(this, proxy, out Mod mod); 91 | if (loadingState) 92 | { 93 | Mods.Add(mod); 94 | } 95 | LogModLoadingState(mod, loadingState); 96 | } 97 | } 98 | 99 | private static IEnumerable EnumerateFileProxiesInModsDirectory() 100 | { 101 | var modsDir = Mod.GetModsDirectory(); 102 | 103 | return new IEnumerable[] 104 | { 105 | DirectoryFileProxy.EnumerateInDirectory(modsDir), 106 | ZipFileProxy.EnumerateInDirectory(modsDir), 107 | } 108 | .SelectMany(x => x); 109 | } 110 | 111 | private void LogModLoadingState(Mod mod, bool loadState) 112 | { 113 | if (loadState) 114 | { 115 | var libraryInfo = "no library"; 116 | if (mod.IsCodeMod) 117 | { 118 | libraryInfo = $"library \"{mod.Info.LibraryName}\""; 119 | } 120 | var assetsText = $"{mod.Assets.Count} asset{(mod.Assets.Count != 1 ? "s" : "")}"; 121 | Logger.Log("HAT", $"Loaded mod \"{mod.Info.Name}\" ver. {mod.Info.Version} by {mod.Info.Author} ({assetsText} and {libraryInfo})"); 122 | } 123 | else 124 | { 125 | if (mod.Info.Name == null) 126 | { 127 | Logger.Log("HAT", LogSeverity.Warning, $"Mod \"{mod.FileProxy.ContainerName}\" does not have a valid metadata file."); 128 | } 129 | else if (mod.Info.LibraryName != null && mod.Info.LibraryName.Length > 0 && !mod.IsCodeMod) 130 | { 131 | var info = $"Mod \"{mod.Info.Name}\" has library name defined (\"{mod.Info.LibraryName}\"), but no such library was found."; 132 | Logger.Log("HAT", LogSeverity.Warning, info); 133 | } 134 | else if (!mod.IsCodeMod && !mod.IsAssetMod) 135 | { 136 | Logger.Log("HAT", LogSeverity.Warning, $"Mod \"{mod.Info.Name}\" is empty and will not be added."); 137 | } 138 | } 139 | } 140 | 141 | private void InitializeIgnoredModsList() 142 | { 143 | var ignoredModsNamesFilePath = Path.Combine(Mod.GetModsDirectory(), "ignorelist.txt"); 144 | var defaultContent = 145 | "# List of directories and zip archives to ignore when loading mods, one per line.\n" + 146 | "# Lines starting with # will be ignored.\n\n" + 147 | "ExampleDirectoryModName\n" + 148 | "ExampleZipPackageName.zip\n"; 149 | ignoredModNames = ModsTextListLoader.LoadOrCreateDefault(ignoredModsNamesFilePath, defaultContent); 150 | } 151 | 152 | private void InitializePriorityList() 153 | { 154 | var priorityListFilePath = Path.Combine(Mod.GetModsDirectory(), "prioritylist.txt"); 155 | var defaultContent = 156 | "# List of directories and zip archives to prioritize during mod loading.\n" + 157 | "# If present on this list, the mod will be loaded before other mods not listed here or listed below it,\n" + 158 | "# including newer versions of the same mod. However, it does not override dependency ordering.\n" + 159 | "# Lines starting with # will be ignored.\n\n" + 160 | "ExampleDirectoryModName\n" + 161 | "ExampleZipPackageName.zip\n"; 162 | priorityModNamesList = ModsTextListLoader.LoadOrCreateDefault(priorityListFilePath, defaultContent); 163 | } 164 | 165 | private void RemoveBlacklistedMods() 166 | { 167 | Mods = Mods.Where(mod => !ignoredModNames.Contains(mod.FileProxy.ContainerName)).ToList(); 168 | } 169 | 170 | private int GetPriorityIndexOfMod(Mod mod) 171 | { 172 | var index = priorityModNamesList.IndexOf(mod.FileProxy.ContainerName); 173 | if (index == -1) index = int.MaxValue; 174 | 175 | return index; 176 | } 177 | private void SortModsByPriority() 178 | { 179 | Mods.Sort((mod1, mod2) => 180 | { 181 | var priorityIndex1 = GetPriorityIndexOfMod(mod1); 182 | var priorityIndex2 = GetPriorityIndexOfMod(mod2); 183 | return priorityIndex1.CompareTo(priorityIndex2); 184 | }); 185 | } 186 | 187 | private int CompareDuplicateMods(Mod mod1, Mod mod2) 188 | { 189 | var priorityIndex1 = GetPriorityIndexOfMod(mod1); 190 | var priorityIndex2 = GetPriorityIndexOfMod(mod2); 191 | var priorityComparison = priorityIndex1.CompareTo(priorityIndex2); 192 | 193 | if(priorityComparison != 0) 194 | { 195 | return priorityComparison; 196 | } 197 | else 198 | { 199 | // Newest (largest) versions should be first, hence the negative sign. 200 | return -mod1.CompareVersionsWith(mod2); 201 | } 202 | } 203 | 204 | private void RemoveDuplicates() 205 | { 206 | var uniqueNames = Mods.Select(mod => mod.Info.Name).Distinct().ToList(); 207 | foreach (var modName in uniqueNames) 208 | { 209 | var sameNamedMods = Mods.Where(mod => mod.Info.Name == modName).ToList(); 210 | if (sameNamedMods.Count() > 1) 211 | { 212 | sameNamedMods.Sort(CompareDuplicateMods); 213 | var newestMod = sameNamedMods.First(); 214 | Logger.Log("HAT", LogSeverity.Warning, $"Multiple instances of mod {modName} detected! Leaving version {newestMod.Info.Version}"); 215 | 216 | foreach (var mod in sameNamedMods) 217 | { 218 | if (mod == newestMod) continue; 219 | Mods.Remove(mod); 220 | } 221 | } 222 | } 223 | } 224 | 225 | private void InitializeDependencies() 226 | { 227 | foreach (var mod in Mods) 228 | { 229 | mod.InitializeDependencies(); 230 | } 231 | 232 | FinalizeDependencies(); 233 | } 234 | 235 | private void FinalizeDependencies() 236 | { 237 | for(int i=0;i<=Mods.Count; i++) 238 | { 239 | if(i == Mods.Count) 240 | { 241 | // there's no possible way to have more dependency nesting levels than the mod count. Escape! 242 | throw new ApplicationException("Stuck in a mod dependency finalization loop!"); 243 | } 244 | 245 | bool noInvalidMods = true; 246 | foreach (var mod in Mods) 247 | { 248 | if (mod.TryFinalizeDependencies()) continue; 249 | 250 | noInvalidMods = false; 251 | } 252 | if (noInvalidMods) 253 | { 254 | break; 255 | } 256 | } 257 | } 258 | 259 | private void FilterOutInvalidMods() 260 | { 261 | InvalidMods = Mods.Where(mod => !mod.AreDependenciesValid()).ToList(); 262 | foreach (var invalidMod in InvalidMods) 263 | { 264 | LogIssuesWithInvalidMod(invalidMod); 265 | Mods.Remove(invalidMod); 266 | } 267 | } 268 | 269 | private void LogIssuesWithInvalidMod(Mod invalidMod) 270 | { 271 | var delegateIssues = invalidMod.Dependencies 272 | .Where(dep => dep.Status != ModDependencyStatus.Valid) 273 | .Select(dependency => $"{dependency.Info.Name} ({dependency.GetStatusString()})") 274 | .ToList(); 275 | 276 | string error = $"Dependency issues in mod {invalidMod.Info.Name} found: {string.Join(", ", delegateIssues)}"; 277 | 278 | Logger.Log("HAT", LogSeverity.Warning, error); 279 | } 280 | 281 | private void SortModsBasedOnDependencies() 282 | { 283 | Mods.Sort((a, b) => 284 | { 285 | if (a.Dependencies.Where(d => d.Instance == b).Any()) return 1; 286 | if (b.Dependencies.Where(d => d.Instance == a).Any()) return -1; 287 | return 0; 288 | }); 289 | } 290 | 291 | private void LogLoadedMods() 292 | { 293 | int codeModsCount = Mods.Count(mod => mod.IsCodeMod); 294 | int assetModsCount = Mods.Count(mod => mod.IsAssetMod); 295 | 296 | var modsText = $"{Mods.Count} mod{(Mods.Count != 1 ? "s" : "")}"; 297 | var codeModsText = $"{codeModsCount} code mod{(codeModsCount != 1 ? "s" : "")}"; 298 | var assetModsText = $"{assetModsCount} asset mod{(assetModsCount != 1 ? "s" : "")}"; 299 | 300 | Logger.Log("HAT", $"Successfully loaded {modsText} ({codeModsText} and {assetModsText})"); 301 | 302 | Logger.Log("HAT", $"Mods in their order of appearance:"); 303 | 304 | foreach (var mod in Mods) 305 | { 306 | Logger.Log("HAT", $" {mod.Info.Name} by {mod.Info.Author} version {mod.Info.Version}"); 307 | } 308 | } 309 | 310 | internal void InitalizeAssemblies() 311 | { 312 | foreach (var mod in Mods) 313 | { 314 | mod.InitializeAssembly(); 315 | } 316 | foreach (var mod in Mods) 317 | { 318 | mod.InitializeComponents(); 319 | } 320 | Logger.Log("HAT", "Assembly initialization completed!"); 321 | } 322 | 323 | internal List GetFullAssetList() 324 | { 325 | var list = new List(); 326 | 327 | foreach (var mod in Mods) 328 | { 329 | list.AddRange(mod.Assets); 330 | } 331 | 332 | return list; 333 | } 334 | 335 | internal void InitalizeComponents() 336 | { 337 | foreach(var mod in Mods) 338 | { 339 | mod.InjectComponents(); 340 | } 341 | Logger.Log("HAT", "Component initialization completed!"); 342 | } 343 | 344 | } 345 | } 346 | -------------------------------------------------------------------------------- /Source/ModDefinition/Mod.cs: -------------------------------------------------------------------------------- 1 | using Common; 2 | using FezEngine.Tools; 3 | using HatModLoader.Source.Assets; 4 | using HatModLoader.Source.FileProxies; 5 | using Microsoft.Xna.Framework; 6 | using System.Reflection; 7 | 8 | namespace HatModLoader.Source.ModDefinition 9 | { 10 | public class Mod : IDisposable 11 | { 12 | public static readonly string ModsDirectoryName = "Mods"; 13 | 14 | public static readonly string AssetsDirectoryName = "Assets"; 15 | public static readonly string ModMetadataFileName = "Metadata.xml"; 16 | 17 | public Hat ModLoader; 18 | 19 | public byte[] RawAssembly { get; private set; } 20 | public Assembly Assembly { get; private set; } 21 | public ModMetadata Info { get; private set; } 22 | public IFileProxy FileProxy { get; private set; } 23 | public List Dependencies { get; private set; } 24 | public List Assets { get; private set; } 25 | public List Components { get; private set; } 26 | 27 | public bool IsAssetMod => Assets.Count > 0; 28 | public bool IsCodeMod => RawAssembly != null; 29 | 30 | public Mod(Hat modLoader, IFileProxy fileProxy) 31 | { 32 | ModLoader = modLoader; 33 | 34 | RawAssembly = null; 35 | Assembly = null; 36 | Assets = new List(); 37 | Components = new List(); 38 | Dependencies = new List(); 39 | FileProxy = fileProxy; 40 | } 41 | 42 | public void InitializeComponents() 43 | { 44 | if (RawAssembly == null || Assembly == null) return; 45 | 46 | foreach (Type type in Assembly.GetExportedTypes()) 47 | { 48 | if (!typeof(GameComponent).IsAssignableFrom(type) || !type.IsPublic || type.IsAbstract) continue; 49 | //Note: The constructor accepting the type (Game) is defined in GameComponent 50 | var gameComponent = (GameComponent)Activator.CreateInstance(type, new object[] { ModLoader.Game }); 51 | Components.Add(gameComponent); 52 | } 53 | 54 | if (Components.Count > 0) 55 | { 56 | var countText = $"{Components.Count} component{(Components.Count != 1 ? "s" : "")}"; 57 | Logger.Log("HAT", $"Initialized {countText} in mod \"{Info.Name}\""); 58 | } 59 | } 60 | 61 | public void InjectComponents() 62 | { 63 | foreach (var component in Components) 64 | { 65 | ServiceHelper.AddComponent(component); 66 | } 67 | } 68 | 69 | public void InitializeAssembly() 70 | { 71 | if (RawAssembly == null) return; 72 | Assembly = Assembly.Load(RawAssembly); 73 | } 74 | 75 | public void Dispose() 76 | { 77 | // TODO: dispose assets 78 | 79 | foreach (var component in Components) 80 | { 81 | ServiceHelper.RemoveComponent(component); 82 | } 83 | } 84 | 85 | public int CompareVersionsWith(Mod mod) 86 | { 87 | return ModMetadata.CompareVersions(Info.Version, mod.Info.Version); 88 | } 89 | 90 | public void InitializeDependencies() 91 | { 92 | if (Info.Dependencies == null || Info.Dependencies.Count() == 0) return; 93 | if (Dependencies.Count() == Info.Dependencies.Length) return; 94 | 95 | Dependencies.Clear(); 96 | foreach (var dependencyInfo in Info.Dependencies) 97 | { 98 | var matchingMod = ModLoader.Mods.FirstOrDefault(mod => mod.Info.Name == dependencyInfo.Name); 99 | var dependency = new ModDependency(dependencyInfo, matchingMod); 100 | Dependencies.Add(dependency); 101 | } 102 | } 103 | 104 | public bool TryFinalizeDependencies() 105 | { 106 | foreach (var dependency in Dependencies) 107 | { 108 | if (dependency.TryFinalize()) continue; 109 | else return false; 110 | } 111 | return true; 112 | } 113 | 114 | public bool AreDependenciesFinalized() 115 | { 116 | return Dependencies.All(dependency => dependency.IsFinalized); 117 | } 118 | 119 | public bool AreDependenciesValid() 120 | { 121 | if (Info.Dependencies == null) return true; // if mod has no dependencies, they are "valid" 122 | if (Info.Dependencies.Count() != Dependencies.Count()) return false; 123 | 124 | return Dependencies.All(dependency => dependency.Status == ModDependencyStatus.Valid); 125 | } 126 | 127 | public static bool TryLoad(Hat modLoader, IFileProxy fileProxy, out Mod mod) 128 | { 129 | mod = new Mod(modLoader, fileProxy); 130 | 131 | if (!mod.TryLoadMetadata()) return false; 132 | 133 | mod.TryLoadAssets(); 134 | mod.TryLoadAssembly(); 135 | 136 | return mod.IsAssetMod || mod.IsCodeMod; 137 | } 138 | 139 | public static string GetModsDirectory() 140 | { 141 | return Path.Combine(AppDomain.CurrentDomain.BaseDirectory, ModsDirectoryName); 142 | } 143 | 144 | private bool TryLoadMetadata() 145 | { 146 | if (!FileProxy.FileExists(ModMetadataFileName)) 147 | { 148 | return false; 149 | } 150 | 151 | using var metadataStream = FileProxy.OpenFile(ModMetadataFileName); 152 | if (!ModMetadata.TryLoadFrom(metadataStream, out var metadata)) 153 | { 154 | return false; 155 | } 156 | 157 | Info = metadata; 158 | 159 | return true; 160 | } 161 | 162 | private bool TryLoadAssets() 163 | { 164 | var files = new Dictionary(); 165 | 166 | foreach (var filePath in FileProxy.EnumerateFiles(AssetsDirectoryName)) 167 | { 168 | var relativePath = filePath.Substring(AssetsDirectoryName.Length + 1).Replace("/", "\\").ToLower(); 169 | var fileStream = FileProxy.OpenFile(filePath); 170 | files.Add(relativePath, fileStream); 171 | } 172 | 173 | Assets = AssetLoaderHelper.GetListFromFileDictionary(files); 174 | 175 | var pakPackagePath = AssetsDirectoryName + ".pak"; 176 | if (FileProxy.FileExists(pakPackagePath)) 177 | { 178 | using var pakPackage = FileProxy.OpenFile(pakPackagePath); 179 | Assets.AddRange(AssetLoaderHelper.LoadPakPackage(pakPackage)); 180 | } 181 | 182 | return Assets.Count > 0; 183 | } 184 | 185 | private bool TryLoadAssembly() 186 | { 187 | if(!IsLibraryNameValid()) return false; 188 | 189 | if (!FileProxy.FileExists(Info.LibraryName)) return false; 190 | 191 | using var assemblyStream = FileProxy.OpenFile(Info.LibraryName); 192 | RawAssembly = new byte[assemblyStream.Length]; 193 | assemblyStream.Read(RawAssembly, 0, RawAssembly.Length); 194 | 195 | return true; 196 | } 197 | 198 | private bool IsLibraryNameValid() 199 | { 200 | var libraryName = Info.LibraryName; 201 | return libraryName != null && libraryName.Length > 0 && libraryName.EndsWith(".dll", StringComparison.OrdinalIgnoreCase); 202 | } 203 | } 204 | } 205 | -------------------------------------------------------------------------------- /Source/ModDefinition/ModDependency.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | 3 | namespace HatModLoader.Source.ModDefinition 4 | { 5 | [Serializable] 6 | public struct ModDependency 7 | { 8 | public ModDependencyInfo Info; 9 | public Mod Instance; 10 | public ModDependencyStatus Status; 11 | public bool IsModLoaderDependency => Info.Name == "HAT"; 12 | public bool IsFinalized => Status != ModDependencyStatus.None; 13 | public string DetectedVersion => IsModLoaderDependency ? Hat.Version : Instance != null ? Instance.Info.Version : null; 14 | 15 | 16 | public ModDependency(ModDependencyInfo info, Mod instance) 17 | { 18 | Info = info; 19 | Instance = instance; 20 | Status = ModDependencyStatus.None; 21 | 22 | Initialize(); 23 | } 24 | public void Initialize() 25 | { 26 | if (IsModLoaderDependency || Instance != null) 27 | { 28 | if (ModMetadata.CompareVersions(DetectedVersion, Info.MinimumVersion) < 0) 29 | { 30 | Status = ModDependencyStatus.InvalidVersion; 31 | } 32 | else 33 | { 34 | Status = ModDependencyStatus.Valid; 35 | } 36 | } 37 | 38 | if (!IsModLoaderDependency) 39 | { 40 | if (Instance == null) 41 | { 42 | Status = ModDependencyStatus.InvalidNotFound; 43 | } 44 | else if (Instance.AreDependenciesValid()) 45 | { 46 | Status = ModDependencyStatus.Valid; 47 | } 48 | 49 | if (IsRecursive()) 50 | { 51 | Status = ModDependencyStatus.InvalidRecursive; 52 | } 53 | } 54 | } 55 | 56 | public bool TryFinalize() 57 | { 58 | if (IsModLoaderDependency) return true; 59 | 60 | if (!Instance.AreDependenciesFinalized()) return false; 61 | 62 | Status = 63 | Instance.AreDependenciesValid() 64 | ? ModDependencyStatus.Valid 65 | : ModDependencyStatus.InvalidDependencyTree; 66 | 67 | return true; 68 | } 69 | 70 | public bool IsRecursive() 71 | { 72 | var currentModQueue = new List() { Instance }; 73 | 74 | var iterationsCount = Instance.ModLoader.Mods.Count(); 75 | 76 | while (currentModQueue.Count > 0) 77 | { 78 | var newDependencyMods = currentModQueue.SelectMany(mod => mod.Dependencies).Select(dep => dep.Instance).ToList(); 79 | if (newDependencyMods.Contains(Instance)) 80 | { 81 | return true; 82 | } 83 | 84 | currentModQueue = newDependencyMods; 85 | 86 | iterationsCount--; 87 | 88 | if (iterationsCount <= 0) 89 | { 90 | break; 91 | } 92 | } 93 | 94 | return false; 95 | } 96 | 97 | public string GetStatusString() 98 | { 99 | return Status switch 100 | { 101 | ModDependencyStatus.Valid => $"valid", 102 | ModDependencyStatus.InvalidVersion => $"needs version >={Info.MinimumVersion}, found {DetectedVersion}", 103 | ModDependencyStatus.InvalidNotFound => $"not found", 104 | ModDependencyStatus.InvalidRecursive => $"recursive dependency - consider merging mods or separating it into modules", 105 | ModDependencyStatus.InvalidDependencyTree => $"couldn't load its own dependencies", 106 | _ => "unknown" 107 | }; 108 | } 109 | 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /Source/ModDefinition/ModDependencyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Xml.Serialization; 2 | 3 | namespace HatModLoader.Source.ModDefinition 4 | { 5 | [Serializable] 6 | public struct ModDependencyInfo 7 | { 8 | [XmlAttribute] public string Name; 9 | [XmlAttribute] public string MinimumVersion; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Source/ModDefinition/ModDependencyStatus.cs: -------------------------------------------------------------------------------- 1 | namespace HatModLoader.Source.ModDefinition 2 | { 3 | public enum ModDependencyStatus 4 | { 5 | None, 6 | Valid, 7 | InvalidVersion, 8 | InvalidNotFound, 9 | InvalidRecursive, 10 | InvalidDependencyTree 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Source/ModDefinition/ModIdentityHelper.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Xna.Framework; 2 | 3 | namespace HatModLoader.Source.ModDefinition 4 | { 5 | public static class ModIdentityHelper 6 | { 7 | public static Mod GetModByGameComponent(this Hat hat) where T : GameComponent 8 | { 9 | return hat.Mods.Where(mod => mod.Components.Any(component => component is T)).FirstOrDefault(); 10 | } 11 | 12 | public static Mod GetOwnMod(this GameComponent gameComponent) 13 | { 14 | return Hat.Instance.Mods.Where(mod => mod.Components.Contains(gameComponent)).FirstOrDefault(); 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Source/ModDefinition/ModMetadata.cs: -------------------------------------------------------------------------------- 1 | using Common; 2 | using System.Text.RegularExpressions; 3 | using System.Xml.Serialization; 4 | 5 | namespace HatModLoader.Source.ModDefinition 6 | { 7 | [Serializable] 8 | [XmlType(TypeName = "Metadata")] 9 | public struct ModMetadata 10 | { 11 | public string Name; 12 | public string Description; 13 | public string Author; 14 | public string Version; 15 | public string LibraryName; 16 | public ModDependencyInfo[] Dependencies; 17 | 18 | public static bool TryLoadFrom(Stream stream, out ModMetadata metadata) 19 | { 20 | try 21 | { 22 | var serializer = new XmlSerializer(typeof(ModMetadata)); 23 | using var reader = new StreamReader(stream); 24 | metadata = (ModMetadata)serializer.Deserialize(reader); 25 | 26 | if (metadata.Name == null || metadata.Name.Length == 0) return false; 27 | if (metadata.Version == null || metadata.Version.Length == 0) return false; 28 | 29 | return true; 30 | } 31 | catch (Exception ex) 32 | { 33 | Logger.Log("HAT", LogSeverity.Warning, $"Failed to load mod metadata: {ex.Message}"); 34 | metadata = default; 35 | return false; 36 | } 37 | } 38 | 39 | public static int CompareVersions(string ver1, string ver2) 40 | { 41 | string tokensPattern = @"(\d+|\D+)"; 42 | string[] TokensVer1 = Regex.Split(ver1, tokensPattern); 43 | string[] TokensVer2 = Regex.Split(ver2, tokensPattern); 44 | 45 | for (int i = 0; i < Math.Min(TokensVer1.Length, TokensVer2.Length); i++) 46 | { 47 | if (int.TryParse(TokensVer1[i], out int tokenInt1) && int.TryParse(TokensVer2[i], out int tokenInt2)) 48 | { 49 | if (tokenInt1 > tokenInt2) return 1; 50 | if (tokenInt1 < tokenInt2) return -1; 51 | continue; 52 | } 53 | int comparison = TokensVer1[i].CompareTo(TokensVer2[i]); 54 | if (comparison < 0) return 1; 55 | if (comparison > 0) return -1; 56 | } 57 | if (TokensVer1.Length > TokensVer2.Length) return 1; 58 | if (TokensVer1.Length < TokensVer2.Length) return -1; 59 | return 0; 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /Source/ModsTextListLoader.cs: -------------------------------------------------------------------------------- 1 |  2 | namespace HatModLoader.Source 3 | { 4 | internal static class ModsTextListLoader 5 | { 6 | private static bool Exists(string path) 7 | { 8 | return File.Exists(path); 9 | } 10 | 11 | public static List Load(string path) 12 | { 13 | var modsList = new List(); 14 | 15 | if(!Exists(path)) return modsList; 16 | 17 | var fileContents = File.ReadAllText(path); 18 | 19 | foreach(var line in fileContents.Split('\n')) 20 | { 21 | if (string.IsNullOrWhiteSpace(line)) continue; 22 | 23 | var clearedLine = line.Trim(); 24 | if(clearedLine.StartsWith("#")) continue; 25 | if(clearedLine.Length > 0) modsList.Add(clearedLine); 26 | } 27 | 28 | return modsList; 29 | } 30 | 31 | public static List LoadOrCreateDefault(string path, string defaultContent) 32 | { 33 | if(!Exists(path)) 34 | { 35 | File.WriteAllText(path, defaultContent); 36 | } 37 | return Load(path); 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /scripts/hat_install.bat: -------------------------------------------------------------------------------- 1 | SET MONOMOD_DEPDIRS=HATDependencies/MonoMod;HATDependencies/FEZRepacker.Core 2 | "HATDependencies/MonoMod/MonoMod.exe" FEZ.exe 3 | pause -------------------------------------------------------------------------------- /scripts/hat_install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Move to script's directory 4 | cd "`dirname "$0"`" 5 | 6 | # Copy all files from HATDependencies for the duration of patching 7 | temp_files=() 8 | while IFS= read -r -d '' file; do 9 | cp "$file" . && copied_files+=("$(basename "$file")") 10 | done < <(find HATDependencies -type f -print0) 11 | 12 | # Patching 13 | mono MonoMod.exe FEZ.exe 14 | 15 | # Cleanup 16 | for f in "${copied_files[@]}"; do 17 | rm -f -- "$f" 18 | done 19 | --------------------------------------------------------------------------------