├── .gitattributes ├── .gitignore ├── Common.Mod.mm ├── MonoModRules.cs └── Patches │ └── Logger.cs ├── FEZ.Mod.mm ├── Content │ └── other textures │ │ └── splash │ │ ├── polytron_neue.png │ │ └── polytron_neue_1440.png ├── Mod │ ├── Components │ │ └── PolytronLogoNeue.cs │ ├── ContentDumper.cs │ ├── Core │ │ └── CoreModule.cs │ ├── FezMod.cs │ └── Services │ │ ├── ModKeyboardStateManager.cs │ │ └── ModMouseStateManager.cs ├── MonoModRules.cs └── Patches │ ├── Components │ └── GlitchyRespawner.cs │ ├── Fez.cs │ └── Program.cs ├── FEZMod.sln ├── FezEngine.Mod.mm ├── Mod │ ├── Core │ │ └── CoreEngineModule.cs │ ├── Dummy.cs │ ├── FezModEngine.cs │ ├── FezModEngineExtensions.cs │ ├── LimitedStream.cs │ ├── ModAsset.cs │ ├── ModAssetSource.cs │ ├── ModBase.cs │ ├── ModContent.cs │ ├── ModLoader.cs │ ├── ModMetadata.cs │ ├── ModRelinker.cs │ ├── NullModule.cs │ ├── RelinkerSymbolReaderProvider.cs │ ├── ServiceHelperHooks.cs │ ├── Services │ │ └── IServiceWrapper.cs │ ├── Tools │ │ ├── MainThreadHelper.cs │ │ └── QueuedTaskHelper.cs │ └── YamlHelper.cs ├── MonoModRules.cs └── Patches │ ├── Structure │ └── OggStream.cs │ └── Tools │ ├── MemoryContentManager.cs │ ├── ServiceHelper.cs │ └── SharedContentManager.cs ├── LICENSE ├── lib-fna ├── MojoShader.dll ├── SDL2.dll ├── SDL2_image.dll ├── libjpeg-9.dll ├── libogg-0.dll ├── libpng16-16.dll ├── libtheoradec-1.dll ├── libtheorafile.dll ├── libvorbis-0.dll ├── libvorbisfile.dll ├── soft_oal.dll └── zlib1.dll ├── lib-stripped ├── Common.dll ├── ContentSerialization.dll ├── EasyStorage.dll ├── FEZ.exe ├── FezEngine.dll ├── README.md ├── SimpleDefinitionLanguage.dll └── XnaWordWrapCore.dll └── lib └── FNA.dll /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Set default behavior for command prompt diff. 8 | # 9 | # This is need for earlier builds of msysgit that does not have it on by 10 | # default for csharp files. 11 | # Note: This is only used by command line 12 | ############################################################################### 13 | #*.cs diff=csharp 14 | 15 | ############################################################################### 16 | # Set the merge driver for project and solution files 17 | # 18 | # Merging from the command prompt will add diff markers to the files if there 19 | # are conflicts (Merging from VS is not affected by the settings below, in VS 20 | # the diff markers are never inserted). Diff markers may cause the following 21 | # file extensions to fail to load in VS. An alternative would be to treat 22 | # these files as binary and thus will always conflict and require user 23 | # intervention with every merge. To do so, just uncomment the entries below 24 | ############################################################################### 25 | #*.sln merge=binary 26 | #*.csproj merge=binary 27 | #*.vbproj merge=binary 28 | #*.vcxproj merge=binary 29 | #*.vcproj merge=binary 30 | #*.dbproj merge=binary 31 | #*.fsproj merge=binary 32 | #*.lsproj merge=binary 33 | #*.wixproj merge=binary 34 | #*.modelproj merge=binary 35 | #*.sqlproj merge=binary 36 | #*.wwaproj merge=binary 37 | 38 | ############################################################################### 39 | # behavior for image files 40 | # 41 | # image files are treated as binary by default. 42 | ############################################################################### 43 | #*.jpg binary 44 | #*.png binary 45 | #*.gif binary 46 | 47 | ############################################################################### 48 | # diff behavior for common document formats 49 | # 50 | # Convert binary document formats to text before diffing them. This feature 51 | # is only available from the command line. Turn it on by uncommenting the 52 | # entries below. 53 | ############################################################################### 54 | #*.doc diff=astextplain 55 | #*.DOC diff=astextplain 56 | #*.docx diff=astextplain 57 | #*.DOCX diff=astextplain 58 | #*.dot diff=astextplain 59 | #*.DOT diff=astextplain 60 | #*.pdf diff=astextplain 61 | #*.PDF diff=astextplain 62 | #*.rtf diff=astextplain 63 | #*.RTF diff=astextplain 64 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # User-specific files 5 | *.suo 6 | *.user 7 | *.userosscache 8 | *.sln.docstates 9 | 10 | # User-specific files (MonoDevelop/Xamarin Studio) 11 | *.userprefs 12 | 13 | # Build results 14 | [Dd]ebug/ 15 | [Dd]ebugPublic/ 16 | [Rr]elease/ 17 | [Rr]eleases/ 18 | [Xx]64/ 19 | [Xx]86/ 20 | [Bb]uild/ 21 | bld/ 22 | [Bb]in/ 23 | [Oo]bj/ 24 | 25 | # Visual Studio 2015 cache/options directory 26 | .vs/ 27 | # Uncomment if you have tasks that create the project's static files in wwwroot 28 | #wwwroot/ 29 | 30 | # MSTest test Results 31 | [Tt]est[Rr]esult*/ 32 | [Bb]uild[Ll]og.* 33 | 34 | # NUNIT 35 | *.VisualState.xml 36 | TestResult.xml 37 | 38 | # Build Results of an ATL Project 39 | [Dd]ebugPS/ 40 | [Rr]eleasePS/ 41 | dlldata.c 42 | 43 | # DNX 44 | project.lock.json 45 | artifacts/ 46 | 47 | *_i.c 48 | *_p.c 49 | *_i.h 50 | *.ilk 51 | *.meta 52 | *.obj 53 | *.pch 54 | *.pdb 55 | *.pgc 56 | *.pgd 57 | *.rsp 58 | *.sbr 59 | *.tlb 60 | *.tli 61 | *.tlh 62 | *.tmp 63 | *.tmp_proj 64 | *.log 65 | *.vspscc 66 | *.vssscc 67 | .builds 68 | *.pidb 69 | *.svclog 70 | *.scc 71 | 72 | # Chutzpah Test files 73 | _Chutzpah* 74 | 75 | # Visual C++ cache files 76 | ipch/ 77 | *.aps 78 | *.ncb 79 | *.opendb 80 | *.opensdf 81 | *.sdf 82 | *.cachefile 83 | *.VC.db 84 | 85 | # Visual Studio profiler 86 | *.psess 87 | *.vsp 88 | *.vspx 89 | *.sap 90 | 91 | # TFS 2012 Local Workspace 92 | $tf/ 93 | 94 | # Guidance Automation Toolkit 95 | *.gpState 96 | 97 | # ReSharper is a .NET coding add-in 98 | _ReSharper*/ 99 | *.[Rr]e[Ss]harper 100 | *.DotSettings.user 101 | 102 | # JustCode is a .NET coding add-in 103 | .JustCode 104 | 105 | # TeamCity is a build add-in 106 | _TeamCity* 107 | 108 | # DotCover is a Code Coverage Tool 109 | *.dotCover 110 | 111 | # NCrunch 112 | _NCrunch_* 113 | .*crunch*.local.xml 114 | nCrunchTemp_* 115 | 116 | # MightyMoose 117 | *.mm.* 118 | AutoTest.Net/ 119 | 120 | # Web workbench (sass) 121 | .sass-cache/ 122 | 123 | # Installshield output folder 124 | [Ee]xpress/ 125 | 126 | # DocProject is a documentation generator add-in 127 | DocProject/buildhelp/ 128 | DocProject/Help/*.HxT 129 | DocProject/Help/*.HxC 130 | DocProject/Help/*.hhc 131 | DocProject/Help/*.hhk 132 | DocProject/Help/*.hhp 133 | DocProject/Help/Html2 134 | DocProject/Help/html 135 | 136 | # Click-Once directory 137 | publish/ 138 | 139 | # Publish Web Output 140 | *.[Pp]ublish.xml 141 | *.azurePubxml 142 | 143 | # TODO: Un-comment the next line if you do not want to checkin 144 | # your web deploy settings because they may include unencrypted 145 | # passwords 146 | #*.pubxml 147 | *.publishproj 148 | 149 | # NuGet Packages 150 | *.nupkg 151 | # The packages folder can be ignored because of Package Restore 152 | **/packages/* 153 | # except build/, which is used as an MSBuild target. 154 | !**/packages/build/ 155 | # Uncomment if necessary however generally it will be regenerated when needed 156 | #!**/packages/repositories.config 157 | # NuGet v3's project.json files produces more ignoreable files 158 | *.nuget.props 159 | *.nuget.targets 160 | 161 | # Microsoft Azure Build Output 162 | csx/ 163 | *.build.csdef 164 | 165 | # Microsoft Azure Emulator 166 | ecf/ 167 | rcf/ 168 | 169 | # Windows Store app package directory 170 | AppPackages/ 171 | BundleArtifacts/ 172 | 173 | # Visual Studio cache files 174 | # files ending in .cache can be ignored 175 | *.[Cc]ache 176 | # but keep track of directories ending in .cache 177 | !*.[Cc]ache/ 178 | 179 | # Others 180 | ClientBin/ 181 | [Ss]tyle[Cc]op.* 182 | ~$* 183 | *~ 184 | *.dbmdl 185 | *.dbproj.schemaview 186 | *.pfx 187 | *.publishsettings 188 | node_modules/ 189 | orleans.codegen.cs 190 | 191 | # RIA/Silverlight projects 192 | Generated_Code/ 193 | 194 | # Backup & report files from converting an old project file 195 | # to a newer Visual Studio version. Backup files are not needed, 196 | # because we have git ;-) 197 | _UpgradeReport_Files/ 198 | Backup*/ 199 | UpgradeLog*.XML 200 | UpgradeLog*.htm 201 | 202 | # SQL Server files 203 | *.mdf 204 | *.ldf 205 | 206 | # Business Intelligence projects 207 | *.rdl.data 208 | *.bim.layout 209 | *.bim_*.settings 210 | 211 | # Microsoft Fakes 212 | FakesAssemblies/ 213 | 214 | # GhostDoc plugin setting file 215 | *.GhostDoc.xml 216 | 217 | # Node.js Tools for Visual Studio 218 | .ntvs_analysis.dat 219 | 220 | # Visual Studio 6 build log 221 | *.plg 222 | 223 | # Visual Studio 6 workspace options file 224 | *.opt 225 | 226 | # Visual Studio LightSwitch build output 227 | **/*.HTMLClient/GeneratedArtifacts 228 | **/*.DesktopClient/GeneratedArtifacts 229 | **/*.DesktopClient/ModelManifest.xml 230 | **/*.Server/GeneratedArtifacts 231 | **/*.Server/ModelManifest.xml 232 | _Pvt_Extensions 233 | 234 | # LightSwitch generated files 235 | GeneratedArtifacts/ 236 | ModelManifest.xml 237 | 238 | # Paket dependency manager 239 | .paket/paket.exe 240 | 241 | # FAKE - F# Make 242 | .fake/ 243 | -------------------------------------------------------------------------------- /Common.Mod.mm/MonoModRules.cs: -------------------------------------------------------------------------------- 1 | using MonoMod.InlineRT; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | 8 | namespace MonoMod { 9 | static class MonoModRules { 10 | 11 | static MonoModRules() { 12 | } 13 | 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Common.Mod.mm/Patches/Logger.cs: -------------------------------------------------------------------------------- 1 | #pragma warning disable CS0626 // Method, operator, or accessor is marked external and has no attributes on it 2 | 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using System.Text; 7 | using System.Threading.Tasks; 8 | 9 | namespace Common { 10 | class patch_Logger { 11 | 12 | public static extern void orig_Log(string component, LogSeverity severity, string message); 13 | public static void Log(string component, LogSeverity severity, string message) { 14 | Console.WriteLine("(" + DateTime.Now.ToString("HH:mm:ss.fff") + ") [" + component + "] " + message); 15 | 16 | orig_Log(component, severity, message); 17 | } 18 | 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /FEZ.Mod.mm/Content/other textures/splash/polytron_neue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0x0ade/FEZMod/3d3da3f7aec3abe06f3f809053337068e80bf17a/FEZ.Mod.mm/Content/other textures/splash/polytron_neue.png -------------------------------------------------------------------------------- /FEZ.Mod.mm/Content/other textures/splash/polytron_neue_1440.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0x0ade/FEZMod/3d3da3f7aec3abe06f3f809053337068e80bf17a/FEZ.Mod.mm/Content/other textures/splash/polytron_neue_1440.png -------------------------------------------------------------------------------- /FEZ.Mod.mm/Mod/Components/PolytronLogoNeue.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Runtime.CompilerServices; 4 | using FezEngine.Effects; 5 | using FezEngine.Services; 6 | using FezEngine.Structure; 7 | using FezEngine.Structure.Geometry; 8 | using FezEngine.Tools; 9 | using Microsoft.Xna.Framework; 10 | using Microsoft.Xna.Framework.Audio; 11 | using Microsoft.Xna.Framework.Graphics; 12 | using MonoMod; 13 | 14 | namespace FezGame.Components { 15 | [MonoModLinkFrom("FezGame.Components.PolytronLogo")] 16 | internal class PolytronLogoNeue : DrawableGameComponent { 17 | 18 | [ServiceDependency] public ITargetRenderingManager TargetRenderer { get; set; } 19 | [ServiceDependency] public IContentManagerProvider CMProvider { get; set; } 20 | 21 | private static readonly Color[] StripColors = new Color[] { 22 | new Color(0, 174, 250), 23 | new Color(255, 242, 0), 24 | new Color(255, 111, 1) 25 | }; 26 | 27 | private Mesh LogoMesh; 28 | private Texture2D PolytronText; 29 | private SpriteBatch spriteBatch; 30 | 31 | private SoundEffect sPolytron; 32 | private SoundEmitter iPolytron; 33 | 34 | private float SinceStarted; 35 | 36 | public PolytronLogoNeue(Game game) : base(game) { 37 | Visible = false; 38 | Enabled = false; 39 | } 40 | 41 | public float Opacity { get; set; } 42 | 43 | public override void Initialize() { 44 | base.Initialize(); 45 | 46 | LogoMesh = new Mesh { 47 | AlwaysOnTop = true 48 | }; 49 | 50 | for (int i = 0; i < StripColors.Length; i++) { 51 | FezVertexPositionColor[] vertices = new FezVertexPositionColor[202]; 52 | for (int j = 0; j < vertices.Length; j++) { 53 | vertices[j] = new FezVertexPositionColor(Vector3.Zero, StripColors[i]); 54 | } 55 | LogoMesh.AddGroup().Geometry = new IndexedUserPrimitives(vertices, Enumerable.Range(0, vertices.Length).ToArray(), PrimitiveType.TriangleStrip); 56 | } 57 | 58 | float viewScale = GraphicsDevice.GetViewScale(); 59 | float xScale = GraphicsDevice.Viewport.Width / (1280f * viewScale); 60 | float yScale = GraphicsDevice.Viewport.Height / (720f * viewScale); 61 | int width = GraphicsDevice.Viewport.Width; 62 | int height = GraphicsDevice.Viewport.Height; 63 | 64 | LogoMesh.Position = new Vector3(-0.1975f / xScale, -0.25f / yScale, 0f); 65 | LogoMesh.Scale = new Vector3(new Vector2(500f) * viewScale / new Vector2(width, height), 1f); 66 | sPolytron = CMProvider.Get(CM.Intro).Load("Sounds/Intro/PolytronJingle"); 67 | 68 | DrawActionScheduler.Schedule(delegate { 69 | PolytronText = CMProvider.Get(CM.Intro).Load("Other Textures/splash/polytron_neue" + (viewScale >= 1.5f ? "_1440" : "")); 70 | spriteBatch = new SpriteBatch(GraphicsDevice); 71 | LogoMesh.Effect = new DefaultEffect.VertexColored { 72 | ForcedProjectionMatrix = Matrix.CreateOrthographic(320f / 224f, 320f / 224f, 0.1f, 100f), 73 | ForcedViewMatrix = Matrix.CreateLookAt(Vector3.UnitZ, -Vector3.UnitZ, Vector3.Up) 74 | }; 75 | }); 76 | } 77 | 78 | protected override void Dispose(bool disposing) { 79 | LogoMesh.Dispose(); 80 | spriteBatch.Dispose(); 81 | } 82 | 83 | private void UpdateStripe(int stripe, float step) { 84 | // Brought back from FEZ. 85 | 86 | FezVertexPositionColor[] vertices = (LogoMesh.Groups[stripe].Geometry as IndexedUserPrimitives).Vertices; 87 | Vector3 posI = Vector3.Zero; // Inner 88 | Vector3 posO = Vector3.Zero; // Outter 89 | 90 | float thickness = 0.364f / StripColors.Length; 91 | const float halfPI = 1.5707963267948966f; 92 | 93 | for (int i = 0; i <= 100; i++) { 94 | int iA = i * 2; 95 | int iB = i * 2 + 1; 96 | 97 | float f; 98 | if (i < 20) { 99 | // Bottom left | 100 | f = i / 20f * FezMath.Saturate(step / 0.2f); 101 | posI = new Vector3((stripe + 1) * thickness, f * 0.5f, 0f); 102 | posO = new Vector3(stripe * thickness, f * 0.5f, 0f); 103 | 104 | } else if (i > 80 && step > 0.8f) { 105 | // Bottom - 106 | f = (i - 80f) / 20f * FezMath.Saturate((step - 0.8f) / 0.2f / 0.272f); 107 | posI = new Vector3(0.5f - f * 0.136f, (stripe + 1) * thickness, 0f); 108 | posO = new Vector3(0.5f - f * 0.136f, stripe * thickness, 0f); 109 | 110 | } else if (i >= 20 && i <= 80 && step > 0.2f) { 111 | // Arc 112 | f = (i - 20f) / 60f * FezMath.Saturate((step - 0.2f) / 0.6f) * halfPI * 3f - halfPI; 113 | posI = new Vector3((float) Math.Sin(f) * (0.5f - (stripe + 1) * thickness) + 0.5f, (float) Math.Cos(f) * (0.5f - (stripe + 1) * thickness) + 0.5f, 0f); 114 | posO = new Vector3((float) Math.Sin(f) * (0.5f - stripe * thickness) + 0.5f, (float) Math.Cos(f) * (0.5f - stripe * thickness) + 0.5f, 0f); 115 | 116 | } 117 | 118 | vertices[iA].Position = posI; 119 | vertices[iB].Position = posO; 120 | } 121 | } 122 | 123 | public void End() { 124 | if (!iPolytron.Dead) 125 | iPolytron.FadeOutAndDie(0.1f); 126 | iPolytron = null; 127 | } 128 | 129 | public override void Update(GameTime gameTime) { 130 | if (SinceStarted == 0f && gameTime.ElapsedGameTime.Ticks != 0L && sPolytron != null) { 131 | iPolytron = sPolytron.Emit(); 132 | sPolytron = null; 133 | } 134 | 135 | SinceStarted += (float) gameTime.ElapsedGameTime.TotalSeconds; 136 | 137 | for (int i = StripColors.Length - 1; i > -1; --i) { 138 | // float ease = FezMath.Saturate(SinceStarted / 1.5f); 139 | // UpdateStripe(i, Easing.EaseOut(Easing.EaseIn(ease, EasingType.Quadratic + (StripColors.Length - 1) - i), EasingType.Quartic) * 0.86f); 140 | 141 | float ease = FezMath.Saturate((SinceStarted - 0.125f * ((StripColors.Length - 1) - i)) / 1.5f); 142 | UpdateStripe(i, Easing.EaseOut(Easing.EaseIn(ease, EasingType.Quadratic), EasingType.Quartic) * 0.86f); 143 | } 144 | } 145 | 146 | public override void Draw(GameTime gameTime) { 147 | GraphicsDevice.SamplerStates[0] = SamplerState.PointClamp; 148 | 149 | Vector2 center = (new Vector2(GraphicsDevice.Viewport.Width, GraphicsDevice.Viewport.Height) / 2f).Round(); 150 | float viewScale = GraphicsDevice.GetViewScale(); 151 | float ease = Easing.EaseOut(FezMath.Saturate((SinceStarted - 1.5f) / 0.25f), EasingType.Quadratic); 152 | 153 | LogoMesh.Material.Opacity = Opacity; 154 | LogoMesh.Draw(); 155 | 156 | spriteBatch.Begin(); 157 | spriteBatch.Draw(PolytronText, center + new Vector2((-PolytronText.Width) / 2f, (128f + 120f / StripColors.Length) * viewScale).Round(), new Color(1f, 1f, 1f, Opacity * ease)); 158 | spriteBatch.End(); 159 | } 160 | 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /FEZ.Mod.mm/Mod/ContentDumper.cs: -------------------------------------------------------------------------------- 1 | using FezEngine.Mod; 2 | using FezEngine.Mod.Core; 3 | using FezEngine.Tools; 4 | using FezGame.Mod.Services; 5 | using Microsoft.Xna.Framework; 6 | using Microsoft.Xna.Framework.Graphics; 7 | using System; 8 | using System.Collections.Generic; 9 | using System.Collections.ObjectModel; 10 | using System.IO; 11 | using System.Linq; 12 | using System.Reflection; 13 | using System.Runtime.CompilerServices; 14 | using System.Text; 15 | using System.Threading.Tasks; 16 | 17 | namespace FezGame.Mod { 18 | internal static class ContentDumper { 19 | 20 | private static string ContentGuessType(string file, out Type type, out string format) { 21 | type = typeof(object); 22 | format = Path.GetExtension(file) ?? ""; 23 | if (format.Length >= 1) 24 | format = format.Substring(1); 25 | 26 | if (file.EndsWith(".png")) { 27 | type = typeof(Texture2D); 28 | return file.Substring(0, file.Length - 4); 29 | } 30 | 31 | return null; 32 | } 33 | 34 | internal static void LoadComponentReplacements(Fez game) { 35 | ServiceHelperHooks.ReplacementServices["FezEngine.Services.MouseStateManager"] = new ModMouseStateManager(); 36 | ServiceHelperHooks.ReplacementServices["FezEngine.Services.KeyboardStateManager"] = new ModKeyboardStateManager(); 37 | } 38 | 39 | internal static void LoadComponents(Fez game) { 40 | // ServiceHelper.AddComponent(new ModGUIHost(game)); 41 | } 42 | 43 | internal static void DumpAllPacks() { 44 | string pathPakDir = ModContent.PathContentOrig; 45 | if (!Directory.Exists(pathPakDir)) 46 | return; 47 | foreach (string pathPak in Directory.GetFiles(pathPakDir)) { 48 | if (!pathPak.EndsWith(".pak")) 49 | continue; 50 | DumpPack(pathPak); 51 | } 52 | } 53 | 54 | internal static void DumpPack(string pathPak) { 55 | if (!File.Exists(pathPak)) 56 | return; 57 | 58 | string pak = Path.GetFileNameWithoutExtension(pathPak); 59 | string pathOutRoot = ModContent.PathDUMP; 60 | 61 | using (FileStream packStream = File.OpenRead(pathPak)) 62 | using (BinaryReader packReader = new BinaryReader(packStream)) { 63 | int count = packReader.ReadInt32(); 64 | for (int i = 0; i < count; i++) { 65 | string path = packReader.ReadString(); 66 | int length = packReader.ReadInt32(); 67 | 68 | if (pak == "Music") { 69 | // The music .pak contains basic .ogg files. 70 | path += ".ogg"; 71 | 72 | } else if (path.StartsWith("effects")) { 73 | // The FEZ 1.12 .paks contains the raw fxb files, which should be dumped with their original fxb extension. 74 | path += ".fxb"; 75 | 76 | } else { 77 | path += ".xnb"; 78 | } 79 | 80 | string pathOut = Path.Combine(pathOutRoot, path.Replace('/', Path.DirectorySeparatorChar).Replace('\\', Path.DirectorySeparatorChar)); 81 | string pathOutDir = Path.GetDirectoryName(pathOut); 82 | 83 | if (!Directory.Exists(pathOutDir)) 84 | Directory.CreateDirectory(pathOutDir); 85 | 86 | Console.WriteLine($"Dumping {pathOut}"); 87 | if (File.Exists(pathOut)) 88 | File.Delete(pathOut); 89 | using (FileStream dumpStream = File.OpenWrite(pathOut)) 90 | dumpStream.Write(packReader.ReadBytes(length), 0, length); 91 | } 92 | } 93 | } 94 | 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /FEZ.Mod.mm/Mod/Core/CoreModule.cs: -------------------------------------------------------------------------------- 1 | using FezEngine.Mod; 2 | using FezEngine.Mod.Core; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using System.Text; 7 | using System.Threading.Tasks; 8 | 9 | namespace FezGame.Mod.Core { 10 | public class CoreModule : CoreEngineModule { 11 | 12 | public static CoreModule Instance; 13 | 14 | public CoreModule() { 15 | Instance = this; 16 | 17 | Metadata = new ModMetadata { 18 | ID = "FEZMod", 19 | VersionString = "0.0.0-fuckno" 20 | }; 21 | Fez.Version = $"{Fez.Version} | FEZMod {Metadata.VersionString}"; 22 | } 23 | 24 | public override void Load() { 25 | } 26 | 27 | public override void Unload() { 28 | } 29 | 30 | public override bool ParseArg(string arg, Queue args) { 31 | if (arg == "--dump-all") { 32 | ContentDumper.DumpAllPacks(); 33 | return true; 34 | } 35 | 36 | return false; 37 | } 38 | 39 | } 40 | 41 | public class CoreModuleSettings : CoreEngineModuleSettings { 42 | 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /FEZ.Mod.mm/Mod/FezMod.cs: -------------------------------------------------------------------------------- 1 | using Common; 2 | using FezEngine.Mod; 3 | using FezEngine.Mod.Core; 4 | using FezEngine.Tools; 5 | using FezGame.Mod.Core; 6 | using FezGame.Mod.Services; 7 | using Microsoft.Xna.Framework; 8 | using Microsoft.Xna.Framework.Graphics; 9 | using System; 10 | using System.Collections.Generic; 11 | using System.Collections.ObjectModel; 12 | using System.IO; 13 | using System.Linq; 14 | using System.Reflection; 15 | using System.Runtime.CompilerServices; 16 | using System.Text; 17 | using System.Threading.Tasks; 18 | 19 | namespace FezGame.Mod { 20 | public sealed class FezMod : FezModEngine { 21 | 22 | public static new FezMod Instance { get; internal set; } 23 | 24 | public Fez Fez { get; private set; } 25 | 26 | // TODO: Move out! 27 | public double GameTimeScale = 1f; 28 | public GameTime GameTimeUpdate { get; internal set; } 29 | public GameTime GameTimeDraw { get; internal set; } 30 | 31 | internal static void Prepare(string[] args) { 32 | Instance = new FezMod { 33 | Args = new ReadOnlyCollection(args) 34 | }; 35 | } 36 | 37 | internal void Boot(Fez game) { 38 | Logger.Log("FezMod", LogSeverity.Information, "Booting FEZMod"); 39 | Logger.Log("FezMod", LogSeverity.Information, $"Version: {Fez.Version}"); 40 | 41 | Boot(game, new CoreModule()); 42 | } 43 | 44 | public override void LoadComponentReplacements() { 45 | base.LoadComponentReplacements(); 46 | ServiceHelperHooks.ReplacementServices["FezEngine.Services.MouseStateManager"] = new ModMouseStateManager(); 47 | ServiceHelperHooks.ReplacementServices["FezEngine.Services.KeyboardStateManager"] = new ModKeyboardStateManager(); 48 | } 49 | 50 | public override void Initialize() { 51 | base.Initialize(); 52 | } 53 | 54 | public override void LoadComponents() { 55 | base.LoadComponents(); 56 | // ServiceHelper.AddComponent(new ModGUIHost(game)); 57 | } 58 | 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /FEZ.Mod.mm/Mod/Services/ModKeyboardStateManager.cs: -------------------------------------------------------------------------------- 1 | using FezEngine.Mod.Services; 2 | using FezEngine.Services; 3 | using FezEngine.Structure.Input; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | using System.Text; 8 | using System.Threading.Tasks; 9 | using Microsoft.Xna.Framework; 10 | using Microsoft.Xna.Framework.Input; 11 | 12 | namespace FezGame.Mod.Services { 13 | public class ModKeyboardStateManager : IKeyboardStateManager, IServiceWrapper { 14 | 15 | public bool ForceDisable = false; 16 | 17 | private IKeyboardStateManager _; 18 | 19 | public void Wrap(object orig) => _ = (IKeyboardStateManager) orig; 20 | 21 | public FezButtonState CancelTalk { 22 | get { 23 | if (ForceDisable) 24 | return FezButtonState.Up; 25 | 26 | return _.CancelTalk; 27 | } 28 | } 29 | 30 | public FezButtonState ClampLook { 31 | get { 32 | if (ForceDisable) 33 | return FezButtonState.Up; 34 | 35 | return _.ClampLook; 36 | } 37 | } 38 | 39 | public FezButtonState Down { 40 | get { 41 | if (ForceDisable) 42 | return FezButtonState.Up; 43 | 44 | return _.Down; 45 | } 46 | } 47 | 48 | public FezButtonState FpViewToggle { 49 | get { 50 | if (ForceDisable) 51 | return FezButtonState.Up; 52 | 53 | return _.FpViewToggle; 54 | } 55 | } 56 | 57 | public FezButtonState GrabThrow { 58 | get { 59 | if (ForceDisable) 60 | return FezButtonState.Up; 61 | 62 | return _.GrabThrow; 63 | } 64 | } 65 | 66 | public bool IgnoreMapping { 67 | get { 68 | return _.IgnoreMapping; 69 | } 70 | 71 | set { 72 | _.IgnoreMapping = value; 73 | } 74 | } 75 | 76 | public FezButtonState Jump { 77 | get { 78 | if (ForceDisable) 79 | return FezButtonState.Up; 80 | 81 | return _.Jump; 82 | } 83 | } 84 | 85 | public FezButtonState Left { 86 | get { 87 | if (ForceDisable) 88 | return FezButtonState.Up; 89 | 90 | return _.Left; 91 | } 92 | } 93 | 94 | public FezButtonState LookDown { 95 | get { 96 | if (ForceDisable) 97 | return FezButtonState.Up; 98 | 99 | return _.LookDown; 100 | } 101 | } 102 | 103 | public FezButtonState LookLeft { 104 | get { 105 | if (ForceDisable) 106 | return FezButtonState.Up; 107 | 108 | return _.LookLeft; 109 | } 110 | } 111 | 112 | public FezButtonState LookRight { 113 | get { 114 | if (ForceDisable) 115 | return FezButtonState.Up; 116 | 117 | return _.LookRight; 118 | } 119 | } 120 | 121 | public FezButtonState LookUp { 122 | get { 123 | if (ForceDisable) 124 | return FezButtonState.Up; 125 | 126 | return _.LookUp; 127 | } 128 | } 129 | 130 | public FezButtonState MapZoomIn { 131 | get { 132 | if (ForceDisable) 133 | return FezButtonState.Up; 134 | 135 | return _.MapZoomIn; 136 | } 137 | } 138 | 139 | public FezButtonState MapZoomOut { 140 | get { 141 | if (ForceDisable) 142 | return FezButtonState.Up; 143 | 144 | return _.MapZoomOut; 145 | } 146 | } 147 | 148 | public FezButtonState OpenInventory { 149 | get { 150 | if (ForceDisable) 151 | return FezButtonState.Up; 152 | 153 | return _.OpenInventory; 154 | } 155 | } 156 | 157 | public FezButtonState OpenMap { 158 | get { 159 | if (ForceDisable) 160 | return FezButtonState.Up; 161 | 162 | return _.OpenMap; 163 | } 164 | } 165 | 166 | public FezButtonState Pause { 167 | get { 168 | if (ForceDisable) 169 | return FezButtonState.Up; 170 | 171 | return _.Pause; 172 | } 173 | } 174 | 175 | public FezButtonState Right { 176 | get { 177 | if (ForceDisable) 178 | return FezButtonState.Up; 179 | 180 | return _.Right; 181 | } 182 | } 183 | 184 | public FezButtonState RotateLeft { 185 | get { 186 | if (ForceDisable) 187 | return FezButtonState.Up; 188 | 189 | return _.RotateLeft; 190 | } 191 | } 192 | 193 | public FezButtonState RotateRight { 194 | get { 195 | if (ForceDisable) 196 | return FezButtonState.Up; 197 | 198 | return _.RotateRight; 199 | } 200 | } 201 | 202 | public FezButtonState Up { 203 | get { 204 | if (ForceDisable) 205 | return FezButtonState.Up; 206 | 207 | return _.Up; 208 | } 209 | } 210 | 211 | public FezButtonState GetKeyState(Keys key) { 212 | if (ForceDisable) 213 | return FezButtonState.Up; 214 | 215 | return _.GetKeyState(key); 216 | } 217 | 218 | public void RegisterKey(Keys key) { 219 | _.RegisterKey(key); 220 | } 221 | 222 | public void Update(KeyboardState state, GameTime time) { 223 | _.Update(state, time); 224 | } 225 | 226 | public void UpdateMapping() { 227 | _.UpdateMapping(); 228 | } 229 | 230 | } 231 | } 232 | -------------------------------------------------------------------------------- /FEZ.Mod.mm/Mod/Services/ModMouseStateManager.cs: -------------------------------------------------------------------------------- 1 | using FezEngine.Mod.Services; 2 | using FezEngine.Services; 3 | using FezEngine.Structure.Input; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | using System.Text; 8 | using System.Threading.Tasks; 9 | using Microsoft.Xna.Framework; 10 | 11 | namespace FezGame.Mod.Services { 12 | public class ModMouseStateManager : IMouseStateManager, IServiceWrapper { 13 | 14 | public bool ForceDisable = false; 15 | 16 | private IMouseStateManager _; 17 | 18 | public void Wrap(object orig) => _ = (IMouseStateManager) orig; 19 | 20 | public MouseButtonState LeftButton { 21 | get { 22 | if (ForceDisable) 23 | return new MouseButtonState(); 24 | 25 | return _.LeftButton; 26 | } 27 | } 28 | 29 | public MouseButtonState MiddleButton { 30 | get { 31 | if (ForceDisable) 32 | return new MouseButtonState(); 33 | 34 | return _.MiddleButton; 35 | } 36 | } 37 | 38 | public MouseButtonState RightButton { 39 | get { 40 | if (ForceDisable) 41 | return new MouseButtonState(); 42 | 43 | return _.RightButton; 44 | } 45 | } 46 | 47 | public int WheelTurns { 48 | get { 49 | if (ForceDisable) 50 | return 0; 51 | 52 | return _.WheelTurns; 53 | } 54 | } 55 | 56 | public FezButtonState WheelTurnedUp { 57 | get { 58 | if (ForceDisable) 59 | return FezButtonState.Up; 60 | 61 | return _.WheelTurnedUp; 62 | } 63 | } 64 | 65 | public FezButtonState WheelTurnedDown { 66 | get { 67 | if (ForceDisable) 68 | return FezButtonState.Up; 69 | 70 | return _.WheelTurnedDown; 71 | } 72 | } 73 | 74 | public Point Position { 75 | get { 76 | return _.Position; 77 | } 78 | } 79 | 80 | public Point Movement { 81 | get { 82 | if (ForceDisable) 83 | return new Point(); 84 | 85 | return _.Movement; 86 | } 87 | } 88 | 89 | public IntPtr RenderPanelHandle { 90 | set { 91 | _.RenderPanelHandle = value; 92 | } 93 | } 94 | 95 | public IntPtr ParentFormHandle { 96 | set { 97 | _.ParentFormHandle = value; 98 | } 99 | } 100 | 101 | public void Update(GameTime time) { 102 | if (ForceDisable) 103 | return; 104 | 105 | _.Update(time); 106 | } 107 | 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /FEZ.Mod.mm/MonoModRules.cs: -------------------------------------------------------------------------------- 1 | using Mono.Cecil; 2 | using Mono.Cecil.Cil; 3 | using MonoMod.Utils; 4 | using MonoMod.InlineRT; 5 | using System; 6 | using System.Collections.Generic; 7 | using System.Linq; 8 | using System.Text; 9 | using System.Threading.Tasks; 10 | 11 | namespace MonoMod { 12 | static class MonoModRules { 13 | 14 | static MonoModRules() { 15 | } 16 | 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /FEZ.Mod.mm/Patches/Components/GlitchyRespawner.cs: -------------------------------------------------------------------------------- 1 | #pragma warning disable CS0626 // Method, operator, or accessor is marked external and has no attributes on it 2 | 3 | using FezEngine.Tools; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | using System.Text; 8 | using System.Threading.Tasks; 9 | 10 | namespace FezGame.Components { 11 | class patch_GlitchyRespawner { 12 | 13 | public extern void orig_Initialize(); 14 | public void Initialize() { 15 | // orig_Initialize creates Effects, which fails when loading a level with it. 16 | DrawActionScheduler.Schedule(orig_Initialize); 17 | } 18 | 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /FEZ.Mod.mm/Patches/Fez.cs: -------------------------------------------------------------------------------- 1 | #pragma warning disable CS0626 // Method, operator, or accessor is marked external and has no attributes on it 2 | 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using System.Text; 7 | using System.Threading.Tasks; 8 | using MonoMod; 9 | using Microsoft.Xna.Framework; 10 | using FezGame.Mod; 11 | using System.Reflection; 12 | using Common; 13 | using FezEngine.Tools; 14 | using FezGame.Components; 15 | using FezEngine.Components; 16 | using FezEngine.Mod; 17 | 18 | namespace FezGame { 19 | class patch_Fez : Fez { 20 | 21 | public static FezMod Mod { get; internal set; } 22 | 23 | private PropertyInfo p_GameTime_ElapsedGameTime; 24 | private PropertyInfo p_GameTime_TotalGameTime; 25 | 26 | private GameTime _MulGameTime(ref GameTime gameTime) { 27 | double scale = Mod.GameTimeScale; 28 | if (scale == 1d) { 29 | return gameTime; 30 | } 31 | 32 | if (p_GameTime_ElapsedGameTime == null) 33 | p_GameTime_ElapsedGameTime = gameTime.GetType().GetProperty("ElapsedGameTime", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); 34 | 35 | if (p_GameTime_TotalGameTime == null) 36 | p_GameTime_TotalGameTime = gameTime.GetType().GetProperty("TotalGameTime", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); 37 | 38 | TimeSpan egt = gameTime.ElapsedGameTime; 39 | TimeSpan tgt = gameTime.TotalGameTime; 40 | tgt -= egt; 41 | egt = TimeSpan.FromTicks((long) (egt.Ticks * scale)); 42 | tgt += egt; 43 | 44 | ReflectionHelper.SetValue(p_GameTime_ElapsedGameTime, gameTime, egt); 45 | ReflectionHelper.SetValue(p_GameTime_TotalGameTime, gameTime, tgt); 46 | 47 | return gameTime; 48 | } 49 | 50 | public extern void orig_Update(GameTime gameTime); 51 | protected override void Update(GameTime gameTime) { 52 | _MulGameTime(ref gameTime); 53 | Mod.GameTimeUpdate = gameTime; 54 | orig_Update(gameTime); 55 | } 56 | 57 | public extern void orig_Draw(GameTime gameTime); 58 | protected override void Draw(GameTime gameTime) { 59 | _MulGameTime(ref gameTime); 60 | Mod.GameTimeDraw = gameTime; 61 | orig_Draw(gameTime); 62 | } 63 | 64 | public extern void orig_Initialize(); 65 | protected override void Initialize() { 66 | Mod.LoadComponentReplacements(); 67 | orig_Initialize(); 68 | Mod.Initialize(); 69 | } 70 | 71 | [MonoModLinkTo("Microsoft.Xna.Framework.Game", "System.Void LoadContent()")] 72 | [MonoModForceCall] 73 | [MonoModRemove] 74 | public extern void base_LoadContent(); 75 | protected override void LoadContent() { 76 | base_LoadContent(); 77 | foreach (ModBase mod in Mod.Modules) 78 | mod.LoadContent(!ModContent.GameLoadedContent); 79 | ModContent.GameLoadedContent = true; 80 | } 81 | 82 | public static extern void orig_LoadComponents(Fez game); 83 | public static void LoadComponents(Fez game) { 84 | if (ServiceHelper.FirstLoadDone) 85 | return; 86 | orig_LoadComponents(game); 87 | Mod.LoadComponents(); 88 | ServiceHelper.FirstLoadDone = true; 89 | } 90 | 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /FEZ.Mod.mm/Patches/Program.cs: -------------------------------------------------------------------------------- 1 | #pragma warning disable CS0626 // Method, operator, or accessor is marked external and has no attributes on it 2 | 3 | using Common; 4 | using FezGame.Mod; 5 | using Microsoft.Xna.Framework.Graphics; 6 | using MonoMod.Utils; 7 | using System; 8 | using System.Collections.Generic; 9 | using System.Globalization; 10 | using System.IO; 11 | using System.Linq; 12 | using System.Reflection; 13 | using System.Text; 14 | using System.Threading; 15 | using System.Threading.Tasks; 16 | 17 | namespace FezGame { 18 | class patch_Program { 19 | 20 | private static Fez fez; 21 | 22 | private static extern void orig_Main(string[] args); 23 | private static void Main(string[] args) { 24 | FezMod.Prepare(args); 25 | 26 | orig_Main(args); 27 | } 28 | 29 | private static extern void orig_MainInternal(); 30 | private static void MainInternal() { 31 | Thread.CurrentThread.CurrentUICulture = CultureInfo.InvariantCulture; 32 | Thread.CurrentThread.Priority = ThreadPriority.AboveNormal; 33 | try { 34 | fez = new Fez(); 35 | patch_Fez.Mod = FezMod.Instance; 36 | if (!fez.IsDisposed) { 37 | FezMod.Instance.Boot(fez); 38 | fez.Run(); 39 | } 40 | } catch (Exception e) { 41 | Logger.Log("FEZMod", "Fatal error!"); 42 | e.LogDetailed(); 43 | throw; 44 | } 45 | } 46 | 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /FEZMod.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.30320.27 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FEZ.Mod.mm", "FEZ.Mod.mm\FEZ.Mod.mm.csproj", "{380C043D-688B-4120-84C2-8ED3F0D008A9}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FezEngine.Mod.mm", "FezEngine.Mod.mm\FezEngine.Mod.mm.csproj", "{0E1ECE98-650E-4F12-A4D8-41BAA0FC94BE}" 9 | EndProject 10 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Common.Mod.mm", "Common.Mod.mm\Common.Mod.mm.csproj", "{B9904294-D81D-413C-B702-99AED0C0EF73}" 11 | EndProject 12 | Global 13 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 14 | Debug|x86 = Debug|x86 15 | Release|x86 = Release|x86 16 | EndGlobalSection 17 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 18 | {380C043D-688B-4120-84C2-8ED3F0D008A9}.Debug|x86.ActiveCfg = Debug|x86 19 | {380C043D-688B-4120-84C2-8ED3F0D008A9}.Debug|x86.Build.0 = Debug|x86 20 | {380C043D-688B-4120-84C2-8ED3F0D008A9}.Release|x86.ActiveCfg = Release|x86 21 | {380C043D-688B-4120-84C2-8ED3F0D008A9}.Release|x86.Build.0 = Release|x86 22 | {0E1ECE98-650E-4F12-A4D8-41BAA0FC94BE}.Debug|x86.ActiveCfg = Debug|x86 23 | {0E1ECE98-650E-4F12-A4D8-41BAA0FC94BE}.Debug|x86.Build.0 = Debug|x86 24 | {0E1ECE98-650E-4F12-A4D8-41BAA0FC94BE}.Release|x86.ActiveCfg = Release|x86 25 | {0E1ECE98-650E-4F12-A4D8-41BAA0FC94BE}.Release|x86.Build.0 = Release|x86 26 | {B9904294-D81D-413C-B702-99AED0C0EF73}.Debug|x86.ActiveCfg = Debug|x86 27 | {B9904294-D81D-413C-B702-99AED0C0EF73}.Debug|x86.Build.0 = Debug|x86 28 | {B9904294-D81D-413C-B702-99AED0C0EF73}.Release|x86.ActiveCfg = Release|x86 29 | {B9904294-D81D-413C-B702-99AED0C0EF73}.Release|x86.Build.0 = Release|x86 30 | EndGlobalSection 31 | GlobalSection(SolutionProperties) = preSolution 32 | HideSolutionNode = FALSE 33 | EndGlobalSection 34 | GlobalSection(ExtensibilityGlobals) = postSolution 35 | SolutionGuid = {1F85709B-7F30-428F-A8FA-104CCBB956DC} 36 | EndGlobalSection 37 | EndGlobal 38 | -------------------------------------------------------------------------------- /FezEngine.Mod.mm/Mod/Core/CoreEngineModule.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | using YamlDotNet.Serialization; 7 | 8 | namespace FezEngine.Mod.Core { 9 | public class CoreEngineModule : ModBase where TSettings : CoreEngineModuleSettings, new() { 10 | 11 | public override void Load() { 12 | } 13 | 14 | public override void Unload() { 15 | } 16 | 17 | } 18 | 19 | public class CoreEngineModuleSettings : ModSettings { 20 | 21 | public bool CodeReload_WIP { get; set; } = false; 22 | 23 | // TODO: Once CodeReload is no longer WIP, remove this and rename ^ to non-WIP. 24 | [YamlIgnore] 25 | public bool CodeReload { 26 | get => CodeReload_WIP; 27 | set => CodeReload_WIP = value; 28 | } 29 | 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /FezEngine.Mod.mm/Mod/Dummy.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Xna.Framework; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Diagnostics; 5 | using System.Diagnostics.CodeAnalysis; 6 | using System.IO; 7 | using System.Linq; 8 | using System.Reflection; 9 | using System.Runtime.CompilerServices; 10 | using System.Text; 11 | using System.Threading.Tasks; 12 | 13 | namespace FezEngine.Mod { 14 | public static class Dummy { 15 | 16 | public static readonly T[] EmptyArray = new T[0]; 17 | public static readonly List EmptyList = new List(); 18 | public static readonly T Default = default; 19 | 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /FezEngine.Mod.mm/Mod/FezModEngine.cs: -------------------------------------------------------------------------------- 1 | using Common; 2 | using FezEngine.Mod.Core; 3 | using Microsoft.Xna.Framework; 4 | using MonoMod.RuntimeDetour; 5 | using MonoMod.RuntimeDetour.HookGen; 6 | using MonoMod.Utils; 7 | using System; 8 | using System.Collections.Generic; 9 | using System.Collections.ObjectModel; 10 | using System.IO; 11 | using System.Linq; 12 | using System.Net; 13 | using System.Reflection; 14 | using System.Text; 15 | using System.Threading; 16 | using System.Threading.Tasks; 17 | 18 | namespace FezEngine.Mod { 19 | public class FezModEngine { 20 | 21 | public static FezModEngine Instance { get; internal set; } 22 | 23 | public Game Game { get; protected set; } 24 | 25 | public ReadOnlyCollection Args { get; protected set; } 26 | 27 | public string PathGame { get; private set; } 28 | 29 | public ModBase CoreModule { get; private set; } 30 | 31 | public CoreEngineModuleSettings Settings => CoreModule.GetSettings() as CoreEngineModuleSettings; 32 | 33 | public readonly List Modules = new List(); 34 | 35 | private DetourModManager _DetourModManager; 36 | private readonly HashSet _DetourOwners = new HashSet(); 37 | internal readonly List _DetourLog = new List(); 38 | 39 | private bool _Initialized; 40 | 41 | public FezModEngine() { 42 | Instance = this; 43 | } 44 | 45 | public virtual void Boot(Game game, ModBase coreModule) { 46 | Game = game; 47 | CoreModule = coreModule; 48 | 49 | if (Type.GetType("Mono.Runtime") != null) { 50 | // Mono hates HTTPS. 51 | ServicePointManager.ServerCertificateValidationCallback = (sender, certificate, chain, sslPolicyErrors) => { 52 | return true; 53 | }; 54 | } 55 | 56 | ServicePointManager.SecurityProtocol |= SecurityProtocolType.Tls12; 57 | 58 | PathGame = Path.GetDirectoryName(typeof(FezModEngine).Assembly.Location); 59 | 60 | // .NET hates it when strong-named dependencies get updated. 61 | AppDomain.CurrentDomain.AssemblyResolve += (asmSender, asmArgs) => { 62 | AssemblyName asmName = new AssemblyName(asmArgs.Name); 63 | if (!asmName.Name.StartsWith("Mono.Cecil") && 64 | !asmName.Name.StartsWith("YamlDotNet")) 65 | return null; 66 | 67 | Assembly asm = AppDomain.CurrentDomain.GetAssemblies().FirstOrDefault(other => other.GetName().Name == asmName.Name); 68 | if (asm != null) 69 | return asm; 70 | 71 | return Assembly.LoadFrom(Path.Combine(PathGame, asmName.Name + ".dll")); 72 | }; 73 | 74 | // .NET hates to acknowledge manually loaded assemblies. 75 | AppDomain.CurrentDomain.AssemblyResolve += (asmSender, asmArgs) => { 76 | AssemblyName asmName = new AssemblyName(asmArgs.Name); 77 | foreach (Assembly asm in ModRelinker.RelinkedAssemblies) { 78 | if (asm.GetName().Name == asmName.Name) 79 | return asm; 80 | } 81 | 82 | return null; 83 | }; 84 | 85 | _DetourModManager = new DetourModManager(); 86 | _DetourModManager.OnILHook += (owner, from, to) => { 87 | _DetourOwners.Add(owner); 88 | object target = to.Target; 89 | _DetourLog.Add($"new ILHook by {owner.GetName().Name}: {from.GetID()} -> {to.Method?.GetID() ?? "???"}" + (target == null ? "" : $" (target: {target})")); 90 | }; 91 | _DetourModManager.OnHook += (owner, from, to, target) => { 92 | _DetourOwners.Add(owner); 93 | _DetourLog.Add($"new Hook by {owner.GetName().Name}: {from.GetID()} -> {to.GetID()}" + (target == null ? "" : $" (target: {target})")); 94 | }; 95 | _DetourModManager.OnDetour += (owner, from, to) => { 96 | _DetourOwners.Add(owner); 97 | _DetourLog.Add($"new Detour by {owner.GetName().Name}: {from.GetID()} -> {to.GetID()}"); 98 | }; 99 | _DetourModManager.OnNativeDetour += (owner, fromMethod, from, to) => { 100 | _DetourOwners.Add(owner); 101 | _DetourLog.Add($"new NativeDetour by {owner.GetName().Name}: {fromMethod?.ToString() ?? from.ToString("16X")} -> {to.ToString("16X")}"); 102 | }; 103 | HookEndpointManager.OnAdd += (from, to) => { 104 | Assembly owner = HookEndpointManager.GetOwner(to) as Assembly ?? typeof(FezModEngine).Assembly; 105 | _DetourOwners.Add(owner); 106 | object target = to.Target; 107 | _DetourLog.Add($"new On.+= by {owner.GetName().Name}: {from.GetID()} -> {to.Method?.GetID() ?? "???"}" + (target == null ? "" : $" (target: {target})")); 108 | return true; 109 | }; 110 | HookEndpointManager.OnModify += (from, to) => { 111 | Assembly owner = HookEndpointManager.GetOwner(to) as Assembly ?? typeof(FezModEngine).Assembly; 112 | _DetourOwners.Add(owner); 113 | object target = to.Target; 114 | _DetourLog.Add($"new IL.+= by {owner.GetName().Name}: {from.GetID()} -> {to.Method?.GetID() ?? "???"}" + (target == null ? "" : $" (target: {target})")); 115 | return true; 116 | }; 117 | 118 | MainThreadHelper.Instance = new MainThreadHelper(Game); 119 | 120 | ModContent.Initialize(); 121 | 122 | Register(coreModule); 123 | ModLoader.LoadAuto(); 124 | 125 | Queue args = new Queue(Args); 126 | while (args.Count > 0) { 127 | string arg = args.Dequeue(); 128 | foreach (ModBase mod in Modules) { 129 | if (mod.ParseArg(arg, args)) 130 | break; 131 | } 132 | } 133 | } 134 | 135 | public virtual void LoadComponentReplacements() { 136 | } 137 | 138 | public virtual void Initialize() { 139 | _Initialized = true; 140 | foreach (ModBase mod in Modules) 141 | mod.Initialize(); 142 | } 143 | 144 | public virtual void LoadComponents() { 145 | Game.Components.Add(MainThreadHelper.Instance); 146 | } 147 | 148 | public void Register(ModBase module) { 149 | lock (Modules) { 150 | Modules.Add(module); 151 | } 152 | 153 | module.LoadSettings(); 154 | module.Load(); 155 | if (ModContent.GameLoadedContent) 156 | module.LoadContent(true); 157 | 158 | if (_Initialized) { 159 | module.Initialize(); 160 | 161 | // FIXME: Port from Everest! 162 | /* 163 | if (SaveData.Instance != null) { 164 | // we are in a save. we are expecting the save data to already be loaded at this point 165 | Logger.Log("core", $"Loading save data slot {SaveData.Instance.FileSlot} for {module.Metadata}"); 166 | module.LoadSaveData(SaveData.Instance.FileSlot); 167 | 168 | if (SaveData.Instance.CurrentSession?.InArea ?? false) { 169 | // we are in a level. we are expecting the session to already be loaded at this point 170 | Logger.Log("core", $"Loading session slot {SaveData.Instance.FileSlot} for {module.Metadata}"); 171 | module.LoadSession(SaveData.Instance.FileSlot, false); 172 | } 173 | } 174 | */ 175 | } 176 | 177 | ModMetadata meta = module.Metadata; 178 | meta.Hash = ModRelinker.GetChecksum(meta); 179 | 180 | Logger.Log("FEZMod", $"Module {module.Metadata} registered."); 181 | 182 | // Attempt to load mods after their dependencies have been loaded. 183 | // Only load and lock the delayed list if we're not already loading delayed mods. 184 | if (Interlocked.CompareExchange(ref ModLoader.DelayedLock, 1, 0) == 0) { 185 | try { 186 | lock (ModLoader.Delayed) { 187 | for (int i = 0; i < ModLoader.Delayed.Count; i++) { 188 | Tuple entry = ModLoader.Delayed[i]; 189 | if (!ModLoader.DependenciesLoaded(entry.Item1)) 190 | continue; // dependencies are still missing! 191 | 192 | Logger.Log("FEZMod", $"Dependencies of mod {entry.Item1} are now satisfied: loading"); 193 | 194 | if (Modules.Any(mod => mod.Metadata.ID == entry.Item1.ID)) { 195 | // a duplicate of the mod was loaded while it was sitting in the delayed list. 196 | Logger.Log("FEZMod", $"Mod {entry.Item1.ID} already loaded!"); 197 | } else { 198 | entry.Item2?.Invoke(); 199 | ModLoader.LoadMod(entry.Item1); 200 | } 201 | ModLoader.Delayed.RemoveAt(i); 202 | 203 | // we now loaded an extra mod, consider all delayed mods again to deal with transitive dependencies. 204 | i = -1; 205 | } 206 | } 207 | } finally { 208 | Interlocked.Decrement(ref ModLoader.DelayedLock); 209 | } 210 | } 211 | } 212 | 213 | internal void Unregister(ModBase module) { 214 | module.Unload(); 215 | 216 | Assembly asm = module.GetType().Assembly; 217 | MainThreadHelper.Do(() => _DetourModManager.Unload(asm)); 218 | ModRelinker.RelinkedAssemblies.Remove(asm); 219 | 220 | // TODO: Undo event listeners 221 | // TODO: Make sure modules depending on this are unloaded as well. 222 | // TODO: Unload content, textures, audio, maps, AAAAAAAAAAAAAAAAAAAAAAA 223 | 224 | lock (Modules) 225 | Modules.Remove(module); 226 | 227 | Logger.Log("FEZMod", $"Module {module.Metadata} unregistered."); 228 | } 229 | 230 | } 231 | } 232 | -------------------------------------------------------------------------------- /FezEngine.Mod.mm/Mod/FezModEngineExtensions.cs: -------------------------------------------------------------------------------- 1 | using Ionic.Zip; 2 | using Microsoft.Xna.Framework; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.IO; 6 | using System.Linq; 7 | using System.Reflection; 8 | using System.Text; 9 | using System.Threading.Tasks; 10 | 11 | namespace FezEngine.Mod { 12 | public static class FezModEngineExtensions { 13 | 14 | public static MemoryStream ExtractStream(this ZipEntry entry) { 15 | MemoryStream ms = new MemoryStream(); 16 | entry.Extract(ms); 17 | ms.Seek(0, SeekOrigin.Begin); 18 | return ms; 19 | } 20 | 21 | public static string ToHexadecimalString(this byte[] data) 22 | => BitConverter.ToString(data).Replace("-", string.Empty); 23 | 24 | public static T InvokePassing(this MulticastDelegate md, T val, params object[] args) { 25 | if (md == null) 26 | return val; 27 | 28 | object[] args_ = new object[args.Length + 1]; 29 | args_[0] = val; 30 | Array.Copy(args, 0, args_, 1, args.Length); 31 | 32 | Delegate[] ds = md.GetInvocationList(); 33 | for (int i = 0; i < ds.Length; i++) 34 | args_[0] = ds[i].DynamicInvoke(args_); 35 | 36 | return (T)args_[0]; 37 | } 38 | 39 | public static bool InvokeWhileTrue(this MulticastDelegate md, params object[] args) { 40 | if (md == null) 41 | return true; 42 | 43 | Delegate[] ds = md.GetInvocationList(); 44 | for (int i = 0; i < ds.Length; i++) 45 | if (!((bool)ds[i].DynamicInvoke(args))) 46 | return false; 47 | 48 | return true; 49 | } 50 | 51 | public static bool InvokeWhileFalse(this MulticastDelegate md, params object[] args) { 52 | if (md == null) 53 | return false; 54 | 55 | Delegate[] ds = md.GetInvocationList(); 56 | for (int i = 0; i < ds.Length; i++) 57 | if ((bool)ds[i].DynamicInvoke(args)) 58 | return true; 59 | 60 | return false; 61 | } 62 | 63 | public static T InvokeWhileNull(this MulticastDelegate md, params object[] args) where T : class { 64 | if (md == null) 65 | return null; 66 | 67 | Delegate[] ds = md.GetInvocationList(); 68 | for (int i = 0; i < ds.Length; i++) { 69 | T result = (T)ds[i].DynamicInvoke(args); 70 | if (result != null) 71 | return result; 72 | } 73 | 74 | return null; 75 | } 76 | 77 | public static Vector2 ToVector2(this Point p) { 78 | return new Vector2(p.X, p.Y); 79 | } 80 | 81 | public static Vector2? ToVector2(this float[] a) { 82 | if (a == null) 83 | return null; 84 | if (a.Length == 1) 85 | return new Vector2(a[0]); 86 | if (a.Length != 2) 87 | return null; 88 | return new Vector2(a[0], a[1]); 89 | } 90 | 91 | public static Vector3? ToVector3(this float[] a) { 92 | if (a == null) 93 | return null; 94 | if (a.Length == 1) 95 | return new Vector3(a[0]); 96 | if (a.Length != 3) 97 | return null; 98 | return new Vector3(a[0], a[1], a[2]); 99 | } 100 | 101 | public static Delegate CastDelegate(this Delegate source, Type type) { 102 | if (source == null) 103 | return null; 104 | Delegate[] delegates = source.GetInvocationList(); 105 | if (delegates.Length == 1) 106 | return Delegate.CreateDelegate(type, delegates[0].Target, delegates[0].Method); 107 | Delegate[] delegatesDest = new Delegate[delegates.Length]; 108 | for (int i = 0; i < delegates.Length; i++) 109 | delegatesDest[i] = delegates[i].CastDelegate(type); 110 | return Delegate.Combine(delegatesDest); 111 | } 112 | 113 | public static Type[] GetTypesSafe(this Assembly asm) { 114 | try { 115 | return asm.GetTypes(); 116 | } catch (ReflectionTypeLoadException e) { 117 | return e.Types.Where(t => t != null).ToArray(); 118 | } 119 | } 120 | 121 | public static string Nullify(this string value) 122 | => string.IsNullOrEmpty(value) ? null : value; 123 | 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /FezEngine.Mod.mm/Mod/LimitedStream.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | 4 | namespace FezEngine.Mod { 5 | // This mess was first created for old FEZMod, was then carried over to other mods / mod loaders, and now it's back here. 6 | public class LimitedStream : MemoryStream { 7 | 8 | public Stream LimitStream; 9 | public long LimitOffset; 10 | public long LimitLength; 11 | public long? LimitPublicLength; 12 | 13 | public bool LimitStreamShared = false; 14 | private long _Position = 0; 15 | 16 | protected byte[] CachedBuffer; 17 | protected long CachedOffset; 18 | protected long CachedLength; 19 | private bool _CacheBuffer = true; 20 | public bool CacheBuffer { 21 | get => _CacheBuffer; 22 | set { 23 | if (!value) { 24 | CachedBuffer = null; 25 | } 26 | _CacheBuffer = value; 27 | } 28 | } 29 | 30 | public override bool CanRead => LimitStream.CanRead; 31 | 32 | public override bool CanSeek => LimitStream.CanSeek; 33 | 34 | public override bool CanWrite => LimitStream.CanWrite; 35 | 36 | public override long Length => LimitPublicLength ?? LimitLength; 37 | 38 | public override long Position { 39 | get => LimitStreamShared || !CanSeek ? _Position : LimitStream.Position - LimitOffset; 40 | set { 41 | if (CanSeek) 42 | LimitStream.Position = value + LimitOffset; 43 | _Position = value; 44 | } 45 | } 46 | 47 | public LimitedStream(Stream stream, long offset, long length) 48 | : base() { 49 | LimitStream = stream; 50 | LimitOffset = offset; 51 | LimitLength = length; 52 | if (LimitStream.CanSeek) 53 | LimitStream.Seek(offset, SeekOrigin.Begin); 54 | } 55 | 56 | public override void Flush() { 57 | LimitStream.Flush(); 58 | } 59 | 60 | public override int Read(byte[] buffer, int offset, int count) { 61 | if (LimitOffset + LimitLength <= Position) { 62 | return 0; 63 | } 64 | if (LimitOffset + LimitLength <= Position + count) { 65 | count = (int)(LimitLength - (Position - LimitOffset)); 66 | } 67 | int read = LimitStream.Read(buffer, offset, count); 68 | _Position += read; 69 | return read; 70 | } 71 | 72 | public override int ReadByte() { 73 | if (LimitOffset + LimitLength <= Position) { 74 | return 0; 75 | } 76 | if (LimitOffset + LimitLength <= Position + 1) { 77 | return 0; 78 | } 79 | int b = LimitStream.ReadByte(); 80 | if (b != -1) { 81 | _Position++; 82 | } 83 | return b; 84 | } 85 | 86 | public override long Seek(long offset, SeekOrigin origin) { 87 | if (!CanSeek) 88 | throw new NotSupportedException("This stream does not support seek operations."); 89 | switch (origin) { 90 | case SeekOrigin.Begin: 91 | if (LimitOffset + LimitLength <= offset) { 92 | throw new Exception("out of something"); 93 | } 94 | _Position = offset; 95 | return LimitStream.Seek(LimitOffset + offset, SeekOrigin.Begin); 96 | case SeekOrigin.Current: 97 | if (LimitOffset + LimitLength <= Position + offset) { 98 | throw new Exception("out of something"); 99 | } 100 | _Position += offset; 101 | return LimitStream.Seek(offset, SeekOrigin.Current); 102 | case SeekOrigin.End: 103 | if (LimitLength - offset < 0) { 104 | throw new Exception("out of something"); 105 | } 106 | _Position = LimitLength - offset; 107 | return LimitStream.Seek(LimitOffset + LimitLength - offset, SeekOrigin.Begin); 108 | default: 109 | return 0; 110 | } 111 | } 112 | 113 | public override void SetLength(long value) { 114 | if (!CanSeek) 115 | throw new NotSupportedException("This stream does not support seek operations."); 116 | if (LimitStreamShared) { 117 | LimitLength = value; 118 | return; 119 | } 120 | LimitStream.SetLength(LimitOffset + value + LimitLength); 121 | } 122 | 123 | public override void Write(byte[] buffer, int offset, int count) { 124 | if (LimitOffset + LimitLength <= Position + count) { 125 | throw new Exception("out of something"); 126 | } 127 | LimitStream.Write(buffer, offset, count); 128 | _Position += count; 129 | } 130 | 131 | public override byte[] GetBuffer() { 132 | if (CachedBuffer != null && CachedOffset == LimitOffset && CachedLength == LimitLength) { 133 | return CachedBuffer; 134 | } 135 | 136 | if (!_CacheBuffer) { 137 | return ToArray(); 138 | } 139 | 140 | CachedOffset = LimitOffset; 141 | CachedLength = LimitLength; 142 | return CachedBuffer = ToArray(); 143 | } 144 | 145 | private readonly byte[] _ToArrayReadBuffer = new byte[2048]; 146 | public override byte[] ToArray() { 147 | byte[] buffer; 148 | int read; 149 | 150 | long origPosition = LimitStream.Position; 151 | if (LimitStream.CanSeek) 152 | LimitStream.Seek(LimitOffset, SeekOrigin.Begin); 153 | 154 | long length = LimitLength == 0 ? LimitStream.Length : LimitLength; 155 | length -= LimitStream.Position - LimitOffset; 156 | 157 | if (length == 0) { 158 | // most performant way would be to use the base MemoryStream, but 159 | // System.NotSupportedException: Stream does not support writing. 160 | using (MemoryStream ms = new MemoryStream()) { 161 | while (0 < (read = LimitStream.Read(_ToArrayReadBuffer, 0, _ToArrayReadBuffer.Length))) { 162 | base.Write(_ToArrayReadBuffer, 0, read); 163 | } 164 | 165 | LimitStream.Seek(origPosition, SeekOrigin.Begin); 166 | buffer = base.ToArray(); 167 | } 168 | return buffer; 169 | } 170 | 171 | buffer = new byte[length]; 172 | int readCompletely = 0; 173 | while (readCompletely < length) { 174 | read = LimitStream.Read(buffer, readCompletely, buffer.Length - readCompletely); 175 | readCompletely += read; 176 | } 177 | 178 | if (LimitStream.CanSeek) 179 | LimitStream.Seek(origPosition, SeekOrigin.Begin); 180 | return buffer; 181 | } 182 | 183 | public override void Close() { 184 | base.Close(); 185 | if (!LimitStreamShared) { 186 | LimitStream.Close(); 187 | } 188 | } 189 | 190 | protected override void Dispose(bool disposing) { 191 | if (!LimitStreamShared) { 192 | LimitStream.Dispose(); 193 | } 194 | base.Dispose(disposing); 195 | } 196 | 197 | } 198 | } -------------------------------------------------------------------------------- /FezEngine.Mod.mm/Mod/ModAsset.cs: -------------------------------------------------------------------------------- 1 | using Ionic.Zip; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.IO; 5 | using System.Linq; 6 | using System.Text; 7 | using System.Threading.Tasks; 8 | 9 | namespace FezEngine.Mod { 10 | public abstract class ModAsset { 11 | 12 | public ModAssetSource Source; 13 | 14 | public Type Type = null; 15 | public string Format = null; 16 | 17 | public string PathVirtual; 18 | 19 | public List Children = new List(); 20 | 21 | public List Targets = new List(); 22 | 23 | public virtual bool HasData => true; 24 | 25 | public virtual byte[] Data { 26 | get { 27 | using (Stream stream = Open()) { 28 | try { 29 | if (stream is MemoryStream ms) 30 | return ms.GetBuffer(); 31 | } catch { 32 | } 33 | 34 | using (MemoryStream ms = new MemoryStream()) { 35 | byte[] buffer = new byte[2048]; 36 | int read; 37 | while (0 < (read = stream.Read(buffer, 0, buffer.Length))) { 38 | ms.Write(buffer, 0, read); 39 | } 40 | return ms.ToArray(); 41 | } 42 | } 43 | } 44 | } 45 | 46 | protected ModAsset(ModAssetSource source) { 47 | Source = source; 48 | } 49 | 50 | public abstract Stream Open(); 51 | 52 | public bool TryDeserialize(out T result) { 53 | if (Type == typeof(AssetTypeYaml)) { 54 | try { 55 | using (StreamReader reader = new StreamReader(Open())) 56 | result = YamlHelper.Deserializer.Deserialize(reader); 57 | } catch { 58 | result = default; 59 | return false; 60 | } 61 | return true; 62 | } 63 | 64 | // TODO: Deserialize AssetTypeXml 65 | 66 | result = default; 67 | return false; 68 | } 69 | 70 | public T Deserialize() { 71 | TryDeserialize(out T result); 72 | return result; 73 | } 74 | 75 | public bool TryGetMeta(out T meta) { 76 | meta = default; 77 | if (ModContent.TryGet(PathVirtual + ".meta", out ModAsset metaAsset) && 78 | metaAsset.TryDeserialize(out meta) 79 | ) 80 | return true; 81 | return false; 82 | } 83 | 84 | public T GetMeta() { 85 | TryGetMeta(out T meta); 86 | return meta; 87 | } 88 | 89 | } 90 | 91 | public abstract class ModAsset : ModAsset where T : ModAssetSource { 92 | public new T Source => base.Source as T; 93 | protected ModAsset(T source) 94 | : base(source) { 95 | } 96 | } 97 | 98 | public sealed class ModAssetBranch : ModAsset { 99 | public override bool HasData => false; 100 | 101 | public ModAssetBranch() 102 | : base(null) { 103 | } 104 | 105 | public override Stream Open() { 106 | return null; 107 | } 108 | } 109 | 110 | public class FileSystemModAsset : ModAsset { 111 | public readonly string Path; 112 | 113 | public FileSystemModAsset(FileSystemModAssetSource source, string path) 114 | : base(source) { 115 | Path = path; 116 | } 117 | 118 | public override Stream Open() { 119 | return File.Exists(Path) ? File.OpenRead(Path) : null; 120 | } 121 | } 122 | 123 | public class AssemblyModAsset : ModAsset { 124 | public readonly string ResourceName; 125 | 126 | public AssemblyModAsset(AssemblyModAssetSource source, string resourceName) 127 | : base(source) { 128 | ResourceName = resourceName; 129 | } 130 | 131 | public override Stream Open() { 132 | return Source.Assembly.GetManifestResourceStream(ResourceName); 133 | } 134 | } 135 | 136 | public class ZipModAsset : ModAsset { 137 | public readonly string Path; 138 | public readonly ZipEntry Entry; 139 | 140 | public ZipModAsset(ZipModAssetSource source, string path) 141 | : base(source) { 142 | Path = path = path.Replace('\\', '/'); 143 | 144 | foreach (ZipEntry entry in source.Zip.Entries) { 145 | if (entry.FileName.Replace('\\', '/') == path) { 146 | Entry = entry; 147 | break; 148 | } 149 | } 150 | } 151 | 152 | public ZipModAsset(ZipModAssetSource source, ZipEntry entry) 153 | : base(source) { 154 | Path = entry.FileName.Replace('\\', '/'); 155 | Entry = entry; 156 | } 157 | 158 | public override Stream Open() { 159 | string path = Path; 160 | 161 | ZipEntry found = Entry; 162 | if (found == null) { 163 | foreach (ZipEntry entry in Source.Zip.Entries) { 164 | if (entry.FileName.Replace('\\', '/') == path) { 165 | return entry.ExtractStream(); 166 | } 167 | } 168 | } 169 | 170 | if (found == null) 171 | throw new KeyNotFoundException($"{GetType().Name} {Path} not found in archive {Source.Path}"); 172 | 173 | return Entry.ExtractStream(); 174 | } 175 | } 176 | 177 | public class MemoryAsset : ModAsset { 178 | public byte[] _Data; 179 | public override byte[] Data => _Data; 180 | 181 | public MemoryAsset(ModAssetSource source, byte[] data) 182 | : base(source) { 183 | _Data = data; 184 | } 185 | 186 | public override Stream Open() { 187 | return new MemoryStream(_Data, 0, _Data.Length, false, true); 188 | } 189 | } 190 | 191 | public class PackedAsset : ModAsset { 192 | public readonly long Position; 193 | public readonly int Length; 194 | 195 | private byte[] CachedData; 196 | public override byte[] Data => CachedData ?? base.Data; 197 | 198 | public PackedAsset(PackedAssetSource source, long position, int length) 199 | : base(source) { 200 | Position = position; 201 | Length = length; 202 | } 203 | 204 | public void Precache() { 205 | if (CachedData != null) 206 | return; 207 | using (LimitedStream stream = Open() as LimitedStream) 208 | CachedData = stream?.GetBuffer(); 209 | } 210 | 211 | public override Stream Open() { 212 | if (CachedData != null) 213 | return new MemoryStream(CachedData, 0, CachedData.Length, false, true); 214 | return new LimitedStream(File.OpenRead(Source.Path), Position, Length); 215 | } 216 | } 217 | } 218 | -------------------------------------------------------------------------------- /FezEngine.Mod.mm/Mod/ModAssetSource.cs: -------------------------------------------------------------------------------- 1 | using Common; 2 | using Ionic.Zip; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Diagnostics; 6 | using System.IO; 7 | using System.Linq; 8 | using System.Reflection; 9 | using System.Text; 10 | using System.Threading; 11 | using System.Threading.Tasks; 12 | 13 | namespace FezEngine.Mod { 14 | public abstract class ModAssetSource : IDisposable { 15 | 16 | public virtual string DefaultID { get; } 17 | private string _ID; 18 | public string ID { 19 | get => !string.IsNullOrEmpty(_ID) ? _ID : DefaultID; 20 | set => _ID = value; 21 | } 22 | 23 | public ModMetadata Mod; 24 | 25 | public readonly List List = new List(); 26 | public readonly Dictionary Map = new Dictionary(); 27 | 28 | protected abstract void Crawl(bool overwrite); 29 | internal void _Crawl(bool overwrite) => Crawl(overwrite); 30 | 31 | protected virtual void Add(string path, ModAsset asset, bool overwrite) { 32 | ModContent.Add(path, asset, overwrite); 33 | List.Add(asset); 34 | Map[asset.PathVirtual] = asset; 35 | } 36 | 37 | protected virtual void Update(string path, ModAsset next) { 38 | if (next == null) { 39 | Update(ModContent.Get(path), null); 40 | return; 41 | } 42 | 43 | next.PathVirtual = path; 44 | Update((ModAsset)null, next); 45 | } 46 | 47 | protected virtual void Update(ModAsset prev, ModAsset next) { 48 | if (prev != null) { 49 | int index = List.IndexOf(prev); 50 | 51 | if (next == null) { 52 | Map.Remove(prev.PathVirtual); 53 | if (index != -1) 54 | List.RemoveAt(index); 55 | 56 | ModContent.Update(prev, null); 57 | foreach (ModAsset child in prev.Children.ToArray()) 58 | if (child.Source == this) 59 | Update(child, null); 60 | 61 | } else { 62 | Map[prev.PathVirtual] = next; 63 | if (index != -1) 64 | List[index] = next; 65 | else 66 | List.Add(next); 67 | 68 | next.PathVirtual = prev.PathVirtual; 69 | next.Type = prev.Type; 70 | next.Format = prev.Format; 71 | 72 | ModContent.Update(prev, next); 73 | foreach (ModAsset child in prev.Children.ToArray()) 74 | if (child.Source == this) 75 | Update(child, null); 76 | foreach (ModAsset child in next.Children.ToArray()) 77 | if (child.Source == this) 78 | Update((ModAsset)null, child); 79 | } 80 | 81 | } else if (next != null) { 82 | Map[next.PathVirtual] = next; 83 | List.Add(next); 84 | ModContent.Update(null, next); 85 | foreach (ModAsset child in next.Children.ToArray()) 86 | if (child.Source == this) 87 | Update((ModAsset)null, child); 88 | } 89 | } 90 | 91 | private bool disposed = false; 92 | 93 | ~ModAssetSource() { 94 | if (disposed) 95 | return; 96 | disposed = true; 97 | 98 | Dispose(false); 99 | } 100 | 101 | public void Dispose() { 102 | if (disposed) 103 | return; 104 | disposed = true; 105 | 106 | Dispose(true); 107 | GC.SuppressFinalize(this); 108 | } 109 | 110 | protected virtual void Dispose(bool disposing) { 111 | } 112 | 113 | } 114 | 115 | public class FileSystemModAssetSource : ModAssetSource { 116 | public override string DefaultID => System.IO.Path.GetFileName(Path); 117 | 118 | public readonly string Path; 119 | 120 | private readonly Dictionary FileSystemMap = new Dictionary(); 121 | 122 | private readonly FileSystemWatcher watcher; 123 | 124 | public FileSystemModAssetSource(string path) { 125 | Path = path; 126 | 127 | watcher = new FileSystemWatcher { 128 | Path = path, 129 | NotifyFilter = NotifyFilters.FileName | NotifyFilters.DirectoryName | NotifyFilters.LastWrite, 130 | IncludeSubdirectories = true 131 | }; 132 | 133 | watcher.Changed += FileUpdated; 134 | watcher.Created += FileUpdated; 135 | watcher.Deleted += FileUpdated; 136 | watcher.Renamed += FileRenamed; 137 | 138 | watcher.EnableRaisingEvents = true; 139 | } 140 | 141 | protected override void Dispose(bool disposing) { 142 | base.Dispose(disposing); 143 | watcher.Dispose(); 144 | } 145 | 146 | protected override void Crawl(bool overwrite) => Crawl(overwrite, null, Path, false); 147 | 148 | protected virtual void Crawl(bool overwrite, string dir, string root, bool update) { 149 | if (dir == null) 150 | dir = Path; 151 | if (root == null) 152 | root = Path; 153 | 154 | int lastIndexOfSlash = dir.LastIndexOf(System.IO.Path.DirectorySeparatorChar); 155 | // Ignore hidden files and directories. 156 | if (lastIndexOfSlash != -1 && 157 | lastIndexOfSlash >= root.Length && // Make sure to not skip crawling in hidden mods. 158 | dir.Length > lastIndexOfSlash + 1 && 159 | dir[lastIndexOfSlash + 1] == '.') { 160 | // Logger.Log(LogLevel.Verbose, "content", $"Skipped crawling hidden file or directory {dir.Substring(root.Length + 1)}"); 161 | return; 162 | } 163 | 164 | if (File.Exists(dir)) { 165 | string path = dir.Substring(root.Length + 1); 166 | ModAsset asset = new FileSystemModAsset(this, dir); 167 | 168 | if (update) 169 | Update(path, asset); 170 | else 171 | Add(path, asset, overwrite); 172 | return; 173 | } 174 | 175 | string[] files = Directory.GetFiles(dir); 176 | for (int i = 0; i < files.Length; i++) { 177 | string file = files[i]; 178 | Crawl(overwrite, file, root, update); 179 | } 180 | 181 | files = Directory.GetDirectories(dir); 182 | for (int i = 0; i < files.Length; i++) { 183 | string file = files[i]; 184 | Crawl(overwrite, file, root, update); 185 | } 186 | } 187 | 188 | protected override void Add(string path, ModAsset asset, bool overwrite) { 189 | FileSystemModAsset fsma = (FileSystemModAsset)asset; 190 | FileSystemMap[fsma.Path] = fsma; 191 | base.Add(path, asset, overwrite); 192 | } 193 | 194 | protected override void Update(string path, ModAsset next) { 195 | if (next is FileSystemModAsset fsma) { 196 | FileSystemMap[fsma.Path] = fsma; 197 | } 198 | base.Update(path, next); 199 | } 200 | 201 | protected override void Update(ModAsset prev, ModAsset next) { 202 | if (prev is FileSystemModAsset fsma) { 203 | FileSystemMap[fsma.Path] = null; 204 | } 205 | 206 | if ((fsma = next as FileSystemModAsset) != null) { 207 | FileSystemMap[fsma.Path] = fsma; 208 | 209 | // Make sure to wait until the file is readable. 210 | Stopwatch timer = Stopwatch.StartNew(); 211 | while (File.Exists(fsma.Path)) { 212 | try { 213 | new FileStream(fsma.Path, FileMode.Open, FileAccess.Read, FileShare.Read | FileShare.Delete).Dispose(); 214 | break; 215 | } catch (ThreadAbortException) { 216 | throw; 217 | } catch (ThreadInterruptedException) { 218 | throw; 219 | } catch { 220 | // Retry, but not infinitely. 221 | if (timer.Elapsed.TotalSeconds >= 2D) 222 | throw; 223 | } 224 | } 225 | timer.Stop(); 226 | } 227 | 228 | base.Update(prev, next); 229 | } 230 | 231 | private void FileUpdated(object source, FileSystemEventArgs e) { 232 | // Directories will be "changed" as soon as their children change. 233 | if (e.ChangeType == WatcherChangeTypes.Changed && Directory.Exists(e.FullPath)) 234 | return; 235 | 236 | Logger.Log("FEZMod.Content", $"File updated: {e.FullPath} - {e.ChangeType}"); 237 | QueuedTaskHelper.Do(e.FullPath, () => Update(e.FullPath, e.FullPath)); 238 | } 239 | 240 | private void FileRenamed(object source, RenamedEventArgs e) { 241 | Logger.Log("FEZMod.Content", $"File renamed: {e.OldFullPath} - {e.FullPath}"); 242 | QueuedTaskHelper.Do(Tuple.Create(e.OldFullPath, e.FullPath), () => Update(e.OldFullPath, e.FullPath)); 243 | } 244 | 245 | private void Update(string pathPrev, string pathNext) { 246 | ModAsset prev; 247 | if (FileSystemMap.TryGetValue(pathPrev, out FileSystemModAsset prevFS)) 248 | prev = prevFS; 249 | else 250 | prev = ModContent.Get(pathPrev.Substring(Path.Length + 1)); 251 | 252 | if (File.Exists(pathNext)) { 253 | if (prev != null) 254 | Update(prev, new FileSystemModAsset(this, pathNext)); 255 | else 256 | Update(pathNext.Substring(Path.Length + 1), new FileSystemModAsset(this, pathNext)); 257 | 258 | } else if (Directory.Exists(pathNext)) { 259 | Update(prev, null); 260 | Crawl(true, pathNext, Path, true); 261 | 262 | } else if (prev != null) { 263 | Update(prev, null); 264 | 265 | } else { 266 | Update(pathPrev, (ModAsset)null); 267 | } 268 | } 269 | } 270 | 271 | public class AssemblyModAssetSource : ModAssetSource { 272 | public override string DefaultID => Assembly.GetName().Name; 273 | 274 | /// 275 | /// The assembly containing the mod content as resources. 276 | /// 277 | public readonly Assembly Assembly; 278 | 279 | public AssemblyModAssetSource(Assembly asm) { 280 | Assembly = asm; 281 | } 282 | 283 | protected override void Crawl(bool overwrite) { 284 | string[] resourceNames = Assembly.GetManifestResourceNames(); 285 | for (int i = 0; i < resourceNames.Length; i++) { 286 | string name = resourceNames[i]; 287 | int indexOfContent = name.IndexOf("Content"); 288 | if (indexOfContent < 0) 289 | continue; 290 | name = name.Substring(indexOfContent + 8); 291 | Add(name, new AssemblyModAsset(this, resourceNames[i]), overwrite); 292 | } 293 | } 294 | } 295 | 296 | public class ZipModAssetSource : ModAssetSource { 297 | public override string DefaultID => System.IO.Path.GetFileName(Path); 298 | 299 | public readonly string Path; 300 | 301 | public readonly ZipFile Zip; 302 | 303 | public ZipModAssetSource(string path) { 304 | Path = path; 305 | Zip = new ZipFile(path); 306 | } 307 | 308 | protected override void Crawl(bool overwrite) { 309 | foreach (ZipEntry entry in Zip.Entries) { 310 | string entryName = entry.FileName.Replace('\\', '/'); 311 | if (entryName.EndsWith("/")) 312 | continue; 313 | Add(entryName, new ZipModAsset(this, entry), overwrite); 314 | } 315 | } 316 | 317 | protected override void Dispose(bool disposing) { 318 | base.Dispose(disposing); 319 | Zip.Dispose(); 320 | } 321 | } 322 | 323 | public class PackedAssetSource : ModAssetSource { 324 | public override string DefaultID => System.IO.Path.GetFileName(Path); 325 | 326 | public readonly string Path; 327 | 328 | public readonly bool Precache; 329 | 330 | public PackedAssetSource(string path, bool precache) { 331 | Path = path; 332 | Precache = precache; 333 | } 334 | 335 | protected override void Crawl(bool overwrite) { 336 | using (FileStream stream = File.OpenRead(Path)) 337 | using (BinaryReader reader = new BinaryReader(stream)) { 338 | int count = reader.ReadInt32(); 339 | for (int i = 0; i < count; i++) { 340 | string path = reader.ReadString().ToLowerInvariant().Replace('\\', '/'); 341 | int length = reader.ReadInt32(); 342 | // Packs don't contain the file extensions. 343 | // This affects SharedContentManager.ReadAsset and anything else that manually checks the asset type, 344 | // but MemoryContentManager.OpenStream keeps working as it doesn't have any asset type checks. 345 | if (Precache) { 346 | Add(path, new MemoryAsset(this, reader.ReadBytes(length)), overwrite); 347 | } else { 348 | Add(path, new PackedAsset(this, stream.Position, length), overwrite); 349 | stream.Seek(length, SeekOrigin.Current); 350 | } 351 | } 352 | } 353 | } 354 | } 355 | } 356 | -------------------------------------------------------------------------------- /FezEngine.Mod.mm/Mod/ModBase.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Xna.Framework.Graphics; 2 | using MonoMod.Utils; 3 | using MonoMod.InlineRT; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.IO; 7 | using System.Linq; 8 | using System.Reflection; 9 | using System.Text; 10 | using System.Threading.Tasks; 11 | using YamlDotNet.Serialization; 12 | using System.Configuration; 13 | using Microsoft.Xna.Framework.Input; 14 | using Common; 15 | 16 | namespace FezEngine.Mod { 17 | public abstract class ModBase { 18 | 19 | public virtual ModMetadata Metadata { get; set; } 20 | 21 | public virtual ModSettings GetSettings() => null; 22 | public virtual ModSettings GetSaveData() => null; 23 | 24 | public virtual void LoadSettings() { 25 | } 26 | 27 | public virtual void SaveSettings() { 28 | } 29 | 30 | public virtual void LoadSaveData(int slot) { 31 | } 32 | 33 | public virtual void SaveSaveData(int slot) { 34 | } 35 | 36 | public virtual void DeleteSaveData(int slot) { 37 | } 38 | 39 | public virtual void Load() { 40 | LoadSettings(); 41 | } 42 | 43 | public virtual void Initialize() { 44 | } 45 | 46 | public virtual void LoadContent(bool firstLoad) { 47 | } 48 | 49 | public virtual void Unload() { 50 | } 51 | 52 | public virtual bool ParseArg(string arg, Queue args) { 53 | return false; 54 | } 55 | 56 | } 57 | 58 | public abstract class ModBase : ModBase where TSettings : ModSettings, new() { 59 | 60 | public TSettings Settings = new TSettings(); 61 | public override ModSettings GetSettings() => Settings; 62 | 63 | public override void LoadSettings() { 64 | (Settings ??= new TSettings()).Load(Path.Combine(Util.LocalConfigFolder, $"Settings-{Metadata.ID}.yaml")); 65 | } 66 | 67 | public override void SaveSettings() { 68 | (Settings ??= new TSettings()).Save(Path.Combine(Util.LocalConfigFolder, $"Settings-{Metadata.ID}.yaml")); 69 | } 70 | 71 | } 72 | 73 | public abstract class ModBase : ModBase where TSettings : ModSettings, new() where TSaveData : ModSettings, new() { 74 | 75 | public TSaveData SaveData = new TSaveData(); 76 | public override ModSettings GetSaveData() => SaveData; 77 | 78 | public override void LoadSaveData(int slot) { 79 | (SaveData ??= new TSaveData()).Load(Path.Combine(Util.LocalConfigFolder, $"SaveSlot{slot}-{Metadata.ID}.yaml")); 80 | } 81 | 82 | public override void SaveSaveData(int slot) { 83 | (SaveData ??= new TSaveData()).Save(Path.Combine(Util.LocalConfigFolder, $"SaveSlot{slot}-{Metadata.ID}.yaml")); 84 | } 85 | 86 | public override void DeleteSaveData(int slot) { 87 | string path = Path.Combine(Util.LocalConfigFolder, $"SaveSlot{slot}-{Metadata.ID}.yaml"); 88 | if (File.Exists(path)) 89 | File.Delete(path); 90 | } 91 | 92 | } 93 | 94 | public abstract class ModSettings { 95 | 96 | [YamlIgnore] 97 | public string FilePath = ""; 98 | 99 | public virtual void Load(string path = "") { 100 | FilePath = path = Path.GetFullPath(path.Nullify() ?? FilePath); 101 | 102 | if (!File.Exists(path)) { 103 | Save(path); 104 | return; 105 | } 106 | 107 | Logger.Log("FEZMod.Settings", $"Loading {GetType().Name} from {path}"); 108 | 109 | using (Stream stream = File.OpenRead(path)) 110 | using (StreamReader reader = new StreamReader(stream)) 111 | Load(reader); 112 | } 113 | 114 | public virtual void Load(TextReader reader) { 115 | YamlHelper.DeserializerUsing(this).Deserialize(reader, GetType()); 116 | } 117 | 118 | public virtual void Save(string path = "") { 119 | path = Path.GetFullPath(path.Nullify() ?? FilePath); 120 | 121 | Logger.Log("FEZMod.Settings", $"Saving {GetType().Name} to {path}"); 122 | 123 | string dir = Path.GetDirectoryName(path); 124 | if (!Directory.Exists(dir)) 125 | Directory.CreateDirectory(dir); 126 | 127 | using (Stream stream = File.OpenWrite(path + ".tmp")) 128 | using (StreamWriter writer = new StreamWriter(stream)) 129 | Save(writer); 130 | 131 | if (File.Exists(path)) 132 | File.Delete(path); 133 | File.Move(path + ".tmp", path); 134 | } 135 | 136 | public virtual void Save(TextWriter writer) { 137 | YamlHelper.Serializer.Serialize(writer, this, GetType()); 138 | } 139 | 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /FezEngine.Mod.mm/Mod/ModContent.cs: -------------------------------------------------------------------------------- 1 | using Common; 2 | using FezEngine.Structure; 3 | using Microsoft.Xna.Framework; 4 | using Microsoft.Xna.Framework.Graphics; 5 | using System; 6 | using System.Collections.Generic; 7 | using System.Diagnostics; 8 | using System.IO; 9 | using System.Linq; 10 | using System.Text; 11 | using System.Threading.Tasks; 12 | using YamlDotNet.Core; 13 | using YamlDotNet.Core.Events; 14 | using YamlDotNet.Serialization; 15 | using YamlDotNet.Serialization.ObjectFactories; 16 | 17 | namespace FezEngine.Mod { 18 | public sealed class AssetTypeDirectory { private AssetTypeDirectory() { } } 19 | public sealed class AssetTypeAssembly { private AssetTypeAssembly() { } } 20 | public sealed class AssetTypeYaml { private AssetTypeYaml() { } } 21 | public sealed class AssetTypeXml { private AssetTypeXml() { } } 22 | public sealed class AssetTypeText { private AssetTypeText() { } } 23 | public sealed class AssetTypeMetadataYaml { private AssetTypeMetadataYaml() { } } 24 | 25 | public delegate string AssetTypeGuesser(string file, out Type type, out string format); 26 | 27 | public static class ModContent { 28 | 29 | public static bool GameLoadedContent = false; 30 | 31 | public static string PathContentOrig; 32 | public static string PathDUMP; 33 | 34 | public static readonly List Mods = new List(); 35 | 36 | public static readonly Dictionary Map = new Dictionary(); 37 | 38 | public static readonly HashSet NonConflictTypes = new HashSet() { 39 | typeof(AssetTypeDirectory), 40 | typeof(AssetTypeMetadataYaml) 41 | }; 42 | 43 | internal static readonly List LoadedAssetPaths = new List(); 44 | internal static readonly List LoadedAssetFullPaths = new List(); 45 | internal static readonly List LoadedAssets = new List(); 46 | 47 | internal static readonly char[] DirSplit = { '/' }; 48 | 49 | internal static void Initialize() { 50 | Directory.CreateDirectory(PathContentOrig = Path.Combine(FezModEngine.Instance.PathGame, FezModEngine.Instance.Game.Content.RootDirectory)); 51 | Directory.CreateDirectory(PathDUMP = Path.Combine(FezModEngine.Instance.PathGame, "ModDUMP")); 52 | 53 | Crawl(new AssemblyModAssetSource(FezModEngine.Instance.GetType().Assembly) { 54 | ID = "FEZMod", 55 | // Mod = CoreModule.Instance.Metadata // Can't actually set Mod this early. 56 | }); 57 | } 58 | 59 | public static bool TryGet(string path, out ModAsset metadata) { 60 | path = path.ToLowerInvariant().Replace('\\', '/'); 61 | 62 | lock (Map) 63 | if (Map.TryGetValue(path, out metadata) && metadata != null) 64 | return true; 65 | 66 | metadata = null; 67 | return false; 68 | } 69 | 70 | public static ModAsset Get(string path) { 71 | if (TryGet(path, out ModAsset metadata)) 72 | return metadata; 73 | return null; 74 | } 75 | 76 | public static bool TryGet(string path, out ModAsset metadata) { 77 | path = path.ToLowerInvariant().Replace('\\', '/'); 78 | 79 | List parts = new List(path.Split(DirSplit, StringSplitOptions.RemoveEmptyEntries)); 80 | for (int i = 0; i < parts.Count; i++) { 81 | string part = parts[i]; 82 | 83 | if (part == "..") { 84 | parts.RemoveAt(i); 85 | parts.RemoveAt(i - 1); 86 | i -= 2; 87 | continue; 88 | } 89 | 90 | if (part == ".") { 91 | parts.RemoveAt(i); 92 | i -= 1; 93 | continue; 94 | } 95 | } 96 | 97 | path = string.Join("/", parts); 98 | 99 | lock (Map) 100 | if (Map.TryGetValue(path, out metadata) && metadata != null && metadata.Type == typeof(T)) 101 | return true; 102 | 103 | metadata = null; 104 | return false; 105 | } 106 | 107 | public static ModAsset Get(string path) { 108 | if (TryGet(path, out ModAsset metadata)) 109 | return metadata; 110 | return null; 111 | } 112 | 113 | public static void Add(string path, ModAsset metadata, bool overwrite = true) { 114 | path = path.ToLowerInvariant().Replace('\\', '/'); 115 | 116 | if (metadata != null) { 117 | if (metadata.Type == null) 118 | path = GuessType(path, out metadata.Type, out metadata.Format); 119 | metadata.PathVirtual = path; 120 | } 121 | string prefix = metadata?.Source?.ID; 122 | 123 | if (metadata != null && metadata.Type == typeof(AssetTypeDirectory) && !(metadata is ModAssetBranch)) 124 | return; 125 | 126 | ModAsset metadataPrev; 127 | 128 | lock (Map) { 129 | // We want our new mapping to replace the previous one, but need to replace the previous one in the shadow structure. 130 | if (!Map.TryGetValue(path, out metadataPrev)) 131 | metadataPrev = null; 132 | 133 | if (!overwrite && metadataPrev != null) 134 | return; 135 | 136 | if (metadata == null && metadataPrev != null && metadataPrev.Type == typeof(AssetTypeDirectory)) 137 | return; 138 | 139 | if (metadata == null) { 140 | Map.Remove(path); 141 | if (prefix != null) 142 | Map.Remove($"{prefix}:/{path}"); 143 | 144 | } else { 145 | if (Map.TryGetValue(path, out ModAsset existing) && existing != null && 146 | existing.Source != metadata.Source && !NonConflictTypes.Contains(existing.Type)) { 147 | Logger.Log("FEZMod.Content", $"CONFLICT for asset path {path} ({existing?.Source?.ID ?? "???"} vs {metadata?.Source?.ID ?? "???"})"); 148 | } 149 | 150 | Map[path] = metadata; 151 | if (prefix != null) 152 | Map[$"{prefix}:/{path}"] = metadata; 153 | } 154 | 155 | // If we're not already the highest level shadow "node"... 156 | if (path != "") { 157 | // Add directories automatically. 158 | string pathDir = Path.GetDirectoryName(path).Replace('\\', '/'); 159 | if (!Map.TryGetValue(pathDir, out ModAsset metadataDir)) { 160 | metadataDir = new ModAssetBranch { 161 | PathVirtual = pathDir, 162 | Type = typeof(AssetTypeDirectory) 163 | }; 164 | Add(pathDir, metadataDir); 165 | } 166 | // If a previous mapping exists, replace it in the shadow structure. 167 | lock (metadataDir.Children) { 168 | int metadataPrevIndex = metadataDir.Children.IndexOf(metadataPrev); 169 | if (metadataPrevIndex != -1) { 170 | if (metadata == null) { 171 | metadataDir.Children.RemoveAt(metadataPrevIndex); 172 | } else { 173 | metadataDir.Children[metadataPrevIndex] = metadata; 174 | } 175 | } else { 176 | metadataDir.Children.Add(metadata); 177 | } 178 | } 179 | } 180 | } 181 | 182 | if (GameLoadedContent) { 183 | // We're late-loading this mod asset and thus need to manually ingest new assets. 184 | // FIXME: Endless recursion on game startup. 185 | // Logger.Log("FEZMod.Content", $"Late ingest via update for {prefix}:/{path}"); 186 | // Update(metadataPrev, metadata); 187 | } 188 | } 189 | 190 | public static event AssetTypeGuesser OnGuessType; 191 | 192 | public static string GuessType(string file, out Type type, out string format) { 193 | type = typeof(object); 194 | format = Path.GetExtension(file) ?? ""; 195 | if (format.Length >= 1) 196 | format = format.Substring(1); 197 | 198 | if (file.EndsWith(".dll")) { 199 | type = typeof(AssetTypeAssembly); 200 | 201 | } else if (file.EndsWith(".png")) { 202 | type = typeof(Texture2D); 203 | file = file.Substring(0, file.Length - 4); 204 | 205 | } else if ( 206 | file == "fezmod.yaml" || 207 | file == "fezmod.yml" 208 | ) { 209 | type = typeof(AssetTypeMetadataYaml); 210 | file = file.Substring(0, file.Length - (file.EndsWith(".yaml") ? 5 : 4)); 211 | format = ".yml"; 212 | 213 | } else if (file.EndsWith(".yaml")) { 214 | type = typeof(AssetTypeYaml); 215 | file = file.Substring(0, file.Length - 5); 216 | format = ".yml"; 217 | } else if (file.EndsWith(".yml")) { 218 | type = typeof(AssetTypeYaml); 219 | file = file.Substring(0, file.Length - 4); 220 | 221 | } else if (file.EndsWith(".xml")) { 222 | type = typeof(AssetTypeXml); 223 | file = file.Substring(0, file.Length - 4); 224 | 225 | } else if (file.EndsWith(".txt")) { 226 | type = typeof(AssetTypeText); 227 | file = file.Substring(0, file.Length - 4); 228 | 229 | } else if (OnGuessType != null) { 230 | // Allow mods to parse custom types. 231 | Delegate[] ds = OnGuessType.GetInvocationList(); 232 | for (int i = 0; i < ds.Length; i++) { 233 | string fileMod = ((AssetTypeGuesser) ds[i])(file, out Type typeMod, out string formatMod); 234 | if (fileMod == null || typeMod == null || formatMod == null) 235 | continue; 236 | file = fileMod; 237 | type = typeMod; 238 | format = formatMod; 239 | break; 240 | } 241 | } 242 | 243 | return file; 244 | } 245 | 246 | public static event Action OnUpdate; 247 | public static void Update(ModAsset prev, ModAsset next) { 248 | if (prev != null) { 249 | foreach (object target in prev.Targets) { 250 | if (target is Dirtyable mtex) { 251 | // FIXME: Port from Everest! 252 | /* 253 | AssetReloadHelper.Do($"{Dialog.Clean("ASSETRELOADHELPER_UNLOADINGTEXTURE")} {Path.GetFileName(prev.PathVirtual)}", () => { 254 | mtex.UndoOverride(prev); 255 | }); 256 | */ 257 | } 258 | } 259 | 260 | if (next == null || prev.PathVirtual != next.PathVirtual) 261 | Add(prev.PathVirtual, null); 262 | } 263 | 264 | 265 | if (next != null) { 266 | Add(next.PathVirtual, next); 267 | 268 | // Loaded assets can be folders, which means that we need to check the updated assets' entire path. 269 | HashSet updated = new HashSet(); 270 | for (ModAsset asset = next; asset != null && !string.IsNullOrEmpty(asset.PathVirtual); asset = Get(Path.GetDirectoryName(asset.PathVirtual).Replace('\\', '/'))) { 271 | int index = LoadedAssetPaths.IndexOf(asset.PathVirtual); 272 | if (index == -1) 273 | continue; 274 | 275 | WeakReference weakref = LoadedAssets[index]; 276 | if (!updated.Add(weakref)) 277 | continue; 278 | 279 | object target = weakref.Target; 280 | if (!weakref.IsAlive) 281 | continue; 282 | 283 | // Don't feed the entire tree into the loaded asset, just the updated asset. 284 | ProcessUpdate(target, next, false); 285 | } 286 | } 287 | 288 | OnUpdate?.Invoke(prev, next); 289 | } 290 | 291 | public static void Crawl(ModAssetSource meta, bool overwrite = true) { 292 | if (!Mods.Contains(meta)) 293 | Mods.Add(meta); 294 | 295 | // FIXME: Trick asset reload helper into insta-reloading like in Everest! 296 | 297 | meta._Crawl(overwrite); 298 | } 299 | 300 | public static event Action OnProcessLoad; 301 | public static void ProcessLoad(object asset, string assetNameFull) { 302 | string assetName = assetNameFull; 303 | if (assetName.StartsWith(PathContentOrig)) { 304 | assetName = assetName.Substring(PathContentOrig.Length + 1); 305 | } 306 | assetName = assetName.Replace('\\', '/'); 307 | 308 | int loadedIndex = LoadedAssetPaths.IndexOf(assetName); 309 | if (loadedIndex == -1) { 310 | LoadedAssetPaths.Add(assetName); 311 | LoadedAssetFullPaths.Add(assetNameFull); 312 | LoadedAssets.Add(new WeakReference(asset)); 313 | } else { 314 | LoadedAssets[loadedIndex] = new WeakReference(asset); 315 | } 316 | 317 | OnProcessLoad?.Invoke(asset, assetName); 318 | 319 | ProcessUpdate(asset, Get(assetName), true); 320 | } 321 | 322 | public static event Action OnProcessUpdate; 323 | public static void ProcessUpdate(object asset, ModAsset mapping, bool load) { 324 | if (asset == null || mapping == null) 325 | return; 326 | 327 | /* 328 | if (asset is Atlas atlas) { 329 | string reloadingText = Dialog.Language == null ? "" : Dialog.Clean(mapping.Children.Count == 0 ? "ASSETRELOADHELPER_RELOADINGTEXTURE" : "ASSETRELOADHELPER_RELOADINGTEXTURES"); 330 | AssetReloadHelper.Do(load, $"{reloadingText} {Path.GetFileName(mapping.PathVirtual)}", () => { 331 | atlas.ResetCaches(); 332 | (atlas as patch_Atlas).Ingest(mapping); 333 | }); 334 | } 335 | */ 336 | 337 | OnProcessUpdate?.Invoke(asset, mapping, load); 338 | } 339 | 340 | } 341 | } 342 | -------------------------------------------------------------------------------- /FezEngine.Mod.mm/Mod/ModLoader.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Xna.Framework.Graphics; 2 | using MonoMod.InlineRT; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Collections.ObjectModel; 6 | using System.IO; 7 | using Ionic.Zip; 8 | using System.Linq; 9 | using System.Reflection; 10 | using System.Text; 11 | using System.Threading.Tasks; 12 | using MonoMod.Utils; 13 | using System.Runtime.CompilerServices; 14 | using Mono.Cecil; 15 | using Mono.Cecil.Cil; 16 | using MonoMod; 17 | using MCC = Mono.Cecil.Cil; 18 | using MonoMod.Cil; 19 | using Microsoft.Xna.Framework; 20 | using System.Diagnostics; 21 | using Common; 22 | 23 | namespace FezEngine.Mod { 24 | public static class ModLoader { 25 | 26 | public static string PathMods { get; internal set; } 27 | public static string PathCache { get; internal set; } 28 | 29 | public static string PathBlacklist { get; internal set; } 30 | internal static List _Blacklist = new List(); 31 | public static ReadOnlyCollection Blacklist => _Blacklist?.AsReadOnly(); 32 | 33 | public static string PathWhitelist { get; internal set; } 34 | internal static string NameWhitelist; 35 | internal static List _Whitelist; 36 | public static ReadOnlyCollection Whitelist => _Whitelist?.AsReadOnly(); 37 | 38 | internal static List> Delayed = new List>(); 39 | internal static int DelayedLock; 40 | 41 | internal static readonly Version _VersionInvalid = new Version(int.MaxValue, int.MaxValue, int.MaxValue, int.MaxValue); 42 | internal static readonly Version _VersionMax = new Version(int.MaxValue, int.MaxValue); 43 | 44 | internal static Dictionary PermanentBlacklist = new Dictionary() { 45 | 46 | // Note: Most, if not all mods use Major.Minor.Build 47 | // Revision is thus set to -1 and < 0 48 | // Entries with a revision of 0 are there because there is no update / fix for those mods. 49 | 50 | }; 51 | 52 | internal static HashSet> PermanentConflictlist = new HashSet>() { 53 | 54 | // See above versioning note. 55 | 56 | }; 57 | 58 | internal static FileSystemWatcher Watcher; 59 | 60 | public static bool AutoLoadNewMods { get; internal set; } 61 | 62 | internal static void LoadAuto() { 63 | Directory.CreateDirectory(PathMods = Path.Combine(FezModEngine.Instance.PathGame, "Mods")); 64 | Directory.CreateDirectory(PathCache = Path.Combine(PathMods, "Cache")); 65 | 66 | PathBlacklist = Path.Combine(PathMods, "blacklist.txt"); 67 | if (File.Exists(PathBlacklist)) { 68 | _Blacklist = File.ReadAllLines(PathBlacklist).Select(l => (l.StartsWith("#") ? "" : l).Trim()).ToList(); 69 | } else { 70 | using (StreamWriter writer = File.CreateText(PathBlacklist)) { 71 | writer.WriteLine("# This is the blacklist. Lines starting with # are ignored."); 72 | writer.WriteLine("ExampleFolder"); 73 | writer.WriteLine("SomeMod.zip"); 74 | } 75 | } 76 | 77 | if (!string.IsNullOrEmpty(NameWhitelist)) { 78 | PathWhitelist = Path.Combine(PathMods, NameWhitelist); 79 | if (File.Exists(PathWhitelist)) { 80 | _Whitelist = File.ReadAllLines(PathWhitelist).Select(l => (l.StartsWith("#") ? "" : l).Trim()).ToList(); 81 | } 82 | } 83 | 84 | Stopwatch watch = Stopwatch.StartNew(); 85 | 86 | string[] files = Directory.GetFiles(PathMods); 87 | for (int i = 0; i < files.Length; i++) { 88 | string file = Path.GetFileName(files[i]); 89 | if (!file.EndsWith(".zip") || _Blacklist.Contains(file)) 90 | continue; 91 | if (_Whitelist != null && !_Whitelist.Contains(file)) 92 | continue; 93 | LoadZip(file); 94 | } 95 | 96 | files = Directory.GetDirectories(PathMods); 97 | for (int i = 0; i < files.Length; i++) { 98 | string file = Path.GetFileName(files[i]); 99 | if (file == "Cache" || _Blacklist.Contains(file)) 100 | continue; 101 | if (_Whitelist != null && !_Whitelist.Contains(file)) 102 | continue; 103 | LoadDir(file); 104 | } 105 | 106 | watch.Stop(); 107 | Logger.Log("FEZMod.Loader", $"ALL MODS LOADED IN {watch.ElapsedMilliseconds}ms"); 108 | 109 | Watcher = new FileSystemWatcher { 110 | Path = PathMods, 111 | NotifyFilter = NotifyFilters.FileName | NotifyFilters.DirectoryName | NotifyFilters.LastWrite 112 | }; 113 | 114 | Watcher.Created += LoadAutoUpdated; 115 | 116 | Watcher.EnableRaisingEvents = true; 117 | AutoLoadNewMods = true; 118 | } 119 | 120 | private static void LoadAutoUpdated(object source, FileSystemEventArgs e) { 121 | if (!AutoLoadNewMods) 122 | return; 123 | 124 | Logger.Log("FEZMod.Loader", $"Possible new mod container: {e.FullPath}"); 125 | QueuedTaskHelper.Do("LoadAutoUpdated:" + e.FullPath, () => MainThreadHelper.Do(() => { 126 | if (Directory.Exists(e.FullPath)) 127 | LoadDir(e.FullPath); 128 | else if (e.FullPath.EndsWith(".zip")) 129 | LoadZip(e.FullPath); 130 | })); 131 | } 132 | 133 | public static void LoadZip(string archive) { 134 | if (!File.Exists(archive)) 135 | archive = Path.Combine(PathMods, archive); 136 | if (!File.Exists(archive)) 137 | return; 138 | 139 | Logger.Log("FEZMod.Loader", $"Loading mod .zip: {archive}"); 140 | 141 | ModMetadata meta = null; 142 | ModMetadata[] multimetas = null; 143 | 144 | using (ZipFile zip = new ZipFile(archive)) { 145 | foreach (ZipEntry entry in zip.Entries) { 146 | if (entry.FileName == "metadata.yaml") { 147 | using (MemoryStream stream = entry.ExtractStream()) 148 | using (StreamReader reader = new StreamReader(stream)) { 149 | try { 150 | meta = YamlHelper.Deserializer.Deserialize(reader); 151 | meta.PathArchive = archive; 152 | meta.PostParse(); 153 | } catch (Exception e) { 154 | Logger.Log("FEZMod.Loader", $"Failed parsing metadata.yaml in {archive}: {e}"); 155 | } 156 | } 157 | continue; 158 | } 159 | if (entry.FileName == "multimetadata.yaml" || 160 | entry.FileName == "everest.yaml" || 161 | entry.FileName == "everest.yml") { 162 | using (MemoryStream stream = entry.ExtractStream()) 163 | using (StreamReader reader = new StreamReader(stream)) { 164 | try { 165 | if (!reader.EndOfStream) { 166 | multimetas = YamlHelper.Deserializer.Deserialize(reader); 167 | foreach (ModMetadata multimeta in multimetas) { 168 | multimeta.PathArchive = archive; 169 | multimeta.PostParse(); 170 | } 171 | } 172 | } catch (Exception e) { 173 | Logger.Log("FEZMod.Loader", $"Failed parsing multimetadata.yaml in {archive}: {e}"); 174 | } 175 | } 176 | continue; 177 | } 178 | } 179 | } 180 | 181 | ZipModAssetSource contentMeta = new ZipModAssetSource(archive); 182 | ModMetadata contentMetaParent = null; 183 | 184 | Action contentCrawl = () => { 185 | if (contentMeta == null) 186 | return; 187 | if (contentMetaParent != null) { 188 | contentMeta.Mod = contentMetaParent; 189 | contentMeta.ID = contentMetaParent.ID; 190 | } 191 | ModContent.Crawl(contentMeta); 192 | contentMeta = null; 193 | }; 194 | 195 | if (multimetas != null) { 196 | foreach (ModMetadata multimeta in multimetas) { 197 | multimeta.Multimeta = multimetas; 198 | if (contentMetaParent == null) 199 | contentMetaParent = multimeta; 200 | LoadModDelayed(multimeta, contentCrawl); 201 | } 202 | } else { 203 | if (meta == null) { 204 | meta = new ModMetadata() { 205 | ID = "_zip_" + Path.GetFileNameWithoutExtension(archive), 206 | VersionString = "0.0.0-dummy", 207 | PathArchive = archive 208 | }; 209 | meta.PostParse(); 210 | } 211 | contentMetaParent = meta; 212 | LoadModDelayed(meta, contentCrawl); 213 | } 214 | } 215 | 216 | public static void LoadDir(string dir) { 217 | if (!Directory.Exists(dir)) 218 | dir = Path.Combine(PathMods, dir); 219 | if (!Directory.Exists(dir)) 220 | return; 221 | 222 | Logger.Log("FEZMod.Loader", $"Loading mod directory: {dir}"); 223 | 224 | ModMetadata meta = null; 225 | ModMetadata[] multimetas = null; 226 | 227 | string metaPath = Path.Combine(dir, "metadata.yaml"); 228 | if (File.Exists(metaPath)) 229 | using (StreamReader reader = new StreamReader(metaPath)) { 230 | try { 231 | meta = YamlHelper.Deserializer.Deserialize(reader); 232 | meta.PathDirectory = dir; 233 | meta.PostParse(); 234 | } catch (Exception e) { 235 | Logger.Log("FEZMod.Loader", $"Failed parsing metadata.yaml in {dir}: {e}"); 236 | } 237 | } 238 | 239 | metaPath = Path.Combine(dir, "multimetadata.yaml"); 240 | if (!File.Exists(metaPath)) 241 | metaPath = Path.Combine(dir, "everest.yaml"); 242 | if (!File.Exists(metaPath)) 243 | metaPath = Path.Combine(dir, "everest.yml"); 244 | if (File.Exists(metaPath)) 245 | using (StreamReader reader = new StreamReader(metaPath)) { 246 | try { 247 | if (!reader.EndOfStream) { 248 | multimetas = YamlHelper.Deserializer.Deserialize(reader); 249 | foreach (ModMetadata multimeta in multimetas) { 250 | multimeta.PathDirectory = dir; 251 | multimeta.PostParse(); 252 | } 253 | } 254 | } catch (Exception e) { 255 | Logger.Log("FEZMod.Loader", $"Failed parsing everest.yaml in {dir}: {e}"); 256 | } 257 | } 258 | 259 | FileSystemModAssetSource contentMeta = new FileSystemModAssetSource(dir); 260 | ModMetadata contentMetaParent = null; 261 | 262 | Action contentCrawl = () => { 263 | if (contentMeta == null) 264 | return; 265 | if (contentMetaParent != null) { 266 | contentMeta.Mod = contentMetaParent; 267 | contentMeta.ID = contentMetaParent.ID; 268 | } 269 | ModContent.Crawl(contentMeta); 270 | contentMeta = null; 271 | }; 272 | 273 | if (multimetas != null) { 274 | foreach (ModMetadata multimeta in multimetas) { 275 | multimeta.Multimeta = multimetas; 276 | if (contentMetaParent == null) 277 | contentMetaParent = multimeta; 278 | LoadModDelayed(multimeta, contentCrawl); 279 | } 280 | } else { 281 | if (meta == null) { 282 | meta = new ModMetadata() { 283 | ID = "_dir_" + Path.GetFileName(dir), 284 | VersionString = "0.0.0-dummy", 285 | PathDirectory = dir 286 | }; 287 | meta.PostParse(); 288 | } 289 | contentMetaParent = meta; 290 | LoadModDelayed(meta, contentCrawl); 291 | } 292 | } 293 | 294 | /// 295 | /// Load a mod .dll given its metadata at runtime. Doesn't load the mod content. 296 | /// If required, loads the mod after all of its dependencies have been loaded. 297 | /// 298 | /// Metadata of the mod to load. 299 | /// Callback to be executed after the mod has been loaded. Executed immediately if meta == null. 300 | public static void LoadModDelayed(ModMetadata meta, Action callback) { 301 | if (meta == null) { 302 | callback?.Invoke(); 303 | return; 304 | } 305 | 306 | if (DependencyLoaded(meta)) { 307 | Logger.Log("FEZMod.Loader", $"Mod {meta} already loaded!"); 308 | return; 309 | } 310 | 311 | if (PermanentBlacklist.TryGetValue(meta.ID, out Version minver) && meta.Version < minver) { 312 | Logger.Log("FEZMod.Loader", $"Mod {meta} permanently blacklisted by Everest!"); 313 | return; 314 | } 315 | 316 | Tuple conflictRow = PermanentConflictlist.FirstOrDefault(row => 317 | (meta.ID == row.Item1 && meta.Version < row.Item2 && (FezModEngine.Instance.Modules.FirstOrDefault(other => other.Metadata.ID == row.Item3)?.Metadata.Version ?? _VersionInvalid) < row.Item4) || 318 | (meta.ID == row.Item3 && meta.Version < row.Item4 && (FezModEngine.Instance.Modules.FirstOrDefault(other => other.Metadata.ID == row.Item1)?.Metadata.Version ?? _VersionInvalid) < row.Item2) 319 | ); 320 | if (conflictRow != null) { 321 | throw new Exception($"CONFLICTING MODS: {conflictRow.Item1} VS {conflictRow.Item3}"); 322 | } 323 | 324 | 325 | foreach (ModMetadata dep in meta.Dependencies) 326 | if (!DependencyLoaded(dep)) { 327 | Logger.Log("FEZMod.Loader", $"Dependency {dep} of mod {meta} not loaded! Delaying."); 328 | lock (Delayed) { 329 | Delayed.Add(Tuple.Create(meta, callback)); 330 | } 331 | return; 332 | } 333 | 334 | callback?.Invoke(); 335 | 336 | LoadMod(meta); 337 | } 338 | 339 | public static void LoadMod(ModMetadata meta) { 340 | if (meta == null) 341 | return; 342 | 343 | AppDomain.CurrentDomain.AssemblyResolve += GenerateModAssemblyResolver(meta); 344 | 345 | Assembly asm = null; 346 | if (!string.IsNullOrEmpty(meta.PathArchive)) { 347 | bool returnEarly = false; 348 | using (ZipFile zip = new ZipFile(meta.PathArchive)) { 349 | foreach (ZipEntry entry in zip.Entries) { 350 | string entryName = entry.FileName.Replace('\\', '/'); 351 | if (entryName == meta.DLL) { 352 | using (MemoryStream stream = entry.ExtractStream()) 353 | asm = ModRelinker.GetRelinkedAssembly(meta, Path.GetFileNameWithoutExtension(meta.DLL), stream); 354 | } 355 | } 356 | } 357 | 358 | if (returnEarly) 359 | return; 360 | 361 | } else { 362 | if (!string.IsNullOrEmpty(meta.DLL) && File.Exists(meta.DLL)) { 363 | using (FileStream stream = File.OpenRead(meta.DLL)) 364 | asm = ModRelinker.GetRelinkedAssembly(meta, Path.GetFileNameWithoutExtension(meta.DLL), stream); 365 | } 366 | } 367 | 368 | if (asm == null) { 369 | FezModEngine.Instance.Register(new NullModule(meta)); 370 | return; 371 | } 372 | 373 | LoadModAssembly(meta, asm); 374 | } 375 | 376 | public static void LoadModAssembly(ModMetadata meta, Assembly asm) { 377 | if (string.IsNullOrEmpty(meta.PathArchive) && File.Exists(meta.DLL) && meta.SupportsCodeReload && FezModEngine.Instance.Settings.CodeReload) { 378 | FileSystemWatcher watcher = meta.DevWatcher = new FileSystemWatcher { 379 | Path = Path.GetDirectoryName(meta.DLL), 380 | NotifyFilter = NotifyFilters.FileName | NotifyFilters.LastWrite, 381 | }; 382 | 383 | watcher.Changed += (s, e) => { 384 | if (e.FullPath != meta.DLL) 385 | return; 386 | ReloadModAssembly(s, e); 387 | }; 388 | 389 | watcher.EnableRaisingEvents = true; 390 | } 391 | 392 | ApplyModHackfixes(meta, asm); 393 | 394 | ModContent.Crawl(new AssemblyModAssetSource(asm) { 395 | Mod = meta, 396 | ID = meta.ID 397 | }); 398 | 399 | Type[] types; 400 | try { 401 | types = asm.GetTypesSafe(); 402 | } catch (Exception e) { 403 | Logger.Log("FEZMod.Loader", $"Failed reading assembly: {e}"); 404 | e.LogDetailed(); 405 | return; 406 | } 407 | 408 | for (int i = 0; i < types.Length; i++) { 409 | Type type = types[i]; 410 | 411 | if (typeof(ModBase).IsAssignableFrom(type) && !type.IsAbstract && !typeof(NullModule).IsAssignableFrom(type)) { 412 | ModBase mod = (ModBase) Activator.CreateInstance(type); 413 | mod.Metadata = meta; 414 | FezModEngine.Instance.Register(mod); 415 | } 416 | } 417 | } 418 | 419 | internal static void ReloadModAssembly(object source, FileSystemEventArgs e, bool retrying = false) { 420 | if (!File.Exists(e.FullPath)) 421 | return; 422 | 423 | Logger.Log("FEZMod.Loader", $"Reloading mod assembly: {e.FullPath}"); 424 | QueuedTaskHelper.Do("ReloadModAssembly:" + e.FullPath, () => { 425 | ModBase module = FezModEngine.Instance.Modules.FirstOrDefault(m => m.Metadata.DLL == e.FullPath); 426 | if (module == null) 427 | return; 428 | 429 | Assembly asm = null; 430 | using (FileStream stream = File.OpenRead(e.FullPath)) 431 | asm = ModRelinker.GetRelinkedAssembly(module.Metadata, Path.GetFileNameWithoutExtension(e.FullPath), stream); 432 | 433 | if (asm == null) { 434 | if (!retrying) { 435 | // Retry. 436 | QueuedTaskHelper.Do("ReloadModAssembly:" + e.FullPath, () => { 437 | ReloadModAssembly(source, e, true); 438 | }); 439 | } 440 | return; 441 | } 442 | 443 | ((FileSystemWatcher) source).Dispose(); 444 | 445 | // FIXME: Port from Everest! 446 | /* 447 | if (SaveData.Instance != null) { 448 | Logger.Log("core", $"Saving save data slot {SaveData.Instance.FileSlot} for {module.Metadata} before reloading"); 449 | module.SaveSaveData(SaveData.Instance.FileSlot); 450 | 451 | if (SaveData.Instance.CurrentSession?.InArea ?? false) { 452 | Logger.Log("core", $"Saving session slot {SaveData.Instance.FileSlot} for {module.Metadata} before reloading"); 453 | module.SaveSession(SaveData.Instance.FileSlot); 454 | } 455 | } 456 | */ 457 | 458 | FezModEngine.Instance.Unregister(module); 459 | LoadModAssembly(module.Metadata, asm); 460 | }); 461 | } 462 | 463 | public static bool DependenciesLoaded(ModMetadata meta) { 464 | foreach (ModMetadata dep in meta.Dependencies) 465 | if (!DependencyLoaded(dep)) 466 | return false; 467 | return true; 468 | } 469 | 470 | public static bool DependencyLoaded(ModMetadata dep) { 471 | string depName = dep.ID; 472 | Version depVersion = dep.Version; 473 | 474 | lock (FezModEngine.Instance.Modules) { 475 | foreach (ModBase other in FezModEngine.Instance.Modules) { 476 | ModMetadata meta = other.Metadata; 477 | if (meta.ID != depName) 478 | continue; 479 | 480 | Version version = meta.Version; 481 | return VersionSatisfiesDependency(depVersion, version); 482 | } 483 | } 484 | 485 | return false; 486 | } 487 | 488 | public static bool VersionSatisfiesDependency(Version requiredVersion, Version installedVersion) { 489 | // Special case: Always true if version == 0.0.* 490 | if (installedVersion.Major == 0 && installedVersion.Minor == 0) 491 | return true; 492 | 493 | // Major version, breaking changes, must match. 494 | if (installedVersion.Major != requiredVersion.Major) 495 | return false; 496 | // Minor version, non-breaking changes, installed can't be lower than what we depend on. 497 | if (installedVersion.Minor < requiredVersion.Minor) 498 | return false; 499 | 500 | // "Build" is "PATCH" in semver, but we'll also check for it and "Revision". 501 | if (installedVersion.Minor == requiredVersion.Minor && installedVersion.Build < requiredVersion.Build) 502 | return false; 503 | if (installedVersion.Minor == requiredVersion.Minor && installedVersion.Build == requiredVersion.Build && installedVersion.Revision < requiredVersion.Revision) 504 | return false; 505 | 506 | return true; 507 | } 508 | 509 | private static ResolveEventHandler GenerateModAssemblyResolver(ModMetadata meta) 510 | => (sender, args) => { 511 | AssemblyName name = args?.Name == null ? null : new AssemblyName(args.Name); 512 | if (string.IsNullOrEmpty(name?.Name)) 513 | return null; 514 | 515 | string path = name.Name + ".dll"; 516 | if (!string.IsNullOrEmpty(meta.DLL)) 517 | path = Path.Combine(Path.GetDirectoryName(meta.DLL), path); 518 | 519 | if (!string.IsNullOrEmpty(meta.PathArchive)) { 520 | string zipPath = path.Replace('\\', '/'); 521 | using (ZipFile zip = new ZipFile(meta.PathArchive)) { 522 | foreach (ZipEntry entry in zip.Entries) { 523 | if (entry.FileName == zipPath) 524 | using (MemoryStream stream = entry.ExtractStream()) 525 | return ModRelinker.GetRelinkedAssembly(meta, Path.GetFileNameWithoutExtension(zipPath), stream); 526 | } 527 | } 528 | } 529 | 530 | if (!string.IsNullOrEmpty(meta.PathDirectory)) { 531 | string filePath = path; 532 | if (!File.Exists(filePath)) 533 | path = Path.Combine(meta.PathDirectory, filePath); 534 | if (File.Exists(filePath)) 535 | using (FileStream stream = File.OpenRead(filePath)) 536 | return ModRelinker.GetRelinkedAssembly(meta, Path.GetFileNameWithoutExtension(filePath), stream); 537 | } 538 | 539 | return null; 540 | }; 541 | 542 | private static void ApplyModHackfixes(ModMetadata meta, Assembly asm) { 543 | 544 | } 545 | 546 | } 547 | } 548 | -------------------------------------------------------------------------------- /FezEngine.Mod.mm/Mod/ModMetadata.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Xna.Framework.Graphics; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.IO; 5 | using System.Linq; 6 | using System.Text; 7 | using System.Threading.Tasks; 8 | using YamlDotNet.Serialization; 9 | 10 | namespace FezEngine.Mod { 11 | public sealed class ModMetadata { 12 | 13 | public ModMetadata[] Multimeta { get; set; } 14 | 15 | [YamlIgnore] 16 | public string PathArchive { get; set; } 17 | 18 | [YamlIgnore] 19 | public string PathDirectory { get; set; } 20 | 21 | public string ID { get; set; } 22 | 23 | [YamlIgnore] 24 | public Version Version { get; set; } = new Version(1, 0); 25 | private string _VersionString; 26 | [YamlMember(Alias = "Version")] 27 | public string VersionString { 28 | get { 29 | return _VersionString; 30 | } 31 | set { 32 | _VersionString = value; 33 | int versionSplitIndex = value.IndexOf('-'); 34 | if (versionSplitIndex == -1) 35 | Version = new Version(value); 36 | else 37 | Version = new Version(value.Substring(0, versionSplitIndex)); 38 | } 39 | } 40 | 41 | public string DLL { get; set; } 42 | 43 | public List Dependencies { get; set; } = new List(); 44 | 45 | public string Hash { get; set; } 46 | 47 | public bool SupportsCodeReload { get; set; } = true; 48 | 49 | internal FileSystemWatcher DevWatcher; 50 | 51 | public override string ToString() { 52 | return ID + " " + Version; 53 | } 54 | 55 | public void PostParse() { 56 | if (!string.IsNullOrEmpty(DLL) && !string.IsNullOrEmpty(PathDirectory) && !File.Exists(DLL)) 57 | DLL = Path.Combine(PathDirectory, DLL.Replace('/', Path.DirectorySeparatorChar).Replace('\\', Path.DirectorySeparatorChar)); 58 | 59 | // Add dependency to API 1.0 if missing. 60 | bool dependsOnAPI = false; 61 | foreach (ModMetadata dep in Dependencies) { 62 | if (dep.ID == "API") 63 | dep.ID = FezModEngine.Instance.CoreModule.Metadata.ID; 64 | if (dep.ID == FezModEngine.Instance.CoreModule.Metadata.ID) { 65 | dependsOnAPI = true; 66 | break; 67 | } 68 | } 69 | if (!dependsOnAPI) 70 | Dependencies.Insert(0, FezModEngine.Instance.CoreModule.Metadata); 71 | } 72 | 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /FezEngine.Mod.mm/Mod/ModRelinker.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Xna.Framework; 2 | using Microsoft.Xna.Framework.Graphics; 3 | using Mono.Cecil; 4 | using MonoMod; 5 | using MonoMod.Utils; 6 | using MonoMod.InlineRT; 7 | using System; 8 | using System.Collections.Generic; 9 | using System.Collections.ObjectModel; 10 | using System.IO; 11 | using Ionic.Zip; 12 | using System.Linq; 13 | using System.Reflection; 14 | using System.Security.Cryptography; 15 | using System.Text; 16 | using System.Threading.Tasks; 17 | using Mono.Cecil.Pdb; 18 | using Common; 19 | 20 | namespace FezEngine.Mod { 21 | public static class ModRelinker { 22 | 23 | public static readonly List RelinkedAssemblies = new List(); 24 | 25 | public static readonly HashAlgorithm ChecksumHasher = MD5.Create(); 26 | 27 | public static string GameChecksum { get; internal set; } 28 | 29 | internal static readonly Dictionary StaticRelinkModuleCache = new Dictionary() { 30 | { "MonoMod", ModuleDefinition.ReadModule(typeof(MonoModder).Assembly.Location, new ReaderParameters(ReadingMode.Immediate)) }, 31 | { FezModEngine.Instance.GetType().Assembly.GetName().Name, ModuleDefinition.ReadModule(FezModEngine.Instance.GetType().Assembly.Location, new ReaderParameters(ReadingMode.Immediate)) }, 32 | { "FezEngine", ModuleDefinition.ReadModule(typeof(FezModEngine).Assembly.Location, new ReaderParameters(ReadingMode.Immediate)) }, 33 | { "Common", ModuleDefinition.ReadModule(typeof(Logger).Assembly.Location, new ReaderParameters(ReadingMode.Immediate)) }, 34 | }; 35 | internal static bool RuntimeRulesParsed = false; 36 | 37 | private static Dictionary _SharedRelinkModuleMap; 38 | public static Dictionary SharedRelinkModuleMap { 39 | get { 40 | if (_SharedRelinkModuleMap != null) 41 | return _SharedRelinkModuleMap; 42 | 43 | _SharedRelinkModuleMap = new Dictionary(); 44 | string[] entries = Directory.GetFiles(FezModEngine.Instance.PathGame); 45 | for (int i = 0; i < entries.Length; i++) { 46 | string path = entries[i]; 47 | string name = Path.GetFileName(path); 48 | string nameNeutral = name.Substring(0, Math.Max(0, name.Length - 4)); 49 | if (name.EndsWith(".mm.dll")) { 50 | if (name.StartsWith("FEZ.")) 51 | _SharedRelinkModuleMap[nameNeutral] = StaticRelinkModuleCache["FEZ"]; 52 | else if (name.StartsWith("FezEngine.")) 53 | _SharedRelinkModuleMap[nameNeutral] = StaticRelinkModuleCache["FezEngine"]; 54 | else if (name.StartsWith("Common.")) 55 | _SharedRelinkModuleMap[nameNeutral] = StaticRelinkModuleCache["Common"]; 56 | else { 57 | Logger.Log("FEZMod.Relinker", $"Found unknown {name}"); 58 | int dot = name.IndexOf('.'); 59 | if (dot < 0) 60 | continue; 61 | string nameRelinkedNeutral = name.Substring(0, dot); 62 | string nameRelinked = nameRelinkedNeutral + ".dll"; 63 | string pathRelinked = Path.Combine(Path.GetDirectoryName(path), nameRelinked); 64 | if (!File.Exists(pathRelinked)) 65 | continue; 66 | if (!StaticRelinkModuleCache.TryGetValue(nameRelinkedNeutral, out ModuleDefinition relinked)) { 67 | relinked = ModuleDefinition.ReadModule(pathRelinked, new ReaderParameters(ReadingMode.Immediate)); 68 | StaticRelinkModuleCache[nameRelinkedNeutral] = relinked; 69 | } 70 | Logger.Log("FEZMod.Relinker", $"Remapped to {nameRelinked}"); 71 | _SharedRelinkModuleMap[nameNeutral] = relinked; 72 | } 73 | } 74 | } 75 | return _SharedRelinkModuleMap; 76 | } 77 | } 78 | 79 | private static Dictionary _SharedRelinkMap; 80 | public static Dictionary SharedRelinkMap { 81 | get { 82 | if (_SharedRelinkMap != null) 83 | return _SharedRelinkMap; 84 | 85 | _SharedRelinkMap = new Dictionary(); 86 | 87 | // Find our current XNA flavour and relink all types to it. 88 | // This relinks mods from XNA to FNA and from FNA to XNA. 89 | 90 | AssemblyName[] asmRefs = typeof(FezModEngine).Assembly.GetReferencedAssemblies(); 91 | for (int ari = 0; ari < asmRefs.Length; ari++) { 92 | AssemblyName asmRef = asmRefs[ari]; 93 | // Ugly hardcoded supported framework list. 94 | if (!asmRef.FullName.ToLowerInvariant().Contains("xna") && 95 | !asmRef.FullName.ToLowerInvariant().Contains("fna") && 96 | !asmRef.FullName.ToLowerInvariant().Contains("monogame") // Contains many differences - we should print a warning. 97 | ) 98 | continue; 99 | Assembly asm = Assembly.Load(asmRef); 100 | ModuleDefinition module = ModuleDefinition.ReadModule(asm.Location, new ReaderParameters(ReadingMode.Immediate)); 101 | SharedRelinkModuleMap[asmRef.FullName] = SharedRelinkModuleMap[asmRef.Name] = module; 102 | Type[] types = asm.GetExportedTypes(); 103 | for (int i = 0; i < types.Length; i++) { 104 | Type type = types[i]; 105 | TypeDefinition typeDef = module.GetType(type.FullName) ?? module.GetType(type.FullName.Replace('+', '/')); 106 | if (typeDef == null) 107 | continue; 108 | SharedRelinkMap[typeDef.FullName] = typeDef; 109 | } 110 | } 111 | 112 | return _SharedRelinkMap; 113 | } 114 | } 115 | 116 | internal static bool SharedModder = true; 117 | private static MonoModder _Modder; 118 | public static MonoModder Modder { 119 | get { 120 | if (_Modder != null) 121 | return _Modder; 122 | 123 | _Modder = new MonoModder() { 124 | CleanupEnabled = false, 125 | RelinkModuleMap = SharedRelinkModuleMap, 126 | RelinkMap = SharedRelinkMap, 127 | DependencyDirs = { 128 | FezModEngine.Instance.PathGame 129 | }, 130 | ReaderParameters = { 131 | SymbolReaderProvider = new RelinkerSymbolReaderProvider() 132 | } 133 | }; 134 | 135 | ((DefaultAssemblyResolver) _Modder.AssemblyResolver).ResolveFailure += OnRelinkerResolveFailure; 136 | 137 | return _Modder; 138 | } 139 | set { 140 | _Modder = value; 141 | } 142 | } 143 | 144 | private static AssemblyDefinition OnRelinkerResolveFailure(object sender, AssemblyNameReference reference) { 145 | if (reference.FullName.ToLowerInvariant().Contains("fna") || reference.FullName.ToLowerInvariant().Contains("xna")) { 146 | AssemblyName[] asmRefs = typeof(FezModEngine).Assembly.GetReferencedAssemblies(); 147 | for (int ari = 0; ari < asmRefs.Length; ari++) { 148 | AssemblyName asmRef = asmRefs[ari]; 149 | if (!asmRef.FullName.ToLowerInvariant().Contains("xna") && 150 | !asmRef.FullName.ToLowerInvariant().Contains("fna") && 151 | !asmRef.FullName.ToLowerInvariant().Contains("monogame") 152 | ) 153 | continue; 154 | return ((DefaultAssemblyResolver) _Modder.AssemblyResolver).Resolve(AssemblyNameReference.Parse(asmRef.FullName)); 155 | } 156 | } 157 | 158 | return null; 159 | } 160 | 161 | public static Assembly GetRelinkedAssembly(ModMetadata meta, string asmname, Stream stream, 162 | MissingDependencyResolver depResolver = null, string[] checksumsExtra = null, Action prePatch = null) { 163 | 164 | string cachedPath = GetCachedPath(meta, asmname); 165 | string cachedChecksumPath = cachedPath.Substring(0, cachedPath.Length - 4) + ".sum"; 166 | 167 | string[] checksums = new string[2 + (checksumsExtra?.Length ?? 0)]; 168 | if (GameChecksum == null) 169 | GameChecksum = GetChecksum(Assembly.GetAssembly(typeof(ModRelinker)).Location); 170 | checksums[0] = GameChecksum; 171 | 172 | checksums[1] = GetChecksum(ref stream).ToHexadecimalString(); 173 | 174 | if (checksumsExtra != null) 175 | for (int i = 0; i < checksumsExtra.Length; i++) { 176 | checksums[i + 2] = checksumsExtra[i]; 177 | } 178 | 179 | if (File.Exists(cachedPath) && File.Exists(cachedChecksumPath) && 180 | ChecksumsEqual(checksums, File.ReadAllLines(cachedChecksumPath))) { 181 | Logger.Log("FEZMod.Relinker", $"Loading cached assembly for {meta} - {asmname}"); 182 | try { 183 | Assembly asm = Assembly.LoadFrom(cachedPath); 184 | RelinkedAssemblies.Add(asm); 185 | return asm; 186 | } catch (Exception e) { 187 | Logger.Log("FEZMod.Relinker", $"Failed loading {meta} - {asmname}"); 188 | e.LogDetailed(); 189 | return null; 190 | } 191 | } 192 | 193 | if (depResolver == null) 194 | depResolver = GenerateModDependencyResolver(meta); 195 | 196 | bool temporaryASM = false; 197 | 198 | try { 199 | MonoModder modder = Modder; 200 | 201 | modder.Input = stream; 202 | modder.OutputPath = cachedPath; 203 | modder.MissingDependencyResolver = depResolver; 204 | 205 | modder.ReaderParameters.SymbolStream = OpenStream(meta, out string symbolPath, meta.DLL.Substring(0, meta.DLL.Length - 4) + ".pdb", meta.DLL + ".mdb"); 206 | modder.ReaderParameters.ReadSymbols = modder.ReaderParameters.SymbolStream != null; 207 | if (modder.ReaderParameters.SymbolReaderProvider != null && 208 | modder.ReaderParameters.SymbolReaderProvider is RelinkerSymbolReaderProvider) { 209 | ((RelinkerSymbolReaderProvider) modder.ReaderParameters.SymbolReaderProvider).Format = 210 | string.IsNullOrEmpty(symbolPath) ? DebugSymbolFormat.Auto : 211 | symbolPath.EndsWith(".mdb") ? DebugSymbolFormat.MDB : 212 | symbolPath.EndsWith(".pdb") ? DebugSymbolFormat.PDB : 213 | DebugSymbolFormat.Auto; 214 | } 215 | 216 | try { 217 | modder.ReaderParameters.ReadSymbols = true; 218 | modder.Read(); 219 | } catch { 220 | modder.ReaderParameters.SymbolStream?.Dispose(); 221 | modder.ReaderParameters.SymbolStream = null; 222 | modder.ReaderParameters.ReadSymbols = false; 223 | stream.Seek(0, SeekOrigin.Begin); 224 | modder.Read(); 225 | } 226 | 227 | if (modder.ReaderParameters.SymbolReaderProvider != null && 228 | modder.ReaderParameters.SymbolReaderProvider is RelinkerSymbolReaderProvider) { 229 | ((RelinkerSymbolReaderProvider) modder.ReaderParameters.SymbolReaderProvider).Format = DebugSymbolFormat.Auto; 230 | } 231 | 232 | modder.MapDependencies(); 233 | 234 | if (!RuntimeRulesParsed) { 235 | RuntimeRulesParsed = true; 236 | 237 | InitMMSharedData(); 238 | 239 | string rulesPath = Path.Combine( 240 | Path.GetDirectoryName(typeof(FezModEngine).Assembly.Location), 241 | "FEZ.Mod.mm.dll" 242 | ); 243 | if (File.Exists(rulesPath)) { 244 | ModuleDefinition rules = ModuleDefinition.ReadModule(rulesPath, new ReaderParameters(ReadingMode.Immediate)); 245 | modder.ParseRules(rules); 246 | rules.Dispose(); // Is this safe? 247 | } 248 | 249 | rulesPath = Path.Combine( 250 | Path.GetDirectoryName(typeof(FezModEngine).Assembly.Location), 251 | "FezEngine.Mod.mm.dll" 252 | ); 253 | if (File.Exists(rulesPath)) { 254 | ModuleDefinition rules = ModuleDefinition.ReadModule(rulesPath, new ReaderParameters(ReadingMode.Immediate)); 255 | modder.ParseRules(rules); 256 | rules.Dispose(); // Is this safe? 257 | } 258 | } 259 | 260 | prePatch?.Invoke(modder); 261 | 262 | modder.ParseRules(modder.Module); 263 | 264 | modder.AutoPatch(); 265 | 266 | RetryWrite: 267 | try { 268 | modder.WriterParameters.WriteSymbols = true; 269 | modder.Write(); 270 | } catch { 271 | try { 272 | modder.WriterParameters.WriteSymbols = false; 273 | modder.Write(); 274 | } catch when (!temporaryASM) { 275 | temporaryASM = true; 276 | long stamp = DateTime.Now.Ticks / TimeSpan.TicksPerMillisecond; 277 | cachedPath = Path.Combine(Path.GetTempPath(), $"Everest.Relinked.{Path.GetFileNameWithoutExtension(cachedPath)}.{stamp}.dll"); 278 | modder.Module.Name += "." + stamp; 279 | modder.Module.Assembly.Name.Name += "." + stamp; 280 | modder.OutputPath = cachedPath; 281 | modder.WriterParameters.WriteSymbols = true; 282 | goto RetryWrite; 283 | } 284 | } 285 | } catch (Exception e) { 286 | Logger.Log("FEZMod.Relinker", $"Failed relinking {meta} - {asmname}"); 287 | e.LogDetailed(); 288 | return null; 289 | } finally { 290 | Modder.ReaderParameters.SymbolStream?.Dispose(); 291 | if (SharedModder) { 292 | Modder.ClearCaches(moduleSpecific: true); 293 | Modder.Module.Dispose(); 294 | Modder.Module = null; 295 | 296 | } else { 297 | Modder.Dispose(); 298 | Modder = null; 299 | } 300 | } 301 | 302 | if (File.Exists(cachedChecksumPath)) { 303 | File.Delete(cachedChecksumPath); 304 | } 305 | if (!temporaryASM) { 306 | File.WriteAllLines(cachedChecksumPath, checksums); 307 | } 308 | 309 | Logger.Log("FEZMod.Relinker", $"Loading assembly for {meta} - {asmname}"); 310 | try { 311 | Assembly asm = Assembly.LoadFrom(cachedPath); 312 | RelinkedAssemblies.Add(asm); 313 | return asm; 314 | } catch (Exception e) { 315 | Logger.Log("FEZMod.Relinker", $"Failed loading {meta} - {asmname}"); 316 | e.LogDetailed(); 317 | return null; 318 | } 319 | } 320 | 321 | private static MissingDependencyResolver GenerateModDependencyResolver(ModMetadata meta) { 322 | if (!string.IsNullOrEmpty(meta.PathArchive)) { 323 | return (mod, main, name, fullName) => { 324 | string path = name + ".dll"; 325 | if (!string.IsNullOrEmpty(meta.DLL)) 326 | path = Path.Combine(Path.GetDirectoryName(meta.DLL), path); 327 | path = path.Replace('\\', '/'); 328 | 329 | using (ZipFile zip = new ZipFile(meta.PathArchive)) { 330 | foreach (ZipEntry entry in zip.Entries) { 331 | if (entry.FileName != path) 332 | continue; 333 | using (MemoryStream stream = entry.ExtractStream()) 334 | return ModuleDefinition.ReadModule(stream, mod.GenReaderParameters(false)); 335 | } 336 | } 337 | return null; 338 | }; 339 | } 340 | 341 | if (!string.IsNullOrEmpty(meta.PathDirectory)) { 342 | return (mod, main, name, fullName) => { 343 | string path = name + ".dll"; 344 | if (!string.IsNullOrEmpty(meta.DLL)) 345 | path = Path.Combine(Path.GetDirectoryName(meta.DLL), path); 346 | if (!File.Exists(path)) 347 | path = Path.Combine(meta.PathDirectory, path); 348 | if (!File.Exists(path)) 349 | return null; 350 | 351 | return ModuleDefinition.ReadModule(path, mod.GenReaderParameters(false, path)); 352 | }; 353 | } 354 | 355 | return null; 356 | } 357 | 358 | private static Stream OpenStream(ModMetadata meta, out string result, params string[] names) { 359 | if (!string.IsNullOrEmpty(meta.PathArchive)) { 360 | using (ZipFile zip = new ZipFile(meta.PathArchive)) { 361 | foreach (ZipEntry entry in zip.Entries) { 362 | if (!names.Contains(entry.FileName)) 363 | continue; 364 | result = entry.FileName; 365 | return entry.ExtractStream(); 366 | } 367 | } 368 | result = null; 369 | return null; 370 | } 371 | 372 | if (!string.IsNullOrEmpty(meta.PathDirectory)) { 373 | foreach (string name in names) { 374 | string path = name; 375 | if (!File.Exists(path)) 376 | path = Path.Combine(meta.PathDirectory, name); 377 | if (!File.Exists(path)) 378 | continue; 379 | result = path; 380 | return File.OpenRead(path); 381 | } 382 | } 383 | 384 | result = null; 385 | return null; 386 | } 387 | 388 | public static string GetCachedPath(ModMetadata meta, string asmname) 389 | => Path.Combine(ModLoader.PathCache, meta.ID + "." + asmname + ".dll"); 390 | 391 | public static string GetChecksum(ModMetadata meta) { 392 | string path = meta.PathArchive; 393 | if (string.IsNullOrEmpty(path)) 394 | path = meta.DLL; 395 | if (string.IsNullOrEmpty(path)) 396 | return ""; 397 | return GetChecksum(path); 398 | } 399 | 400 | public static string GetChecksum(string path) { 401 | using (FileStream fs = File.OpenRead(path)) 402 | return ChecksumHasher.ComputeHash(fs).ToHexadecimalString(); 403 | } 404 | 405 | public static byte[] GetChecksum(ref Stream stream) { 406 | if (!stream.CanSeek) { 407 | MemoryStream ms = new MemoryStream(); 408 | stream.CopyTo(ms); 409 | stream.Dispose(); 410 | stream = ms; 411 | stream.Seek(0, SeekOrigin.Begin); 412 | } 413 | 414 | long pos = stream.Position; 415 | stream.Seek(0, SeekOrigin.Begin); 416 | byte[] hash = ChecksumHasher.ComputeHash(stream); 417 | stream.Seek(pos, SeekOrigin.Begin); 418 | return hash; 419 | } 420 | 421 | public static bool ChecksumsEqual(string[] a, string[] b) { 422 | if (a.Length != b.Length) 423 | return false; 424 | for (int i = 0; i < a.Length; i++) 425 | if (a[i].Trim() != b[i].Trim()) 426 | return false; 427 | return true; 428 | } 429 | 430 | [PatchInitMMSharedData] 431 | private static void InitMMSharedData() { 432 | // This method is automatically filled via MonoModRules. 433 | } 434 | private static void SetMMSharedData(string key, bool value) { 435 | Modder.SharedData[key] = value; 436 | } 437 | 438 | } 439 | } 440 | -------------------------------------------------------------------------------- /FezEngine.Mod.mm/Mod/NullModule.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Xna.Framework; 2 | using System; 3 | using System.Collections; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using System.Text; 7 | using System.Threading.Tasks; 8 | using System.Reflection; 9 | using System.IO; 10 | using MonoMod.Utils; 11 | using Microsoft.Xna.Framework.Input; 12 | using System.Threading; 13 | 14 | namespace FezEngine.Mod { 15 | internal class NullModule : ModBase { 16 | 17 | public NullModule(ModMetadata metadata) { 18 | Metadata = metadata; 19 | } 20 | 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /FezEngine.Mod.mm/Mod/RelinkerSymbolReaderProvider.cs: -------------------------------------------------------------------------------- 1 | using Mono.Cecil.Cil; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | using YamlDotNet.Serialization; 8 | using Mono.Cecil; 9 | using Mono.Cecil.Mdb; 10 | using Mono.Cecil.Pdb; 11 | using System.IO; 12 | using MonoMod; 13 | 14 | namespace FezEngine.Mod { 15 | public class RelinkerSymbolReaderProvider : ISymbolReaderProvider { 16 | 17 | public DebugSymbolFormat Format; 18 | 19 | public ISymbolReader GetSymbolReader(ModuleDefinition module, Stream symbolStream) { 20 | switch (Format) { 21 | case DebugSymbolFormat.MDB: 22 | return new MdbReaderProvider().GetSymbolReader(module, symbolStream); 23 | 24 | case DebugSymbolFormat.PDB: 25 | if (IsPortablePdb(symbolStream)) 26 | return new PortablePdbReaderProvider().GetSymbolReader(module, symbolStream); 27 | return new NativePdbReaderProvider().GetSymbolReader(module, symbolStream); 28 | 29 | default: 30 | return null; 31 | } 32 | } 33 | 34 | public ISymbolReader GetSymbolReader(ModuleDefinition module, string fileName) { 35 | return null; 36 | } 37 | 38 | public static bool IsPortablePdb(Stream stream) { 39 | long start = stream.Position; 40 | if (stream.Length - start < 4) 41 | return false; 42 | try { 43 | using (BinaryReader reader = new BinaryReader(stream, Encoding.UTF8, true)) 44 | return reader.ReadUInt32() == 0x424a5342; 45 | } finally { 46 | stream.Seek(start, SeekOrigin.Begin); 47 | } 48 | } 49 | 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /FezEngine.Mod.mm/Mod/ServiceHelperHooks.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Xna.Framework; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | 8 | namespace FezEngine.Mod { 9 | public static class ServiceHelperHooks { 10 | 11 | public static Dictionary ReplacementComponents = new Dictionary(); 12 | public static Dictionary ReplacementServices = new Dictionary(); 13 | 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /FezEngine.Mod.mm/Mod/Services/IServiceWrapper.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Xna.Framework; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | 8 | namespace FezEngine.Mod.Services { 9 | public interface IServiceWrapper { 10 | 11 | void Wrap(object orig); 12 | 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /FezEngine.Mod.mm/Mod/Tools/MainThreadHelper.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Xna.Framework; 2 | using Microsoft.Xna.Framework.Input.Touch; 3 | using MonoMod.Utils; 4 | using MonoMod.InlineRT; 5 | using System; 6 | using System.Collections; 7 | using System.Collections.Generic; 8 | using System.Collections.ObjectModel; 9 | using System.Diagnostics; 10 | using System.IO; 11 | using System.Linq; 12 | using System.Net; 13 | using System.Net.Security; 14 | using System.Reflection; 15 | using System.Security.Cryptography.X509Certificates; 16 | using System.Text; 17 | using System.Threading; 18 | using System.Threading.Tasks; 19 | using System.Runtime.CompilerServices; 20 | 21 | namespace FezEngine.Mod { 22 | public class MainThreadHelper : GameComponent { 23 | 24 | public static MainThreadHelper Instance; 25 | 26 | private static readonly Queue Queue = new Queue(); 27 | private static readonly HashSet Enqueued = new HashSet(); 28 | private static readonly Dictionary EnqueuedWaiting = new Dictionary(); 29 | public static Thread MainThread { get; private set; } 30 | 31 | public MainThreadHelper(Game game) 32 | : base(game) { 33 | Instance = this; 34 | 35 | UpdateOrder = -500000; 36 | MainThread = Thread.CurrentThread; 37 | } 38 | 39 | public static void Do(Action a) { 40 | if (Thread.CurrentThread == MainThread) { 41 | a(); 42 | return; 43 | } 44 | 45 | lock (Queue) { 46 | Queue.Enqueue(a); 47 | } 48 | } 49 | 50 | public static void Do(object key, Action a) { 51 | if (Thread.CurrentThread == MainThread) { 52 | a(); 53 | return; 54 | } 55 | 56 | lock (Queue) { 57 | if (!Enqueued.Add(key)) 58 | return; 59 | 60 | Queue.Enqueue(() => { 61 | a?.Invoke(); 62 | lock (Queue) { 63 | Enqueued.Remove(key); 64 | } 65 | }); 66 | } 67 | } 68 | 69 | public static MaybeAwaitable Get(Func f) { 70 | if (Thread.CurrentThread == MainThread) { 71 | return new MaybeAwaitable(f()); 72 | } 73 | 74 | lock (Queue) { 75 | T result = default; 76 | Task proxy = new Task(() => result); 77 | MaybeAwaitable awaitable = new MaybeAwaitable(proxy.GetAwaiter()); 78 | 79 | Queue.Enqueue(() => { 80 | result = f != null ? f.Invoke() : default; 81 | proxy.Start(); 82 | }); 83 | 84 | return awaitable; 85 | } 86 | } 87 | 88 | public static MaybeAwaitable Get(object key, Func f) { 89 | if (Thread.CurrentThread == MainThread) { 90 | return new MaybeAwaitable(f()); 91 | } 92 | 93 | lock (Queue) { 94 | if (!Enqueued.Add(key)) 95 | return (MaybeAwaitable) EnqueuedWaiting[key]; 96 | 97 | T result = default; 98 | Task proxy = new Task(() => result); 99 | MaybeAwaitable awaitable = new MaybeAwaitable(proxy.GetAwaiter()); 100 | EnqueuedWaiting[key] = awaitable; 101 | 102 | Queue.Enqueue(() => { 103 | result = f != null ? f.Invoke() : default; 104 | proxy.Start(); 105 | lock (Queue) { 106 | EnqueuedWaiting.Remove(key); 107 | Enqueued.Remove(key); 108 | } 109 | }); 110 | 111 | return awaitable; 112 | } 113 | } 114 | 115 | public override void Update(GameTime gameTime) { 116 | if (Queue.Count > 0) { 117 | // run as many tasks as possible in 10 milliseconds (a frame is ~16ms). 118 | Stopwatch stopwatch = Stopwatch.StartNew(); 119 | while (stopwatch.ElapsedMilliseconds < 10) { 120 | Action action = null; 121 | lock (Queue) { 122 | if (Queue.Count > 0) { 123 | action = Queue.Dequeue(); 124 | } 125 | } 126 | action?.Invoke(); 127 | } 128 | stopwatch.Stop(); 129 | } 130 | 131 | if (gameTime == null) 132 | return; 133 | 134 | base.Update(gameTime); 135 | } 136 | 137 | } 138 | 139 | public struct MaybeAwaitable { 140 | 141 | private MaybeAwaiter _Awaiter; 142 | 143 | public MaybeAwaitable(T result) { 144 | _Awaiter = new MaybeAwaiter(); 145 | _Awaiter._IsImmediate = true; 146 | _Awaiter._Result = result; 147 | } 148 | 149 | public MaybeAwaitable(TaskAwaiter task) { 150 | _Awaiter = new MaybeAwaiter(); 151 | _Awaiter._IsImmediate = false; 152 | _Awaiter._Result = default; 153 | _Awaiter._Task = task; 154 | } 155 | 156 | public MaybeAwaiter GetAwaiter() => _Awaiter; 157 | public T GetResult() => _Awaiter.GetResult(); 158 | 159 | public struct MaybeAwaiter : ICriticalNotifyCompletion { 160 | 161 | internal bool _IsImmediate; 162 | internal T _Result; 163 | internal TaskAwaiter _Task; 164 | 165 | public bool IsCompleted => _IsImmediate || _Task.IsCompleted; 166 | 167 | public T GetResult() { 168 | if (_IsImmediate) 169 | return _Result; 170 | return _Task.GetResult(); 171 | } 172 | 173 | public void OnCompleted(Action continuation) { 174 | if (_IsImmediate) { 175 | continuation(); 176 | return; 177 | } 178 | _Task.OnCompleted(continuation); 179 | } 180 | 181 | public void UnsafeOnCompleted(Action continuation) { 182 | if (_IsImmediate) { 183 | continuation(); 184 | return; 185 | } 186 | _Task.UnsafeOnCompleted(continuation); 187 | } 188 | } 189 | 190 | } 191 | } 192 | -------------------------------------------------------------------------------- /FezEngine.Mod.mm/Mod/Tools/QueuedTaskHelper.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Xna.Framework; 2 | using Microsoft.Xna.Framework.Input.Touch; 3 | using MonoMod.Utils; 4 | using MonoMod.InlineRT; 5 | using System; 6 | using System.Collections; 7 | using System.Collections.Generic; 8 | using System.Collections.ObjectModel; 9 | using System.Diagnostics; 10 | using System.IO; 11 | using System.Linq; 12 | using System.Net; 13 | using System.Net.Security; 14 | using System.Reflection; 15 | using System.Security.Cryptography.X509Certificates; 16 | using System.Text; 17 | using System.Threading; 18 | using System.Threading.Tasks; 19 | using System.Runtime.CompilerServices; 20 | 21 | namespace FezEngine.Mod { 22 | public static class QueuedTaskHelper { 23 | 24 | private static readonly Dictionary Map = new Dictionary(); 25 | private static readonly Dictionary Timers = new Dictionary(); 26 | 27 | public static readonly double DefaultDelay = 0.5D; 28 | 29 | public static void Cancel(object key) { 30 | lock (Map) { 31 | if (Timers.TryGetValue(key, out Stopwatch timer)) { 32 | timer.Stop(); 33 | Map.Remove(key); 34 | Timers.Remove(key); 35 | } 36 | } 37 | } 38 | 39 | public static Task Do(object key, Action a) 40 | => Do(key, DefaultDelay, a); 41 | public static Task Do(object key, double delay, Action a) { 42 | lock (Map) { 43 | if (Map.TryGetValue(key, out object queued)) { 44 | Timers[key].Restart(); 45 | return (Task) queued; 46 | } 47 | 48 | Stopwatch timer = Stopwatch.StartNew(); 49 | Timers[key] = timer; 50 | Task t = new Func(async () => { 51 | do { 52 | await Task.Delay(TimeSpan.FromSeconds(delay - timer.Elapsed.TotalSeconds)); 53 | } while (timer.Elapsed.TotalSeconds < delay); 54 | 55 | if (!timer.IsRunning) 56 | return; 57 | 58 | lock (Map) { 59 | Map.Remove(key); 60 | Timers.Remove(key); 61 | } 62 | timer.Stop(); 63 | 64 | a?.Invoke(); 65 | })(); 66 | 67 | Map[key] = t; 68 | return t; 69 | } 70 | } 71 | 72 | public static Task Get(object key, Func f) 73 | => Get(key, DefaultDelay, f); 74 | public static Task Get(object key, double delay, Func f) { 75 | lock (Map) { 76 | if (Map.TryGetValue(key, out object queued)) { 77 | Timers[key].Restart(); 78 | return (Task) queued; 79 | } 80 | 81 | Stopwatch timer = Stopwatch.StartNew(); 82 | Timers[key] = timer; 83 | Task t = new Func>(async () => { 84 | do { 85 | await Task.Delay(TimeSpan.FromSeconds(delay - timer.Elapsed.TotalSeconds)); 86 | } while (timer.Elapsed.TotalSeconds < delay); 87 | 88 | lock (Map) { 89 | Map.Remove(key); 90 | Timers.Remove(key); 91 | } 92 | timer.Stop(); 93 | 94 | return f != null ? f.Invoke() : default; 95 | })(); 96 | 97 | Map[key] = t; 98 | return t; 99 | } 100 | } 101 | 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /FezEngine.Mod.mm/Mod/YamlHelper.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Xna.Framework; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | using YamlDotNet.Core; 8 | using YamlDotNet.Core.Events; 9 | using YamlDotNet.Serialization; 10 | using YamlDotNet.Serialization.ObjectFactories; 11 | 12 | namespace FezEngine.Mod { 13 | public static class YamlHelper { 14 | 15 | public static IDeserializer Deserializer = new DeserializerBuilder() 16 | .IgnoreUnmatchedProperties() 17 | .Build(); 18 | 19 | public static ISerializer Serializer = new SerializerBuilder() 20 | .ConfigureDefaultValuesHandling(DefaultValuesHandling.Preserve) 21 | .Build(); 22 | 23 | public static IDeserializer DeserializerUsing(object objectToBind) { 24 | IObjectFactory defaultObjectFactory = new DefaultObjectFactory(); 25 | Type objectType = objectToBind.GetType(); 26 | 27 | return new DeserializerBuilder() 28 | .IgnoreUnmatchedProperties() 29 | .WithObjectFactory(type => type == objectType ? objectToBind : defaultObjectFactory.Create(type)) 30 | .Build(); 31 | } 32 | 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /FezEngine.Mod.mm/MonoModRules.cs: -------------------------------------------------------------------------------- 1 | using Mono.Cecil; 2 | using Mono.Cecil.Cil; 3 | using MonoMod.Utils; 4 | using MonoMod.InlineRT; 5 | using System; 6 | using System.Collections.Generic; 7 | using System.Linq; 8 | using System.Text; 9 | using System.Threading.Tasks; 10 | 11 | namespace MonoMod { 12 | [MonoModCustomMethodAttribute("PatchInitMMSharedData")] 13 | class PatchInitMMSharedDataAttribute : Attribute { } 14 | 15 | static class MonoModRules { 16 | 17 | static MonoModRules() { 18 | } 19 | 20 | public static void PatchInitMMSharedData(MethodDefinition method, CustomAttribute attrib) { 21 | MethodDefinition m_Set = method.DeclaringType.FindMethod("System.Void SetMMSharedData(System.String,System.Boolean)"); 22 | if (m_Set == null) 23 | return; 24 | 25 | if (!method.HasBody) 26 | return; 27 | 28 | method.Body.Instructions.Clear(); 29 | ILProcessor il = method.Body.GetILProcessor(); 30 | 31 | foreach (KeyValuePair kvp in MonoModRule.Modder.SharedData) { 32 | if (!(kvp.Value is bool)) 33 | return; 34 | il.Emit(OpCodes.Ldstr, kvp.Key); 35 | il.Emit((bool)kvp.Value ? OpCodes.Ldc_I4_1 : OpCodes.Ldc_I4_0); 36 | il.Emit(OpCodes.Call, m_Set); 37 | } 38 | 39 | il.Emit(OpCodes.Ret); 40 | } 41 | 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /FezEngine.Mod.mm/Patches/Structure/OggStream.cs: -------------------------------------------------------------------------------- 1 | #pragma warning disable CS0626 // Method, operator, or accessor is marked external and has no attributes on it 2 | 3 | using FezEngine.Mod; 4 | using FezEngine.Mod.Core; 5 | using Microsoft.Xna.Framework.Audio; 6 | using Microsoft.Xna.Framework.Graphics; 7 | using MonoMod; 8 | using MonoMod.Utils; 9 | using System; 10 | using System.Collections.Concurrent; 11 | using System.Collections.Generic; 12 | using System.IO; 13 | using System.Linq; 14 | using System.Text; 15 | using System.Threading; 16 | using System.Threading.Tasks; 17 | 18 | namespace FezEngine.Structure { 19 | // ctor is private 20 | class patch_OggStream { 21 | 22 | [MonoModIgnore] private static readonly ConcurrentQueue ToPrecache; 23 | [MonoModIgnore] private static readonly AutoResetEvent WakeUpPrecacher; 24 | [MonoModIgnore] private static Thread ThreadedPrecacher; 25 | [MonoModIgnore] private static IntPtr bufferPtr; 26 | [MonoModIgnore] private static byte[] vorbisBuffer; 27 | 28 | [MonoModIgnore] private IntPtr vorbisFile; 29 | [MonoModIgnore] private DynamicSoundEffectInstance soundEffect; 30 | [MonoModIgnore] private bool hitEof; 31 | [MonoModIgnore] public float Volume { get; set; } 32 | [MonoModIgnore] public bool IsLooped { get; set; } 33 | 34 | [MonoModIgnore] 35 | private static extern void PrecacheStreams(); 36 | 37 | [MonoModIgnore] 38 | private extern void OnBufferNeeded(object sender, EventArgs e); 39 | 40 | [MonoModReplace] 41 | private void Initialize() { 42 | // The original method refers to long vorbis_info.rate, which has been changed to IntPtr in newer FNA releases. 43 | 44 | Vorbisfile.vorbis_info vorbis_info = Vorbisfile.ov_info(vorbisFile, -1); 45 | soundEffect = new DynamicSoundEffectInstance((int) vorbis_info.rate, (vorbis_info.channels == 1) ? AudioChannels.Mono : AudioChannels.Stereo); 46 | Volume = 1f; 47 | 48 | ToPrecache.Enqueue(this); 49 | if (ThreadedPrecacher == null) { 50 | ThreadedPrecacher = new Thread(PrecacheStreams) { 51 | Priority = ThreadPriority.Lowest 52 | }; 53 | ThreadedPrecacher.Start(); 54 | } 55 | WakeUpPrecacher.Set(); 56 | } 57 | 58 | private void QueueBuffer(object source, EventArgs ea) { 59 | // The original method refers to Int64 Vorbisfile.ov_read(IntPtr, IntPtr, Int32, Int32, Int32, Int32, Int32 ByRef), 60 | // which has been changed in newer FNA releases. The last parameter is now out, not ref. 61 | 62 | int pos = 0; 63 | int read; 64 | do { 65 | read = (int) Vorbisfile.ov_read(vorbisFile, bufferPtr + pos, 4096, 0, 2, 1, out int current_section); 66 | pos += read; 67 | } 68 | while (read > 0 && pos < 187904); 69 | 70 | if (pos != 0) { 71 | soundEffect.SubmitBuffer(vorbisBuffer, 0, pos); 72 | return; 73 | } 74 | 75 | if (IsLooped) { 76 | Vorbisfile.ov_time_seek(vorbisFile, 0.0); 77 | QueueBuffer(source, ea); 78 | return; 79 | } 80 | 81 | hitEof = true; 82 | soundEffect.BufferNeeded -= OnBufferNeeded; 83 | } 84 | 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /FezEngine.Mod.mm/Patches/Tools/MemoryContentManager.cs: -------------------------------------------------------------------------------- 1 | #pragma warning disable CS0626 // Method, operator, or accessor is marked external and has no attributes on it 2 | 3 | using FezEngine.Mod; 4 | using FezEngine.Mod.Core; 5 | using Microsoft.Xna.Framework.Graphics; 6 | using MonoMod; 7 | using MonoMod.Utils; 8 | using System; 9 | using System.Collections.Generic; 10 | using System.IO; 11 | using System.Linq; 12 | using System.Text; 13 | using System.Threading.Tasks; 14 | 15 | namespace FezEngine.Tools { 16 | class patch_MemoryContentManager : MemoryContentManager { 17 | 18 | private static List assetNames; 19 | private static int assetNamesCachedCount; 20 | 21 | public static new IEnumerable AssetNames { 22 | [MonoModReplace] 23 | get { 24 | if (assetNames != null && 25 | assetNamesCachedCount == ModContent.Map.Count) 26 | return assetNames; 27 | 28 | assetNamesCachedCount = ModContent.Map.Count; 29 | assetNames = ModContent.Map.Values 30 | .Where(asset => asset.HasData) 31 | .Select(asset => asset.PathVirtual.Replace('/', '\\')) // Game expects \ over / 32 | .Distinct() 33 | .ToList(); 34 | 35 | return assetNames; 36 | } 37 | } 38 | 39 | public patch_MemoryContentManager(IServiceProvider serviceProvider, string rootDirectory) 40 | : base(serviceProvider, rootDirectory) { 41 | // no-op. 42 | } 43 | 44 | [MonoModReplace] 45 | protected override Stream OpenStream(string assetName) { 46 | if (ModContent.TryGet(assetName, out ModAsset modAsset)) 47 | return modAsset.Open(); 48 | 49 | throw new FileNotFoundException($"Asset not found: {assetName}"); 50 | } 51 | 52 | [MonoModReplace] 53 | public static new bool AssetExists(string assetName) { 54 | assetName = assetName.ToLowerInvariant().Replace('\\', '/'); 55 | 56 | if (ModContent.Get(assetName) != null) 57 | return true; 58 | 59 | return false; 60 | } 61 | 62 | [MonoModReplace] 63 | public new void LoadEssentials() { 64 | ModContent.Crawl(new PackedAssetSource(Path.Combine(RootDirectory, "Essentials.pak"), true) { 65 | ID = "FEZ.Essentials" 66 | }, false); 67 | } 68 | 69 | [MonoModReplace] 70 | public new void Preload() { 71 | ModContent.Crawl(new PackedAssetSource(Path.Combine(RootDirectory, "Updates.pak"), true) { 72 | ID = "FEZ.Updates" 73 | }, false); 74 | 75 | // FEZ originally precaches Other.pak - let's just scan it instead. 76 | ModContent.Crawl(new PackedAssetSource(Path.Combine(RootDirectory, "Other.pak"), false) { 77 | ID = "FEZ.Other" 78 | }, false); 79 | } 80 | 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /FezEngine.Mod.mm/Patches/Tools/ServiceHelper.cs: -------------------------------------------------------------------------------- 1 | #pragma warning disable CS0626 // Method, operator, or accessor is marked external and has no attributes on it 2 | 3 | using FezEngine.Mod; 4 | using FezEngine.Mod.Services; 5 | using Microsoft.Xna.Framework; 6 | using MonoMod; 7 | using System; 8 | using System.Collections.Generic; 9 | using System.Linq; 10 | using System.Text; 11 | using System.Threading.Tasks; 12 | 13 | namespace FezEngine.Tools { 14 | static class patch_ServiceHelper { 15 | 16 | [MonoModIgnore] 17 | private static readonly List services; 18 | 19 | public static extern void orig_AddComponent(IGameComponent component, bool addServices); 20 | public static void AddComponent(IGameComponent component, bool addServices) { 21 | if (ServiceHelperHooks.ReplacementComponents.TryGetValue(component.GetType().FullName, out IGameComponent repl)) { 22 | if (repl is IServiceWrapper) 23 | ((IServiceWrapper) repl).Wrap(component); 24 | else 25 | (component as IDisposable)?.Dispose(); 26 | component = repl; 27 | } 28 | 29 | orig_AddComponent(component, addServices); 30 | } 31 | 32 | public static extern void orig_AddService(object service); 33 | public static void AddService(object service) { 34 | if (ServiceHelperHooks.ReplacementServices.TryGetValue(service.GetType().FullName, out object repl)) { 35 | if (repl is IServiceWrapper) 36 | ((IServiceWrapper) repl).Wrap(service); 37 | else 38 | (service as IDisposable)?.Dispose(); 39 | service = repl; 40 | } 41 | 42 | orig_AddService(service); 43 | if (repl is IServiceWrapper) 44 | ServiceHelper.Game.Services.RemoveService(typeof(IServiceWrapper)); 45 | } 46 | 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /FezEngine.Mod.mm/Patches/Tools/SharedContentManager.cs: -------------------------------------------------------------------------------- 1 | #pragma warning disable CS0626 // Method, operator, or accessor is marked external and has no attributes on it 2 | 3 | using Common; 4 | using FezEngine.Mod; 5 | using Microsoft.Xna.Framework.Graphics; 6 | using System; 7 | using System.Collections.Generic; 8 | using System.IO; 9 | using System.Linq; 10 | using System.Text; 11 | using System.Threading.Tasks; 12 | 13 | namespace FezEngine.Tools { 14 | class patch_SharedContentManager : SharedContentManager { 15 | 16 | public patch_SharedContentManager(string name) 17 | : base(name) { 18 | // no-op. 19 | } 20 | 21 | public class patch_CommonContentManager { 22 | 23 | private extern T orig_ReadAsset(string assetName) where T : class; 24 | private T ReadAsset(string assetName) where T : class { 25 | string modName = assetName.ToLowerInvariant().Replace('\\', '/'); 26 | if (ModContent.TryGet(modName, out ModAsset mod)) { 27 | if (typeof(T) == typeof(Texture2D)) { 28 | using (Stream s = mod.Open()) 29 | return Texture2D.FromStream(ServiceHelper.Game.GraphicsDevice, s) as T; 30 | } 31 | } 32 | 33 | try { 34 | return orig_ReadAsset(assetName); 35 | } catch (Exception e) { 36 | Logger.Log("FEZMod.Content", "orig_ReadAsset failed on " + assetName); 37 | Logger.Log("FEZMod.Content", e.ToString()); 38 | throw; 39 | } 40 | } 41 | 42 | } 43 | 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2020 Jade Macho 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 | -------------------------------------------------------------------------------- /lib-fna/MojoShader.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0x0ade/FEZMod/3d3da3f7aec3abe06f3f809053337068e80bf17a/lib-fna/MojoShader.dll -------------------------------------------------------------------------------- /lib-fna/SDL2.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0x0ade/FEZMod/3d3da3f7aec3abe06f3f809053337068e80bf17a/lib-fna/SDL2.dll -------------------------------------------------------------------------------- /lib-fna/SDL2_image.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0x0ade/FEZMod/3d3da3f7aec3abe06f3f809053337068e80bf17a/lib-fna/SDL2_image.dll -------------------------------------------------------------------------------- /lib-fna/libjpeg-9.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0x0ade/FEZMod/3d3da3f7aec3abe06f3f809053337068e80bf17a/lib-fna/libjpeg-9.dll -------------------------------------------------------------------------------- /lib-fna/libogg-0.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0x0ade/FEZMod/3d3da3f7aec3abe06f3f809053337068e80bf17a/lib-fna/libogg-0.dll -------------------------------------------------------------------------------- /lib-fna/libpng16-16.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0x0ade/FEZMod/3d3da3f7aec3abe06f3f809053337068e80bf17a/lib-fna/libpng16-16.dll -------------------------------------------------------------------------------- /lib-fna/libtheoradec-1.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0x0ade/FEZMod/3d3da3f7aec3abe06f3f809053337068e80bf17a/lib-fna/libtheoradec-1.dll -------------------------------------------------------------------------------- /lib-fna/libtheorafile.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0x0ade/FEZMod/3d3da3f7aec3abe06f3f809053337068e80bf17a/lib-fna/libtheorafile.dll -------------------------------------------------------------------------------- /lib-fna/libvorbis-0.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0x0ade/FEZMod/3d3da3f7aec3abe06f3f809053337068e80bf17a/lib-fna/libvorbis-0.dll -------------------------------------------------------------------------------- /lib-fna/libvorbisfile.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0x0ade/FEZMod/3d3da3f7aec3abe06f3f809053337068e80bf17a/lib-fna/libvorbisfile.dll -------------------------------------------------------------------------------- /lib-fna/soft_oal.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0x0ade/FEZMod/3d3da3f7aec3abe06f3f809053337068e80bf17a/lib-fna/soft_oal.dll -------------------------------------------------------------------------------- /lib-fna/zlib1.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0x0ade/FEZMod/3d3da3f7aec3abe06f3f809053337068e80bf17a/lib-fna/zlib1.dll -------------------------------------------------------------------------------- /lib-stripped/Common.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0x0ade/FEZMod/3d3da3f7aec3abe06f3f809053337068e80bf17a/lib-stripped/Common.dll -------------------------------------------------------------------------------- /lib-stripped/ContentSerialization.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0x0ade/FEZMod/3d3da3f7aec3abe06f3f809053337068e80bf17a/lib-stripped/ContentSerialization.dll -------------------------------------------------------------------------------- /lib-stripped/EasyStorage.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0x0ade/FEZMod/3d3da3f7aec3abe06f3f809053337068e80bf17a/lib-stripped/EasyStorage.dll -------------------------------------------------------------------------------- /lib-stripped/FEZ.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0x0ade/FEZMod/3d3da3f7aec3abe06f3f809053337068e80bf17a/lib-stripped/FEZ.exe -------------------------------------------------------------------------------- /lib-stripped/FezEngine.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0x0ade/FEZMod/3d3da3f7aec3abe06f3f809053337068e80bf17a/lib-stripped/FezEngine.dll -------------------------------------------------------------------------------- /lib-stripped/README.md: -------------------------------------------------------------------------------- 1 | The versions here are versions stripped with mono-cil-strip. 2 | 3 | # DON'T PUSH THE ORIGINAL VERSION OF THE STRIPPED LIBS! 4 | 5 | Feel free to replace the .dlls **locally** though. 6 | -------------------------------------------------------------------------------- /lib-stripped/SimpleDefinitionLanguage.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0x0ade/FEZMod/3d3da3f7aec3abe06f3f809053337068e80bf17a/lib-stripped/SimpleDefinitionLanguage.dll -------------------------------------------------------------------------------- /lib-stripped/XnaWordWrapCore.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0x0ade/FEZMod/3d3da3f7aec3abe06f3f809053337068e80bf17a/lib-stripped/XnaWordWrapCore.dll -------------------------------------------------------------------------------- /lib/FNA.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0x0ade/FEZMod/3d3da3f7aec3abe06f3f809053337068e80bf17a/lib/FNA.dll --------------------------------------------------------------------------------