├── .gitattributes ├── .gitignore ├── Content ├── background_larger.png ├── journal_go_left.png ├── journal_go_left_hover.png ├── journal_go_right.png ├── journal_go_right_hover.png ├── missing.png └── vertical_bar_centre_tall.png ├── Patches ├── MonoModRules.cs ├── patch_AtomType.cs ├── patch_AtomTypes.cs ├── patch_Campaign.cs ├── patch_CampaignItem.cs ├── patch_Color.cs ├── patch_CompiledProgramGrid.cs ├── patch_ErrorHandler.cs ├── patch_EscapeMenu.cs ├── patch_GameLogic.cs ├── patch_GifRecorder.cs ├── patch_JournalScreen.cs ├── patch_LocVignette.cs ├── patch_Molecule.cs ├── patch_MoleculeEditorScreen.cs ├── patch_Part.cs ├── patch_PartType.cs ├── patch_Puzzle.cs ├── patch_PuzzleEditorScreen.cs ├── patch_PuzzleInputOutput.cs ├── patch_PuzzleSelectScreen.cs ├── patch_Renderer.cs ├── patch_ScoreManager.cs ├── patch_Settings.cs ├── patch_Sim.cs ├── patch_SolutionEditorPartsPanelSection.cs ├── patch_SolutionEdtorBase.cs ├── patch_Steam.cs ├── patch_StringLoader.cs ├── patch_TitleScreen.cs └── patch_WorkshopManager.cs ├── Properties └── Resources.resx ├── Quintessential.csproj ├── Quintessential.csproj.DotSettings ├── Quintessential.sln ├── Quintessential ├── AtomSelectScreen.cs ├── Input.cs ├── Internal │ ├── MessageBoxScreenEx.cs │ ├── QuintessentialAsMod.cs │ └── Screenshotter.cs ├── Logger.cs ├── ModMeta.cs ├── ModsScreen.cs ├── NoticeScreen.cs ├── Pair.cs ├── PuzzleOption.cs ├── QApi.cs ├── QuintessentialLoader.cs ├── QuintessentialMod.cs ├── QuintessentialSettings.cs ├── Serialization │ ├── CampaignModel.cs │ ├── JournalModel.cs │ └── PuzzleModel.cs ├── Settings │ ├── ChangeKeybindScreen.cs │ ├── Keybinding.cs │ ├── SettingsButton.cs │ ├── SettingsGroup.cs │ └── SettingsLabel.cs ├── UI.cs └── YamlHelper.cs └── README.md /.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 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Mono auto generated files 17 | mono_crash.* 18 | 19 | # Build results 20 | [Dd]ebug/ 21 | [Dd]ebugPublic/ 22 | [Rr]elease/ 23 | [Rr]eleases/ 24 | x64/ 25 | x86/ 26 | [Ww][Ii][Nn]32/ 27 | [Aa][Rr][Mm]/ 28 | [Aa][Rr][Mm]64/ 29 | bld/ 30 | [Bb]in/ 31 | [Oo]bj/ 32 | [Oo]ut/ 33 | [Ll]og/ 34 | [Ll]ogs/ 35 | 36 | # Visual Studio 2015/2017 cache/options directory 37 | .vs/ 38 | # Uncomment if you have tasks that create the project's static files in wwwroot 39 | #wwwroot/ 40 | 41 | # Visual Studio 2017 auto generated files 42 | Generated\ Files/ 43 | 44 | # MSTest test Results 45 | [Tt]est[Rr]esult*/ 46 | [Bb]uild[Ll]og.* 47 | 48 | # NUnit 49 | *.VisualState.xml 50 | TestResult.xml 51 | nunit-*.xml 52 | 53 | # Build Results of an ATL Project 54 | [Dd]ebugPS/ 55 | [Rr]eleasePS/ 56 | dlldata.c 57 | 58 | # Benchmark Results 59 | BenchmarkDotNet.Artifacts/ 60 | 61 | # .NET Core 62 | project.lock.json 63 | project.fragment.lock.json 64 | artifacts/ 65 | 66 | # ASP.NET Scaffolding 67 | ScaffoldingReadMe.txt 68 | 69 | # StyleCop 70 | StyleCopReport.xml 71 | 72 | # Files built by Visual Studio 73 | *_i.c 74 | *_p.c 75 | *_h.h 76 | *.ilk 77 | *.meta 78 | *.obj 79 | *.iobj 80 | *.pch 81 | *.pdb 82 | *.ipdb 83 | *.pgc 84 | *.pgd 85 | *.rsp 86 | *.sbr 87 | *.tlb 88 | *.tli 89 | *.tlh 90 | *.tmp 91 | *.tmp_proj 92 | *_wpftmp.csproj 93 | *.log 94 | *.vspscc 95 | *.vssscc 96 | .builds 97 | *.pidb 98 | *.svclog 99 | *.scc 100 | 101 | # Chutzpah Test files 102 | _Chutzpah* 103 | 104 | # Visual C++ cache files 105 | ipch/ 106 | *.aps 107 | *.ncb 108 | *.opendb 109 | *.opensdf 110 | *.sdf 111 | *.cachefile 112 | *.VC.db 113 | *.VC.VC.opendb 114 | 115 | # Visual Studio profiler 116 | *.psess 117 | *.vsp 118 | *.vspx 119 | *.sap 120 | 121 | # Visual Studio Trace Files 122 | *.e2e 123 | 124 | # TFS 2012 Local Workspace 125 | $tf/ 126 | 127 | # Guidance Automation Toolkit 128 | *.gpState 129 | 130 | # ReSharper is a .NET coding add-in 131 | _ReSharper*/ 132 | *.[Rr]e[Ss]harper 133 | *.DotSettings.user 134 | 135 | # TeamCity is a build add-in 136 | _TeamCity* 137 | 138 | # DotCover is a Code Coverage Tool 139 | *.dotCover 140 | 141 | # AxoCover is a Code Coverage Tool 142 | .axoCover/* 143 | !.axoCover/settings.json 144 | 145 | # Coverlet is a free, cross platform Code Coverage Tool 146 | coverage*.json 147 | coverage*.xml 148 | coverage*.info 149 | 150 | # Visual Studio code coverage results 151 | *.coverage 152 | *.coveragexml 153 | 154 | # NCrunch 155 | _NCrunch_* 156 | .*crunch*.local.xml 157 | nCrunchTemp_* 158 | 159 | # MightyMoose 160 | *.mm.* 161 | AutoTest.Net/ 162 | 163 | # Web workbench (sass) 164 | .sass-cache/ 165 | 166 | # Installshield output folder 167 | [Ee]xpress/ 168 | 169 | # DocProject is a documentation generator add-in 170 | DocProject/buildhelp/ 171 | DocProject/Help/*.HxT 172 | DocProject/Help/*.HxC 173 | DocProject/Help/*.hhc 174 | DocProject/Help/*.hhk 175 | DocProject/Help/*.hhp 176 | DocProject/Help/Html2 177 | DocProject/Help/html 178 | 179 | # Click-Once directory 180 | publish/ 181 | 182 | # Publish Web Output 183 | *.[Pp]ublish.xml 184 | *.azurePubxml 185 | # Note: Comment the next line if you want to checkin your web deploy settings, 186 | # but database connection strings (with potential passwords) will be unencrypted 187 | *.pubxml 188 | *.publishproj 189 | 190 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 191 | # checkin your Azure Web App publish settings, but sensitive information contained 192 | # in these scripts will be unencrypted 193 | PublishScripts/ 194 | 195 | # NuGet Packages 196 | *.nupkg 197 | # NuGet Symbol Packages 198 | *.snupkg 199 | # The packages folder can be ignored because of Package Restore 200 | **/[Pp]ackages/* 201 | # except build/, which is used as an MSBuild target. 202 | !**/[Pp]ackages/build/ 203 | # Uncomment if necessary however generally it will be regenerated when needed 204 | #!**/[Pp]ackages/repositories.config 205 | # NuGet v3's project.json files produces more ignorable files 206 | *.nuget.props 207 | *.nuget.targets 208 | 209 | # Microsoft Azure Build Output 210 | csx/ 211 | *.build.csdef 212 | 213 | # Microsoft Azure Emulator 214 | ecf/ 215 | rcf/ 216 | 217 | # Windows Store app package directories and files 218 | AppPackages/ 219 | BundleArtifacts/ 220 | Package.StoreAssociation.xml 221 | _pkginfo.txt 222 | *.appx 223 | *.appxbundle 224 | *.appxupload 225 | 226 | # Visual Studio cache files 227 | # files ending in .cache can be ignored 228 | *.[Cc]ache 229 | # but keep track of directories ending in .cache 230 | !?*.[Cc]ache/ 231 | 232 | # Others 233 | ClientBin/ 234 | ~$* 235 | *~ 236 | *.dbmdl 237 | *.dbproj.schemaview 238 | *.jfm 239 | *.pfx 240 | *.publishsettings 241 | orleans.codegen.cs 242 | 243 | # Including strong name files can present a security risk 244 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 245 | #*.snk 246 | 247 | # Since there are multiple workflows, uncomment next line to ignore bower_components 248 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 249 | #bower_components/ 250 | 251 | # RIA/Silverlight projects 252 | Generated_Code/ 253 | 254 | # Backup & report files from converting an old project file 255 | # to a newer Visual Studio version. Backup files are not needed, 256 | # because we have git ;-) 257 | _UpgradeReport_Files/ 258 | Backup*/ 259 | UpgradeLog*.XML 260 | UpgradeLog*.htm 261 | ServiceFabricBackup/ 262 | *.rptproj.bak 263 | 264 | # SQL Server files 265 | *.mdf 266 | *.ldf 267 | *.ndf 268 | 269 | # Business Intelligence projects 270 | *.rdl.data 271 | *.bim.layout 272 | *.bim_*.settings 273 | *.rptproj.rsuser 274 | *- [Bb]ackup.rdl 275 | *- [Bb]ackup ([0-9]).rdl 276 | *- [Bb]ackup ([0-9][0-9]).rdl 277 | 278 | # Microsoft Fakes 279 | FakesAssemblies/ 280 | 281 | # GhostDoc plugin setting file 282 | *.GhostDoc.xml 283 | 284 | # Node.js Tools for Visual Studio 285 | .ntvs_analysis.dat 286 | node_modules/ 287 | 288 | # Visual Studio 6 build log 289 | *.plg 290 | 291 | # Visual Studio 6 workspace options file 292 | *.opt 293 | 294 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 295 | *.vbw 296 | 297 | # Visual Studio LightSwitch build output 298 | **/*.HTMLClient/GeneratedArtifacts 299 | **/*.DesktopClient/GeneratedArtifacts 300 | **/*.DesktopClient/ModelManifest.xml 301 | **/*.Server/GeneratedArtifacts 302 | **/*.Server/ModelManifest.xml 303 | _Pvt_Extensions 304 | 305 | # Paket dependency manager 306 | .paket/paket.exe 307 | paket-files/ 308 | 309 | # FAKE - F# Make 310 | .fake/ 311 | 312 | # CodeRush personal settings 313 | .cr/personal 314 | 315 | # Python Tools for Visual Studio (PTVS) 316 | __pycache__/ 317 | *.pyc 318 | 319 | # Cake - Uncomment if you are using it 320 | # tools/** 321 | # !tools/packages.config 322 | 323 | # Tabs Studio 324 | *.tss 325 | 326 | # Telerik's JustMock configuration file 327 | *.jmconfig 328 | 329 | # BizTalk build output 330 | *.btp.cs 331 | *.btm.cs 332 | *.odx.cs 333 | *.xsd.cs 334 | 335 | # OpenCover UI analysis results 336 | OpenCover/ 337 | 338 | # Azure Stream Analytics local run output 339 | ASALocalRun/ 340 | 341 | # MSBuild Binary and Structured Log 342 | *.binlog 343 | 344 | # NVidia Nsight GPU debugger configuration file 345 | *.nvuser 346 | 347 | # MFractors (Xamarin productivity tool) working folder 348 | .mfractor/ 349 | 350 | # Local History for Visual Studio 351 | .localhistory/ 352 | 353 | # BeatPulse healthcheck temp database 354 | healthchecksdb 355 | 356 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 357 | MigrationBackup/ 358 | 359 | # Ionide (cross platform F# VS Code tools) working folder 360 | .ionide/ 361 | 362 | # Fody - auto-generated XML schema 363 | FodyWeavers.xsd -------------------------------------------------------------------------------- /Content/background_larger.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuintessentialOM/Quintessential/d86fafce42ecd2d4b0560f7181fe4c124b0bb45b/Content/background_larger.png -------------------------------------------------------------------------------- /Content/journal_go_left.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuintessentialOM/Quintessential/d86fafce42ecd2d4b0560f7181fe4c124b0bb45b/Content/journal_go_left.png -------------------------------------------------------------------------------- /Content/journal_go_left_hover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuintessentialOM/Quintessential/d86fafce42ecd2d4b0560f7181fe4c124b0bb45b/Content/journal_go_left_hover.png -------------------------------------------------------------------------------- /Content/journal_go_right.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuintessentialOM/Quintessential/d86fafce42ecd2d4b0560f7181fe4c124b0bb45b/Content/journal_go_right.png -------------------------------------------------------------------------------- /Content/journal_go_right_hover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuintessentialOM/Quintessential/d86fafce42ecd2d4b0560f7181fe4c124b0bb45b/Content/journal_go_right_hover.png -------------------------------------------------------------------------------- /Content/missing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuintessentialOM/Quintessential/d86fafce42ecd2d4b0560f7181fe4c124b0bb45b/Content/missing.png -------------------------------------------------------------------------------- /Content/vertical_bar_centre_tall.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuintessentialOM/Quintessential/d86fafce42ecd2d4b0560f7181fe4c124b0bb45b/Content/vertical_bar_centre_tall.png -------------------------------------------------------------------------------- /Patches/MonoModRules.cs: -------------------------------------------------------------------------------- 1 | using Mono.Cecil; 2 | using Mono.Cecil.Cil; 3 | using MonoMod.Cil; 4 | using MonoMod.InlineRT; 5 | using System; 6 | using System.Linq; 7 | 8 | namespace MonoMod; 9 | 10 | [MonoModCustomMethodAttribute(nameof(MonoModRules.PatchSettingsStaticInit))] 11 | class PatchSettingsStaticInit : Attribute{} 12 | 13 | [MonoModCustomMethodAttribute(nameof(MonoModRules.PatchPuzzleIdWrite))] 14 | class PatchPuzzleIdWrite : Attribute{} 15 | 16 | [MonoModCustomMethodAttribute(nameof(MonoModRules.PatchScoreManagerLoad))] 17 | class PatchScoreManagerLoad : Attribute{} 18 | 19 | [MonoModCustomMethodAttribute(nameof(MonoModRules.PatchGifRecorderFrame))] 20 | class PatchGifRecorderFrame : Attribute{} 21 | 22 | [MonoModCustomMethodAttribute(nameof(MonoModRules.PatchPuzzleEditorScreen))] 23 | class PatchPuzzleEditorScreen : Attribute{} 24 | 25 | [MonoModCustomMethodAttribute(nameof(MonoModRules.PatchJournalScreen))] 26 | class PatchJournalScreen : Attribute{} 27 | 28 | [MonoModCustomMethodAttribute(nameof(MonoModRules.PatchJournalPuzzleBackgrounds))] 29 | class PatchJournalPuzzleBackgrounds : Attribute{} 30 | 31 | static class MonoModRules{ 32 | 33 | static MonoModRules(){ 34 | MonoModRule.Modder.Log("Patching OM"); 35 | } 36 | 37 | public static void PatchSettingsStaticInit(MethodDefinition method, CustomAttribute attrib){ 38 | MonoModRule.Modder.Log("Patching settings static init"); 39 | // Set "class_110.field_1012" (Steam support) to false 40 | if(method.HasBody){ 41 | ILCursor cursor = new(new ILContext(method)); 42 | if(cursor.TryGotoNext(MoveType.Before, 43 | instr => instr.MatchLdcI4(1), 44 | instr => instr.MatchStsfld("class_110", "field_1012"))){ 45 | cursor.Remove(); 46 | cursor.Emit(OpCodes.Ldc_I4_0); 47 | }else{ 48 | Console.WriteLine("Failed to disable Steam setting in class_110!"); 49 | throw new Exception(); 50 | } 51 | }else{ 52 | Console.WriteLine("Failed to disable Steam setting in class_110!"); 53 | throw new Exception(); 54 | } 55 | } 56 | 57 | public static void PatchPuzzleIdWrite(MethodDefinition method, CustomAttribute attrib){ 58 | MonoModRule.Modder.Log("Patching puzzle ids"); 59 | // Replace "SteamUser.GetSteamID().m_SteamID" with "0" (until a proper format is created) 60 | if(method.HasBody){ 61 | ILCursor cursor = new(new ILContext(method)); 62 | if(cursor.TryGotoNext(MoveType.Before, 63 | instr => instr.MatchCall("Steamworks.SteamUser", "GetSteamID"), 64 | instr => instr.MatchLdfld("Steamworks.CSteamID", "m_SteamID"))){ 65 | cursor.Remove(); 66 | cursor.Remove(); 67 | cursor.Emit(OpCodes.Ldc_I8, (long)0); 68 | } 69 | }else{ 70 | Console.WriteLine("Failed to modify puzzle serialization!"); 71 | throw new Exception(); 72 | } 73 | } 74 | 75 | public static void PatchScoreManagerLoad(MethodDefinition method, CustomAttribute attrib){ 76 | MonoModRule.Modder.Log("Patching ScoreManager loading"); 77 | if(method.HasBody){ 78 | ILCursor cursor = new(new ILContext(method)); 79 | if(cursor.TryGotoNext(MoveType.After, instr => instr.Match(OpCodes.Brfalse_S)) 80 | && cursor.TryGotoNext(MoveType.After, instr => instr.Match(OpCodes.Brfalse_S)) 81 | && cursor.TryGotoNext(MoveType.After, instr => instr.Match(OpCodes.Brfalse_S))){ 82 | cursor.Emit(OpCodes.Ret); 83 | }else{ 84 | Console.WriteLine("Failed to modify ScoreManager loading (no match)!"); 85 | throw new Exception(); 86 | } 87 | }else{ 88 | Console.WriteLine("Failed to modify ScoreManager loading (no body)!"); 89 | throw new Exception(); 90 | } 91 | } 92 | 93 | public static void PatchGifRecorderFrame(MethodDefinition method, CustomAttribute attrib){ 94 | MonoModRule.Modder.Log("Patching GIF recorder frame rendering"); 95 | if(method.HasBody) { 96 | ILCursor cursor = new(new ILContext(method)); 97 | if(cursor.TryGotoNext(MoveType.After, instr => instr.MatchCall("class_135", "method_272"))){ 98 | // "class_135.method_272(class_238.field_1989.field_81.field_613.field_632, new Vector2());" 99 | TypeDefinition holder = MonoModRule.Modder.FindType("class_250").Resolve(); 100 | MethodDefinition to = holder.Methods.First(m => m.Name.Equals("MarkOnFrame")); 101 | cursor.Emit(OpCodes.Call, to); 102 | }else{ 103 | Console.WriteLine("Failed to modify GIF recorder frame rendering (no match)!"); 104 | throw new Exception(); 105 | } 106 | }else{ 107 | Console.WriteLine("Failed to modify GIF recorder frame rendering (no body)!"); 108 | throw new Exception(); 109 | } 110 | } 111 | 112 | public static void PatchPuzzleEditorScreen(MethodDefinition method, CustomAttribute attrib) { 113 | MonoModRule.Modder.Log("Patching puzzle editor screen"); 114 | if(method.HasBody){ 115 | ILCursor cursor = new(new ILContext(method)); 116 | Instruction target = null; // will definitely be set 117 | 118 | // kill off `flag5` and make the Upload puzzle button never clickable 119 | if(cursor.TryGotoNext(MoveType.Before, instr => instr.MatchLdloc(27))){ 120 | cursor.Remove(); 121 | cursor.Emit(OpCodes.Ldc_I4_0); 122 | }else{ 123 | Console.WriteLine("Failed to modify puzzle editor screen (no 1st match)!"); 124 | throw new Exception(); 125 | } 126 | 127 | if(cursor.TryGotoNext(MoveType.After, instr => instr.MatchLdflda("PuzzleEditorScreen", "field_2789"), 128 | instr => instr.MatchCall(out MethodReference mref) && mref.Name.Equals("method_1085"), 129 | instr => { 130 | bool ret = instr.OpCode == OpCodes.Brfalse; 131 | if(ret) 132 | target = (Instruction)instr.Operand; 133 | return ret; 134 | })){ 135 | // "if(this.field_2789.method_1085()){" ... "} if (!this.field_2789.method_1085()){" 136 | TypeDefinition holder = MonoModRule.Modder.FindType("PuzzleEditorScreen").Resolve(); 137 | MethodDefinition to = holder.Methods.First(m => m.Name.Equals("DisplayEditorPanelScreen")); 138 | cursor.Emit(OpCodes.Ldarg_0); // this 139 | cursor.Emit(OpCodes.Call, to); // call reimplementation 140 | cursor.Emit(OpCodes.Br, target); // skip rest of `if` statement 141 | }else{ 142 | Console.WriteLine("Failed to modify puzzle editor screen (no 2nd match)!"); 143 | throw new Exception(); 144 | } 145 | }else{ 146 | Console.WriteLine("Failed to modify puzzle editor screen (no body)!"); 147 | throw new Exception(); 148 | } 149 | } 150 | 151 | public static void PatchJournalScreen(MethodDefinition method, CustomAttribute attrib){ 152 | MonoModRule.Modder.Log("Patching journal screen"); 153 | if(method.HasBody){ 154 | ILCursor cursor = new(new ILContext(method)); 155 | if(cursor.TryGotoNext(MoveType.Before, instr => instr.MatchLdstr("The Journal of Alchemical Engineering"))){ 156 | cursor.Remove(); 157 | TypeDefinition holder = MonoModRule.Modder.FindType("JournalScreen").Resolve(); 158 | MethodDefinition to = holder.Methods.First(m => m.Name.Equals("CurrentJournalName")); 159 | cursor.Emit(OpCodes.Call, to); 160 | }else{ 161 | Console.WriteLine("Failed to modify journal screen (no match)!"); 162 | throw new Exception(); 163 | } 164 | }else{ 165 | Console.WriteLine("Failed to modify journal screen (no body)!"); 166 | throw new Exception(); 167 | } 168 | } 169 | 170 | public static void PatchJournalPuzzleBackgrounds(MethodDefinition method, CustomAttribute attrib){ 171 | MonoModRule.Modder.Log("Patching journal screen puzzle backgrounds"); 172 | if(method.HasBody){ 173 | ILCursor cursor = new(new ILContext(method)); 174 | if(cursor.TryGotoNext(MoveType.After, instr => instr.MatchStloc(1))){ 175 | cursor.Emit(OpCodes.Ldloc_1); 176 | cursor.Emit(OpCodes.Ldarg_3); 177 | TypeDefinition holder = MonoModRule.Modder.FindType("JournalScreen").Resolve(); 178 | MethodDefinition to = holder.Methods.First(m => m.Name.Equals("CurrentJournalBg")); 179 | cursor.Emit(OpCodes.Call, to); 180 | cursor.Emit(OpCodes.Stloc_1); 181 | }else{ 182 | Console.WriteLine("Failed to modify journal screen puzzle backgrounds (no match)!"); 183 | throw new Exception(); 184 | } 185 | }else{ 186 | Console.WriteLine("Failed to modify journal screen puzzle backgrounds (no body)!"); 187 | throw new Exception(); 188 | } 189 | } 190 | } -------------------------------------------------------------------------------- /Patches/patch_AtomType.cs: -------------------------------------------------------------------------------- 1 | class patch_AtomType { 2 | 3 | // String atom type ID 4 | public string QuintAtomType; 5 | } -------------------------------------------------------------------------------- /Patches/patch_AtomTypes.cs: -------------------------------------------------------------------------------- 1 | using MonoMod; 2 | using AtomTypes = class_175; 3 | 4 | #pragma warning disable CS0626 // Method, operator, or accessor is marked external and has no attributes on it 5 | 6 | [MonoModPatch("class_175")] 7 | class patch_AtomTypes{ 8 | 9 | public static extern void orig_method_248(); 10 | 11 | public static void method_248(){ 12 | orig_method_248(); 13 | ((patch_AtomType)(object)AtomTypes.field_1675).QuintAtomType = "salt"; 14 | ((patch_AtomType)(object)AtomTypes.field_1676).QuintAtomType = "air"; 15 | ((patch_AtomType)(object)AtomTypes.field_1677).QuintAtomType = "earth"; 16 | ((patch_AtomType)(object)AtomTypes.field_1678).QuintAtomType = "fire"; 17 | ((patch_AtomType)(object)AtomTypes.field_1679).QuintAtomType = "water"; 18 | ((patch_AtomType)(object)AtomTypes.field_1680).QuintAtomType = "quicksilver"; 19 | ((patch_AtomType)(object)AtomTypes.field_1681).QuintAtomType = "lead"; 20 | ((patch_AtomType)(object)AtomTypes.field_1682).QuintAtomType = "copper"; 21 | ((patch_AtomType)(object)AtomTypes.field_1683).QuintAtomType = "tin"; 22 | ((patch_AtomType)(object)AtomTypes.field_1684).QuintAtomType = "iron"; 23 | ((patch_AtomType)(object)AtomTypes.field_1685).QuintAtomType = "silver"; 24 | ((patch_AtomType)(object)AtomTypes.field_1686).QuintAtomType = "gold"; 25 | ((patch_AtomType)(object)AtomTypes.field_1687).QuintAtomType = "vitae"; 26 | ((patch_AtomType)(object)AtomTypes.field_1688).QuintAtomType = "mors"; 27 | ((patch_AtomType)(object)AtomTypes.field_1689).QuintAtomType = "repeat"; 28 | ((patch_AtomType)(object)AtomTypes.field_1690).QuintAtomType = "quintessence"; 29 | } 30 | } -------------------------------------------------------------------------------- /Patches/patch_Campaign.cs: -------------------------------------------------------------------------------- 1 | class patch_Campaign { 2 | 3 | public string QuintTitle = "Opus Magnum"; 4 | } -------------------------------------------------------------------------------- /Patches/patch_CampaignItem.cs: -------------------------------------------------------------------------------- 1 | #pragma warning disable CS0626 // Method, operator, or accessor is marked external and has no attributes on it 2 | 3 | public class patch_CampaignItem{ 4 | 5 | public class_256 Icon, IconSmall; 6 | 7 | public extern class_256 orig_method_826(); 8 | public class_256 method_826() => Icon ?? orig_method_826(); 9 | 10 | public extern class_256 orig_method_827(); 11 | public class_256 method_827() => IconSmall ?? orig_method_827(); 12 | } -------------------------------------------------------------------------------- /Patches/patch_Color.cs: -------------------------------------------------------------------------------- 1 | public class patch_Color{ 2 | 3 | public static Color FromInts(int r, int g, int b, float alpha) => new Color(r / 255f, g / 255f, b / 255f, alpha); 4 | } -------------------------------------------------------------------------------- /Patches/patch_CompiledProgramGrid.cs: -------------------------------------------------------------------------------- 1 | using MonoMod; 2 | using MonoMod.Utils; 3 | 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | 7 | #pragma warning disable CS0626 // Method, operator, or accessor is marked external and has no attributes on it 8 | 9 | public class patch_CompiledProgramGrid 10 | { 11 | [MonoModIgnore] 12 | Dictionary field_2368; 13 | 14 | public extern int orig_method_853(int param_4510); 15 | 16 | public int method_853(int param_4510) 17 | { 18 | if (this.field_2368.Count == 0) return 0; 19 | 20 | int num = this.field_2368.Values.First().field_2367.Length; 21 | if (num == 0) return 0; 22 | 23 | return param_4510 % num; 24 | } 25 | } -------------------------------------------------------------------------------- /Patches/patch_ErrorHandler.cs: -------------------------------------------------------------------------------- 1 | using MonoMod; 2 | using Quintessential; 3 | using System; 4 | 5 | #pragma warning disable CS0626 // Method, operator, or accessor is marked external and has no attributes on it 6 | 7 | [MonoModPatch("class_129")] 8 | class patch_ErrorHandler { 9 | 10 | // error logging 11 | // replaces the regular method (opening a (broken by string parsing?) website) with logging 12 | 13 | public static extern void orig_method_238(); 14 | public static void method_238() { 15 | AppDomain.CurrentDomain.UnhandledException += (sender, args) => { 16 | Logger.Log("Encountered an error!"); 17 | Exception e = args.ExceptionObject as Exception; 18 | Logger.Log(e.ToString()); 19 | }; 20 | } 21 | } -------------------------------------------------------------------------------- /Patches/patch_EscapeMenu.cs: -------------------------------------------------------------------------------- 1 | using MonoMod; 2 | using Quintessential; 3 | 4 | #pragma warning disable CS0626 // Method, operator, or accessor is marked external and has no attributes on it 5 | 6 | [MonoModPatch("class_178")] 7 | class patch_EscapeMenu { 8 | 9 | public extern void orig_method_50(float param_3782); 10 | 11 | public void method_50(float param_3782) { 12 | if(GameLogic.field_2434.method_938() is ModsScreen) 13 | return; 14 | orig_method_50(param_3782); 15 | float num = 65f; 16 | Vector2 vector2_1 = new(570f, 440f); 17 | Vector2 vector2_2 = (class_115.field_1433 / 2 - vector2_1 / 2).Rounded(); 18 | Vector2 vector2_3 = new(161f, 256f); 19 | Vector2 vector2_4 = vector2_3 + new Vector2(0.0f, -num); 20 | Vector2 vector2_5 = vector2_4 + new Vector2(0.0f, -num); 21 | Vector2 vector2_6 = vector2_5 + new Vector2(0.0f, -num * 2); 22 | if(class_140.method_314(class_134.method_253("Mods", string.Empty), vector2_2 + vector2_6).method_824(true, true)) { 23 | // show mod options 24 | UI.OpenScreen(new ModsScreen()); 25 | } 26 | } 27 | } 28 | 29 | -------------------------------------------------------------------------------- /Patches/patch_GameLogic.cs: -------------------------------------------------------------------------------- 1 | using Quintessential; 2 | 3 | #pragma warning disable CS0626 // Method, operator, or accessor is marked external and has no attributes on it 4 | #pragma warning disable IDE1006 // Naming Styles 5 | 6 | class patch_GameLogic{ 7 | 8 | // calls mod loading 9 | public extern void orig_method_942(); 10 | public void method_942(){ 11 | QuintessentialLoader.PreInit(); 12 | orig_method_942(); 13 | QuintessentialLoader.PostLoad(); 14 | } 15 | 16 | public extern void orig_method_963(int exitCode); 17 | public void method_963(int exitCode){ 18 | QuintessentialLoader.Unload(); 19 | orig_method_963(exitCode); 20 | } 21 | 22 | public extern void orig_method_956(); 23 | public void method_956(){ 24 | orig_method_956(); 25 | QuintessentialLoader.LoadPuzzleContent(); 26 | } 27 | } -------------------------------------------------------------------------------- /Patches/patch_GifRecorder.cs: -------------------------------------------------------------------------------- 1 | #pragma warning disable IDE0051 // Remove unused private members 2 | #pragma warning disable IDE1006 // Naming Styles 3 | 4 | using MonoMod; 5 | using Quintessential; 6 | 7 | [MonoModPatch("class_250")] 8 | class patch_GifRecorder{ 9 | 10 | [PatchGifRecorderFrame] 11 | [MonoModIgnore] 12 | public extern void method_50(float param_4165); 13 | 14 | // name is used in MonoModRules 15 | private static void MarkOnFrame(){ 16 | var markerPos = new Vector2(826 - 60 - 40, 647 - 61); 17 | var verPos = new Vector2(826 - 60 - 40 - 20, 647 - 40); 18 | class_135.method_272(class_238.field_1989.field_81.field_613.field_632, markerPos); 19 | class_135.method_290(QuintessentialLoader.VersionString, verPos, class_238.field_1990.field_2145, Color.LightGray, (enum_0)1, 1f, 0.6f, float.MaxValue, float.MaxValue, 0, new Color(), class_238.field_1989.field_71, int.MaxValue, true, true); 20 | } 21 | } -------------------------------------------------------------------------------- /Patches/patch_JournalScreen.cs: -------------------------------------------------------------------------------- 1 | using MonoMod; 2 | using Quintessential; 3 | 4 | #pragma warning disable CS0626 // Method, operator, or accessor is marked external and has no attributes on it 5 | 6 | using Texture = class_256; 7 | 8 | public class patch_JournalScreen{ 9 | 10 | public static int currentJournal; 11 | 12 | private static Texture JournalGoLeft, JournalGoLeftHover, JournalGoRight, JournalGoRightHover; 13 | 14 | // mirror real version 15 | private static int field_2554; 16 | 17 | [PatchJournalScreen] 18 | public extern void orig_method_50(float deltaTime); 19 | public void method_50(float deltaTime){ 20 | orig_method_50(deltaTime); 21 | 22 | if(QuintessentialLoader.AllJournals.Count == 1) 23 | return; 24 | 25 | JournalGoLeft ??= class_235.method_615("Quintessential/journal_go_left"); 26 | JournalGoLeftHover ??= class_235.method_615("Quintessential/journal_go_left_hover"); 27 | JournalGoRight ??= class_235.method_615("Quintessential/journal_go_right"); 28 | JournalGoRightHover ??= class_235.method_615("Quintessential/journal_go_right_hover"); 29 | 30 | Vector2 size = new Vector2(1516f, 922f); 31 | Vector2 corner = (Input.ScreenSize() / 2 - size / 2 + new Vector2(-2f, -11f)).Rounded(); 32 | Vector2 lPos = corner + new Vector2(84, 812f); 33 | Vector2 rPos = corner + new Vector2(188, 812f); 34 | bool inLeftBound = Bounds2.WithSize(lPos, JournalGoLeft.field_2056.ToVector2()).Contains(Input.MousePos()); 35 | bool inRightBound = Bounds2.WithSize(rPos, JournalGoRight.field_2056.ToVector2()).Contains(Input.MousePos()); 36 | UI.DrawTexture(inLeftBound ? JournalGoLeftHover : JournalGoLeft, lPos); 37 | UI.DrawTexture(inRightBound ? JournalGoRightHover : JournalGoRight, rPos); 38 | UI.DrawText($"{currentJournal + 1}/{QuintessentialLoader.AllJournals.Count}", corner + new Vector2(157, 824f), UI.Text, UI.TextColor, TextAlignment.Centred); 39 | 40 | if(Input.IsLeftClickPressed() && (inLeftBound || inRightBound)){ 41 | class_238.field_1991.field_1821.method_28(1f); 42 | 43 | if(inLeftBound){ 44 | var next = currentJournal - 1; 45 | if(next < 0) 46 | next += QuintessentialLoader.AllJournals.Count; 47 | currentJournal = next; 48 | } 49 | 50 | if(inRightBound){ 51 | var next = currentJournal + 1; 52 | if(next >= QuintessentialLoader.AllJournals.Count) 53 | next = 0; 54 | currentJournal = next; 55 | } 56 | 57 | JournalVolumes.field_2572 = QuintessentialLoader.AllJournals[currentJournal].ToArray(); 58 | field_2554 = JournalVolumes.field_2572.Length - 1; 59 | UI.InstantCloseScreen(); 60 | UI.OpenScreen(new JournalScreen(false)); 61 | } 62 | } 63 | 64 | [MonoModIgnore] 65 | [PatchJournalPuzzleBackgrounds] 66 | private extern void method_1040(Puzzle puzzle, Vector2 pos, bool big); 67 | 68 | public static void ResetPosition(){ 69 | currentJournal = 0; 70 | field_2554 = JournalVolumes.field_2572.Length - 1; 71 | } 72 | 73 | // found by name in MonoModRules 74 | public static string CurrentJournalName(){ 75 | return currentJournal == 0 ? "The Journal of Alchemical Engineering" : QuintessentialLoader.ModJournalModels[currentJournal - 1].Title; 76 | } 77 | 78 | public static Texture CurrentJournalBg(Texture before, bool large){ 79 | if(currentJournal == 0) 80 | return before; 81 | var journal = QuintessentialLoader.ModJournalModels[currentJournal - 1]; 82 | return large switch{ 83 | true when !string.IsNullOrWhiteSpace(journal.PuzzleBackgroundLarge) => (journal.PuzzleBackgroundLargeTex ??= class_235.method_615(journal.PuzzleBackgroundLarge)), 84 | false when !string.IsNullOrWhiteSpace(journal.PuzzleBackgroundSmall) => (journal.PuzzleBackgroundSmallTex ??= class_235.method_615(journal.PuzzleBackgroundSmall)), 85 | _ => before 86 | }; 87 | } 88 | } -------------------------------------------------------------------------------- /Patches/patch_LocVignette.cs: -------------------------------------------------------------------------------- 1 | using MonoMod; 2 | using Quintessential; 3 | using System.Collections.Generic; 4 | using System.IO; 5 | 6 | [MonoModPatch("class_264")] 7 | class patch_LocVignette { 8 | 9 | [MonoModConstructor] 10 | public void ctor(string key) { 11 | class_264 self = (class_264)(object)this; 12 | 13 | self.field_2091 = new Dictionary(); 14 | self.field_2090 = key; 15 | Language[] languages = { 16 | Language.English, 17 | Language.German, 18 | Language.French, 19 | Language.Russian, 20 | Language.Chinese, 21 | Language.Japanese, 22 | Language.Spanish, 23 | Language.Korean, 24 | Language.Turkish, 25 | Language.Ukrainian, 26 | Language.Portuguese, 27 | Language.Czech 28 | }; 29 | foreach(Language lang in languages) { 30 | string path1 = Path.Combine("Content", "vignettes", $"{key}.{class_134.field_1498[lang]}.txt"); 31 | 32 | for(int i = 0; i < QuintessentialLoader.ModContentDirectories.Count && !File.Exists(path1); i++) { 33 | string content = QuintessentialLoader.ModContentDirectories[i]; 34 | path1 = Path.Combine(content, "Content", "vignettes", $"{key}.{class_134.field_1498[Language.English]}.txt"); 35 | } 36 | 37 | string text = File.Exists(path1) ? File.ReadAllText(path1) : ""; 38 | 39 | self.field_2091[lang] = new Vignette(text, Path.GetFileNameWithoutExtension(path1), lang); 40 | if(lang == Language.English) { 41 | Vignette vignette = new(text, Path.GetFileNameWithoutExtension(path1), Language.Pseudo); 42 | self.field_2091[Language.Pseudo] = vignette; 43 | vignette.field_4124 = class_134.method_249(vignette.field_4124); 44 | foreach(List vignetteEventList in vignette.field_4125) { 45 | for(int index = 0; index < vignetteEventList.Count; ++index) { 46 | if(vignetteEventList[index].method_2215()) { 47 | VignetteEvent.LineFields lineFields = vignetteEventList[index].method_2218(); 48 | vignetteEventList[index] = VignetteEvent.method_2212(lineFields.field_4136, class_134.method_249(lineFields.field_4093)); 49 | } 50 | } 51 | } 52 | } 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /Patches/patch_Molecule.cs: -------------------------------------------------------------------------------- 1 | #pragma warning disable CS0626 // Method, operator, or accessor is marked external and has no attributes on it 2 | 3 | public class patch_Molecule{ 4 | 5 | // Copy molecule names when copying the molecule 6 | private extern Molecule orig_method_1104(); 7 | public Molecule method_1104(){ 8 | Molecule ret = orig_method_1104(); 9 | ret.field_2639 = ((Molecule)(object)this).field_2639; 10 | return ret; 11 | } 12 | } -------------------------------------------------------------------------------- /Patches/patch_MoleculeEditorScreen.cs: -------------------------------------------------------------------------------- 1 |  2 | #pragma warning disable CS0626 // Method, operator, or accessor is marked external and has no attributes on it 3 | 4 | using Quintessential; 5 | 6 | class patch_MoleculeEditorScreen{ 7 | 8 | internal patch_Puzzle editing; 9 | 10 | public extern void orig_method_50(float param); 11 | public void method_50(float param) { 12 | orig_method_50(param); 13 | if(editing is { IsModdedPuzzle: true }){ // find the correct position to put the atoms 14 | Vector2 uiSize = new(1516f, 922f); 15 | Vector2 corner = (Input.ScreenSize() / 2 - uiSize / 2 + new Vector2(-2f, -11f)).Rounded(); 16 | Vector2 atomSize = new(95f, -90f); 17 | Vector2 atomPos = corner + new Vector2(169f, 754f); // vanilla atoms 18 | atomPos.X += atomSize.X * 3; 19 | foreach(var type in QApi.ModAtomTypes){ 20 | method_1130(atomPos, type, true); 21 | atomPos.Y += atomSize.Y; 22 | } 23 | } 24 | } 25 | 26 | [MonoMod.MonoModIgnore] 27 | private extern void method_1130(Vector2 pos, AtomType type, bool b); 28 | } -------------------------------------------------------------------------------- /Patches/patch_Part.cs: -------------------------------------------------------------------------------- 1 | using MonoMod; 2 | 3 | using PartType = class_139; 4 | 5 | class patch_Part{ 6 | 7 | // this part type 8 | [MonoModIgnore] 9 | public extern PartType method_1159(); 10 | // this IO index 11 | [MonoModIgnore] 12 | public extern int method_1167(); 13 | // setter for output amount 14 | [MonoModIgnore] 15 | private extern void method_1170(int param_2840); 16 | 17 | // handle output count overrides 18 | public extern void orig_method_1176(Solution solution, int param_4911); 19 | 20 | public void method_1176(Solution solution, int param_4911){ 21 | orig_method_1176(solution, param_4911); 22 | 23 | bool isPolymer = this.method_1159().field_1554; 24 | if(!isPolymer){ 25 | PuzzleInputOutput[] list = (!method_1159().field_1541 ? solution.method_1934().field_2771 : solution.method_1934().field_2770); 26 | if(list == null || list.Length <= method_1167()) 27 | return; 28 | 29 | PuzzleInputOutput io = list[method_1167()]; 30 | if(io == null) 31 | return; 32 | 33 | int amount = ((patch_PuzzleInputOutput)(object)io).AmountOverride; 34 | if(amount > 0) 35 | method_1170(amount); 36 | } 37 | } 38 | } -------------------------------------------------------------------------------- /Patches/patch_PartType.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using MonoMod; 4 | 5 | [MonoModPatch("class_139")] 6 | class patch_PartType{ 7 | 8 | // When non-null, the predicate is run on the puzzle's set of custom permissions to check that the part is allowed 9 | public Predicate> CustomPermissionCheck; 10 | } -------------------------------------------------------------------------------- /Patches/patch_Puzzle.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.IO; 3 | using MonoMod; 4 | using Quintessential; 5 | using Quintessential.Serialization; 6 | 7 | class patch_Puzzle{ 8 | 9 | // Custom puzzle data 10 | public HashSet CustomPermissions = new(); 11 | 12 | // Is modded content allowed in this puzzle? 13 | // Controls whether this is saved to/from a vanilla `.puzzle` file, or a Quintessential `.puzzle.yaml` file 14 | // Don't set this if you don't know what you're doing! 15 | public bool IsModdedPuzzle = false; 16 | 17 | // Save using the right format, and set Steam user ID to 0 18 | [PatchPuzzleIdWrite] 19 | public extern void orig_method_1248(string path); 20 | public void method_1248(string path){ 21 | if(IsModdedPuzzle) 22 | File.WriteAllText(path, YamlHelper.Serializer.Serialize(PuzzleModel.FromPuzzle((Puzzle)(object)this))); 23 | else 24 | orig_method_1248(path); 25 | } 26 | 27 | public static extern Puzzle orig_method_1249(string path); 28 | public static Puzzle method_1249(string path){ 29 | if(Path.GetExtension(path) == ".yaml"){ 30 | Puzzle p = PuzzleModel.FromModel(YamlHelper.Deserializer.Deserialize(File.ReadAllText(path))); 31 | ((patch_Puzzle)(object)p).IsModdedPuzzle = true; 32 | return p; 33 | } 34 | return orig_method_1249(path); 35 | } 36 | 37 | public void ConvertFormat(bool modded){ 38 | Puzzle self = (Puzzle)(object)this; 39 | WorkshopManager wm = GameLogic.field_2434.field_2460; 40 | // delete 41 | File.Delete(((patch_WorkshopManager)(object)wm).method_2237(self)); 42 | // update 43 | IsModdedPuzzle = modded; 44 | // save 45 | wm.method_2241(self); 46 | } 47 | } -------------------------------------------------------------------------------- /Patches/patch_PuzzleEditorScreen.cs: -------------------------------------------------------------------------------- 1 | #pragma warning disable IDE0051 // Remove unused private members 2 | #pragma warning disable IDE1006 // Naming Styles 3 | 4 | // ReSharper disable InconsistentNaming 5 | // ReSharper disable UnusedType.Global 6 | // ReSharper disable UnusedMember.Local 7 | // ReSharper disable UnusedMember.Global 8 | // ReSharper disable ArrangeTypeModifiers 9 | 10 | using System; 11 | using System.Linq; 12 | using MonoMod; 13 | using MonoMod.Utils; 14 | using Quintessential; 15 | using Quintessential.Internal; 16 | using Scrollbar = class_262; 17 | using InstructionTypes = class_169; 18 | using Permissions = enum_149; 19 | using AtomTypes = class_175; 20 | 21 | class patch_PuzzleEditorScreen{ 22 | private Scrollbar scrollbar; // initializer is not merged 23 | 24 | [MonoModIgnore] 25 | [PatchPuzzleEditorScreen] 26 | public extern void method_50(float param); 27 | 28 | // name is used in MonoModRules 29 | private void DisplayEditorPanelScreen(){ 30 | scrollbar ??= new(); 31 | 32 | // reimplement this section 33 | DynamicData self = new(this); 34 | Vector2 size = new(1516f, 922f); 35 | Vector2 corner = (class_115.field_1433 / 2 - size / 2 + new Vector2(-2, -11)).Rounded(); 36 | Bounds2 panelSize = Bounds2.WithSize(corner + new Vector2(0, 88 + 5), size + new Vector2(-152f + 78, -158f - 40 - 10)); 37 | Bounds2 coverBounds = panelSize.Translated(new(80, 0)); 38 | 39 | // add scrollbar/scroll region 40 | using(var _ = scrollbar.method_709(panelSize.Min, panelSize.Size.CeilingToInt(), 0, -30)){ 41 | // clear scroll zone 42 | class_226.method_600(Color.Transparent); 43 | // draw headers 44 | var nCorner = new Vector2(-10, scrollbar.field_2078 - 100); 45 | 46 | class_140.method_317(class_134.method_253("Products", "FULL LENGTH"), nCorner + new Vector2(489, 774), 900, false, true); 47 | class_140.method_317(class_134.method_253("Reagents", ""), nCorner + new Vector2(489, 506), 900, false, true); 48 | class_140.method_317(class_134.method_253("Mechanisms and Glyphs", ""), nCorner + new Vector2(489, 233), 900, false, true); 49 | 50 | Puzzle myPuzzle = self.Get>("field_2789").method_1087(); 51 | // CustomPermissions may have just not been set? TODO: find a better place for the "canonical" setter 52 | var conv = (patch_Puzzle)(object)myPuzzle; 53 | conv.CustomPermissions ??= new(); 54 | 55 | // draw inputs/outputs 56 | bool b = self.Get("field_2788") == 0; 57 | bool screenOpened = false; 58 | for(var row = 0; row < 2; row++){ 59 | bool isInput = row == 0; 60 | PuzzleInputOutput[] puzzleIOs = row == 0 ? myPuzzle.field_2771 : myPuzzle.field_2770; 61 | for(var column = 0; column < 4; ++column){ 62 | Bounds2 puzzleIoBox = Bounds2.WithSize(nCorner + new Vector2(495f, 576f) + new Vector2(column * 235, isInput ? -28f : -297f), new Vector2(226f, 226f)); 63 | if(puzzleIOs.Length > column){ 64 | class_135.method_272(b ? class_238.field_1989.field_94.field_805 : class_238.field_1989.field_94.field_808, puzzleIoBox.Min); 65 | var isHover = false; 66 | var molecule = puzzleIOs[column].field_2813; 67 | if(b){ 68 | Bounds2 deleteBounds = Bounds2.WithSize(puzzleIoBox.Min + new Vector2(176f, 175f), class_238.field_1989.field_94.field_806.field_2056.ToVector2()); 69 | bool isDelete = deleteBounds.Contains(Input.MousePos()); 70 | // open editor if clicked 71 | if(!isDelete && puzzleIoBox.Contains(Input.MousePos())){ 72 | isHover = true; 73 | if(Input.IsLeftClickPressed()){ 74 | int columnTemp = column; // otherwise it's modified after(?) 75 | var moleculeEditorScreen = new MoleculeEditorScreen(molecule, isInput, value => { 76 | puzzleIOs[columnTemp].field_2813 = value; 77 | GameLogic.field_2434.field_2460.method_2241(myPuzzle); 78 | }); 79 | ((patch_MoleculeEditorScreen)(object)moleculeEditorScreen).editing = conv; 80 | screenOpened = true; 81 | GameLogic.field_2434.method_946(moleculeEditorScreen); 82 | class_238.field_1991.field_1821.method_28(1f); 83 | } 84 | } 85 | 86 | class_135.method_272(class_238.field_1989.field_94.field_806, deleteBounds.Min); 87 | // open "are you sure" menu if X is clicked 88 | if(isDelete){ 89 | class_135.method_272(class_238.field_1989.field_94.field_807, deleteBounds.Min); 90 | if(Input.IsLeftClickPressed()){ 91 | int rowTemp = row; 92 | int columnTemp = column; 93 | GameLogic.field_2434.method_946(MessageBoxScreen.method_1095(coverBounds, true, isInput ? class_134.method_253("Do you really want to delete this product?", string.Empty) : class_134.method_253("Do you really want to delete this reagent?", string.Empty), struct_18.field_1431, isInput ? class_134.method_253("Delete Product", string.Empty) : class_134.method_253("Delete Reagent", string.Empty), class_134.method_253("Cancel", string.Empty), 94 | () => { 95 | if(rowTemp == 0) 96 | myPuzzle.field_2771 = myPuzzle.field_2771.Take(columnTemp).Concat(myPuzzle.field_2771.Skip(columnTemp + 1)).ToArray(); 97 | else 98 | myPuzzle.field_2770 = myPuzzle.field_2770.Take(columnTemp).Concat(myPuzzle.field_2770.Skip(columnTemp + 1)).ToArray(); 99 | GameLogic.field_2434.field_2460.method_2241(myPuzzle); 100 | }, /* cancel is no-op */ () => { })); 101 | class_238.field_1991.field_1821.method_28(1f); 102 | } 103 | } 104 | } 105 | 106 | class_256 moleculeSprite = Editor.method_928(molecule, (uint)row > 0U, isHover, new Vector2(156f, 146f), false, struct_18.field_1431).method_1351().field_937; 107 | Vector2 halfSize = (moleculeSprite.field_2056.ToVector2() / 2).Rounded(); 108 | var centre = puzzleIoBox.Center.Rounded() - halfSize; 109 | class_135.method_272(moleculeSprite, centre + new Vector2(-8f, -10f)); 110 | 111 | if(conv.IsModdedPuzzle){ 112 | Vector2 namePos = puzzleIoBox.BottomLeft + new Vector2(puzzleIoBox.Width / 2f - 7, -17); 113 | var isElement = molecule.method_1100().Count == 1; 114 | var fallbackPvw = "_(" + (isElement ? molecule.method_1100().Values.First().field_2275.field_2285 : "Unnamed") + ")_"; 115 | Bounds2 textArea = UI.DrawText(molecule.field_2639.method_1090(class_134.method_253(fallbackPvw, "")), namePos, class_238.field_1990.field_2143, UI.TextColor, TextAlignment.Centred, maxWidth: 236, ellipsesCutoff: 206); 116 | if(textArea.Contains(Input.MousePos()) && Input.IsLeftClickPressed() && !screenOpened){ 117 | screenOpened = true; 118 | GameLogic.field_2434.method_946(MessageBoxScreenEx.textbox(coverBounds, class_134.method_253("Please enter a new name for this " + (isInput ? "product:" : "reagent:"), string.Empty), molecule.field_2639.method_1085() ? molecule.field_2639.method_1087() : (isElement ? molecule.method_1100().Values.First().field_2275.field_2285 : ""), class_134.method_253("Rename " + (isInput ? "Product" : "Reagent"), string.Empty), s => { 119 | molecule.field_2639 = class_134.method_253(s, ""); 120 | GameLogic.field_2434.field_2460.method_2241(myPuzzle); 121 | })); 122 | class_238.field_1991.field_1821.method_28(1f); 123 | } 124 | } 125 | }else if(b){ 126 | Vector2 offset = new(-2f, -3f); 127 | class_135.method_272(class_238.field_1989.field_94.field_802, puzzleIoBox.Min + offset); 128 | class_135.method_290(isInput ? class_134.method_253("Create New Product", string.Empty).method_1060() : class_134.method_253("Create New Reagent", string.Empty).method_1060(), puzzleIoBox.Center + new Vector2(-6f, -8f), class_238.field_1990.field_2143, class_181.field_1718, (enum_0)1, 1f, 0.6f, 120f, float.MaxValue, 0, new Color(), null, int.MaxValue, false, true); 129 | 130 | if(!puzzleIoBox.Contains(Input.MousePos())) continue; 131 | class_135.method_272(class_238.field_1989.field_94.field_803, puzzleIoBox.Min + offset); 132 | 133 | if(!class_115.method_206((enum_142)1)) continue; 134 | int rowTemp = row; // otherwise it's modified after 135 | var moleculeEditorScreen = new MoleculeEditorScreen(new Molecule(), isInput, value => { 136 | if(rowTemp == 0) 137 | myPuzzle.field_2771 = myPuzzle.field_2771.method_451(new PuzzleInputOutput(value)).ToArray(); 138 | else 139 | myPuzzle.field_2770 = myPuzzle.field_2770.method_451(new PuzzleInputOutput(value)).ToArray(); 140 | GameLogic.field_2434.field_2460.method_2241(myPuzzle); 141 | }); 142 | ((patch_MoleculeEditorScreen)(object)moleculeEditorScreen).editing = conv; 143 | GameLogic.field_2434.method_946(moleculeEditorScreen); 144 | class_238.field_1991.field_1821.method_28(1f); 145 | } 146 | } 147 | } 148 | 149 | // draw vanilla rule checkboxes 150 | Vector2 ruleSize = new(236, -37); 151 | Vector2 partsCorner = nCorner + new Vector2(494, 180); 152 | self.Invoke("method_1261", partsCorner + new Vector2(ruleSize.X * 0, ruleSize.Y * 0), (string)class_191.field_1772.field_1529, enum_149.Bonder, myPuzzle); 153 | self.Invoke("method_1261", partsCorner + new Vector2(ruleSize.X * 1, ruleSize.Y * 0), (string)class_191.field_1774.field_1529, enum_149.SpeedBonder, myPuzzle); 154 | self.Invoke("method_1261", partsCorner + new Vector2(ruleSize.X * 2, ruleSize.Y * 0), (string)class_191.field_1775.field_1529, enum_149.PrismaBonder, myPuzzle); 155 | self.Invoke("method_1261", partsCorner + new Vector2(ruleSize.X * 3, ruleSize.Y * 0), (string)class_191.field_1773.field_1529, enum_149.Unbonder, myPuzzle); 156 | self.Invoke("method_1261", partsCorner + new Vector2(ruleSize.X * 0, ruleSize.Y * 1), (string)class_191.field_1776.field_1529, enum_149.Calcification, myPuzzle); 157 | self.Invoke("method_1261", partsCorner + new Vector2(ruleSize.X * 1, ruleSize.Y * 1), (string)class_191.field_1777.field_1529, enum_149.Duplication, myPuzzle); 158 | self.Invoke("method_1261", partsCorner + new Vector2(ruleSize.X * 2, ruleSize.Y * 1), (string)class_191.field_1771.field_1529, enum_149.BaronWheel, myPuzzle); 159 | self.Invoke("method_1261", partsCorner + new Vector2(ruleSize.X * 3, ruleSize.Y * 1), (string)class_191.field_1780.field_1529, enum_149.LifeAndDeath, myPuzzle); 160 | self.Invoke("method_1261", partsCorner + new Vector2(ruleSize.X * 0, ruleSize.Y * 2), (string)class_191.field_1778.field_1529, enum_149.Projection, myPuzzle); 161 | self.Invoke("method_1261", partsCorner + new Vector2(ruleSize.X * 1, ruleSize.Y * 2), (string)class_191.field_1779.field_1529, enum_149.Purification, myPuzzle); 162 | self.Invoke("method_1261", partsCorner + new Vector2(ruleSize.X * 2, ruleSize.Y * 2), (string)class_191.field_1781.field_1529, enum_149.Disposal, myPuzzle); 163 | self.Invoke("method_1261", partsCorner + new Vector2(ruleSize.X * 3, ruleSize.Y * 2), (string)class_134.method_253("Glyphs of Quintessence", string.Empty), enum_149.Quintessence, myPuzzle); 164 | 165 | // instructions selection 166 | Vector2 instructionsCorner = new(nCorner.X + 489, partsCorner.Y + ruleSize.Y * 3); 167 | class_140.method_317(class_134.method_253("Instructions", ""), instructionsCorner, 900, false, true); 168 | 169 | InstructionType[] types = InstructionTypes.field_1667; 170 | var i = 0; 171 | foreach(var type in types){ 172 | var basePos = instructionsCorner + new Vector2(80 + 60 * i, -60); 173 | var pos = basePos; 174 | if(type.field_2550 == Permissions.None) 175 | continue; 176 | bool enabled = myPuzzle.field_2773.HasFlag(type.field_2550); 177 | 178 | class_256 @base; 179 | if(enabled) 180 | @base = class_238.field_1989.field_99.field_706.field_716; 181 | else{ 182 | @base = class_238.field_1989.field_99.field_706.field_717; 183 | pos += new Vector2(3, -3); 184 | } 185 | 186 | bool hovered = Bounds2.WithSize(basePos, @base.field_2056.ToVector2()).Contains(Input.MousePos()); 187 | class_256 highlight = class_238.field_1989.field_99.field_706.field_720; 188 | 189 | UI.DrawTexture(@base, basePos); 190 | UI.DrawTexture(type.field_2546, pos + new Vector2(1, 2)); 191 | if(hovered) 192 | UI.DrawTexture(highlight, pos + new Vector2(2, 4)); 193 | 194 | if(hovered && Input.IsLeftClickPressed()){ 195 | myPuzzle.field_2773 ^= type.field_2550; 196 | GameLogic.field_2434.field_2460.method_2241(myPuzzle); 197 | } 198 | 199 | i++; 200 | } 201 | 202 | // quintessential rules 203 | var rulesCorner = instructionsCorner + new Vector2(0, ruleSize.Y * 3.5f); 204 | class_140.method_317(class_134.method_253("Quintessential Rules", ""), rulesCorner - new Vector2(0, ruleSize.Y * .5f), 900, false, true); 205 | if(UI.DrawCheckbox(rulesCorner + new Vector2(ruleSize.X * 0 + 5, ruleSize.Y * 1), "Enable Modded Content", conv.IsModdedPuzzle)) 206 | conv.ConvertFormat(!conv.IsModdedPuzzle); 207 | // TODO: will probably move to a separate mod 208 | //UI.DrawCheckbox(rulesCorner + new Vector2(ruleSize.X * 1 + 5, ruleSize.Y * 1), "Allow Overlap", false); 209 | 210 | // modded categories, if enabled 211 | Vector2 cursor = rulesCorner + new Vector2(0, ruleSize.Y * 2.5f); 212 | if(conv.IsModdedPuzzle) 213 | foreach(var category in QApi.PuzzleOptions.GroupBy(k => k.SectionName)){ 214 | class_140.method_317(category.Key, cursor, 900, false, true); 215 | 216 | var idx = 0; 217 | foreach(var option in category){ 218 | // ReSharper disable once PossibleLossOfFraction 219 | Vector2 pos = cursor + new Vector2(ruleSize.X * (idx % 4) + 5, ruleSize.Y * (idx / 4 + 1.5f)); 220 | // TODO: other option types 221 | if(option.Type == PuzzleOptionType.Boolean){ 222 | bool enabled = conv.CustomPermissions.Contains(option.ID); 223 | if(UI.DrawCheckbox(pos, option.Name, enabled)){ 224 | if(enabled) 225 | conv.CustomPermissions.Remove(option.ID); 226 | else 227 | conv.CustomPermissions.Add(option.ID); 228 | GameLogic.field_2434.field_2460.method_2241(myPuzzle); 229 | } 230 | }else if(option.Type == PuzzleOptionType.Atom){ 231 | var currentChoice = option.AtomIn(myPuzzle); 232 | if(DrawAtomSelector(pos, option.Name, currentChoice ?? AtomTypes.field_1689)) 233 | UI.OpenScreen(new AtomSelectScreen("Select: " + option.Name, type => { 234 | option.SetAtomIn(myPuzzle, type); 235 | GameLogic.field_2434.field_2460.method_2241(myPuzzle); 236 | }, currentChoice)); 237 | } 238 | 239 | idx++; 240 | } 241 | 242 | var rows = (int)Math.Ceiling(idx / 4f); 243 | cursor += new Vector2(0, ruleSize.Y * (rows + 2)); 244 | } 245 | 246 | // expand the scroll area to cover the entire displayed area 247 | // we're off by one row 248 | scrollbar.method_707(nCorner.Y - cursor.Y + panelSize.Height - ruleSize.Y + 24); 249 | } 250 | } 251 | 252 | // TODO: generalize? 253 | private static bool DrawAtomSelector(Vector2 pos, string label, AtomType atom){ 254 | Bounds2 labelBounds = UI.DrawText(label, pos + new Vector2(45f, 13f), UI.SubTitle, UI.TextColor, TextAlignment.Left); 255 | Vector2 atomPos = pos + new Vector2(17, 16); 256 | const float scale = 0.7f; 257 | Editor.method_927(atom, atomPos, scale, 1, 1, 1, -21, 0, null, null, false); 258 | 259 | if(Vector2.Distance(atomPos, Input.MousePos()) < (37 * scale) || labelBounds.Contains(Input.MousePos())){ 260 | Vector2 outlinePos = (atomPos - class_238.field_1989.field_89.field_124.field_2056.ToVector2() * scale / 2).Rounded(); 261 | var tex = class_238.field_1989.field_89.field_124; 262 | class_135.method_263(tex, Color.White, outlinePos, tex.field_2056.ToVector2() * 0.7f); 263 | if(Input.IsLeftClickPressed()){ 264 | class_238.field_1991.field_1821.method_28(1); 265 | return true; 266 | } 267 | } 268 | return false; 269 | } 270 | } -------------------------------------------------------------------------------- /Patches/patch_PuzzleInputOutput.cs: -------------------------------------------------------------------------------- 1 | class patch_PuzzleInputOutput{ 2 | 3 | /// 4 | /// When >0, the overrides the required amount for this output. 5 | /// 6 | /// For example, if the puzzle has an output multiplier of 2, but this field is set to 7 for an output, that output 7 | /// only requires 7 products to be placed on it to validate. 8 | /// 9 | /// This has no effect on inputs. 10 | /// 11 | public int AmountOverride = 0; 12 | } -------------------------------------------------------------------------------- /Patches/patch_PuzzleSelectScreen.cs: -------------------------------------------------------------------------------- 1 | using MonoMod.Utils; 2 | using Quintessential; 3 | 4 | #pragma warning disable CS0626 // Method, operator, or accessor is marked external and has no attributes on it 5 | 6 | class patch_PuzzleSelectScreen { 7 | 8 | private static int currentCampaign = 0; 9 | 10 | public extern void orig_method_50(float time); 11 | 12 | public void method_50(float time) { 13 | orig_method_50(time); 14 | if(QuintessentialSettings.Instance.EnableCustomCampaigns && QuintessentialLoader.AllCampaigns.Count > 1) { 15 | var dyn = new DynamicData(typeof(PuzzleSelectScreen), this); 16 | float y1 = class_162.method_417(-220f, 0.0f, dyn.Get("field_2937")); 17 | // add campaign change buttons 18 | Vector2 leftPos = new(class_115.field_1433.X / 2f - 305, 30 + y1); 19 | Vector2 rightPos = new(class_115.field_1433.X / 2f + 269, 30 + y1); 20 | Bounds2 leftBounds = Bounds2.WithSize(leftPos, new Vector2(36f, 37f)); 21 | Bounds2 rightBounds = Bounds2.WithSize(rightPos, new Vector2(36f, 37f)); 22 | if(leftBounds.Contains(Input.MousePos())) 23 | class_135.method_271(class_238.field_1989.field_101.field_774, Color.White.WithAlpha(0.7f), leftPos); 24 | else class_135.method_271(class_238.field_1989.field_101.field_772, Color.White.WithAlpha(0.7f), leftPos); 25 | if(rightBounds.Contains(Input.MousePos())) 26 | class_135.method_271(class_238.field_1989.field_101.field_774, Color.White.WithAlpha(0.7f), rightPos); 27 | else class_135.method_271(class_238.field_1989.field_101.field_772, Color.White.WithAlpha(0.7f), rightPos); 28 | UI.DrawTexture(class_238.field_1989.field_87.field_669, leftPos); 29 | UI.DrawTexture(class_238.field_1989.field_87.field_668, rightPos); 30 | // show the currently displayed campaign 31 | UI.DrawText(((patch_Campaign)(object)QuintessentialLoader.AllCampaigns[currentCampaign]).QuintTitle, new Vector2(Input.ScreenSize().X / 2f, 20 + y1), UI.Text, Color.LightGray, TextAlignment.Centred); 32 | // reopen the menu if clicked 33 | var settings = QuintessentialSettings.Instance.SwitcherSettings; 34 | bool keyLeft = settings.SwitchCampaignLeft.Pressed(); 35 | bool keyRight = settings.SwitchCampaignRight.Pressed(); 36 | 37 | if((leftBounds.Contains(Input.MousePos()) && Input.IsLeftClickPressed()) || keyLeft){ 38 | class_238.field_1991.field_1821.method_28(1f); 39 | var next = currentCampaign - 1; 40 | if(next < 0) 41 | next += QuintessentialLoader.AllCampaigns.Count; 42 | currentCampaign = next; 43 | Campaigns.field_2330 = QuintessentialLoader.AllCampaigns[currentCampaign]; 44 | Campaigns.field_2331[0] = QuintessentialLoader.AllCampaigns[currentCampaign]; 45 | UI.InstantCloseScreen(); 46 | UI.OpenScreen(new PuzzleSelectScreen()); 47 | }else if((rightBounds.Contains(Input.MousePos()) && Input.IsLeftClickPressed()) || keyRight) { 48 | class_238.field_1991.field_1821.method_28(1f); 49 | currentCampaign = (currentCampaign + 1) % QuintessentialLoader.AllCampaigns.Count; 50 | Campaigns.field_2330 = QuintessentialLoader.AllCampaigns[currentCampaign]; 51 | Campaigns.field_2331[0] = QuintessentialLoader.AllCampaigns[currentCampaign]; 52 | UI.InstantCloseScreen(); 53 | UI.OpenScreen(new PuzzleSelectScreen()); 54 | } 55 | } 56 | } 57 | 58 | public static void ResetPosition(){ 59 | currentCampaign = 0; 60 | } 61 | } -------------------------------------------------------------------------------- /Patches/patch_Renderer.cs: -------------------------------------------------------------------------------- 1 | using Quintessential; 2 | using System; 3 | using System.IO; 4 | 5 | #pragma warning disable CS0626 // Method, operator, or accessor is marked external and has no attributes on it 6 | 7 | class patch_Renderer { 8 | 9 | // checks mods for textures before vanilla 10 | 11 | public static extern bool orig_method_1339(class_256 param_5118); 12 | 13 | public static bool method_1339(class_256 textureInfo){ 14 | string origPath = null; 15 | if(textureInfo.field_2062.method_1085() /*Exists*/){ 16 | origPath = textureInfo.field_2062.method_1087(); 17 | if(textureInfo.field_2062.method_1087() /*Get*/.StartsWith("Content")) { 18 | for(int i = QuintessentialLoader.ModContentDirectories.Count - 1; i >= 0; i--){ 19 | string dir = QuintessentialLoader.ModContentDirectories[i]; 20 | try{ 21 | textureInfo.field_2062 = Path.Combine(dir, origPath); 22 | return orig_method_1339(textureInfo); 23 | }catch(Exception e){ 24 | HandleException(e); 25 | }finally { 26 | textureInfo.field_2062 = origPath; 27 | } 28 | } 29 | } 30 | } 31 | try{ 32 | return orig_method_1339(textureInfo); 33 | }catch(Exception e) { 34 | HandleException(e); 35 | } 36 | // none match -> use missing texture 37 | try{ 38 | Logger.Log($"Texture {origPath} does not exist, using fallback texture"); 39 | textureInfo.field_2062 = Path.Combine(QuintessentialLoader.ModContentDirectories[0], "Content", "Quintessential", "missing"); 40 | return orig_method_1339(textureInfo); 41 | }finally{ 42 | textureInfo.field_2062 = origPath; 43 | } 44 | } 45 | 46 | private static void HandleException(Exception e){ 47 | if(e.Message.StartsWith("Texture file not found!")) 48 | return; 49 | if(e is class_266 && e.Message.StartsWith("Failed to load PNG file:")) 50 | throw new Exception($"SDL failed to load a PNG: \"{SDL2.SDL.SDL_GetError()}\"", e); 51 | throw e; 52 | } 53 | } -------------------------------------------------------------------------------- /Patches/patch_ScoreManager.cs: -------------------------------------------------------------------------------- 1 | using MonoMod; 2 | 3 | #pragma warning disable CS0626 // Method, operator, or accessor is marked external and has no attributes on it 4 | 5 | class patch_ScoreManager { 6 | 7 | // removes a steam-related call to upload scores 8 | 9 | [PatchScoreManagerLoad] 10 | [MonoModIgnore] 11 | public extern void method_1369(Puzzle param_5132, enum_133 param_5133, int param_5134); 12 | 13 | public extern void orig_method_1370(string str); 14 | public void method_1370(string str) { 15 | // no-op 16 | } 17 | } -------------------------------------------------------------------------------- /Patches/patch_Settings.cs: -------------------------------------------------------------------------------- 1 | using MonoMod; 2 | 3 | [MonoModPatch("class_110")] 4 | class patch_Settings { 5 | 6 | // settings init 7 | // disabling steam 8 | 9 | [PatchSettingsStaticInit] 10 | public static extern void orig_cctor(); 11 | 12 | [MonoModConstructor] 13 | public static void cctor() { 14 | orig_cctor(); 15 | } 16 | } -------------------------------------------------------------------------------- /Patches/patch_Sim.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using MonoMod; 4 | using Quintessential; 5 | 6 | #pragma warning disable CS0649 // Field is never assigned to 7 | #pragma warning disable CS0626 // Method, operator, or accessor is marked external and has no attributes on it 8 | 9 | class patch_Sim{ 10 | 11 | // Make important fields public 12 | [MonoModPublic] 13 | public SolutionEditorBase field_3818; 14 | [MonoModPublic] 15 | public Dictionary field_3821; 16 | [MonoModPublic] 17 | public Dictionary field_3822; 18 | [MonoModPublic] 19 | public List field_3823; 20 | [MonoModPublic] 21 | public List field_3826; 22 | 23 | // Hold onto held grippers 24 | public List HeldGrippers; 25 | 26 | // Helper methods to find held or unheld atoms 27 | public Maybe FindAtomRelative(Part part, HexIndex offset){ 28 | return FindAtom(part.method_1184(offset)); 29 | } 30 | 31 | public Maybe FindAtom(HexIndex position){ 32 | var simStates = field_3821; 33 | foreach(Molecule molecule in field_3823){ 34 | if(molecule.method_1100().TryGetValue(position, out Atom atom)){ 35 | bool isHeld = HeldGrippers != null && HeldGrippers.Any(part => simStates[part].field_2724 == position); 36 | return new AtomReference(molecule, position, atom.field_2275, atom, isHeld); 37 | } 38 | } 39 | 40 | return struct_18.field_1431; 41 | } 42 | 43 | // Run custom behaviours 44 | public extern void orig_method_1832(bool first); 45 | public void method_1832(bool first){ 46 | // fill the list of grippers 47 | List allParts = field_3818.method_502().field_3919; 48 | Dictionary simStates = field_3821; 49 | HeldGrippers = new(); 50 | foreach(var part in allParts) 51 | foreach(var gripper in part.field_2696) 52 | if(simStates[gripper].field_2729.method_1085()) 53 | HeldGrippers.Add(gripper); 54 | // run the cycle 55 | orig_method_1832(first); 56 | // and then process things that happen after 57 | foreach(var action in QApi.ToRunAfterCycle) 58 | action((Sim)(object)this, first); 59 | } 60 | } -------------------------------------------------------------------------------- /Patches/patch_SolutionEditorPartsPanelSection.cs: -------------------------------------------------------------------------------- 1 | using MonoMod; 2 | using MonoMod.Utils; 3 | 4 | using Quintessential; 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | using PartType = class_139; 8 | 9 | #pragma warning disable CS0626 // Method, operator, or accessor is marked external and has no attributes on it 10 | #pragma warning disable IDE1006 // Naming Styles 11 | 12 | [MonoModPatch("SolutionEditorPartsPanel/class_428")] 13 | class patch_SolutionEditorPartsPanelSection { 14 | 15 | // add our parts to the panel 16 | [MonoModIgnore] 17 | SolutionEditorPartsPanel field_3972; 18 | 19 | public extern void orig_method_2046(List parts, PartType type); 20 | public void method_2046(List parts, PartType type) { 21 | // find the puzzle we're in 22 | DynamicData selfData = new(field_3972); 23 | var sol = selfData.Get("field_2007"); 24 | Puzzle puzzle = sol.method_502().method_1934(); 25 | // check if we have the appropriate custom permissions 26 | var perms = (((patch_Puzzle)(object)puzzle).CustomPermissions ??= new()); 27 | var checker = ((patch_PartType)(object)type).CustomPermissionCheck; 28 | 29 | if(checker == null || checker(perms)) 30 | orig_method_2046(parts, type); 31 | 32 | if(((patch_Puzzle)(object)puzzle).IsModdedPuzzle) 33 | foreach(var pair in QApi.PanelParts.Where(pair => type.Equals(pair.Right))) 34 | method_2046(parts, pair.Left); 35 | } 36 | } -------------------------------------------------------------------------------- /Patches/patch_SolutionEdtorBase.cs: -------------------------------------------------------------------------------- 1 | using Quintessential; 2 | 3 | #pragma warning disable CS0626 // Method, operator, or accessor is marked external and has no attributes on it 4 | 5 | abstract class patch_SolutionEditorBase : SolutionEditorBase { 6 | 7 | // renders parts 8 | // adds support for custom part renderers 9 | 10 | public extern void orig_method_1996(Part part, Vector2 pos); 11 | public void method_1996(Part part, Vector2 pos) { 12 | orig_method_1996(part, pos); 13 | class_236 class195 = method_1989(part, pos); 14 | class_195 renderer = new(class195.field_1984, class195.field_1985, Editor.method_922()); 15 | foreach(var r in QApi.PartRenderers) 16 | if(r.Left(part)) 17 | r.Right(part, pos, this, renderer); 18 | } 19 | } -------------------------------------------------------------------------------- /Patches/patch_Steam.cs: -------------------------------------------------------------------------------- 1 | using Quintessential; 2 | using Quintessential.Internal; 3 | 4 | #pragma warning disable CS0626 // Method, operator, or accessor is marked external and has no attributes on it 5 | #pragma warning disable IDE1006 // Naming Styles 6 | 7 | internal class patch_Steam{ 8 | 9 | public static extern void orig_method_2150(); 10 | public static void method_2150(){ 11 | orig_method_2150(); // is this necessary? 12 | Screenshotter.CheckScreenshot(); 13 | QuintessentialLoader.CheckCampaignReload(); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Patches/patch_StringLoader.cs: -------------------------------------------------------------------------------- 1 | using MonoMod; 2 | using Quintessential; 3 | using System; 4 | 5 | #pragma warning disable CS0626 // Method, operator, or accessor is marked external and has no attributes on it 6 | 7 | [MonoModPatch("class_103")] 8 | class patch_StringLoader { 9 | 10 | // string loader patches 11 | // should never be called 12 | 13 | public static extern string orig_method_131(int idc); 14 | 15 | public static string method_131(int n) { 16 | try { 17 | return orig_method_131(n); 18 | } catch(Exception e) { 19 | if(Logger.Setup) { 20 | Logger.Log("Missing text key: " + n + "!"); 21 | Logger.Log(e); 22 | } 23 | 24 | return "!!! quintessential: missing string !!!"; 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /Patches/patch_TitleScreen.cs: -------------------------------------------------------------------------------- 1 | using MonoMod; 2 | using Quintessential; 3 | 4 | #pragma warning disable CS0626 // Method, operator, or accessor is marked external and has no attributes on it 5 | 6 | [MonoModPatch("class_93")] 7 | class patch_TitleScreen { 8 | 9 | // renders main menu 10 | // adds notice mod count 11 | 12 | public static extern void orig_method_90(float param_3772, float param_3773, bool param_3774); 13 | public static void method_90(float param_3772, float time, bool renderText) { 14 | orig_method_90(param_3772, time, renderText); 15 | if(renderText) { 16 | class_135.method_290($"Quintessential v{QuintessentialLoader.VersionString} ({QuintessentialLoader.VersionNumber})", new Vector2(49f, 100f), class_238.field_1990.field_2144, class_181.field_1718.WithAlpha(0.7f), 0, 1f, 0.6f, float.MaxValue, float.MaxValue, 0, new Color(), null, int.MaxValue, false, true); 17 | class_135.method_290($"{QuintessentialLoader.Mods.Count} mods loaded.", new Vector2(49f, 77f), class_238.field_1990.field_2144, class_181.field_1718.WithAlpha(0.7f), 0, 1f, 0.6f, float.MaxValue, float.MaxValue, 0, new Color(), null, int.MaxValue, false, true); 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /Patches/patch_WorkshopManager.cs: -------------------------------------------------------------------------------- 1 | // ReSharper disable InconsistentNaming 2 | // ReSharper disable UnusedType.Global 3 | // ReSharper disable UnusedMember.Global 4 | // ReSharper disable SuspiciousTypeConversion.Global 5 | 6 | using System; 7 | using System.Collections.Generic; 8 | using System.IO; 9 | using System.Linq; 10 | using MonoMod; 11 | using Quintessential; 12 | using Quintessential.Serialization; 13 | 14 | internal class patch_WorkshopManager{ 15 | 16 | public void method_2230(){ 17 | ((WorkshopManager)(object)this).method_2234(); 18 | ((WorkshopManager)(object)this).method_2235(); 19 | } 20 | 21 | // make the Browse button a no-op rather than crashing 22 | public void method_2233(){} 23 | 24 | // load YAML-based puzzles alongside binary ones 25 | private extern IEnumerable orig_method_2236(string folder); 26 | private IEnumerable method_2236(string folder){ 27 | return orig_method_2236(folder).Concat(YamlPuzzles(folder)); 28 | } 29 | 30 | private static IEnumerable YamlPuzzles(string folder){ 31 | string path = Path.Combine(class_269.field_2102, folder); 32 | foreach(var puzzleFilePath in Directory.EnumerateFiles(path, "*.puzzle.yaml")){ 33 | PuzzleModel model = YamlHelper.Deserializer.Deserialize(File.ReadAllText(puzzleFilePath)); 34 | Puzzle fromModel; 35 | try{ 36 | fromModel = PuzzleModel.FromModel(model); 37 | }catch(Exception e){ 38 | Logger.Log($"Exception loading custom puzzle \"{model.ID}\":"); 39 | Logger.Log(e); 40 | continue; 41 | } 42 | // ReSharper disable once PossibleInvalidCastException 43 | ((patch_Puzzle)(object)fromModel).IsModdedPuzzle = true; 44 | yield return fromModel; 45 | } 46 | } 47 | 48 | // give YAML-based puzzles the right file location 49 | // used for both finding and saving, though saving in the correct format is handled in `Puzzle` 50 | private extern string orig_method_2237(Puzzle puzzle); 51 | [MonoModPublic] 52 | public string method_2237(Puzzle puzzle){ 53 | return ((patch_Puzzle)(object)puzzle).IsModdedPuzzle 54 | ? Path.Combine(class_269.field_2102, "custom", puzzle.field_2766 + ".puzzle.yaml") 55 | : orig_method_2237(puzzle); 56 | } 57 | } -------------------------------------------------------------------------------- /Properties/Resources.resx: -------------------------------------------------------------------------------- 1 |  2 | 3 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | text/microsoft-resx 110 | 111 | 112 | 2.0 113 | 114 | 115 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 116 | 117 | 118 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 119 | 120 | 121 | 122 | ..\Content\missing.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 123 | 124 | 125 | ..\Content\journal_go_left.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 126 | 127 | 128 | ..\Content\journal_go_left_hover.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 129 | 130 | 131 | ..\Content\journal_go_right.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 132 | 133 | 134 | ..\Content\journal_go_right_hover.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 135 | 136 | 137 | ..\Content\background_larger.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 138 | 139 | 140 | ..\Content\vertical_bar_centre_tall.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 141 | 142 | -------------------------------------------------------------------------------- /Quintessential.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net452 5 | 6 | Quintessential 7 | true 8 | 9 | 10 | 11 | 10 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Opus Magnum\Ionic.Zip.Reduced.dll 25 | False 26 | 27 | 28 | ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Opus Magnum\IntermediaryLightning.exe 29 | false 30 | False 31 | 32 | 33 | ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Opus Magnum\Mono.Cecil.dll 34 | False 35 | 36 | 37 | ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Opus Magnum\MonoMod.exe 38 | False 39 | 40 | 41 | ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Opus Magnum\MonoMod.Common.dll 42 | False 43 | 44 | 45 | ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Opus Magnum\MonoMod.Utils.dll 46 | False 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | ResXFileCodeGenerator 57 | Resources.Designer.cs 58 | 59 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /Quintessential.csproj.DotSettings: -------------------------------------------------------------------------------- 1 |  2 | True -------------------------------------------------------------------------------- /Quintessential.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.31320.298 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Quintessential", "Quintessential.csproj", "{AFAAB5A5-A57E-41EC-8BFB-7550FDB857EC}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|Any CPU = Debug|Any CPU 11 | Release|Any CPU = Release|Any CPU 12 | EndGlobalSection 13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 14 | {AFAAB5A5-A57E-41EC-8BFB-7550FDB857EC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 15 | {AFAAB5A5-A57E-41EC-8BFB-7550FDB857EC}.Debug|Any CPU.Build.0 = Debug|Any CPU 16 | {AFAAB5A5-A57E-41EC-8BFB-7550FDB857EC}.Release|Any CPU.ActiveCfg = Release|Any CPU 17 | {AFAAB5A5-A57E-41EC-8BFB-7550FDB857EC}.Release|Any CPU.Build.0 = Release|Any CPU 18 | EndGlobalSection 19 | GlobalSection(SolutionProperties) = preSolution 20 | HideSolutionNode = FALSE 21 | EndGlobalSection 22 | GlobalSection(ExtensibilityGlobals) = postSolution 23 | SolutionGuid = {E99B4FA5-E26B-4253-8614-377BAAD742F6} 24 | EndGlobalSection 25 | EndGlobal 26 | -------------------------------------------------------------------------------- /Quintessential/AtomSelectScreen.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using SDL2; 3 | 4 | namespace Quintessential; 5 | 6 | using AtomTypes = class_175; 7 | 8 | public class AtomSelectScreen : IScreen{ 9 | 10 | public string Label; 11 | public Action OnClick; 12 | public AtomType Preselected; 13 | 14 | public AtomSelectScreen(string label, Action onClick = null, AtomType preselected = null){ 15 | Label = label; 16 | OnClick = onClick; 17 | Preselected = preselected; 18 | } 19 | 20 | public bool method_1037(){ 21 | return false; 22 | } 23 | 24 | public void method_47(bool param_4687){ 25 | // Add gray BG 26 | GameLogic.field_2434.field_2464 = true; 27 | } 28 | 29 | public void method_48(){} 30 | 31 | public void method_50(float param_4686){ 32 | // Display a title 33 | UI.DrawText(Label, (Input.ScreenSize() / 2) + new Vector2(0, 170), UI.Title, Color.White, TextAlignment.Centred); 34 | 35 | // draw atom options 36 | var numAtoms = AtomTypes.field_1691.Length; 37 | for(var idx = 0; idx < numAtoms; idx++){ 38 | var type = AtomTypes.field_1691[idx]; 39 | if(ClickableAtom((Input.ScreenSize() / 2) + new Vector2(-(numAtoms - 1) * 45 + idx * 90, 0), type, true, type.Equals(Preselected))){ 40 | OnClick?.Invoke(type); 41 | UI.CloseScreen(); 42 | } 43 | } 44 | 45 | // "press esc to CANCEL" 46 | Bounds2 labelBounds = UI.DrawText("Press ESC to ", (Input.ScreenSize() / 2) + new Vector2(-40, -170), UI.SubTitle, class_181.field_1718, TextAlignment.Centred); 47 | if(Input.IsSdlKeyPressed(SDL.enum_160.SDLK_ESCAPE) || UI.DrawAndCheckSimpleButton("CANCEL", labelBounds.BottomRight + new Vector2(10, -7), new Vector2(70, (int)labelBounds.Height + 10))) 48 | UI.HandleCloseButton(); 49 | } 50 | 51 | private static bool ClickableAtom(Vector2 pos, AtomType atom, bool selectable, bool selected){ 52 | float alpha = selectable ? 1 : .3f; 53 | Vector2 centred = (pos - class_238.field_1989.field_89.field_117.field_2056.ToVector2() / 2).Rounded(); 54 | // slot around the atom 55 | class_135.method_271(selected ? class_238.field_1989.field_89.field_118 : class_238.field_1989.field_89.field_117, Color.White.WithAlpha(alpha), centred); 56 | // draw the atom 57 | Editor.method_927(atom, pos, 1, alpha, 1, 1, -21, 0, null, null, false); 58 | // are we hovering over it? 59 | if(!selectable || Vector2.Distance(pos, Input.MousePos()) > 37) 60 | return false; 61 | // draw the hovering overlay 62 | Vector2 outlineCentred = (pos - class_238.field_1989.field_89.field_124.field_2056.ToVector2() / 2).Rounded(); 63 | class_135.method_272(class_238.field_1989.field_89.field_124, outlineCentred); 64 | // are we clicking? 65 | if(Input.IsLeftClickHeld()){ 66 | // make a sound 67 | class_238.field_1991.field_1821.method_28(1); 68 | return true; 69 | } 70 | return false; 71 | } 72 | } -------------------------------------------------------------------------------- /Quintessential/Input.cs: -------------------------------------------------------------------------------- 1 | using SDL2; 2 | 3 | namespace Quintessential; 4 | 5 | using OMInput = class_115; 6 | using SdlKey = SDL.enum_160; 7 | 8 | /// 9 | /// Helper class containing functions for querying keyboard or mouse input. 10 | /// 11 | public static class Input { 12 | 13 | #region Keyboard input 14 | 15 | public static bool IsShiftHeld() { 16 | return OMInput.method_193(0); 17 | } 18 | 19 | public static bool IsControlHeld() { 20 | return OMInput.method_193((enum_143)1); 21 | } 22 | 23 | public static bool IsAltHeld() { 24 | return OMInput.method_193((enum_143)2); 25 | } 26 | 27 | public static bool IsSdlKeyPressed(SdlKey key) { 28 | return OMInput.method_198(key); 29 | } 30 | 31 | public static bool IsSdlKeyReleased(SdlKey key) { 32 | return OMInput.method_199(key); 33 | } 34 | 35 | public static bool IsSdlKeyHeld(SdlKey key) { 36 | return OMInput.method_200(key); 37 | } 38 | 39 | public static SdlKey GetSdlKeyForCharacter(string character) { 40 | return SDL.SDL_GetKeyFromName(character); 41 | } 42 | 43 | public static bool IsKeyPressed(string key) { 44 | return OMInput.method_198(GetSdlKeyForCharacter(key)); 45 | } 46 | 47 | public static bool IsKeyReleased(string key) { 48 | return OMInput.method_199(GetSdlKeyForCharacter(key)); 49 | } 50 | 51 | public static bool IsKeyHeld(string key) { 52 | return OMInput.method_200(GetSdlKeyForCharacter(key)); 53 | } 54 | 55 | #endregion 56 | 57 | #region Mouse input 58 | 59 | public static Vector2 MousePos() { 60 | return OMInput.method_202(); 61 | } 62 | 63 | // Not sure if there is functionality for other values, like "(enum_142) 0" or "(enum_142) 4" 64 | 65 | public static bool IsLeftClickHeld() { 66 | return OMInput.method_205((enum_142)1); 67 | } 68 | public static bool IsLeftClickPressed() { 69 | return OMInput.method_206((enum_142)1); 70 | } 71 | public static bool IsLeftClickReleased() { 72 | return OMInput.method_207((enum_142)1); 73 | } 74 | 75 | public static bool IsMiddleClickHeld() { 76 | return OMInput.method_205((enum_142)2); 77 | } 78 | public static bool IsMiddleClickPressed() { 79 | return OMInput.method_206((enum_142)2); 80 | } 81 | public static bool IsMiddleClickReleased() { 82 | return OMInput.method_207((enum_142)2); 83 | } 84 | 85 | public static bool IsRightClickHeld() { 86 | return OMInput.method_205((enum_142)3); 87 | } 88 | public static bool IsRightClickPressed() { 89 | return OMInput.method_206((enum_142)3); 90 | } 91 | public static bool IsRightClickReleased() { 92 | return OMInput.method_207((enum_142)3); 93 | } 94 | 95 | #endregion 96 | 97 | #region Other 98 | 99 | public static Vector2 ScreenSize() { 100 | return OMInput.field_1433; 101 | } 102 | 103 | #endregion 104 | } 105 | -------------------------------------------------------------------------------- /Quintessential/Internal/MessageBoxScreenEx.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using SDL2; 3 | 4 | namespace Quintessential.Internal; 5 | 6 | // TODO: replace with more generic free text input 7 | internal sealed class MessageBoxScreenEx : IScreen{ 8 | private const float cursorBlinkSpeed = 1.2f; 9 | private Bounds2 bounds; 10 | private bool field_2624 = true; 11 | private string title; 12 | private Maybe field_2626; 13 | private string confirmText; 14 | private bool confirmable; 15 | private string cancelText; 16 | private bool isTextbox; 17 | private string text; 18 | private float cursorBlink; 19 | private Action onConfirm = () => {}; 20 | private Action onCancel = () => {}; 21 | 22 | internal static MessageBoxScreenEx textbox(Bounds2 bounds, string title, string initialText, string confirmText, Action onConfirm){ 23 | MessageBoxScreenEx ret = new(){ 24 | bounds = bounds, 25 | title = title, 26 | text = initialText, 27 | confirmText = confirmText, 28 | confirmable = true, 29 | cancelText = "Cancel", 30 | isTextbox = true 31 | }; 32 | ret.onConfirm = () => onConfirm(ret.text); 33 | return ret; 34 | } 35 | 36 | public bool method_1037() => false; 37 | 38 | public void method_50(float deltaTime){ 39 | if(field_2624){ 40 | class_135.method_268(class_238.field_1989.field_102.field_819, Color.White, bounds.Min, Bounds2.WithSize(bounds.Min.X, bounds.Min.Y, bounds.Width - 27f, bounds.Height)); 41 | class_135.method_268(class_238.field_1989.field_102.field_819, Color.White, bounds.Min, Bounds2.WithSize(bounds.Max.X - 27f, bounds.Min.Y, 27f, bounds.Height - 27f)); 42 | }else 43 | class_135.method_268(class_238.field_1989.field_102.field_819, Color.White, bounds.Min, bounds); 44 | 45 | Vector2 centre = bounds.Center.Rounded(); 46 | if(isTextbox) 47 | centre.Y -= 34f; 48 | if(isTextbox){ 49 | class_135.method_290(title, centre + new Vector2(4f, 100f), class_238.field_1990.field_2145, class_181.field_1718, (enum_0)1, 1f, 0.6f, float.MaxValue, float.MaxValue, 0, new Color(), null, int.MaxValue, false, true); 50 | class_135.method_275(class_238.field_1989.field_101.field_778, Color.White, Bounds2.WithSize(centre + new Vector2(-265f, 24f), new Vector2(532f, 48f))); 51 | Bounds2 bounds2 = class_135.method_290(text.Length == 0 ? " " : text, centre + new Vector2(0.0f, 43f), class_238.field_1990.field_2144, class_181.field_1718, (enum_0)1, 1f, 0.6f, float.MaxValue, float.MaxValue, 0, new Color(), null, int.MaxValue, true, true); 52 | cursorBlink = (cursorBlink + deltaTime) % cursorBlinkSpeed; 53 | if(cursorBlink < cursorBlinkSpeed / 2.0) 54 | class_135.method_280(class_181.field_1718, Bounds2.WithSize(bounds2.BottomRight + new Vector2(2f, 1f), new Vector2(2f, 22f))); 55 | char upper = /*char.ToUpper(*/class_115.method_201()/*)*/; 56 | if(upper != char.MinValue){ 57 | text = (text + upper).method_437(); 58 | cursorBlink = 0.0f; 59 | } 60 | 61 | if(class_115.method_200(SDL.enum_160.SDLK_BACKSPACE) && text.Length > 0){ 62 | if(class_115.method_193((enum_143)1)){ 63 | text = text.TrimEnd(); 64 | text = text.Substring(0, text.LastIndexOf(' ') + 1); 65 | }else 66 | text = text.Substring(0, text.Length - 1); 67 | 68 | cursorBlink = 0.0f; 69 | } 70 | }else if(field_2626.method_1085()){ 71 | class_135.method_290(title, centre + new Vector2(0.0f, 70f), class_238.field_1990.field_2145, class_181.field_1718, (enum_0)1, 1f, 0.6f, float.MaxValue, float.MaxValue, 0, new Color(), null, int.MaxValue, false, true); 72 | class_135.method_290(field_2626.method_1087(), centre + new Vector2(0.0f, 30f), class_238.field_1990.field_2145, class_181.field_1718, (enum_0)1, 1f, 0.6f, float.MaxValue, float.MaxValue, 0, new Color(), null, int.MaxValue, false, true); 73 | }else 74 | class_135.method_290(title, centre + new Vector2(0.0f, 30f), class_238.field_1990.field_2145, class_181.field_1718, (enum_0)1, 1f, 0.6f, float.MaxValue, float.MaxValue, 0, new Color(), null, int.MaxValue, false, true); 75 | 76 | ButtonDrawingLogic buttonDrawingLogic; 77 | if(confirmable){ 78 | bool textValid = !isTextbox || text.Length > 0; 79 | bool pressedEnter = textValid && class_115.method_196(); 80 | buttonDrawingLogic = class_140.method_314(confirmText, centre + new Vector2(15f, -50f)); 81 | if(buttonDrawingLogic.method_824(textValid, true) || pressedEnter){ 82 | onConfirm(); 83 | UI.CloseScreen(); 84 | class_238.field_1991.field_1821.method_28(1f); 85 | } 86 | } 87 | 88 | int x = confirmable ? -265 : -127; 89 | buttonDrawingLogic = class_140.method_314(cancelText, centre + new Vector2(x, -50f)); 90 | if(buttonDrawingLogic.method_824(true, true) || class_115.method_198(SDL.enum_160.SDLK_ESCAPE)){ 91 | onCancel(); 92 | UI.CloseScreen(); 93 | class_238.field_1991.field_1821.method_28(1f); 94 | } 95 | 96 | if(bounds.Contains(Input.MousePos()) || !Input.IsLeftClickPressed()) 97 | return; 98 | onCancel(); 99 | UI.CloseScreen(); 100 | class_238.field_1991.field_1821.method_28(1f); 101 | } 102 | 103 | public void method_47(bool param_4687){} 104 | 105 | public void method_48(){} 106 | } -------------------------------------------------------------------------------- /Quintessential/Internal/QuintessentialAsMod.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Quintessential.Internal; 4 | 5 | public class QuintessentialAsMod : QuintessentialMod { 6 | 7 | public override Type SettingsType => typeof(QuintessentialSettings); 8 | 9 | public override void Load() { } 10 | 11 | public override void PostLoad() { } 12 | 13 | public override void Unload() { } 14 | } 15 | -------------------------------------------------------------------------------- /Quintessential/Internal/Screenshotter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Runtime.InteropServices; 4 | 5 | using SDL2; 6 | 7 | namespace Quintessential.Internal; 8 | 9 | internal class Screenshotter{ 10 | 11 | public static void CheckScreenshot(){ 12 | if(/*QuintessentialSettings.Instance.Screenshot.Pressed()*/ false){ 13 | var size = Input.ScreenSize(); 14 | SDL.SDL_Rect rect = new(){ 15 | w = (int)Math.Ceiling(size.X), 16 | h = (int)Math.Ceiling(size.Y), 17 | x = 0, 18 | y = 0 19 | }; 20 | var buffer = new byte[rect.x * rect.y * 4]; 21 | 22 | unsafe{ 23 | fixed(byte* pixels = buffer){ 24 | IntPtr pixPtr = new IntPtr(pixels); 25 | //IntPtr sdlRenderer = SDL.SDL_GetRenderer(GameLogic.field_2434.field_2437.field_2192); 26 | //SDL.SDL_RenderReadPixels(sdlRenderer, ref rect, SDL.SDL_PIXELFORMAT_ABGR8888, pixPtr, rect.w * 4); 27 | IntPtr surface = SDL.SDL_CreateRGBSurfaceFrom(pixPtr, rect.w, rect.h, 32, rect.w * 4, 0xFF, 0xFF00, 0xFF0000, 0xFF000000); 28 | if(surface.Equals(IntPtr.Zero)) 29 | throw new Exception($"Failed to create surface! \"{SDL.SDL_GetError()}\""); 30 | Logger.Log("Created surface!"); 31 | 32 | var result = class_267.method_726(surface, Path.Combine(QuintessentialLoader.PathScreenshots, "screenshot-" + 1 + ".png")); 33 | if(result != 0) 34 | throw new Exception($"Failed to save screenshot! \"{SDL.SDL_GetError()}\""); 35 | Logger.Log("Took screenshot!"); 36 | 37 | SDL.SDL_FreeSurface(surface); 38 | } 39 | } 40 | } 41 | } 42 | } -------------------------------------------------------------------------------- /Quintessential/Logger.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | 4 | namespace Quintessential; 5 | 6 | // don't actually know how logging works in OM, but rn it looks like it just doesn't? 7 | // so let's do it ourself 8 | public static class Logger { 9 | 10 | private static string LogPath; 11 | public static bool Setup { 12 | get; 13 | private set; 14 | } = false; 15 | 16 | public static void Init() { 17 | LogPath = Path.Combine(QuintessentialLoader.PathLightning, "log.txt"); 18 | File.Delete(LogPath); 19 | Log("Quintessential log"); 20 | Setup = true; 21 | } 22 | 23 | public static void Log(string text) { 24 | File.AppendAllText(LogPath, $"({DateTime.Now}) {text ?? "null"}\n"); 25 | } 26 | 27 | public static void Log(object e) { 28 | Log(e?.ToString()); 29 | } 30 | } -------------------------------------------------------------------------------- /Quintessential/ModMeta.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using YamlDotNet.Serialization; 4 | 5 | namespace Quintessential; 6 | 7 | using Texture = class_256; 8 | 9 | public class ModMeta { 10 | 11 | public string Name { get; set; } 12 | 13 | public string Title { get; set; } 14 | 15 | public string Icon { get; set; } 16 | 17 | public string Desc { get; set; } 18 | 19 | public string DLL { get; set; } 20 | 21 | public string Mappings { get; set; } 22 | 23 | public Dependency[] Dependencies { get; set; } = new Dependency[0]; 24 | 25 | public Dependency[] OptionalDependencies { get; set; } = new Dependency[0]; 26 | 27 | [YamlIgnore] 28 | public string PathArchive { get; set; } 29 | 30 | [YamlIgnore] 31 | public string PathDirectory { get; set; } 32 | 33 | [YamlIgnore] 34 | public Version Version { get; set; } = new Version(1, 0); 35 | private string _VersionString; 36 | 37 | [YamlIgnore] 38 | public Texture IconCache = null; 39 | 40 | [YamlMember(Alias = "Version")] 41 | public string VersionString { 42 | get { 43 | return _VersionString; 44 | } 45 | set { 46 | _VersionString = value; 47 | int versionSplitIndex = value.IndexOf('-'); 48 | if(versionSplitIndex == -1) 49 | Version = new Version(value); 50 | else 51 | Version = new Version(value.Substring(0, versionSplitIndex)); 52 | } 53 | } 54 | 55 | public override string ToString() { 56 | return Name + " " + Version; 57 | } 58 | 59 | public void PostParse() { 60 | if(!string.IsNullOrEmpty(DLL) && !string.IsNullOrEmpty(PathDirectory) && !File.Exists(DLL)) 61 | DLL = Path.Combine(PathDirectory, DLL.Replace('/', Path.DirectorySeparatorChar).Replace('\\', Path.DirectorySeparatorChar)); 62 | } 63 | 64 | public class Dependency { 65 | 66 | public string Name { get; set; } 67 | 68 | [YamlIgnore] 69 | public Version Version { get; set; } = new Version(1, 0); 70 | private string _VersionString; 71 | 72 | [YamlMember(Alias = "Version")] 73 | public string VersionString { 74 | get { 75 | return _VersionString; 76 | } 77 | set { 78 | _VersionString = value; 79 | int versionSplitIndex = value.IndexOf('-'); 80 | if(versionSplitIndex == -1) 81 | Version = new Version(value); 82 | else 83 | Version = new Version(value.Substring(0, versionSplitIndex)); 84 | } 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /Quintessential/ModsScreen.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Linq; 4 | using System.Reflection; 5 | 6 | using Quintessential.Settings; 7 | 8 | namespace Quintessential; 9 | 10 | using Scrollbar = class_262; 11 | 12 | class ModsScreen : IScreen { 13 | 14 | private const int modButtonWidth = 300; 15 | private static readonly class_256 verticalBarCentreTall = class_235.method_615("Quintessential/vertical_bar_centre_tall"); 16 | 17 | private ModMeta selected = QuintessentialLoader.QuintessentialModMeta; 18 | private Scrollbar modsListScrollbar = new(); 19 | 20 | private struct DrawProgress { 21 | public bool pressed; 22 | public float curY; 23 | } 24 | 25 | public bool method_1037() { 26 | return false; 27 | } 28 | 29 | public void method_47(bool param_4687) { 30 | 31 | } 32 | 33 | public void method_48() { 34 | 35 | } 36 | 37 | // update & render 38 | public void method_50(float time) { 39 | Vector2 size = new(1220, 1000); 40 | Vector2 pos = (Input.ScreenSize() / 2 - size / 2).Rounded(); 41 | Vector2 bgPos = pos + new Vector2(78, 88); 42 | Vector2 bgSize = size - new Vector2(78 * 2, 77 * 2); 43 | 44 | UI.DrawLargeUiBackground(bgPos, bgSize); 45 | UI.DrawUiFrame(pos, size); 46 | UI.DrawTexture(verticalBarCentreTall, pos + new Vector2(modButtonWidth + 130, 76f)); 47 | 48 | if(UI.DrawAndCheckCloseButton(pos, size, new Vector2(104, 98))) 49 | UI.HandleCloseButton(); 50 | 51 | // draw mod buttons 52 | using(var _ = modsListScrollbar.method_709(bgPos + new Vector2(0, 5), new(modButtonWidth + 60, (int)bgSize.Y - 10), 0, -30)){ 53 | // clear scroll zone 54 | class_226.method_600(Color.Transparent); 55 | 56 | int y = -(int)modsListScrollbar.field_2078; 57 | UI.DrawHeader("Mods", new Vector2(20, size.Y - 200 - y), modButtonWidth, true, true); 58 | 59 | if(UI.DrawAndCheckSolutionButton("Quintessential", $"{QuintessentialLoader.VersionString} ({QuintessentialLoader.VersionNumber})", new Vector2(20, size.Y - 285 - y), modButtonWidth, selected == QuintessentialLoader.QuintessentialModMeta)) 60 | selected = QuintessentialLoader.QuintessentialModMeta; 61 | class_135.method_275(class_238.field_1989.field_102.field_822, Color.White, Bounds2.WithSize(new Vector2(20, size.Y - 305 - y), new Vector2(modButtonWidth, 3f))); 62 | y += 100; 63 | foreach(var mod in QuintessentialLoader.Mods) 64 | if(mod != QuintessentialLoader.QuintessentialModMeta){ 65 | if(UI.DrawAndCheckSolutionButton(mod.Title ?? mod.Name, mod.Version.ToString(), new Vector2(20, size.Y - 290 - y), modButtonWidth, selected == mod)) 66 | selected = mod; 67 | y += 70; 68 | } 69 | 70 | // expand the scroll area to cover the entire displayed area 71 | modsListScrollbar.method_707(y + 132); 72 | } 73 | 74 | // draw mod options panel 75 | DrawModOptions(pos + new Vector2(modButtonWidth + 160, -10), size - new Vector2(160, 10), selected); 76 | } 77 | 78 | private void DrawModOptions(Vector2 pos, Vector2 size, ModMeta mod) { 79 | float descHeight = DrawModLabel(mod, pos, size); 80 | foreach(var cmod in QuintessentialLoader.CodeMods) 81 | if(cmod.Meta == mod) 82 | if(DrawModSettings(cmod, pos - new Vector2(0, descHeight), size)) 83 | SaveSettings(cmod); 84 | } 85 | 86 | private float DrawModLabel(ModMeta mod, Vector2 pos, Vector2 bgSize){ 87 | bool hasIcon = !string.IsNullOrWhiteSpace(mod.Icon); 88 | Vector2 titlePos = hasIcon ? pos + new Vector2(140, -30) : pos; 89 | if(mod.Icon != null) 90 | UI.DrawTexture(mod.IconCache ??= class_235.method_615(mod.Icon), pos + new Vector2(20, bgSize.Y - 99f - 100)); 91 | UI.DrawText(mod.Title ?? mod.Name, titlePos + new Vector2(20, bgSize.Y - 99f), UI.Title, UI.TextColor, TextAlignment.Left); 92 | string ver = mod.Version.ToString(); 93 | if(mod.Title != null) 94 | ver = mod.Name + " - " + ver; 95 | UI.DrawText(ver, titlePos + new Vector2(20, bgSize.Y - 130f), UI.Text, Color.LightGray, TextAlignment.Left); 96 | if(mod.Desc != null) { 97 | var desc = UI.DrawText(mod.Desc, pos + new Vector2(20, bgSize.Y - 170f - (hasIcon ? 70 : 0)), UI.Text, UI.TextColor, TextAlignment.Left, maxWidth: 460); 98 | return desc.Height + 80; 99 | } 100 | return 20; 101 | } 102 | 103 | private bool DrawModSettings(QuintessentialMod mod, Vector2 pos, Vector2 bgSize) { 104 | var settings = mod.Settings; 105 | if(settings == null) 106 | return false; 107 | return DrawSettingsObject(mod, settings, pos, bgSize, 170).pressed; 108 | } 109 | 110 | private DrawProgress DrawSettingsObject(QuintessentialMod mod, object settings, Vector2 pos, Vector2 bgSize, float startY) { 111 | float y = startY; 112 | bool settingsChanged = false; 113 | if(settings == null) 114 | return new DrawProgress { pressed = false, curY = 0 }; 115 | foreach(var field in settings.GetType().GetFields()) { 116 | if(field.IsStatic) 117 | continue; 118 | 119 | string label = field.GetCustomAttribute()?.Label ?? field.Name; 120 | 121 | if(field.FieldType == typeof(bool)) { 122 | if(UI.DrawCheckbox(pos + new Vector2(20, bgSize.Y - y), label, (bool)field.GetValue(settings))) { 123 | field.SetValue(settings, !(bool)field.GetValue(settings)); 124 | settingsChanged = true; 125 | } 126 | } else if(field.FieldType == typeof(SettingsButton)) { 127 | if(UI.DrawAndCheckBoxButton(label, pos + new Vector2(20, bgSize.Y - y - 15))) 128 | ((SettingsButton)field.GetValue(settings))(); 129 | y += 20; 130 | } else if(field.FieldType == typeof(Keybinding)) { 131 | Keybinding key = (Keybinding)field.GetValue(settings); 132 | Bounds2 labelBounds = UI.DrawText(label + ": " + key.ControlKeysText(), pos + new Vector2(20, bgSize.Y - y - 15), UI.SubTitle, UI.TextColor, TextAlignment.Left); 133 | var text = !string.IsNullOrWhiteSpace(key.Key) ? key.Key : "None"; 134 | if(UI.DrawAndCheckSimpleButton(text, labelBounds.BottomRight + new Vector2(10, 0), new Vector2(50, (int)labelBounds.Height))) 135 | UI.OpenScreen(new ChangeKeybindScreen(key, label, mod)); 136 | y += 20; 137 | } else if(typeof(SettingsGroup).IsAssignableFrom(field.FieldType)) { 138 | SettingsGroup group = (SettingsGroup)field.GetValue(settings); 139 | var textPos = pos + new Vector2(20, bgSize.Y - y + 5); 140 | if(group.Enabled) { 141 | UI.DrawText("*" + label + "*", textPos, UI.SubTitle, UI.TextColor, TextAlignment.Left); 142 | y += 25; 143 | var progress = DrawSettingsObject(mod, field.GetValue(settings), pos + new Vector2(15, 0), bgSize, y); 144 | settingsChanged |= progress.pressed; 145 | y = progress.curY; 146 | y += 10; 147 | } 148 | } 149 | y += 40; 150 | } 151 | return new DrawProgress { pressed = settingsChanged, curY = y }; 152 | } 153 | 154 | [Obsolete("Use UI.DrawCheckbox instead")] 155 | public static bool DrawCheckbox(Vector2 pos, string label, bool enabled) { 156 | Bounds2 boxBounds = Bounds2.WithSize(pos, new Vector2(36f, 37f)); 157 | Bounds2 labelBounds = UI.DrawText(label, pos + new Vector2(45f, 13f), UI.SubTitle, UI.TextColor, TextAlignment.Left); 158 | if(enabled) 159 | UI.DrawTexture(class_238.field_1989.field_101.field_773, boxBounds.Min); 160 | if(boxBounds.Contains(Input.MousePos()) || labelBounds.Contains(Input.MousePos())) { 161 | UI.DrawTexture(class_238.field_1989.field_101.field_774, boxBounds.Min); 162 | if(!Input.IsLeftClickPressed()) 163 | return false; 164 | class_238.field_1991.field_1821.method_28(1f); 165 | return true; 166 | } 167 | UI.DrawTexture(class_238.field_1989.field_101.field_772, boxBounds.Min); 168 | return false; 169 | } 170 | 171 | public static void SaveSettings(QuintessentialMod mod){ 172 | mod.ApplySettings(); 173 | ModMeta meta = mod.Meta; 174 | object settings = mod.Settings; 175 | string name = meta.Name; 176 | string path = Path.Combine(QuintessentialLoader.PathModSaves, name + ".yaml"); 177 | if(!Directory.Exists(QuintessentialLoader.PathModSaves)) 178 | Directory.CreateDirectory(QuintessentialLoader.PathModSaves); 179 | 180 | using StreamWriter writer = new(path); 181 | YamlHelper.Serializer.Serialize(writer, settings, QuintessentialLoader.CodeMods.First(c => c.Meta == meta).SettingsType); 182 | } 183 | } -------------------------------------------------------------------------------- /Quintessential/NoticeScreen.cs: -------------------------------------------------------------------------------- 1 | using SDL2; 2 | 3 | namespace Quintessential; 4 | 5 | /// 6 | /// Generic info popup screen. 7 | /// 8 | public class NoticeScreen : IScreen { 9 | 10 | private readonly string Title, Tooltip; 11 | 12 | public NoticeScreen(string title, string tooltip) { 13 | Title = title; 14 | Tooltip = tooltip; 15 | } 16 | 17 | public bool method_1037() { 18 | return false; 19 | } 20 | 21 | public void method_47(bool param_4687) { 22 | // Add gray BG 23 | GameLogic.field_2434.field_2464 = true; 24 | } 25 | 26 | public void method_48() { 27 | 28 | } 29 | 30 | public void method_50(float param_4686) { 31 | UI.DrawText(Title, (Input.ScreenSize() / 2) + new Vector2(0, 120), UI.Title, Color.White, TextAlignment.Centred); 32 | UI.DrawText(Tooltip, Input.ScreenSize() / 2, UI.SubTitle, class_181.field_1718, TextAlignment.Centred); 33 | if(Input.IsSdlKeyPressed(SDL.enum_160.SDLK_ESCAPE) || UI.DrawAndCheckBoxButton("OK", (Input.ScreenSize() / 2) + new Vector2(-130, -160))) 34 | UI.CloseScreen(); 35 | } 36 | } -------------------------------------------------------------------------------- /Quintessential/Pair.cs: -------------------------------------------------------------------------------- 1 | namespace Quintessential; 2 | 3 | public class Pair { 4 | 5 | public Pair(){} 6 | 7 | public Pair(A left, B right) { 8 | Left = left; 9 | Right = right; 10 | } 11 | 12 | public A Left { 13 | get; set; 14 | } 15 | 16 | public B Right { 17 | get; set; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Quintessential/PuzzleOption.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace Quintessential; 4 | 5 | using PartType = class_139; 6 | using PartTypes = class_191; 7 | using AtomTypes = class_175; 8 | 9 | public class PuzzleOption{ 10 | 11 | // Puzzle options are always saved as strings 12 | // booleans -> ID present or not 13 | // multi-choice -> {ID}::{choice} 14 | // atom -> {ID}::{atom ID} 15 | // part -> {ID}::{part ID} 16 | 17 | public string ID, Name, SectionName; 18 | public PuzzleOptionType Type; 19 | 20 | private List choices; 21 | 22 | public static PuzzleOption BoolOption(string id, string name, string sectionName){ 23 | return new PuzzleOption{ 24 | ID = id, 25 | Name = name, 26 | SectionName = sectionName, 27 | Type = PuzzleOptionType.Boolean 28 | }; 29 | } 30 | 31 | public static PuzzleOption MultiChoiceOption(string id, string name, string sectionName, params string[] choices){ 32 | return new PuzzleOption{ 33 | ID = id, 34 | Name = name, 35 | SectionName = sectionName, 36 | Type = PuzzleOptionType.MultiChoice, 37 | choices = new List(choices) 38 | }; 39 | } 40 | 41 | public static PuzzleOption PartTypeOption(string id, string name, string sectionName){ 42 | return new PuzzleOption{ 43 | ID = id, 44 | Name = name, 45 | SectionName = sectionName, 46 | Type = PuzzleOptionType.Part 47 | }; 48 | } 49 | 50 | public static PuzzleOption AtomTypeOption(string id, string name, string sectionName){ 51 | return new PuzzleOption{ 52 | ID = id, 53 | Name = name, 54 | SectionName = sectionName, 55 | Type = PuzzleOptionType.Atom 56 | }; 57 | } 58 | 59 | // Getters that each correspond to a PuzzleOptionType 60 | 61 | public bool EnabledIn(Puzzle from){ 62 | return Convert(from).CustomPermissions?.Contains(ID) ?? false; 63 | } 64 | 65 | public string ChoiceIn(Puzzle from){ 66 | foreach(string permission in Convert(from).CustomPermissions) 67 | if(permission.StartsWith(ID + "::")) 68 | return permission.Substring(ID.Length + 2); 69 | return null; 70 | } 71 | 72 | public PartType PartIn(Puzzle from){ 73 | string choice = ChoiceIn(from); 74 | foreach(PartType type in PartTypes.field_1785) 75 | if(type.field_1528.Equals(choice)) 76 | return type; 77 | 78 | return null; 79 | } 80 | 81 | public AtomType AtomIn(Puzzle from){ 82 | string choice = ChoiceIn(from); 83 | foreach(AtomType type in AtomTypes.field_1691) 84 | if(Convert(type).QuintAtomType.Equals(choice)) 85 | return type; 86 | 87 | return null; 88 | } 89 | 90 | public void SetEnabledIn(Puzzle from, bool enabled){ 91 | if(enabled) 92 | Convert(from).CustomPermissions.Add(ID); 93 | else 94 | Convert(from).CustomPermissions.Remove(ID); 95 | } 96 | 97 | public void SetChoiceIn(Puzzle from, string choice){ 98 | var perms = Convert(from).CustomPermissions; 99 | perms.RemoveWhere(s => s.StartsWith(ID + "::")); 100 | perms.Add(ID + "::" + choice); 101 | } 102 | 103 | public void SetAtomIn(Puzzle from, AtomType atom){ 104 | SetChoiceIn(from, Convert(atom).QuintAtomType); 105 | } 106 | 107 | public void SetPartIn(Puzzle from, PartType part){ 108 | SetChoiceIn(from, part.field_1528); 109 | } 110 | 111 | private static patch_Puzzle Convert(Puzzle from){ 112 | return (patch_Puzzle)(object)from; 113 | } 114 | 115 | private static patch_AtomType Convert(AtomType from){ 116 | return (patch_AtomType)(object)from; 117 | } 118 | } 119 | 120 | public enum PuzzleOptionType{ 121 | Boolean, 122 | MultiChoice, 123 | Part, 124 | Atom 125 | } -------------------------------------------------------------------------------- /Quintessential/QApi.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace Quintessential; 5 | 6 | using PartType = class_139; 7 | using RenderHelper = class_195; 8 | using PartTypes = class_191; 9 | using AtomTypes = class_175; 10 | using ChamberType = class_183; 11 | 12 | public static class QApi { 13 | 14 | public static readonly List, PartRenderer>> PartRenderers = new(); 15 | public static readonly List> PanelParts = new(); 16 | public static readonly List ModAtomTypes = new(); 17 | public static readonly List> ToRunAfterCycle = new(); 18 | public static readonly List PuzzleOptions = new(); 19 | 20 | public static void Init(){ 21 | 22 | } 23 | 24 | /// 25 | /// Adds a part type to the end of a part panel section, making it accessible for placement. 26 | /// This does not allow for adding inputs or outputs. 27 | /// 28 | /// The part type to be added. 29 | /// Whether to add to the mechanisms section or the glyphs section. 30 | public static void AddPartTypeToPanel(PartType type, bool mechanism){ 31 | AddPartTypeToPanel(type, mechanism ? PartTypes.field_1771 : PartTypes.field_1782); 32 | } 33 | 34 | /// 35 | /// Adds a part type to the part panel after another given type, making it accessible for placement. 36 | /// 37 | /// 38 | /// 39 | public static void AddPartTypeToPanel(PartType type, PartType after) { 40 | if(type == null || after == null) 41 | Logger.Log("Tried to add a null part to the parts panel, or tried to add a part after a null part, not adding."); 42 | else if(type.Equals(after)) 43 | Logger.Log("Tried to add a part to the part panel after itself (circular reference), not adding."); 44 | else 45 | PanelParts.Add(new Pair(type, after)); 46 | } 47 | 48 | /// 49 | /// Adds a PartRenderer, which renders any parts that satisfy the given predicate. Usually, this predicate simply checks the part type of the part. 50 | /// 51 | /// The PartRenderer to be added and displayed. 52 | /// A predicate that determines which parts the renderer should try to display. 53 | public static void AddPartTypesRenderer(PartRenderer renderer, Predicate typeChecker) { 54 | PartRenderers.Add(new Pair, PartRenderer>(typeChecker, renderer)); 55 | } 56 | 57 | /// 58 | /// Adds a part type to the list of all part types. 59 | /// 60 | /// The part type to be added. 61 | public static void AddPartType(PartType type) { 62 | Array.Resize(ref PartTypes.field_1785, PartTypes.field_1785.Length + 1); 63 | PartTypes.field_1785[PartTypes.field_1785.Length - 1] = type; 64 | } 65 | 66 | /// 67 | /// Adds a part type, adding it to the list of part types and adding a renderer for that part type. 68 | /// 69 | /// The part type to be added. 70 | /// A PartRenderer to render instances of that part type. 71 | public static void AddPartType(PartType type, PartRenderer renderer) { 72 | AddPartType(type); 73 | AddPartTypesRenderer(renderer, part => part.method_1159() == type); 74 | } 75 | 76 | /// 77 | /// Adds an atom type, adding it to the list of atom types and the molecule editor. 78 | /// 79 | /// The atom type to add. 80 | public static void AddAtomType(AtomType type) { 81 | ModAtomTypes.Add(type); 82 | 83 | Array.Resize(ref AtomTypes.field_1691, AtomTypes.field_1691.Length + 1); 84 | var len = AtomTypes.field_1691.Length; 85 | AtomTypes.field_1691[len - 1] = type; 86 | } 87 | 88 | /// 89 | /// Runs the given action at the end of every half-cycle. 90 | /// 91 | /// An action to be run every half-cycle, given the sim and whether it is the start or end. 92 | public static void RunAfterCycle(Action runnable) { 93 | ToRunAfterCycle.Add(runnable); 94 | } 95 | 96 | /// 97 | /// Adds a permission to the puzzle editor. These can be used by setting the `CustomPermissionCheck` field of your part type and 98 | /// checking for your permission ID. 99 | /// 100 | /// Permissions with the same section name will be grouped together. If no name is chosen, this defaults to "Other Parts & Mechanisms". 101 | /// 102 | /// The ID of the permission that is used during checks and saved to puzzle files. 103 | /// The name of the permission that is displayed in the UI, e.g. "Glyphs of Quintessence". 104 | /// The name of the section that the permission will appear under. 105 | public static void AddPuzzlePermission(string id, string displayName, string sectionName = "Other Parts and Mechanisms"){ 106 | PuzzleOptions.Add(PuzzleOption.BoolOption(id, displayName, sectionName)); 107 | } 108 | 109 | public static void AddPuzzleOption(PuzzleOption option){ 110 | PuzzleOptions.Add(option); 111 | } 112 | 113 | /// 114 | /// Adds a chamber type, used by name in production puzzle files. 115 | /// 116 | /// The chamber type to add. 117 | /// 118 | /// Whether to automatically assign a centred offset for the chamber's overlay texture. 119 | /// Otherwise, the chamber's field_1730 must have its offset assigned by UI.AssignOffset, or the chamber will be visually incorrect. 120 | /// 121 | public static void AddChamberType(ChamberType chamberType, bool autoCentre = true){ 122 | int length = Puzzles.field_2932.Length; 123 | Array.Resize(ref Puzzles.field_2932, length + 1); 124 | Puzzles.field_2932[length] = chamberType; 125 | 126 | if(autoCentre) 127 | UI.AssignOffset(chamberType.field_1730, -0.5f * chamberType.field_1730.field_2056.ToVector2()); 128 | } 129 | 130 | /// 131 | /// Returns the settings of the given type for the first registered mod, or null if no registered mod has settings of that type. 132 | /// 133 | /// The type of settings to get. 134 | /// 135 | public static T GetSettingsByType() { 136 | foreach(var mod in QuintessentialLoader.CodeMods) { 137 | if(mod.Settings is T settings) { 138 | return settings; 139 | } 140 | } 141 | return default; 142 | } 143 | } 144 | 145 | /// 146 | /// A function that renders a part. 147 | /// 148 | /// The part to be displayed. 149 | /// The position of the part. 150 | /// The solution editor that the part is being displayed in. 151 | /// An object containing functions for rendering images, at different positions/rotations and lightmaps. 152 | public delegate void PartRenderer(Part part, Vector2 position, SolutionEditorBase editor, RenderHelper helper); 153 | 154 | /// 155 | /// A static class containing extensions that make PartRenderers easier to use. 156 | /// 157 | public static class PartRendererExtensions { 158 | 159 | public static PartRenderer Then(this PartRenderer first, PartRenderer second) { 160 | return (a, b, c, d) => { 161 | first(a, b, c, d); 162 | second(a, b, c, d); 163 | }; 164 | } 165 | 166 | public static PartRenderer WithOffsets(this PartRenderer renderer, params Vector2[] offsets) { 167 | return (part, pos, editor, helper) => { 168 | foreach(var offset in offsets) 169 | renderer(part, pos + offset, editor, helper); 170 | }; 171 | } 172 | 173 | /*public static PartRenderer WithOffsets(this PartRenderer renderer, params HexIndex[] offsets) { 174 | const double angle = (1/3) * Math.PI; 175 | return renderer.WithOffsets(offsets.Select(off => new Vector2((float)(off.Q + Math.Cos(angle) * off.R), -(float)(Math.Sin(angle) * off.R))).ToArray()); 176 | }*/ 177 | 178 | public static PartRenderer OfTexture(class_256 texture, params HexIndex[] hexes) { 179 | return (part, pos, editor, helper) => { 180 | foreach(var hex in hexes) 181 | helper.method_528(texture, hex, Vector2.Zero); 182 | }; 183 | } 184 | 185 | public static PartRenderer OfTexture(string texture, params HexIndex[] hexes) { 186 | return OfTexture(class_235.method_615(texture), hexes); 187 | } 188 | } 189 | -------------------------------------------------------------------------------- /Quintessential/QuintessentialLoader.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using System.Globalization; 5 | using System.IO; 6 | using System.Linq; 7 | using System.Reflection; 8 | using System.Resources; 9 | 10 | using Ionic.Zip; 11 | 12 | using MonoMod.Utils; 13 | 14 | using Quintessential.Serialization; 15 | 16 | namespace Quintessential; 17 | 18 | public class QuintessentialLoader { 19 | 20 | public static readonly string VersionString = "0.5.1"; 21 | public static readonly int VersionNumber = 9; 22 | 23 | public static string PathLightning; 24 | public static string PathMods, PathUnpackedMods; 25 | public static string PathBlacklist; 26 | public static string PathModSaves; 27 | public static string PathScreenshots; 28 | 29 | public static List CodeMods = new(); 30 | public static List Mods = new(); 31 | public static List ModContentDirectories = new(); 32 | public static List ModPuzzleDirectories = new(); 33 | 34 | public static List AllCampaigns = new(); 35 | public static Campaign VanillaCampaign; 36 | public static List> AllJournals = new(); 37 | public static List VanillaJournal; 38 | 39 | public static ModMeta QuintessentialModMeta; 40 | public static QuintessentialMod QuintessentialAsMod; 41 | 42 | public static List ModCampaignModels = new(); 43 | public static List ModJournalModels = new(); 44 | 45 | private static List blacklisted = new(); 46 | private static List loaded = new(); 47 | private static List waiting = new(); 48 | 49 | private static readonly string zipExtractSuffix = "__quintessential_from_zip"; 50 | private static readonly string quintAssetFolder = "__quintessential_assets"; 51 | 52 | public static void PreInit() { 53 | try { 54 | PathLightning = Path.GetDirectoryName(typeof(GameLogic).Assembly.Location); 55 | PathMods = Path.Combine(PathLightning, "Mods"); 56 | PathUnpackedMods = Path.Combine(PathLightning, "UnpackedMods"); 57 | PathScreenshots = Path.Combine(PathLightning, "Screenshots"); 58 | 59 | Logger.Init(); 60 | Logger.Log($"Quintessential v{VersionString} ({VersionNumber})"); 61 | Logger.Log("Starting pre-init loading."); 62 | 63 | QApi.Init(); 64 | 65 | if(!Directory.Exists(PathMods)) 66 | Directory.CreateDirectory(PathMods); 67 | 68 | if(Directory.Exists(PathUnpackedMods)) 69 | Directory.Delete(PathUnpackedMods, true); 70 | Directory.CreateDirectory(PathUnpackedMods); 71 | 72 | PathBlacklist = Path.Combine(PathMods, "blacklist.txt"); 73 | if(File.Exists(PathBlacklist)) 74 | blacklisted = File.ReadAllLines(PathBlacklist).Select(l => (l.StartsWith("#") ? "" : l).Trim()).ToList(); 75 | else { 76 | File.WriteAllText(PathBlacklist, @"# This is the blacklist. Lines starting with # are ignored. 77 | ExampleFolderThatIWantToBlacklist 78 | SomeZipIDontLike.zip"); 79 | } 80 | 81 | // Find mods in Mods/ 82 | // Delete legacy quintessential extracted zips and assets 83 | CleanupLegacyExtractedData(); 84 | 85 | // Add Quintessential mod & mod meta 86 | QuintessentialModMeta = new ModMeta { 87 | Name = "Quintessential", 88 | Version = new Version(VersionString) 89 | }; 90 | Mods.Add(QuintessentialModMeta); 91 | QuintessentialAsMod = new Internal.QuintessentialAsMod { 92 | Meta = QuintessentialModMeta, 93 | Settings = new QuintessentialSettings() 94 | }; 95 | CodeMods.Add(QuintessentialAsMod); 96 | 97 | // Extract bundled assets 98 | Logger.Log("Extracting Quintessential resources..."); 99 | string outDir = Path.Combine(PathUnpackedMods, quintAssetFolder, "Content", "Quintessential"); 100 | Directory.CreateDirectory(outDir); 101 | ResourceManager manager = new("Properties.Resources", typeof(Renderer).Assembly); 102 | var set = manager.GetResourceSet(CultureInfo.InvariantCulture, true, true); 103 | foreach(object item in set){ 104 | if(item is DictionaryEntry de){ 105 | string name = (string)de.Key; 106 | using var toStream = File.OpenWrite(Path.Combine(outDir, name)); 107 | byte[] content = (byte[])de.Value; 108 | toStream.Write(content, 0, content.Length); 109 | } 110 | } 111 | ModContentDirectories.Add(Path.Combine(PathUnpackedMods, quintAssetFolder)); 112 | 113 | Logger.Log("Finding mods to load..."); 114 | // Unzip zips 115 | string[] files = Directory.GetFiles(PathMods); 116 | foreach(var file in files) { 117 | string filename = Path.GetFileName(file); 118 | if(blacklisted.Contains(filename)) 119 | continue; 120 | if(filename.EndsWith(".zip")) 121 | FindZipMod(file); 122 | } 123 | 124 | // Find folder mods 125 | string[] folders = Directory.GetDirectories(PathMods); 126 | foreach(var folder in folders) { 127 | string filename = Path.GetFileName(folder); 128 | if(blacklisted.Contains(filename)) 129 | continue; 130 | FindFolderMod(folder); 131 | } 132 | 133 | // Load mods 134 | Logger.Log("Loading mods..."); 135 | bool Contains(ModMeta.Dependency dep, List list) 136 | => list.Any(m => m.Name.Equals(dep.Name) && m.Version >= dep.Version); 137 | List rem = new(); 138 | foreach(var mod in Mods) { 139 | // check dependencies 140 | bool willLoad = true, wait = false; 141 | foreach(var dep in mod.Dependencies) { 142 | // if a dependency is missing, don't load the mod 143 | if(!Contains(dep, Mods)) 144 | willLoad = false; 145 | // if a dependency is present but not loaded, add to waiting list and load later 146 | else if(!Contains(dep, loaded)) { 147 | willLoad = false; 148 | wait = true; 149 | } 150 | } 151 | // check optional deps 152 | foreach(var opDep in mod.OptionalDependencies) { 153 | // if an optional dep is present but outdated, don't load 154 | if(Mods.Any(m => m.Name.Equals(opDep.Name) && m.Version < opDep.Version)) 155 | willLoad = false; 156 | // if an optional dep is present but not yet loaded, add to waiting list and load later 157 | if(Contains(opDep, Mods) && !Contains(opDep, loaded)) { 158 | willLoad = false; 159 | wait = true; 160 | } 161 | } 162 | if(willLoad) 163 | LoadModFromMeta(mod); 164 | else if(wait) 165 | waiting.Add(mod); 166 | else { 167 | Logger.Log("Not loading " + mod.Name + ": missing dependency, or outdated optional dependency."); 168 | rem.Add(mod); 169 | } 170 | } 171 | Mods.RemoveAll(m => rem.Contains(m)); 172 | 173 | while(waiting.Any()){ 174 | var toRemove = new List(); 175 | var removeFromMods = new List(); 176 | foreach(var mod in waiting) { 177 | // if deps are now loaded, load and remove from waiting list 178 | bool willLoad = true; 179 | foreach(var dep in mod.Dependencies) 180 | if(!Contains(dep, loaded)) 181 | willLoad = false; 182 | foreach(var dep in mod.OptionalDependencies) 183 | if(!Contains(dep, loaded)) 184 | willLoad = false; 185 | if(willLoad) { 186 | LoadModFromMeta(mod); 187 | toRemove.Add(mod); 188 | } else { 189 | // check if a dependency has missing deps itself 190 | bool wontLoad = false; 191 | foreach(var dep in mod.Dependencies) 192 | if(!Contains(dep, Mods)) 193 | wontLoad = true; 194 | if(wontLoad) { 195 | Logger.Log("Not loading " + mod.Name + ": missing dependency."); 196 | removeFromMods.Add(mod); 197 | } else { 198 | // check that outdated optional dependencies still exist 199 | bool skipLoad = true; 200 | foreach(var opDep in mod.OptionalDependencies) 201 | if(Mods.Any(m => m.Name.Equals(opDep.Name) && m.Version < opDep.Version)) 202 | skipLoad = false; 203 | if(skipLoad) { 204 | LoadModFromMeta(mod); 205 | toRemove.Add(mod); 206 | } 207 | } 208 | } 209 | } 210 | // if we don't load any mods, we have a circular dep, don't load any more 211 | waiting.RemoveAll(m => toRemove.Contains(m)); 212 | Mods.RemoveAll(m => removeFromMods.Contains(m)); 213 | if(!toRemove.Any()) { 214 | foreach(var item in waiting) 215 | Logger.Log("Not loading " + item.Name + ": circular dependency!"); 216 | break; 217 | } 218 | } 219 | 220 | // Add mod content 221 | // Load mods 222 | foreach(var mod in CodeMods) 223 | mod.Load(); 224 | Logger.Log($"Finished pre-init loading - {Mods.Count} mods loaded; {CodeMods.Count} assemblies, {ModContentDirectories.Count} content directories, and {ModCampaignModels.Count} custom campaigns found."); 225 | } catch(Exception e) { 226 | if(Logger.Setup) { 227 | Logger.Log("Failed to pre-initialize!"); 228 | Logger.Log(e); 229 | } 230 | throw; 231 | } 232 | } 233 | 234 | private static void LoadModFromMeta(ModMeta mod) { 235 | if(mod == QuintessentialModMeta) 236 | return; 237 | if(!string.IsNullOrWhiteSpace(mod.DLL)) { 238 | string dllPath = mod.DLL; 239 | LoadModAssembly(mod, GetRemappedAssembly(dllPath, mod)); 240 | } 241 | // Get mod content 242 | // - Consider modded folders when fetching any content 243 | // - Custom language files: vanilla stores in a big CSV, but for custom dialogue (and languages) we'll want seperate files (e.g. English.txt, French.txt) 244 | // - Custom solitaires too? 245 | var content = Path.Combine(mod.PathDirectory, "Content"); 246 | if(Directory.Exists(content)) 247 | ModContentDirectories.Add(mod.PathDirectory); 248 | 249 | LoadModCampaigns(mod); 250 | 251 | loaded.Add(mod); 252 | Logger.Log($"Will load mod \"{mod.Name}\"."); 253 | } 254 | 255 | private static void LoadModCampaigns(ModMeta mod){ 256 | var puzzles = Path.Combine(mod.PathDirectory, "Puzzles"); 257 | if(Directory.Exists(puzzles)){ 258 | if(!ModPuzzleDirectories.Contains(puzzles)) 259 | ModPuzzleDirectories.Add(puzzles); 260 | // Look for name.campaign.yaml and name.journal.yaml files in the folder 261 | foreach(var item in Directory.GetFiles(puzzles)){ 262 | string filename = Path.GetFileName(item); 263 | if(filename.EndsWith(".campaign.yaml")){ 264 | using StreamReader reader = new(item); 265 | 266 | CampaignModel c = YamlHelper.Deserializer.Deserialize(reader); 267 | Logger.Log($"Campaign \"{c.Title}\" ({c.Name}) has {c.Chapters.Count} chapters."); 268 | c.Path = Path.GetDirectoryName(item); 269 | ModCampaignModels.Add(c); 270 | } 271 | 272 | if(filename.EndsWith(".journal.yaml")){ 273 | using StreamReader reader = new(item); 274 | 275 | JournalModel c = YamlHelper.Deserializer.Deserialize(reader); 276 | Logger.Log($"Journal \"{c.Title}\" has {c.Chapters.Count} chapters."); 277 | foreach(var chapter in new List(c.Chapters)){ 278 | if(chapter.Puzzles.Count != 5){ 279 | Logger.Log($"Journal chapter \"{chapter.Title}\" in \"{c.Title}\" has {chapter.Puzzles.Count} puzzles instead of 5; skipping chapter."); 280 | c.Chapters.Remove(chapter); 281 | } 282 | } 283 | 284 | if(c.Chapters.Count > 0){ 285 | c.Path = Path.GetDirectoryName(item); 286 | ModJournalModels.Add(c); 287 | }else 288 | Logger.Log($"Journal \"{c.Title}\" has no chapters, skipping."); 289 | } 290 | } 291 | } 292 | } 293 | 294 | public static void PostLoad() { 295 | Logger.Log("Starting post-init loading."); 296 | // Read mod save data 297 | PathModSaves = Path.Combine(class_161.method_402(), "ModSettings"); 298 | Logger.Log($"Mod settings directory: \"{PathModSaves}\""); 299 | if(!Directory.Exists(PathModSaves)) 300 | Directory.CreateDirectory(PathModSaves); 301 | foreach(var mod in CodeMods) { 302 | var savePath = Path.Combine(PathModSaves, mod.Meta.Name + ".yaml"); 303 | if(File.Exists(savePath)) { 304 | using StreamReader reader = new(savePath); 305 | 306 | var settings = YamlHelper.Deserializer.Deserialize(reader, mod.SettingsType); 307 | if(settings != null) { 308 | mod.Settings = settings; 309 | mod.ApplySettings(); 310 | } else 311 | Logger.Log("Loaded null settings for mod " + mod.Meta.Name); 312 | } 313 | } 314 | foreach(var mod in CodeMods) 315 | mod.PostLoad(); 316 | Logger.Log("Finished post-init loading."); 317 | } 318 | 319 | public static void LoadPuzzleContent() { 320 | Logger.Log("Starting puzzle content loading."); 321 | foreach(var mod in CodeMods) 322 | mod.LoadPuzzleContent(); 323 | 324 | Logger.Log("Loading campaigns and journals."); 325 | LoadCampaigns(); 326 | LoadJournals(); 327 | 328 | Logger.Log("Finished puzzle content loading."); 329 | } 330 | 331 | public static void Unload() { 332 | Logger.Log("Starting mod unloading."); 333 | foreach(var mod in CodeMods) 334 | mod.Unload(); 335 | Logger.Log("Finished unloading."); 336 | } 337 | 338 | protected static void FindZipMod(string zip) { 339 | Logger.Log("Unzipping zip mod: " + zip); 340 | // Check that the zip exists 341 | if(!File.Exists(zip)) // Relative path? 342 | zip = Path.Combine(PathMods, zip); 343 | if(!File.Exists(zip)) // It just doesn't exist. 344 | return; 345 | 346 | var dest = Path.Combine(PathUnpackedMods, Path.GetFileNameWithoutExtension(zip)); 347 | using(ZipFile file = new(zip)) 348 | file.ExtractAll(dest); 349 | FindFolderMod(dest, zip); 350 | } 351 | 352 | protected static void FindFolderMod(string dir, string zipName = null) { 353 | // don't load zip mods again, ignore quintessential assets 354 | if(dir.EndsWith(quintAssetFolder) || (string.IsNullOrEmpty(zipName) && dir.EndsWith(zipExtractSuffix))) 355 | return; 356 | 357 | Logger.Log($"Finding mod in folder: \"{dir}\""); 358 | // Check that the folder exists 359 | if(!Directory.Exists(dir)) // Relative path? 360 | dir = Path.Combine(PathMods, dir); 361 | if(!Directory.Exists(dir)) // It just doesn't exist. 362 | return; 363 | 364 | // Look for a mod meta 365 | ModMeta meta; 366 | string metaPath = Path.Combine(dir, "quintessential.yaml"); 367 | if(!File.Exists(metaPath)) 368 | metaPath = Path.Combine(dir, "quintessential.yml"); 369 | if(File.Exists(metaPath)) { 370 | using StreamReader reader = new(metaPath); 371 | 372 | try { 373 | if(!reader.EndOfStream) { 374 | meta = YamlHelper.Deserializer.Deserialize(reader); 375 | meta.Name = meta.Name.Trim().Replace(" ", "_"); 376 | meta.PathDirectory = dir; 377 | if(!string.IsNullOrEmpty(zipName)) 378 | meta.PathArchive = zipName; 379 | meta.PostParse(); 380 | Mods.Add(meta); 381 | Logger.Log($"Queuing mod \"{meta.Name}\", version {meta.VersionString}."); 382 | } 383 | } catch(Exception e) { 384 | Logger.Log($"Failed parsing quintessential.yaml in {dir}: {e}"); 385 | } 386 | } else { 387 | meta = new ModMeta { 388 | Name = "NoMetaMod__" + Path.GetFileName(dir), 389 | PathDirectory = dir 390 | }; 391 | if(!string.IsNullOrEmpty(zipName)) 392 | meta.PathArchive = zipName; 393 | meta.PostParse(); 394 | Mods.Add(meta); 395 | Logger.Log($"Will load mod without metadata from \"{dir}\"."); 396 | } 397 | } 398 | 399 | protected static void LoadModAssembly(ModMeta meta, Assembly asm) { 400 | Type[] types; 401 | try { 402 | try { 403 | types = asm.GetTypes(); 404 | } catch(ReflectionTypeLoadException e) { 405 | types = e.Types.Where(t => t != null).ToArray(); 406 | } 407 | } catch(Exception e) { 408 | Logger.Log($"Failed reading assembly for {meta.Name}: {e}"); 409 | e.LogDetailed(); 410 | return; 411 | } 412 | 413 | foreach(var type in types){ 414 | if(typeof(QuintessentialMod).IsAssignableFrom(type) && !type.IsAbstract) { 415 | QuintessentialMod mod = (QuintessentialMod)type.GetConstructor(new Type[] { }).Invoke(new object[] { }); 416 | mod.Meta = meta; 417 | Register(mod); 418 | } 419 | } 420 | } 421 | 422 | protected static void CleanupLegacyExtractedData() { 423 | string[] folders = Directory.GetDirectories(PathMods); 424 | foreach(var folder in folders) 425 | if(folder.EndsWith(zipExtractSuffix) || folder.EndsWith(quintAssetFolder)) 426 | Directory.Delete(folder, true); 427 | } 428 | 429 | protected static void Register(QuintessentialMod mod) { 430 | CodeMods.Add(mod); 431 | } 432 | 433 | public static Assembly GetRemappedAssembly(string asmPath, ModMeta meta) { 434 | if(!string.IsNullOrEmpty(meta.Mappings)) { 435 | // load mappings 436 | // load assembly def 437 | // remap 438 | // save in cache 439 | // load that 440 | //OpusMutatum.OpusMutatum.DoRemap(); 441 | } 442 | return Assembly.LoadFrom(asmPath); 443 | } 444 | 445 | public static void LoadCampaigns(){ 446 | AllCampaigns.Clear(); 447 | 448 | VanillaCampaign = Campaigns.field_2330; 449 | ((patch_Campaign)(object)VanillaCampaign).QuintTitle = "Opus Magnum"; 450 | AllCampaigns.Add(VanillaCampaign); 451 | 452 | foreach(var c in ModCampaignModels){ 453 | var campaign = new Campaign{ 454 | field_2309 = new CampaignChapter[c.Chapters.Count] 455 | }; 456 | 457 | ((patch_Campaign)(object)campaign).QuintTitle = c.Title; 458 | 459 | for(int j = 0; j < c.Chapters.Count; j++) { 460 | ChapterModel chapter = c.Chapters[j]; 461 | campaign.field_2309[j] = new CampaignChapter( 462 | class_134.method_253(chapter.Title, string.Empty), 463 | class_134.method_253(chapter.Subtitle, string.Empty), 464 | class_134.method_253(chapter.Place, string.Empty), 465 | chapter.Background != null ? class_235.method_615(chapter.Background) : Campaigns.field_2330.field_2309[j].field_2315, 466 | Campaigns.field_2330.field_2309[j].field_2316, 467 | Campaigns.field_2330.field_2309[j].field_2317, 468 | Campaigns.field_2330.field_2309[j].field_2318, 469 | Campaigns.field_2330.field_2309[j].field_2319, 470 | Campaigns.field_2330.field_2309[j].field_2320, 471 | false 472 | ); 473 | 474 | foreach(var entry in chapter.Entries){ 475 | class_259 requirement = string.IsNullOrEmpty(entry.Requires) ? (class_259)new class_174() : new class_243(entry.Requires); 476 | 477 | var lower = entry.Type.ToLowerInvariant(); 478 | CampaignItem cItem; 479 | switch(lower){ 480 | case "puzzle": { 481 | if(!TryLoadPuzzle(c.Path, entry.Puzzle, c.Title, out var puzzle)) 482 | continue; 483 | 484 | puzzle.field_2766 = entry.ID; 485 | // ensure all inputs/outputs have names 486 | foreach(PuzzleInputOutput io in puzzle.field_2770.Union(puzzle.field_2771)){ 487 | if(!io.field_2813.field_2639.method_1085()){ 488 | io.field_2813.field_2639 = class_134.method_253("Molecule", string.Empty); 489 | } 490 | } 491 | 492 | // TODO: optimize 493 | cItem = AddEntryToCampaign(campaign, j, entry.ID, class_134.method_253(entry.Title, string.Empty), (enum_129)0, struct_18.field_1431, puzzle, class_238.field_1992.field_972, class_238.field_1991.field_1832, requirement, entry.NoStoryPanel); 494 | Array.Resize(ref Puzzles.field_2816, Puzzles.field_2816.Length + 1); 495 | Puzzles.field_2816[Puzzles.field_2816.Length - 1] = puzzle; 496 | break; 497 | } 498 | case "solitaire": { 499 | cItem = new(entry.ID, class_134.method_253("Sigmar's Garden", string.Empty), (enum_129) 3, struct_18.field_1431, requirement, class_238.field_1992.field_970, class_238.field_1991.field_1830); 500 | campaign.field_2309[j].field_2314.Add(cItem); 501 | break; 502 | } 503 | default: 504 | Logger.Log($"Campaign entry in {c.Name} has unknown type {entry.Type}, skipping"); 505 | continue; 506 | } 507 | 508 | patch_CampaignItem conv = (patch_CampaignItem)(object)cItem; 509 | // probably not great to reload the images every time, in the case that a campaign uses the same image on every puzzle? 510 | // but these are small, and we can definitely handle the case where every puzzle has a unique icon 511 | if(!string.IsNullOrWhiteSpace(entry.Icon)) 512 | conv.Icon = class_235.method_615(entry.Icon); 513 | if(!string.IsNullOrWhiteSpace(entry.IconSmall)) 514 | conv.IconSmall = class_235.method_615(entry.IconSmall); 515 | } 516 | } 517 | 518 | for(int index = 0; index < campaign.field_2309.Length; ++index) 519 | campaign.field_2309[index].field_2310 = index; 520 | 521 | AllCampaigns.Add(campaign); 522 | } 523 | } 524 | 525 | public static void LoadJournals(){ 526 | AllJournals.Clear(); 527 | 528 | VanillaJournal = JournalVolumes.field_2572.ToList(); 529 | AllJournals.Add(VanillaJournal); 530 | 531 | foreach(JournalModel journal in ModJournalModels){ 532 | List volumes = journal.Chapters.Select(chapter => 533 | new JournalVolume{ 534 | field_2569 = chapter.Title, 535 | field_2570 = chapter.Description, 536 | field_2571 = chapter.Puzzles.SelectMany(puzzleName => 537 | TryLoadPuzzle(journal.Path, puzzleName, journal.Title, out var puzzle) ? new[]{ puzzle } : new Puzzle[0]).ToArray() 538 | }).ToList(); 539 | foreach(JournalVolume jv in volumes){ 540 | Logger.Log($"Journal {jv.field_2569} has {jv.field_2571.Length} puzzles"); 541 | } 542 | AllJournals.Add(volumes); 543 | } 544 | } 545 | 546 | private static bool TryLoadPuzzle(string basePath, string puzzleName, string campaignTitle, out Puzzle puzzle){ 547 | try{ 548 | string baseName = Path.Combine(basePath, puzzleName); 549 | if(File.Exists(baseName + ".puzzle")){ 550 | puzzle = Puzzle.method_1249(baseName + ".puzzle"); 551 | }else if(File.Exists(baseName + ".puzzle.yaml")){ 552 | puzzle = PuzzleModel.FromModel(YamlHelper.Deserializer.Deserialize(File.ReadAllText(baseName + ".puzzle.yaml"))); 553 | }else{ 554 | Logger.Log($"Puzzle \"{puzzleName}\" from \"{campaignTitle}\" doesn't exist, ignoring"); 555 | puzzle = null; 556 | return false; 557 | } 558 | 559 | // even if it was loaded from a vanilla format puzzle file, it was included in a mod and may rely on modded behaviour 560 | // these are never saved over and could have been modified directly by the campaign mod, so this is safe 561 | ((patch_Puzzle)(object)puzzle).IsModdedPuzzle = true; 562 | 563 | return true; 564 | }catch(Exception e){ 565 | Logger.Log($"Exception loading puzzle \"{puzzleName}\" from \"{campaignTitle}\", ignoring"); 566 | Logger.Log(e); 567 | puzzle = null; 568 | return false; 569 | } 570 | } 571 | 572 | public static void CheckCampaignReload(){ 573 | if(QuintessentialSettings.Instance.HotReloadCampaigns.Pressed() && GameLogic.field_2434.method_938() is PuzzleSelectScreen){ 574 | Logger.Log("Reloading campaigns and journals!"); 575 | 576 | ModPuzzleDirectories.Clear(); 577 | ModCampaignModels.Clear(); 578 | ModJournalModels.Clear(); 579 | 580 | Campaigns.field_2330 = VanillaCampaign; 581 | Campaigns.field_2331[0] = VanillaCampaign; 582 | JournalVolumes.field_2572 = VanillaJournal.ToArray(); 583 | patch_PuzzleSelectScreen.ResetPosition(); 584 | patch_JournalScreen.ResetPosition(); 585 | 586 | foreach(ModMeta mod in Mods) 587 | if(mod != QuintessentialModMeta) 588 | LoadModCampaigns(mod); 589 | 590 | LoadCampaigns(); 591 | LoadJournals(); 592 | UI.InstantCloseScreen(); 593 | UI.OpenScreen(new PuzzleSelectScreen()); 594 | } 595 | } 596 | 597 | private static CampaignItem AddEntryToCampaign( 598 | Campaign campaign, 599 | int chapter, 600 | string entryId, 601 | LocString entryTitle, 602 | enum_129 type, 603 | Maybe param_4485, 604 | Maybe puzzle, 605 | class_186 param_4487, 606 | Sound clickSound, 607 | class_259 requirement, 608 | bool noStoryPanel 609 | ) { 610 | if(puzzle.method_1085()) { 611 | //puzzle.method_1087().field_2767 = entryTitle; 612 | puzzle.method_1087().field_2769 = param_4485; 613 | } 614 | CampaignItem campaignItem = new(entryId, entryTitle, type, puzzle, requirement, param_4487, clickSound); 615 | campaign.field_2309[chapter].field_2314.Add(campaignItem); 616 | // no cutscene to see here 617 | if(noStoryPanel) 618 | campaignItem.field_2327 = struct_18.field_1431; 619 | 620 | return campaignItem; 621 | } 622 | 623 | internal static void DumpVanillaPuzzles() { 624 | string outDir = Path.Combine(PathModSaves, "Quintessential", "DumpedPuzzles"); 625 | Directory.CreateDirectory(outDir); 626 | foreach(var p in Puzzles.field_2816) { 627 | PuzzleModel m = PuzzleModel.FromPuzzle(p); 628 | string yaml = YamlHelper.Serializer.Serialize(m); 629 | File.WriteAllText(Path.Combine(outDir, m.ID + ".yaml"), yaml); 630 | } 631 | foreach(var p in JournalVolumes.field_2572.SelectMany(k => k.field_2571)) { 632 | PuzzleModel m = PuzzleModel.FromPuzzle(p); 633 | string yaml = YamlHelper.Serializer.Serialize(m); 634 | File.WriteAllText(Path.Combine(outDir, "X" + m.ID + ".yaml"), yaml); 635 | } 636 | Logger.Log($"Dumped puzzles to {outDir}"); 637 | UI.OpenScreen(new NoticeScreen("Puzzle Dumping", $"Saved puzzles to \"{outDir.Replace('\\', '/')}\"")); 638 | } 639 | 640 | internal static void DumpAtomSprites(){ 641 | string outDir = Path.Combine(PathModSaves, "Quintessential", "DumpedAtomSprites"); 642 | Directory.CreateDirectory(outDir); 643 | foreach(AtomType atomType in class_175.field_1691){ 644 | RenderTargetHandle v = RenderAtomToTarget(atomType); 645 | Renderer.method_1313(v.method_1351().field_937).method_735(Path.Combine(outDir, ((patch_AtomType)(object)atomType).QuintAtomType.Replace(":", "_") + ".png")); 646 | } 647 | Logger.Log($"Dumped atom sprites to {outDir}"); 648 | UI.OpenScreen(new NoticeScreen("Sprite Dumping", $"Saved atom sprites to \"{outDir.Replace('\\', '/')}\"")); 649 | } 650 | 651 | internal static RenderTargetHandle RenderAtomToTarget(AtomType type){ 652 | RenderTargetHandle renderTargetHandle = new RenderTargetHandle(); 653 | Bounds2 bounds = Bounds2.CenteredOn(class_187.field_1742.method_491(new HexIndex(0, 0), Vector2.Zero), class_187.field_1742.field_1744.X, class_187.field_1742.field_1744.Y * 1.3f); 654 | Index2 size = bounds.Size.CeilingToInt() + new Index2(20 * 2, 20 * 2); 655 | Vector2 pos = size.ToVector2() / 2 / 1f - bounds.Center; 656 | pos.Y = -pos.Y; 657 | renderTargetHandle.field_2987 = size; 658 | class_95 class95 = renderTargetHandle.method_1352(out var flag); 659 | if(flag){ 660 | using(class_226.method_597(class95, Matrix4.method_1074(new Vector3(1, -1, 1)))){ 661 | class_226.method_600(Color.Transparent); 662 | Editor.method_927(type, pos, 1, 1, 1, 1, -21, 0, class_238.field_1989.field_71, null, false); 663 | } 664 | } 665 | 666 | return renderTargetHandle; 667 | } 668 | } -------------------------------------------------------------------------------- /Quintessential/QuintessentialMod.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Quintessential; 4 | 5 | public abstract class QuintessentialMod { 6 | 7 | public ModMeta Meta; 8 | public object Settings = new(); 9 | public virtual Type SettingsType => typeof(object); 10 | 11 | public abstract void Load(); 12 | 13 | public abstract void PostLoad(); 14 | 15 | public abstract void Unload(); 16 | 17 | public virtual void LoadPuzzleContent() { 18 | 19 | } 20 | 21 | public virtual void ApplySettings() { 22 | 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Quintessential/QuintessentialSettings.cs: -------------------------------------------------------------------------------- 1 | using Quintessential.Settings; 2 | 3 | using YamlDotNet.Serialization; 4 | 5 | namespace Quintessential; 6 | 7 | public class QuintessentialSettings { 8 | 9 | public static QuintessentialSettings Instance => QuintessentialLoader.QuintessentialAsMod.Settings as QuintessentialSettings; 10 | 11 | //[SettingsLabel("Take Screenshot")] 12 | //public Keybinding Screenshot = new("F12"); 13 | 14 | [SettingsLabel("Hot Reload Campaigns")] 15 | public Keybinding HotReloadCampaigns = new("F11"); 16 | 17 | [SettingsLabel("Enable Campaign Switcher")] 18 | public bool EnableCustomCampaigns = true; 19 | 20 | [SettingsLabel("Campaign Switcher Options:")] 21 | public CampaignSwitcherSettings SwitcherSettings = new(); 22 | 23 | public class CampaignSwitcherSettings : SettingsGroup { 24 | 25 | public override bool Enabled => Instance.EnableCustomCampaigns; 26 | 27 | [SettingsLabel("Switch Campaign Left")] 28 | public Keybinding SwitchCampaignLeft = new() { Key = "K", Control = true }; 29 | 30 | [SettingsLabel("Switch Campaign Right")] 31 | public Keybinding SwitchCampaignRight = new() { Key = "L", Control = true }; 32 | } 33 | 34 | [SettingsLabel("Dump Puzzles")] 35 | [YamlIgnore] 36 | public SettingsButton DumpPuzzles = QuintessentialLoader.DumpVanillaPuzzles; 37 | 38 | [SettingsLabel("Dump Atom Sprites")] 39 | [YamlIgnore] 40 | public SettingsButton DumpAtomSprites = QuintessentialLoader.DumpAtomSprites; 41 | } 42 | -------------------------------------------------------------------------------- /Quintessential/Serialization/CampaignModel.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | using YamlDotNet.Serialization; 4 | 5 | namespace Quintessential.Serialization; 6 | 7 | public class CampaignModel { 8 | 9 | public string Name { get; set; } 10 | 11 | public string Title { get; set; } 12 | 13 | public IList Chapters { get; set; } 14 | 15 | [YamlIgnore] 16 | public string Path = ""; 17 | } 18 | 19 | public class ChapterModel { 20 | 21 | public string Title { get; set; } 22 | 23 | public string Subtitle { get; set; } 24 | 25 | public string Place { get; set; } 26 | 27 | public string Background { get; set; } 28 | 29 | // TODO: wheel icons 30 | 31 | public IList Entries { get; set; } 32 | } 33 | 34 | public class EntryModel { 35 | 36 | // TODO: multiple requirements, documents, tutorials 37 | 38 | public string Type { get; set; } = "puzzle"; 39 | 40 | public string ID { get; set; } 41 | 42 | public string Title { get; set; } 43 | 44 | public string Puzzle { get; set; } 45 | 46 | public string Requires { get; set; } 47 | 48 | public string Icon { get; set; } 49 | public string IconSmall { get; set; } 50 | 51 | public bool NoStoryPanel{ get; set; } 52 | } 53 | -------------------------------------------------------------------------------- /Quintessential/Serialization/JournalModel.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | using YamlDotNet.Serialization; 4 | 5 | namespace Quintessential.Serialization; 6 | 7 | using Texture = class_256; 8 | 9 | public class JournalModel { 10 | 11 | public string Title { get; set; } 12 | 13 | public string PuzzleBackgroundLarge { get; set; } 14 | public string PuzzleBackgroundSmall { get; set; } 15 | 16 | public List Chapters = new(); 17 | 18 | [YamlIgnore] 19 | public string Path = ""; 20 | 21 | [YamlIgnore] 22 | public Texture PuzzleBackgroundSmallTex, PuzzleBackgroundLargeTex; 23 | } 24 | 25 | public class JournalChapterModel { 26 | 27 | public string Title { get; set; } 28 | 29 | public string Description { get; set; } 30 | 31 | public List Puzzles = new(); 32 | } 33 | -------------------------------------------------------------------------------- /Quintessential/Serialization/PuzzleModel.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | 5 | using Bond = class_277; 6 | using BondType = enum_126; 7 | using PermissionFlags = enum_149; 8 | using ProductionInfo = class_261; 9 | using Chamber = class_189; 10 | using Conduit = class_117; 11 | using Vial = class_128; 12 | using AtomTypes = class_175; 13 | 14 | namespace Quintessential.Serialization; 15 | 16 | public class PuzzleModel { 17 | 18 | // display name, internal name, journal author 19 | public string Name, ID, Author; 20 | // the inputs 21 | public List Inputs = new(); 22 | // the outputs 23 | public List Outputs = new(); 24 | // output multiplier 25 | public int OutputMultiplier = 1; 26 | // vanilla permission info 27 | public PermissionFlags PermissionFlags; 28 | // modded permisisons, can be used for parts, instructions, or anything else 29 | public HashSet CustomPermissions = new(); 30 | // set of highlighted hexes 31 | public HashSet Highlights = new(); 32 | // production-related stuff, or null for non-production puzzles 33 | public ProductionInfoM ProductionInfo = null; 34 | 35 | public static PuzzleModel FromPuzzle(Puzzle puzzle) { 36 | PuzzleModel model = new(){ 37 | ID = puzzle.field_2766, 38 | PermissionFlags = puzzle.field_2773, 39 | Name = puzzle.field_2767?.method_620() ?? "Unnamed", 40 | Author = puzzle.field_2768.method_1085() ? puzzle.field_2768.method_1087() : "", 41 | CustomPermissions = ((patch_Puzzle)(object)puzzle).CustomPermissions, 42 | OutputMultiplier = puzzle.field_2780 43 | }; 44 | foreach(var @in in puzzle.field_2770) 45 | model.Inputs.Add(new PuzzleIoM(@in)); 46 | foreach(var @out in puzzle.field_2771) 47 | model.Outputs.Add(new PuzzleIoM(@out)); 48 | foreach(var item in puzzle.field_2774) 49 | model.Highlights.Add(new HexIndexM(item)); 50 | if(puzzle.field_2779.method_1085()) 51 | model.ProductionInfo = new ProductionInfoM(puzzle.field_2779.method_1087()); 52 | return model; 53 | } 54 | 55 | public static Puzzle FromModel(PuzzleModel model) { 56 | Puzzle ret = new(){ 57 | field_2766 = model.ID, 58 | field_2767 = class_134.method_253(model.Name, string.Empty), 59 | field_2770 = model.Inputs.Select(k => k.FromModel()).ToArray(), 60 | field_2771 = model.Outputs.Select(k => k.FromModel()).ToArray(), 61 | field_2773 = model.PermissionFlags, 62 | field_2768 = model.Author.Equals("") ? new Maybe(false, null) : model.Author, 63 | field_2774 = model.Highlights.Select(k => k.FromModel()).ToArray(), 64 | field_2780 = model.OutputMultiplier 65 | }; 66 | if(model.ProductionInfo != null && model.ProductionInfo.Chambers.Count > 0){ 67 | ret.field_2779 = model.ProductionInfo.FromModel(); 68 | // Calculate bounds 69 | ret.method_1247(); 70 | } 71 | ((patch_Puzzle)(object)ret).CustomPermissions = model.CustomPermissions; 72 | return ret; 73 | } 74 | 75 | public class HexIndexM { 76 | public string Pos; 77 | 78 | public HexIndexM(HexIndex ind) { 79 | Pos = ind.Q + "," + ind.R; 80 | } 81 | 82 | public HexIndexM(){} 83 | 84 | public HexIndex FromModel() { 85 | return new(Q(), R()); 86 | } 87 | 88 | public int Q() { 89 | return int.Parse(Pos.Split(',')[0]); 90 | } 91 | 92 | public int R() { 93 | return int.Parse(Pos.Split(',')[1]); 94 | } 95 | } 96 | 97 | public class PuzzleIoM { 98 | public MoleculeM Molecule; 99 | public int AmountOverride = 0; 100 | 101 | public PuzzleIoM(PuzzleInputOutput io) { 102 | Molecule = new MoleculeM(io.field_2813); 103 | AmountOverride = ((patch_PuzzleInputOutput)(object)io).AmountOverride; 104 | } 105 | 106 | public PuzzleIoM(){} 107 | 108 | public PuzzleInputOutput FromModel(){ 109 | PuzzleInputOutput io = new PuzzleInputOutput(Molecule.FromModel()); 110 | ((patch_PuzzleInputOutput)(object)(io)).AmountOverride = AmountOverride; 111 | return io; 112 | } 113 | } 114 | 115 | public class MoleculeM { 116 | public List Atoms = new(); 117 | public List Bonds = new(); 118 | public string Name = ""; 119 | 120 | public MoleculeM(Molecule mol) { 121 | // clean molecules first 122 | mol = MoleculeEditorScreen.method_1134(mol); 123 | foreach(var atom in mol.method_1100()) 124 | Atoms.Add(new AtomM(atom.Value, new HexIndexM(atom.Key))); 125 | foreach(var bond in mol.method_1101()) 126 | Bonds.Add(new BondM(bond)); 127 | Name = mol.field_2639.method_1090(null)?.method_620() ?? ""; 128 | } 129 | 130 | public MoleculeM(){} 131 | 132 | public Molecule FromModel() { 133 | Molecule ret = new(); 134 | foreach(var item in Atoms) 135 | ret.method_1105(item.FromModel(), item.Position.FromModel()); 136 | foreach(var item in Bonds) 137 | ret.method_1111((BondType)item.BondBits(), item.A.FromModel(), item.B.FromModel()); 138 | if(!Name.Equals("")) 139 | ret.field_2639 = class_134.method_253(Name, string.Empty); 140 | return MoleculeEditorScreen.method_1133(ret, class_181.field_1716); 141 | } 142 | } 143 | 144 | public class AtomM { 145 | public string AtomType; 146 | public HexIndexM Position; 147 | 148 | public AtomM(Atom atom, HexIndexM hex) { 149 | AtomType = ((patch_AtomType)(object)atom.field_2275).QuintAtomType; 150 | Position = hex; 151 | } 152 | 153 | public AtomM(){} 154 | 155 | public Atom FromModel() { 156 | if(AtomType == null) 157 | throw new NullReferenceException("Missing atom type!"); 158 | 159 | return new Atom( 160 | AtomTypes.field_1691.FirstOrDefault(k => AtomType.Equals(((patch_AtomType)(object)k).QuintAtomType)) 161 | ?? throw new Exception($"Atom type \"{AtomType}\" does not exist!") 162 | ); 163 | } 164 | } 165 | 166 | public class BondM { 167 | public HexIndexM A, B; 168 | public HashSet BondTypes = new(); 169 | 170 | public BondM(Bond bond) { 171 | A = new HexIndexM(bond.field_2187); 172 | B = new HexIndexM(bond.field_2188); 173 | if((bond.field_2186 & BondType.Standard) == BondType.Standard) 174 | BondTypes.Add("standard"); 175 | if((bond.field_2186 & BondType.Prisma0) == BondType.Prisma0) 176 | BondTypes.Add("triplex_0"); 177 | if((bond.field_2186 & BondType.Prisma1) == BondType.Prisma1) 178 | BondTypes.Add("triplex_1"); 179 | if((bond.field_2186 & BondType.Prisma2) == BondType.Prisma2) 180 | BondTypes.Add("triplex_2"); 181 | } 182 | 183 | public BondM(){} 184 | 185 | public byte BondBits() { 186 | byte bits = 0; 187 | if(BondTypes.Contains("standard")) 188 | bits |= (byte)BondType.Standard; 189 | if(BondTypes.Contains("triplex_0")) 190 | bits |= (byte)BondType.Prisma0; 191 | if(BondTypes.Contains("triplex_1")) 192 | bits |= (byte)BondType.Prisma1; 193 | if(BondTypes.Contains("triplex_2")) 194 | bits |= (byte)BondType.Prisma2; 195 | return bits; 196 | } 197 | } 198 | 199 | public class ProductionInfoM { 200 | public List Chambers = new(); 201 | public List Conduits = new(); 202 | public List Vials = new(); 203 | public bool Isolation = false, ShrinkLeft = false, ShrinkRight = false; 204 | 205 | public ProductionInfoM(ProductionInfo info) { 206 | foreach(var chamber in info.field_2071) 207 | Chambers.Add(new ChamberM(chamber)); 208 | foreach(Conduit conduit in info.field_2072) 209 | Conduits.Add(new ConduitM(conduit)); 210 | foreach(Vial vial in info.field_2073) 211 | Vials.Add(new VialM(vial)); 212 | ShrinkLeft = info.field_2075; 213 | ShrinkRight = info.field_2076; 214 | Isolation = info.field_2077; 215 | } 216 | 217 | public ProductionInfoM(){} 218 | 219 | public ProductionInfo FromModel() { 220 | ProductionInfo ret = new(){ 221 | field_2071 = Chambers.Select(k => k.FromModel()).ToArray(), 222 | field_2072 = Conduits.Select(k => k.FromModel()).ToArray(), 223 | field_2073 = Vials.Select(k => k.FromModel()).ToArray(), 224 | field_2075 = ShrinkLeft, 225 | field_2076 = ShrinkRight, 226 | field_2077 = Isolation 227 | }; 228 | return ret; 229 | } 230 | } 231 | 232 | public class ChamberM { 233 | public string ChamberType; 234 | public HexIndexM Position; 235 | 236 | public ChamberM(Chamber chamber) { 237 | ChamberType = chamber.field_1747.field_1727; 238 | Position = new HexIndexM(chamber.field_1746); 239 | } 240 | 241 | public ChamberM(){} 242 | 243 | public Chamber FromModel() { 244 | return new(Position.Q(), Position.R(), Puzzles.field_2932.First(k => k.field_1727.Equals(ChamberType))); 245 | } 246 | } 247 | 248 | public class ConduitM { 249 | public HexIndexM PosA, PosB; 250 | public List Shape = new(); 251 | 252 | public ConduitM(){} 253 | 254 | public ConduitM(Conduit c) { 255 | foreach(HexIndex hex in c.field_1440) 256 | Shape.Add(new HexIndexM(hex)); 257 | // TODO: when are there ever more than two? 258 | PosA = new HexIndexM(c.field_1441[0].field_1879); 259 | PosB = new HexIndexM(c.field_1441[1].field_1879); 260 | } 261 | 262 | public Conduit FromModel() { 263 | return new Conduit(PosA.Q(), PosA.R(), PosB.Q(), PosB.R(), Shape.Select(k => k.FromModel()).ToArray()); 264 | } 265 | } 266 | 267 | public class VialM { 268 | public HexIndexM Position; 269 | public bool Top; 270 | public List> Sprites = new(); 271 | 272 | public VialM(){} 273 | 274 | public VialM(Vial v) { 275 | Position = new HexIndexM(v.field_1471); 276 | Top = v.field_1472; 277 | foreach(Tuple sprites in v.field_1473) 278 | Sprites.Add(new(CleanName(sprites.Item1), CleanName(sprites.Item2))); 279 | } 280 | 281 | public Vial FromModel() { 282 | return new Vial(Position.Q(), Position.R(), Top, 283 | Sprites.Select(xs => Tuple.Create(class_235.method_615(xs.Left), class_235.method_615(xs.Right))).ToArray()); 284 | } 285 | 286 | private static string CleanName(class_256 texture){ 287 | string name = texture.field_2062.method_1087(); 288 | if(name.StartsWith("Content/") || name.StartsWith("Content\\")) 289 | name = name.Substring("Content/".Length); 290 | return name; 291 | } 292 | } 293 | } 294 | -------------------------------------------------------------------------------- /Quintessential/Settings/ChangeKeybindScreen.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | using SDL2; 4 | 5 | namespace Quintessential.Settings; 6 | 7 | class ChangeKeybindScreen : IScreen { 8 | 9 | Keybinding Key; 10 | string Label; 11 | QuintessentialMod ToSave; 12 | 13 | // SDL doesn't make an event when Control or Alt are pressed unless it makes a character (or maybe OM doesn't pick it up) 14 | // So we just use this 15 | public static List BindableKeys = new(); 16 | 17 | static ChangeKeybindScreen(){ 18 | foreach(var letter in "abcdefghijklmnopqrstuvwxyz ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890-+_=/*!\"£$%^&()<>,.?{}[]:;@'~#|\\`¬¦".ToCharArray()) 19 | BindableKeys.Add(letter.ToString()); 20 | for(int i = 1; i < 25; i++) // F1 -> F24 21 | BindableKeys.Add("F" + i); 22 | for(int i = 0; i < 10; i++) // Keypad numbers 23 | BindableKeys.Add("Keypad " + i); 24 | BindableKeys.AddRange(new[]{ "Insert", "PageUp", "PageDown", "Home", "End" }); 25 | } 26 | 27 | public ChangeKeybindScreen(Keybinding key, string label, QuintessentialMod save){ 28 | Key = key; 29 | Label = label; 30 | ToSave = save; 31 | } 32 | 33 | public bool method_1037(){ 34 | return false; 35 | } 36 | public void method_47(bool param_4687){ 37 | // Add gray BG 38 | GameLogic.field_2434.field_2464 = true; 39 | } 40 | 41 | public void method_48(){} 42 | 43 | public void method_50(float param_4686){ 44 | // "Please enter a new key:" 45 | UI.DrawText("Please enter a new key for: " + Label, (Input.ScreenSize() / 2) + new Vector2(0, 170), UI.Title, Color.White, TextAlignment.Centred); 46 | // display ctrl/shift 47 | string preview = ""; 48 | bool shift = Input.IsShiftHeld(); 49 | bool ctrl = Input.IsControlHeld(); 50 | bool alt = Input.IsAltHeld(); 51 | if(shift) 52 | preview = "Shift + " + preview; 53 | if(alt) 54 | preview = "Alt + " + preview; 55 | if(ctrl) 56 | preview = "Control + " + preview; 57 | if(!string.IsNullOrWhiteSpace(preview)) 58 | UI.DrawText(preview, Input.ScreenSize() / 2, UI.Title, class_181.field_1718, TextAlignment.Centred); 59 | // "press esc to CANCEL" 60 | Bounds2 labelBounds = UI.DrawText("Press ESC to ", (Input.ScreenSize() / 2) + new Vector2(-40, -170), UI.SubTitle, class_181.field_1718, TextAlignment.Centred); 61 | if(Input.IsSdlKeyPressed(SDL.enum_160.SDLK_ESCAPE) || UI.DrawAndCheckSimpleButton("CANCEL", labelBounds.BottomRight + new Vector2(10, -7), new Vector2(70, (int)labelBounds.Height + 10))) 62 | UI.HandleCloseButton(); 63 | // handle keypresses 64 | string key = ""; 65 | foreach(var bindable in BindableKeys) 66 | if(Input.IsKeyPressed(bindable)) 67 | key = bindable; 68 | if(key != ""){ 69 | Keybinding old = Key.Copy(); 70 | Key.Key = key.Length == 1 ? key.ToUpper() : key; // make all letters uppercase, but keep e.g. PageUp 71 | Key.Shift = shift; 72 | Key.Control = ctrl; 73 | Key.Alt = alt; 74 | Logger.Log($"Changed keybind for \"{Label}\": from \"{old}\" to \"{Key}\"."); 75 | ModsScreen.SaveSettings(ToSave); 76 | UI.CloseScreen(); 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /Quintessential/Settings/Keybinding.cs: -------------------------------------------------------------------------------- 1 | namespace Quintessential.Settings; 2 | 3 | public class Keybinding { 4 | 5 | // only one character 6 | public string Key = ""; 7 | 8 | public bool Shift = false, Control = false, Alt = false; 9 | 10 | public Keybinding(){} 11 | 12 | public Keybinding(string key, bool shift = false, bool control = false, bool alt = false){ 13 | Key = key; 14 | Shift = shift; 15 | Control = control; 16 | Alt = alt; 17 | } 18 | 19 | public bool IsControlKeysPressed(){ 20 | return (!Shift || Input.IsShiftHeld()) && (!Control || Input.IsControlHeld()) && (!Alt || Input.IsAltHeld()); 21 | } 22 | 23 | public bool Pressed(){ 24 | return IsControlKeysPressed() && Input.IsKeyPressed(Key); 25 | } 26 | 27 | public bool Held(){ 28 | return IsControlKeysPressed() && Input.IsKeyHeld(Key); 29 | } 30 | 31 | public bool Released(){ 32 | return IsControlKeysPressed() && Input.IsKeyReleased(Key); 33 | } 34 | 35 | public Keybinding Copy() { 36 | Keybinding copy = new(); 37 | copy.Key = Key; 38 | copy.Shift = Shift; 39 | copy.Control = Control; 40 | copy.Alt = Alt; 41 | return copy; 42 | } 43 | 44 | public string ControlKeysText() { 45 | return (Control ? "Control + " : "") + (Alt ? "Alt + " : "") + (Shift ? "Shift + " : ""); 46 | } 47 | 48 | public override string ToString() { 49 | return ControlKeysText() + Key; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /Quintessential/Settings/SettingsButton.cs: -------------------------------------------------------------------------------- 1 | namespace Quintessential.Settings; 2 | 3 | /// 4 | /// Settings fields of this type are displayed as a button on the Mods menu. 5 | /// You must additionally annotate these fields with [YamlDotNet.Serialization.YamlIgnore]. 6 | /// 7 | public delegate void SettingsButton(); 8 | -------------------------------------------------------------------------------- /Quintessential/Settings/SettingsGroup.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Quintessential.Settings; 4 | 5 | public class SettingsGroup { 6 | 7 | public virtual bool Enabled => true; 8 | } 9 | -------------------------------------------------------------------------------- /Quintessential/Settings/SettingsLabel.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Quintessential.Settings; 4 | 5 | [AttributeUsage(AttributeTargets.Field)] 6 | public class SettingsLabel : Attribute { 7 | 8 | public string Label; 9 | 10 | public SettingsLabel(string label) { 11 | Label = label; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Quintessential/UI.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using MonoMod.Utils; 3 | 4 | namespace Quintessential; 5 | 6 | using OMDraw = class_135; 7 | using OMUI = class_140; 8 | using OMFont = class_1; 9 | using OMAssets = class_238; 10 | using OMTexture = class_256; 11 | 12 | public static class UI { 13 | 14 | #region Constants 15 | 16 | public static readonly OMFont Title = OMAssets.field_1990.field_2146; 17 | public static readonly OMFont Text = OMAssets.field_1990.field_2145; 18 | public static readonly OMFont SubTitle = OMAssets.field_1990.field_2143; 19 | 20 | public static readonly Color TextColor = class_181.field_1718; 21 | 22 | public static readonly OMTexture BackgroundLarger = class_235.method_615("Quintessential/background_larger"); 23 | 24 | #endregion 25 | 26 | #region Texture drawing methods 27 | 28 | public static void DrawTexture(OMTexture texture, Vector2 pos) { 29 | OMDraw.method_272(texture, pos); 30 | } 31 | 32 | public static void DrawRepeatingTexture(OMTexture texture, Vector2 pos, Vector2 size) { 33 | OMDraw.method_268(texture, Color.White, pos, Bounds2.WithSize(pos, size)); 34 | } 35 | 36 | public static void DrawResizableTexture(OMTexture texture, Vector2 pos, Vector2 size) { 37 | OMDraw.method_276(texture, Color.White, pos, size); 38 | } 39 | 40 | #endregion 41 | 42 | #region Text drawing methods 43 | 44 | public static Bounds2 DrawText(string text, Vector2 pos, OMFont font, Color color, TextAlignment alignment, float maxWidth = float.MaxValue, float ellipsesCutoff = float.MaxValue) { 45 | return OMDraw.method_290(text, pos, font, color, (enum_0)(int)alignment, 1f, 0.6f, maxWidth, ellipsesCutoff, 0, new Color(), (OMTexture)null, int.MaxValue, true, true); 46 | } 47 | 48 | public static void DrawHeader(string text, Vector2 pos, int width, bool a, bool b) { 49 | OMUI.method_317(class_134.method_253(text, string.Empty), pos, width, true, true); 50 | } 51 | 52 | #endregion 53 | 54 | #region Button drawing methods 55 | 56 | public static bool DrawAndCheckCloseButton(Vector2 framePos, Vector2 frameSize, Vector2 closeButtonOffset) { 57 | return OMUI.method_323(framePos, frameSize, frameSize - closeButtonOffset); 58 | } 59 | 60 | public static bool DrawAndCheckSolutionButton(string text, string subtext, Vector2 pos, int width, bool selected) { 61 | return OMUI.method_315(text, subtext == null ? struct_18.field_1431 : Maybe.method_1089(subtext), pos, width, selected).method_824(true, true); 62 | } 63 | 64 | public static bool DrawAndCheckBoxButton(string text, Vector2 pos) { 65 | return OMUI.method_314(text, pos).method_824(true, true); 66 | } 67 | 68 | public static bool DrawAndCheckSimpleButton(string text, Vector2 pos, Vector2 size) { 69 | return OMUI.class_149.method_348(text, pos, size).method_824(true, true); 70 | } 71 | 72 | #endregion 73 | 74 | #region Screen stack 75 | 76 | public static void HandleCloseButton() { 77 | CloseScreen(); 78 | // Play close sound 79 | OMAssets.field_1991.field_1873.method_28(1f); 80 | } 81 | 82 | public static void CloseScreen() { 83 | GameLogic.field_2434.field_2464 = false; 84 | GameLogic.field_2434.method_949(); 85 | } 86 | 87 | public static void InstantCloseScreen() { 88 | GameLogic.field_2434.method_950(1); 89 | } 90 | 91 | public static void OpenScreen(IScreen toOpen) { 92 | GameLogic.field_2434.method_945(toOpen, struct_18.field_1431, struct_18.field_1431); 93 | } 94 | 95 | #endregion 96 | 97 | #region UI helpers 98 | 99 | public static void DrawUiBackground(Vector2 pos, Vector2 size) { 100 | DrawRepeatingTexture(OMAssets.field_1989.field_102.field_810, pos, size); 101 | } 102 | 103 | public static void DrawLargeUiBackground(Vector2 pos, Vector2 size) { 104 | DrawRepeatingTexture(BackgroundLarger, pos, size); 105 | } 106 | 107 | public static void DrawUiFrame(Vector2 pos, Vector2 size) { 108 | DrawResizableTexture(OMAssets.field_1989.field_102.field_817, pos, size); 109 | } 110 | 111 | public static bool DrawCheckbox(Vector2 pos, string label, bool enabled) { 112 | Bounds2 boxBounds = Bounds2.WithSize(pos, new Vector2(36f, 37f)); 113 | Bounds2 labelBounds = DrawText(label, pos + new Vector2(45f, 13f), SubTitle, TextColor, TextAlignment.Left); 114 | if(enabled) 115 | DrawTexture(class_238.field_1989.field_101.field_773, boxBounds.Min); 116 | if(boxBounds.Contains(Input.MousePos()) || labelBounds.Contains(Input.MousePos())) { 117 | DrawTexture(class_238.field_1989.field_101.field_774, boxBounds.Min); 118 | if(!Input.IsLeftClickPressed()) 119 | return false; 120 | class_238.field_1991.field_1821.method_28(1f); 121 | return true; 122 | } 123 | DrawTexture(class_238.field_1989.field_101.field_772, boxBounds.Min); 124 | return false; 125 | } 126 | 127 | #endregion 128 | 129 | #region Texture control methods 130 | 131 | public static OMTexture AssignOffset(OMTexture tex, Vector2 offset){ 132 | new DynamicData(typeof(class_107)).Get>("field_996")[tex] = offset; 133 | return tex; 134 | } 135 | 136 | public static OMTexture AssignCentre(OMTexture tex, Vector2 offset){ 137 | new DynamicData(typeof(class_107)).Get>("field_997")[tex] = offset; 138 | return tex; 139 | } 140 | 141 | #endregion 142 | } 143 | 144 | public enum TextAlignment { 145 | Left, Centred, Right 146 | } 147 | -------------------------------------------------------------------------------- /Quintessential/YamlHelper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using YamlDotNet.Serialization; 3 | using YamlDotNet.Serialization.ObjectFactories; 4 | 5 | namespace Quintessential; 6 | 7 | // From https://github.com/EverestAPI/Everest/blob/dev/Celeste.Mod.mm/Mod/Helpers/YamlHelper.cs 8 | // Credit goes to 0x0ade, max4805, and coloursofnoise 9 | public static class YamlHelper { 10 | 11 | public static IDeserializer Deserializer = new DeserializerBuilder().IgnoreUnmatchedProperties().Build(); 12 | public static ISerializer Serializer = new SerializerBuilder().ConfigureDefaultValuesHandling(DefaultValuesHandling.Preserve).Build(); 13 | 14 | /// 15 | /// Builds a deserializer that will provide YamlDotNet with the given object instead of creating a new one. 16 | /// This will make YamlDotNet update this object when deserializing. 17 | /// 18 | /// The object to set fields on 19 | /// The newly-created deserializer 20 | public static IDeserializer DeserializerUsing(object objectToBind) { 21 | IObjectFactory defaultObjectFactory = new DefaultObjectFactory(); 22 | Type objectType = objectToBind.GetType(); 23 | 24 | return new DeserializerBuilder() 25 | .IgnoreUnmatchedProperties() 26 | // provide the given object if type matches, fall back to default behavior otherwise. 27 | .WithObjectFactory(type => type == objectType ? objectToBind : defaultObjectFactory.Create(type)) 28 | .Build(); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Quintessential 2 | the Opus Magnum mod loader. 3 | 4 | Quintessential extends the game Opus Magnum with the ability to load mods, and provides mods with the groundwork that makes compatibility possible. 5 | 6 | To install or update Quintessential, use the installer tool [Opus Mutatum](https://github.com/QuintessentialOM/OpusMutatum). You can find a guide [here](https://github.com/QuintessentialOM/Quintessential/wiki/How-to-mod-the-game). To install a mod, place it zipped into the `Opus Magnum/Mods` folder generated by Quintessential. 7 | 8 | The modded version of the game becomes a separate executable to the vanilla game, does not connect to Steam, and stores save data at a separate location, so you can safely use it alongside the ordinary game without breaking anything. 9 | 10 | ## For Users 11 | 12 | Quintessential includes some user-facing features and changes. 13 | - A "Mods" button appears on the pause screen, allowing you to view and configure installed mods. 14 | - The puzzle editor becomes scrollable, and all puzzles can have their allowed instructions modified, and be converted to/from a more flexible modded format. 15 | - Modded puzzles can also have reagent and product names modified, as well as any options introduced by mods (such as allowing the use of new parts). 16 | - Exported GIFs include a Quintessence symbol marker at the top right plus the Quintessential version currently used. 17 | - For a number of reasons, Steam integration is disabled. You cannot download Steam Workshop puzzles in-game, but they can be manually copied from the vanilla version and played in the modded version as custom puzzles. 18 | 19 | ## For Modders 20 | 21 | Quintessential allows mods to hook into the game using [MonoMod](https://github.com/MonoMod/MonoMod) to perform arbitrary changes, and provides a number of helper functions and scaffolding to make modding easier and more compatible. 22 | 23 | It can also load mods consisting only of texture replacements; any modded `Content` directory takes precedence over vanilla textures, or the textures of its dependencies. 24 | 25 | Custom campaigns (and in the future, journals) can also be created using only puzzle files and assets, along with YAML files specifying their setup. These are still a work in progress. 26 | 27 | For an up-to-date example of a mod with custom mechanics, see [Unstable Elements](https://github.com/l-Luna/UnstableElements). --------------------------------------------------------------------------------