├── .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 | 
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 |
--------------------------------------------------------------------------------