├── .gitattributes ├── .gitignore ├── BareMinimum ├── BareMinimum.cs ├── BareMinimum.csproj └── Properties │ └── AssemblyInfo.cs ├── ExampleSlugcat ├── ExampleSlugcat.csproj ├── Properties │ └── AssemblyInfo.cs ├── SlugBase │ └── Sprinter │ │ ├── Illustrations │ │ ├── MultiplayerPortrait00.png │ │ ├── MultiplayerPortrait01.png │ │ ├── MultiplayerPortrait10.png │ │ ├── MultiplayerPortrait11.png │ │ ├── MultiplayerPortrait20.png │ │ ├── MultiplayerPortrait21.png │ │ ├── MultiplayerPortrait30.png │ │ └── MultiplayerPortrait31.png │ │ ├── Scenes │ │ ├── Intro 1 - Pebbles Thinking │ │ │ ├── Blue Light.png │ │ │ ├── Hand.png │ │ │ ├── Pebbles.png │ │ │ └── scene.json │ │ ├── Intro 2 - Critter List │ │ │ ├── Arm.png │ │ │ ├── Cell Wall.png │ │ │ ├── List.png │ │ │ ├── Pebbles.png │ │ │ └── scene.json │ │ ├── Intro 3 - Wait No │ │ │ ├── Arm.png │ │ │ ├── Glow.png │ │ │ ├── Pebbles.png │ │ │ ├── Projection.png │ │ │ └── scene.json │ │ ├── Intro 4 - What Have You Done │ │ │ ├── Cell Wall.png │ │ │ ├── Floor.png │ │ │ ├── Pipe.png │ │ │ ├── Slug.png │ │ │ └── scene.json │ │ ├── Intro 5 - It's Alive! │ │ │ ├── Ground.png │ │ │ ├── Slug Up.png │ │ │ └── scene.json │ │ ├── SelectMenu │ │ │ ├── .eyes red.png │ │ │ ├── Background.png │ │ │ ├── Bg Plants.png │ │ │ ├── Flat.png │ │ │ ├── Hand.png │ │ │ ├── Plants.png │ │ │ ├── Spores.png │ │ │ ├── Sprinter.png │ │ │ └── scene.json │ │ ├── SelectMenuAscended │ │ │ ├── Bg.png │ │ │ ├── Ghost.png │ │ │ ├── Glow.png │ │ │ └── scene.json │ │ └── SleepScreen │ │ │ ├── Sleep - 1.png │ │ │ ├── Sleep - 2 - Sprinter.png │ │ │ ├── Sleep - 3.png │ │ │ ├── Sleep - 4.png │ │ │ ├── Sleep - 5.png │ │ │ └── scene.json │ │ └── Slideshows │ │ └── intro.json ├── SprinterMod.cs ├── SprinterSaveState.cs ├── SprinterSlugcat.cs └── SprinterStart.cs ├── Readme.md ├── SlugBase.sln └── SlugBase ├── ArenaAdditions.cs ├── AttachedField.cs ├── Compatibility ├── FancySlugcats.cs ├── FlatmodeFix.cs ├── HookGenFix.cs └── JollyCoop.cs ├── Config ├── CharacterSelectButton.cs └── CharacterSelectGroup.cs ├── CustomSaveState.cs ├── MultiplayerTweaks.cs ├── PlayerColors.cs ├── PlayerManager.cs ├── Properties └── AssemblyInfo.cs ├── RegionTools.cs ├── SaveManager.cs ├── Scenes ├── CustomScene.cs ├── CustomSceneManager.cs ├── CustomSlideshow.cs ├── SceneEditor.cs ├── SelectMenu.cs └── ShelterScreens.cs ├── SlugBase.csproj ├── SlugBaseCharacter.cs ├── SlugBaseEx.cs ├── SlugBaseMod.cs └── WorldFixes.cs /.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 | # SlugBase custom ignore 7 | Private/ 8 | 9 | # User-specific files 10 | *.rsuser 11 | *.suo 12 | *.user 13 | *.userosscache 14 | *.sln.docstates 15 | 16 | # User-specific files (MonoDevelop/Xamarin Studio) 17 | *.userprefs 18 | 19 | # Build results 20 | [Dd]ebug/ 21 | [Dd]ebugPublic/ 22 | [Rr]elease/ 23 | [Rr]eleases/ 24 | x64/ 25 | x86/ 26 | [Aa][Rr][Mm]/ 27 | [Aa][Rr][Mm]64/ 28 | bld/ 29 | [Bb]in/ 30 | [Oo]bj/ 31 | [Ll]og/ 32 | 33 | # Visual Studio 2015/2017 cache/options directory 34 | .vs/ 35 | # Uncomment if you have tasks that create the project's static files in wwwroot 36 | #wwwroot/ 37 | 38 | # Visual Studio 2017 auto generated files 39 | Generated\ Files/ 40 | 41 | # MSTest test Results 42 | [Tt]est[Rr]esult*/ 43 | [Bb]uild[Ll]og.* 44 | 45 | # NUNIT 46 | *.VisualState.xml 47 | TestResult.xml 48 | 49 | # Build Results of an ATL Project 50 | [Dd]ebugPS/ 51 | [Rr]eleasePS/ 52 | dlldata.c 53 | 54 | # Benchmark Results 55 | BenchmarkDotNet.Artifacts/ 56 | 57 | # .NET Core 58 | project.lock.json 59 | project.fragment.lock.json 60 | artifacts/ 61 | 62 | # StyleCop 63 | StyleCopReport.xml 64 | 65 | # Files built by Visual Studio 66 | *_i.c 67 | *_p.c 68 | *_h.h 69 | *.ilk 70 | *.meta 71 | *.obj 72 | *.iobj 73 | *.pch 74 | *.pdb 75 | *.ipdb 76 | *.pgc 77 | *.pgd 78 | *.rsp 79 | *.sbr 80 | *.tlb 81 | *.tli 82 | *.tlh 83 | *.tmp 84 | *.tmp_proj 85 | *_wpftmp.csproj 86 | *.log 87 | *.vspscc 88 | *.vssscc 89 | .builds 90 | *.pidb 91 | *.svclog 92 | *.scc 93 | 94 | # Chutzpah Test files 95 | _Chutzpah* 96 | 97 | # Visual C++ cache files 98 | ipch/ 99 | *.aps 100 | *.ncb 101 | *.opendb 102 | *.opensdf 103 | *.sdf 104 | *.cachefile 105 | *.VC.db 106 | *.VC.VC.opendb 107 | 108 | # Visual Studio profiler 109 | *.psess 110 | *.vsp 111 | *.vspx 112 | *.sap 113 | 114 | # Visual Studio Trace Files 115 | *.e2e 116 | 117 | # TFS 2012 Local Workspace 118 | $tf/ 119 | 120 | # Guidance Automation Toolkit 121 | *.gpState 122 | 123 | # ReSharper is a .NET coding add-in 124 | _ReSharper*/ 125 | *.[Rr]e[Ss]harper 126 | *.DotSettings.user 127 | 128 | # JustCode is a .NET coding add-in 129 | .JustCode 130 | 131 | # TeamCity is a build add-in 132 | _TeamCity* 133 | 134 | # DotCover is a Code Coverage Tool 135 | *.dotCover 136 | 137 | # AxoCover is a Code Coverage Tool 138 | .axoCover/* 139 | !.axoCover/settings.json 140 | 141 | # Visual Studio code coverage results 142 | *.coverage 143 | *.coveragexml 144 | 145 | # NCrunch 146 | _NCrunch_* 147 | .*crunch*.local.xml 148 | nCrunchTemp_* 149 | 150 | # MightyMoose 151 | *.mm.* 152 | AutoTest.Net/ 153 | 154 | # Web workbench (sass) 155 | .sass-cache/ 156 | 157 | # Installshield output folder 158 | [Ee]xpress/ 159 | 160 | # DocProject is a documentation generator add-in 161 | DocProject/buildhelp/ 162 | DocProject/Help/*.HxT 163 | DocProject/Help/*.HxC 164 | DocProject/Help/*.hhc 165 | DocProject/Help/*.hhk 166 | DocProject/Help/*.hhp 167 | DocProject/Help/Html2 168 | DocProject/Help/html 169 | 170 | # Click-Once directory 171 | publish/ 172 | 173 | # Publish Web Output 174 | *.[Pp]ublish.xml 175 | *.azurePubxml 176 | # Note: Comment the next line if you want to checkin your web deploy settings, 177 | # but database connection strings (with potential passwords) will be unencrypted 178 | *.pubxml 179 | *.publishproj 180 | 181 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 182 | # checkin your Azure Web App publish settings, but sensitive information contained 183 | # in these scripts will be unencrypted 184 | PublishScripts/ 185 | 186 | # NuGet Packages 187 | *.nupkg 188 | # The packages folder can be ignored because of Package Restore 189 | **/[Pp]ackages/* 190 | # except build/, which is used as an MSBuild target. 191 | !**/[Pp]ackages/build/ 192 | # Uncomment if necessary however generally it will be regenerated when needed 193 | #!**/[Pp]ackages/repositories.config 194 | # NuGet v3's project.json files produces more ignorable files 195 | *.nuget.props 196 | *.nuget.targets 197 | 198 | # Microsoft Azure Build Output 199 | csx/ 200 | *.build.csdef 201 | 202 | # Microsoft Azure Emulator 203 | ecf/ 204 | rcf/ 205 | 206 | # Windows Store app package directories and files 207 | AppPackages/ 208 | BundleArtifacts/ 209 | Package.StoreAssociation.xml 210 | _pkginfo.txt 211 | *.appx 212 | 213 | # Visual Studio cache files 214 | # files ending in .cache can be ignored 215 | *.[Cc]ache 216 | # but keep track of directories ending in .cache 217 | !?*.[Cc]ache/ 218 | 219 | # Others 220 | ClientBin/ 221 | ~$* 222 | *~ 223 | *.dbmdl 224 | *.dbproj.schemaview 225 | *.jfm 226 | *.pfx 227 | *.publishsettings 228 | orleans.codegen.cs 229 | 230 | # Including strong name files can present a security risk 231 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 232 | #*.snk 233 | 234 | # Since there are multiple workflows, uncomment next line to ignore bower_components 235 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 236 | #bower_components/ 237 | 238 | # RIA/Silverlight projects 239 | Generated_Code/ 240 | 241 | # Backup & report files from converting an old project file 242 | # to a newer Visual Studio version. Backup files are not needed, 243 | # because we have git ;-) 244 | _UpgradeReport_Files/ 245 | Backup*/ 246 | UpgradeLog*.XML 247 | UpgradeLog*.htm 248 | ServiceFabricBackup/ 249 | *.rptproj.bak 250 | 251 | # SQL Server files 252 | *.mdf 253 | *.ldf 254 | *.ndf 255 | 256 | # Business Intelligence projects 257 | *.rdl.data 258 | *.bim.layout 259 | *.bim_*.settings 260 | *.rptproj.rsuser 261 | *- Backup*.rdl 262 | 263 | # Microsoft Fakes 264 | FakesAssemblies/ 265 | 266 | # GhostDoc plugin setting file 267 | *.GhostDoc.xml 268 | 269 | # Node.js Tools for Visual Studio 270 | .ntvs_analysis.dat 271 | node_modules/ 272 | 273 | # Visual Studio 6 build log 274 | *.plg 275 | 276 | # Visual Studio 6 workspace options file 277 | *.opt 278 | 279 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 280 | *.vbw 281 | 282 | # Visual Studio LightSwitch build output 283 | **/*.HTMLClient/GeneratedArtifacts 284 | **/*.DesktopClient/GeneratedArtifacts 285 | **/*.DesktopClient/ModelManifest.xml 286 | **/*.Server/GeneratedArtifacts 287 | **/*.Server/ModelManifest.xml 288 | _Pvt_Extensions 289 | 290 | # Paket dependency manager 291 | .paket/paket.exe 292 | paket-files/ 293 | 294 | # FAKE - F# Make 295 | .fake/ 296 | 297 | # JetBrains Rider 298 | .idea/ 299 | *.sln.iml 300 | 301 | # CodeRush personal settings 302 | .cr/personal 303 | 304 | # Python Tools for Visual Studio (PTVS) 305 | __pycache__/ 306 | *.pyc 307 | 308 | # Cake - Uncomment if you are using it 309 | # tools/** 310 | # !tools/packages.config 311 | 312 | # Tabs Studio 313 | *.tss 314 | 315 | # Telerik's JustMock configuration file 316 | *.jmconfig 317 | 318 | # BizTalk build output 319 | *.btp.cs 320 | *.btm.cs 321 | *.odx.cs 322 | *.xsd.cs 323 | 324 | # OpenCover UI analysis results 325 | OpenCover/ 326 | 327 | # Azure Stream Analytics local run output 328 | ASALocalRun/ 329 | 330 | # MSBuild Binary and Structured Log 331 | *.binlog 332 | 333 | # NVidia Nsight GPU debugger configuration file 334 | *.nvuser 335 | 336 | # MFractors (Xamarin productivity tool) working folder 337 | .mfractor/ 338 | 339 | # Local History for Visual Studio 340 | .localhistory/ 341 | 342 | # BeatPulse healthcheck temp database 343 | healthchecksdb -------------------------------------------------------------------------------- /BareMinimum/BareMinimum.cs: -------------------------------------------------------------------------------- 1 | using SlugBase; 2 | 3 | /* 4 | * This example interacts with SlugBase as little as possible. 5 | * 6 | * The player select menu and sleep screen will display Survivor. 7 | * This makes the select screen ambiguous once a game is started, since the name is hidden. 8 | * Consider copying one of the slugcat select scenes and editing it. 9 | */ 10 | 11 | namespace BareMinimum 12 | { 13 | public class BareMinimum : Partiality.Modloader.PartialityMod 14 | { 15 | public BareMinimum() 16 | { 17 | ModID = "Bare Minimum Example Slugcat"; 18 | Version = "1.1"; 19 | author = "Slime_Cubed"; 20 | } 21 | 22 | public override void OnLoad() 23 | { 24 | PlayerManager.RegisterCharacter(new BareMinimumSlugcat()); 25 | } 26 | } 27 | 28 | public class BareMinimumSlugcat : SlugBaseCharacter 29 | { 30 | public BareMinimumSlugcat() : base("Bare Minimum", FormatVersion.V1, 0, true) 31 | { 32 | } 33 | 34 | public override string DisplayName => "The Prototype"; 35 | public override string Description => 36 | @"A new slugcat that demonstrates the bare minimum amount required. 37 | This is an example slugcat for the SlugBase framework."; 38 | 39 | protected override void Disable() {} 40 | 41 | protected override void Enable() {} 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /BareMinimum/BareMinimum.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | {C462D418-374C-4B99-BAF7-925248899242} 8 | Library 9 | Properties 10 | BareMinimum 11 | BareMinimum 12 | v3.5 13 | 512 14 | true 15 | 16 | 17 | true 18 | full 19 | false 20 | bin\Debug\ 21 | DEBUG;TRACE 22 | prompt 23 | 4 24 | 25 | 26 | pdbonly 27 | true 28 | bin\Release\ 29 | TRACE 30 | prompt 31 | 4 32 | 33 | 34 | 35 | ..\..\rwmodlibspublic\Assembly-CSharp.dll 36 | False 37 | 38 | 39 | ..\..\rwmodlibspublic\HOOKS-Assembly-CSharp.dll 40 | False 41 | 42 | 43 | ..\..\rwmodlibspublic\Partiality.dll 44 | False 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | ..\..\rwmodlibspublic\UnityEngine.dll 54 | False 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | {e7b1aabf-4be5-49d7-847d-24883f06261a} 64 | SlugBase 65 | False 66 | 67 | 68 | 69 | 70 | if defined RWMods (copy /Y "$(TargetPath)" "%25RWMods%25") 71 | 72 | -------------------------------------------------------------------------------- /BareMinimum/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | // General Information about an assembly is controlled through the following 6 | // set of attributes. Change these attribute values to modify the information 7 | // associated with an assembly. 8 | [assembly: AssemblyTitle("BareMinimum")] 9 | [assembly: AssemblyDescription("")] 10 | [assembly: AssemblyConfiguration("")] 11 | [assembly: AssemblyCompany("")] 12 | [assembly: AssemblyProduct("BareMinimum")] 13 | [assembly: AssemblyCopyright("Copyright © 2021")] 14 | [assembly: AssemblyTrademark("")] 15 | [assembly: AssemblyCulture("")] 16 | 17 | // Setting ComVisible to false makes the types in this assembly not visible 18 | // to COM components. If you need to access a type in this assembly from 19 | // COM, set the ComVisible attribute to true on that type. 20 | [assembly: ComVisible(false)] 21 | 22 | // The following GUID is for the ID of the typelib if this project is exposed to COM 23 | [assembly: Guid("c462d418-374c-4b99-baf7-925248899242")] 24 | 25 | // Version information for an assembly consists of the following four values: 26 | // 27 | // Major Version 28 | // Minor Version 29 | // Build Number 30 | // Revision 31 | // 32 | // You can specify all the values or you can default the Build and Revision Numbers 33 | // by using the '*' as shown below: 34 | // [assembly: AssemblyVersion("1.0.*")] 35 | [assembly: AssemblyVersion("1.0.0.0")] 36 | [assembly: AssemblyFileVersion("1.0.0.0")] 37 | -------------------------------------------------------------------------------- /ExampleSlugcat/ExampleSlugcat.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | {87055141-6463-4F6F-B072-FB85F52B5A48} 8 | Library 9 | Properties 10 | ExampleSlugcat 11 | ExampleSlugcat 12 | v3.5 13 | 512 14 | true 15 | 16 | 17 | true 18 | full 19 | false 20 | bin\Debug\ 21 | DEBUG;TRACE 22 | prompt 23 | 4 24 | 25 | 26 | pdbonly 27 | true 28 | bin\Release\ 29 | TRACE 30 | prompt 31 | 4 32 | 33 | 34 | 35 | ..\..\rwmodlibspublic\Assembly-CSharp.dll 36 | False 37 | 38 | 39 | ..\..\rwmodlibspublic\HOOKS-Assembly-CSharp.dll 40 | False 41 | 42 | 43 | ..\..\rwmodlibspublic\MonoMod.RuntimeDetour.dll 44 | False 45 | 46 | 47 | ..\..\rwmodlibspublic\Partiality.dll 48 | False 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | ..\..\rwmodlibspublic\UnityEngine.dll 58 | False 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | {e7b1aabf-4be5-49d7-847d-24883f06261a} 71 | SlugBase 72 | False 73 | 74 | 75 | 76 | 77 | if defined RWMods (copy /Y "$(TargetPath)" "%25RWMods%25") 78 | 79 | -------------------------------------------------------------------------------- /ExampleSlugcat/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | // General Information about an assembly is controlled through the following 6 | // set of attributes. Change these attribute values to modify the information 7 | // associated with an assembly. 8 | [assembly: AssemblyTitle("ExampleSlugcat")] 9 | [assembly: AssemblyDescription("")] 10 | [assembly: AssemblyConfiguration("")] 11 | [assembly: AssemblyCompany("")] 12 | [assembly: AssemblyProduct("ExampleSlugcat")] 13 | [assembly: AssemblyCopyright("Copyright © 2021")] 14 | [assembly: AssemblyTrademark("")] 15 | [assembly: AssemblyCulture("")] 16 | 17 | // Setting ComVisible to false makes the types in this assembly not visible 18 | // to COM components. If you need to access a type in this assembly from 19 | // COM, set the ComVisible attribute to true on that type. 20 | [assembly: ComVisible(false)] 21 | 22 | // The following GUID is for the ID of the typelib if this project is exposed to COM 23 | [assembly: Guid("87055141-6463-4f6f-b072-fb85f52b5a48")] 24 | 25 | // Version information for an assembly consists of the following four values: 26 | // 27 | // Major Version 28 | // Minor Version 29 | // Build Number 30 | // Revision 31 | // 32 | // You can specify all the values or you can default the Build and Revision Numbers 33 | // by using the '*' as shown below: 34 | // [assembly: AssemblyVersion("1.0.*")] 35 | [assembly: AssemblyVersion("1.0.0.0")] 36 | [assembly: AssemblyFileVersion("1.0.0.0")] 37 | -------------------------------------------------------------------------------- /ExampleSlugcat/SlugBase/Sprinter/Illustrations/MultiplayerPortrait00.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SlimeCubed/SlugBase/d16129271beaed8dba8f373d4888340587d2f86d/ExampleSlugcat/SlugBase/Sprinter/Illustrations/MultiplayerPortrait00.png -------------------------------------------------------------------------------- /ExampleSlugcat/SlugBase/Sprinter/Illustrations/MultiplayerPortrait01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SlimeCubed/SlugBase/d16129271beaed8dba8f373d4888340587d2f86d/ExampleSlugcat/SlugBase/Sprinter/Illustrations/MultiplayerPortrait01.png -------------------------------------------------------------------------------- /ExampleSlugcat/SlugBase/Sprinter/Illustrations/MultiplayerPortrait10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SlimeCubed/SlugBase/d16129271beaed8dba8f373d4888340587d2f86d/ExampleSlugcat/SlugBase/Sprinter/Illustrations/MultiplayerPortrait10.png -------------------------------------------------------------------------------- /ExampleSlugcat/SlugBase/Sprinter/Illustrations/MultiplayerPortrait11.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SlimeCubed/SlugBase/d16129271beaed8dba8f373d4888340587d2f86d/ExampleSlugcat/SlugBase/Sprinter/Illustrations/MultiplayerPortrait11.png -------------------------------------------------------------------------------- /ExampleSlugcat/SlugBase/Sprinter/Illustrations/MultiplayerPortrait20.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SlimeCubed/SlugBase/d16129271beaed8dba8f373d4888340587d2f86d/ExampleSlugcat/SlugBase/Sprinter/Illustrations/MultiplayerPortrait20.png -------------------------------------------------------------------------------- /ExampleSlugcat/SlugBase/Sprinter/Illustrations/MultiplayerPortrait21.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SlimeCubed/SlugBase/d16129271beaed8dba8f373d4888340587d2f86d/ExampleSlugcat/SlugBase/Sprinter/Illustrations/MultiplayerPortrait21.png -------------------------------------------------------------------------------- /ExampleSlugcat/SlugBase/Sprinter/Illustrations/MultiplayerPortrait30.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SlimeCubed/SlugBase/d16129271beaed8dba8f373d4888340587d2f86d/ExampleSlugcat/SlugBase/Sprinter/Illustrations/MultiplayerPortrait30.png -------------------------------------------------------------------------------- /ExampleSlugcat/SlugBase/Sprinter/Illustrations/MultiplayerPortrait31.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SlimeCubed/SlugBase/d16129271beaed8dba8f373d4888340587d2f86d/ExampleSlugcat/SlugBase/Sprinter/Illustrations/MultiplayerPortrait31.png -------------------------------------------------------------------------------- /ExampleSlugcat/SlugBase/Sprinter/Scenes/Intro 1 - Pebbles Thinking/Blue Light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SlimeCubed/SlugBase/d16129271beaed8dba8f373d4888340587d2f86d/ExampleSlugcat/SlugBase/Sprinter/Scenes/Intro 1 - Pebbles Thinking/Blue Light.png -------------------------------------------------------------------------------- /ExampleSlugcat/SlugBase/Sprinter/Scenes/Intro 1 - Pebbles Thinking/Hand.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SlimeCubed/SlugBase/d16129271beaed8dba8f373d4888340587d2f86d/ExampleSlugcat/SlugBase/Sprinter/Scenes/Intro 1 - Pebbles Thinking/Hand.png -------------------------------------------------------------------------------- /ExampleSlugcat/SlugBase/Sprinter/Scenes/Intro 1 - Pebbles Thinking/Pebbles.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SlimeCubed/SlugBase/d16129271beaed8dba8f373d4888340587d2f86d/ExampleSlugcat/SlugBase/Sprinter/Scenes/Intro 1 - Pebbles Thinking/Pebbles.png -------------------------------------------------------------------------------- /ExampleSlugcat/SlugBase/Sprinter/Scenes/Intro 1 - Pebbles Thinking/scene.json: -------------------------------------------------------------------------------- 1 | { 2 | "images": [ 3 | {"name":"Pebbles.png", "x":339, "y":7, "depth":3.0}, 4 | {"name":"Hand.png", "x":498, "y":6, "depth":2.25, "shader":"LightEdges"}, 5 | {"name":"Blue Light.png", "x":454, "y":0, "depth":1.0, "alpha":0.5, "shader":"Lighten"} 6 | ] 7 | } -------------------------------------------------------------------------------- /ExampleSlugcat/SlugBase/Sprinter/Scenes/Intro 2 - Critter List/Arm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SlimeCubed/SlugBase/d16129271beaed8dba8f373d4888340587d2f86d/ExampleSlugcat/SlugBase/Sprinter/Scenes/Intro 2 - Critter List/Arm.png -------------------------------------------------------------------------------- /ExampleSlugcat/SlugBase/Sprinter/Scenes/Intro 2 - Critter List/Cell Wall.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SlimeCubed/SlugBase/d16129271beaed8dba8f373d4888340587d2f86d/ExampleSlugcat/SlugBase/Sprinter/Scenes/Intro 2 - Critter List/Cell Wall.png -------------------------------------------------------------------------------- /ExampleSlugcat/SlugBase/Sprinter/Scenes/Intro 2 - Critter List/List.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SlimeCubed/SlugBase/d16129271beaed8dba8f373d4888340587d2f86d/ExampleSlugcat/SlugBase/Sprinter/Scenes/Intro 2 - Critter List/List.png -------------------------------------------------------------------------------- /ExampleSlugcat/SlugBase/Sprinter/Scenes/Intro 2 - Critter List/Pebbles.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SlimeCubed/SlugBase/d16129271beaed8dba8f373d4888340587d2f86d/ExampleSlugcat/SlugBase/Sprinter/Scenes/Intro 2 - Critter List/Pebbles.png -------------------------------------------------------------------------------- /ExampleSlugcat/SlugBase/Sprinter/Scenes/Intro 2 - Critter List/scene.json: -------------------------------------------------------------------------------- 1 | { 2 | "images": [ 3 | {"name":"Cell Wall.png", "x":306, "y":47, "depth":4.5}, 4 | {"name":"List.png", "x":619, "y":215, "depth":3.0, "shader":"Lighten"}, 5 | {"name":"Pebbles.png", "x":396, "y":186, "depth":2.0}, 6 | {"name":"Arm.png", "x":53, "y":80, "depth":1.5} 7 | ] 8 | } -------------------------------------------------------------------------------- /ExampleSlugcat/SlugBase/Sprinter/Scenes/Intro 3 - Wait No/Arm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SlimeCubed/SlugBase/d16129271beaed8dba8f373d4888340587d2f86d/ExampleSlugcat/SlugBase/Sprinter/Scenes/Intro 3 - Wait No/Arm.png -------------------------------------------------------------------------------- /ExampleSlugcat/SlugBase/Sprinter/Scenes/Intro 3 - Wait No/Glow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SlimeCubed/SlugBase/d16129271beaed8dba8f373d4888340587d2f86d/ExampleSlugcat/SlugBase/Sprinter/Scenes/Intro 3 - Wait No/Glow.png -------------------------------------------------------------------------------- /ExampleSlugcat/SlugBase/Sprinter/Scenes/Intro 3 - Wait No/Pebbles.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SlimeCubed/SlugBase/d16129271beaed8dba8f373d4888340587d2f86d/ExampleSlugcat/SlugBase/Sprinter/Scenes/Intro 3 - Wait No/Pebbles.png -------------------------------------------------------------------------------- /ExampleSlugcat/SlugBase/Sprinter/Scenes/Intro 3 - Wait No/Projection.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SlimeCubed/SlugBase/d16129271beaed8dba8f373d4888340587d2f86d/ExampleSlugcat/SlugBase/Sprinter/Scenes/Intro 3 - Wait No/Projection.png -------------------------------------------------------------------------------- /ExampleSlugcat/SlugBase/Sprinter/Scenes/Intro 3 - Wait No/scene.json: -------------------------------------------------------------------------------- 1 | { 2 | "images": [ 3 | {"name":"Arm.png", "x":914, "y":20, "depth":5.0}, 4 | {"name":"Pebbles.png", "x":369, "y":0, "depth":4.0}, 5 | {"name":"Projection.png", "x":279, "y":21, "depth":1.5, "shader":"Lighten"}, 6 | {"name":"Glow.png", "x":46, "y":0, "depth":1.25, "alpha":0.25, "shader":"Lighten"} 7 | ] 8 | } -------------------------------------------------------------------------------- /ExampleSlugcat/SlugBase/Sprinter/Scenes/Intro 4 - What Have You Done/Cell Wall.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SlimeCubed/SlugBase/d16129271beaed8dba8f373d4888340587d2f86d/ExampleSlugcat/SlugBase/Sprinter/Scenes/Intro 4 - What Have You Done/Cell Wall.png -------------------------------------------------------------------------------- /ExampleSlugcat/SlugBase/Sprinter/Scenes/Intro 4 - What Have You Done/Floor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SlimeCubed/SlugBase/d16129271beaed8dba8f373d4888340587d2f86d/ExampleSlugcat/SlugBase/Sprinter/Scenes/Intro 4 - What Have You Done/Floor.png -------------------------------------------------------------------------------- /ExampleSlugcat/SlugBase/Sprinter/Scenes/Intro 4 - What Have You Done/Pipe.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SlimeCubed/SlugBase/d16129271beaed8dba8f373d4888340587d2f86d/ExampleSlugcat/SlugBase/Sprinter/Scenes/Intro 4 - What Have You Done/Pipe.png -------------------------------------------------------------------------------- /ExampleSlugcat/SlugBase/Sprinter/Scenes/Intro 4 - What Have You Done/Slug.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SlimeCubed/SlugBase/d16129271beaed8dba8f373d4888340587d2f86d/ExampleSlugcat/SlugBase/Sprinter/Scenes/Intro 4 - What Have You Done/Slug.png -------------------------------------------------------------------------------- /ExampleSlugcat/SlugBase/Sprinter/Scenes/Intro 4 - What Have You Done/scene.json: -------------------------------------------------------------------------------- 1 | { 2 | "images": [ 3 | {"name":"Cell Wall.png", "x":270, "y":119, "depth":4.0}, 4 | {"name":"Floor.png", "x":64, "y":98, "depth":3.0}, 5 | {"name":"Slug.png", "x":618, "y":177, "depth":2.5, "alphakeys":[ 0.35,0.0, 0.375,1.0 ]}, 6 | {"name":"Pipe.png", "x":557, "y":531, "depth":2.5} 7 | ] 8 | } -------------------------------------------------------------------------------- /ExampleSlugcat/SlugBase/Sprinter/Scenes/Intro 5 - It's Alive!/Ground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SlimeCubed/SlugBase/d16129271beaed8dba8f373d4888340587d2f86d/ExampleSlugcat/SlugBase/Sprinter/Scenes/Intro 5 - It's Alive!/Ground.png -------------------------------------------------------------------------------- /ExampleSlugcat/SlugBase/Sprinter/Scenes/Intro 5 - It's Alive!/Slug Up.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SlimeCubed/SlugBase/d16129271beaed8dba8f373d4888340587d2f86d/ExampleSlugcat/SlugBase/Sprinter/Scenes/Intro 5 - It's Alive!/Slug Up.png -------------------------------------------------------------------------------- /ExampleSlugcat/SlugBase/Sprinter/Scenes/Intro 5 - It's Alive!/scene.json: -------------------------------------------------------------------------------- 1 | { 2 | "images": [ 3 | {"name":"Ground.png", "x":0, "y":114, "depth":-1}, 4 | {"name":"Slug Up.png", "x":385, "y":191, "depth":-1, "alphakeys":[ 0.5,0.0, 0.525,1.0 ]} 5 | ] 6 | } -------------------------------------------------------------------------------- /ExampleSlugcat/SlugBase/Sprinter/Scenes/SelectMenu/.eyes red.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SlimeCubed/SlugBase/d16129271beaed8dba8f373d4888340587d2f86d/ExampleSlugcat/SlugBase/Sprinter/Scenes/SelectMenu/.eyes red.png -------------------------------------------------------------------------------- /ExampleSlugcat/SlugBase/Sprinter/Scenes/SelectMenu/Background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SlimeCubed/SlugBase/d16129271beaed8dba8f373d4888340587d2f86d/ExampleSlugcat/SlugBase/Sprinter/Scenes/SelectMenu/Background.png -------------------------------------------------------------------------------- /ExampleSlugcat/SlugBase/Sprinter/Scenes/SelectMenu/Bg Plants.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SlimeCubed/SlugBase/d16129271beaed8dba8f373d4888340587d2f86d/ExampleSlugcat/SlugBase/Sprinter/Scenes/SelectMenu/Bg Plants.png -------------------------------------------------------------------------------- /ExampleSlugcat/SlugBase/Sprinter/Scenes/SelectMenu/Flat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SlimeCubed/SlugBase/d16129271beaed8dba8f373d4888340587d2f86d/ExampleSlugcat/SlugBase/Sprinter/Scenes/SelectMenu/Flat.png -------------------------------------------------------------------------------- /ExampleSlugcat/SlugBase/Sprinter/Scenes/SelectMenu/Hand.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SlimeCubed/SlugBase/d16129271beaed8dba8f373d4888340587d2f86d/ExampleSlugcat/SlugBase/Sprinter/Scenes/SelectMenu/Hand.png -------------------------------------------------------------------------------- /ExampleSlugcat/SlugBase/Sprinter/Scenes/SelectMenu/Plants.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SlimeCubed/SlugBase/d16129271beaed8dba8f373d4888340587d2f86d/ExampleSlugcat/SlugBase/Sprinter/Scenes/SelectMenu/Plants.png -------------------------------------------------------------------------------- /ExampleSlugcat/SlugBase/Sprinter/Scenes/SelectMenu/Spores.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SlimeCubed/SlugBase/d16129271beaed8dba8f373d4888340587d2f86d/ExampleSlugcat/SlugBase/Sprinter/Scenes/SelectMenu/Spores.png -------------------------------------------------------------------------------- /ExampleSlugcat/SlugBase/Sprinter/Scenes/SelectMenu/Sprinter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SlimeCubed/SlugBase/d16129271beaed8dba8f373d4888340587d2f86d/ExampleSlugcat/SlugBase/Sprinter/Scenes/SelectMenu/Sprinter.png -------------------------------------------------------------------------------- /ExampleSlugcat/SlugBase/Sprinter/Scenes/SelectMenu/scene.json: -------------------------------------------------------------------------------- 1 | { 2 | "images": [ 3 | {"name":"Flat.png", "x":0, "y":0, "depth":-1.0, "flatmode":true}, 4 | {"name":"Background.png", "x":442, "y":134, "depth":3.5}, 5 | {"name":"Bg Plants.png", "x":488, "y":197, "depth":3.2}, 6 | {"name":"Sprinter.png", "x":516, "y":185, "depth":3.0, "shader":"LightEdges"}, 7 | {"name":"Hand.png", "x":687, "y":276, "depth":2.8, "shader":"LightEdges"}, 8 | {"name":".eyes red.png", "x":649, "y":334, "depth":3.0, "shader":"Basic", "turboonly":true}, 9 | {"name":"Spores.png", "x":487, "y":151, "depth":2, "shader":"Overlay"}, 10 | {"name":"Plants.png", "x":301, "y":18, "depth":1.5} 11 | ], 12 | "glowx":730, 13 | "glowy":335, 14 | "markx":740, 15 | "marky":575, 16 | "selectmenux":-10, 17 | "selectmenuy":100 18 | } -------------------------------------------------------------------------------- /ExampleSlugcat/SlugBase/Sprinter/Scenes/SelectMenuAscended/Bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SlimeCubed/SlugBase/d16129271beaed8dba8f373d4888340587d2f86d/ExampleSlugcat/SlugBase/Sprinter/Scenes/SelectMenuAscended/Bg.png -------------------------------------------------------------------------------- /ExampleSlugcat/SlugBase/Sprinter/Scenes/SelectMenuAscended/Ghost.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SlimeCubed/SlugBase/d16129271beaed8dba8f373d4888340587d2f86d/ExampleSlugcat/SlugBase/Sprinter/Scenes/SelectMenuAscended/Ghost.png -------------------------------------------------------------------------------- /ExampleSlugcat/SlugBase/Sprinter/Scenes/SelectMenuAscended/Glow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SlimeCubed/SlugBase/d16129271beaed8dba8f373d4888340587d2f86d/ExampleSlugcat/SlugBase/Sprinter/Scenes/SelectMenuAscended/Glow.png -------------------------------------------------------------------------------- /ExampleSlugcat/SlugBase/Sprinter/Scenes/SelectMenuAscended/scene.json: -------------------------------------------------------------------------------- 1 | { 2 | "images": [ 3 | { "name":"Bg.png" , "x":330, "y":-18, "depth": 4.5}, 4 | { "name":"Ghost.png", "x":557, "y": 89, "depth":2.85}, 5 | { "name":"Glow.png" , "x":574, "y": 74, "depth": 2.7, "shader":"Overlay", "alpha":0.7} 6 | ], 7 | "idledepths": [ 8 | 3.1, 9 | 2.8 10 | ] 11 | } -------------------------------------------------------------------------------- /ExampleSlugcat/SlugBase/Sprinter/Scenes/SleepScreen/Sleep - 1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SlimeCubed/SlugBase/d16129271beaed8dba8f373d4888340587d2f86d/ExampleSlugcat/SlugBase/Sprinter/Scenes/SleepScreen/Sleep - 1.png -------------------------------------------------------------------------------- /ExampleSlugcat/SlugBase/Sprinter/Scenes/SleepScreen/Sleep - 2 - Sprinter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SlimeCubed/SlugBase/d16129271beaed8dba8f373d4888340587d2f86d/ExampleSlugcat/SlugBase/Sprinter/Scenes/SleepScreen/Sleep - 2 - Sprinter.png -------------------------------------------------------------------------------- /ExampleSlugcat/SlugBase/Sprinter/Scenes/SleepScreen/Sleep - 3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SlimeCubed/SlugBase/d16129271beaed8dba8f373d4888340587d2f86d/ExampleSlugcat/SlugBase/Sprinter/Scenes/SleepScreen/Sleep - 3.png -------------------------------------------------------------------------------- /ExampleSlugcat/SlugBase/Sprinter/Scenes/SleepScreen/Sleep - 4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SlimeCubed/SlugBase/d16129271beaed8dba8f373d4888340587d2f86d/ExampleSlugcat/SlugBase/Sprinter/Scenes/SleepScreen/Sleep - 4.png -------------------------------------------------------------------------------- /ExampleSlugcat/SlugBase/Sprinter/Scenes/SleepScreen/Sleep - 5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SlimeCubed/SlugBase/d16129271beaed8dba8f373d4888340587d2f86d/ExampleSlugcat/SlugBase/Sprinter/Scenes/SleepScreen/Sleep - 5.png -------------------------------------------------------------------------------- /ExampleSlugcat/SlugBase/Sprinter/Scenes/SleepScreen/scene.json: -------------------------------------------------------------------------------- 1 | { 2 | "images": [ 3 | {"name":"Sleep - 5.png", "x":672, "y":236, "depth":3.5, "fade":0.2}, 4 | {"name":"Sleep - 4.png", "x":669, "y":138, "depth":2.8, "alpha":0.24, "fade":0.1}, 5 | {"name":"Sleep - 3.png", "x":696, "y":118, "depth":2.2, "fade":0.35}, 6 | {"name":"Sleep - 2 - Sprinter.png", "x":677, "y":63 , "depth":1.7}, 7 | {"name":"Sleep - 1.png", "x":486, "y":-54, "depth":1.2} 8 | ] 9 | } -------------------------------------------------------------------------------- /ExampleSlugcat/SlugBase/Sprinter/Slideshows/intro.json: -------------------------------------------------------------------------------- 1 | { 2 | "slides": [ 3 | {"name":"Empty", "duration":1.0}, 4 | {"name":"Intro 1 - Pebbles Thinking" , "duration":7.0, "campath":[-300,0,0, 300,0,0 ]}, 5 | {"name":"Intro 2 - Critter List" , "duration":7.0, "campath":[-400,0,0, 200,0,0.2, 200,300,0.4]}, 6 | {"name":"Intro 3 - Wait No" , "duration":7.0, "campath":[0,200,0.5, 0,0,0]}, 7 | {"name":"Intro 4 - What Have You Done", "duration":6.0, "campath":[0,400,0, 0,-400,0]}, 8 | {"name":"Intro 5 - It's Alive!" , "duration":7.0}, 9 | {"name":"Empty", "duration":0.0} 10 | ], 11 | "music":"RW_Intro_Theme", 12 | } -------------------------------------------------------------------------------- /ExampleSlugcat/SprinterMod.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | using SlugBase; 3 | 4 | /* 5 | * This is a basic example of a SlugBase character. 6 | * 7 | * It adds The Sprinter, a slugcat with faster movement and higher jumps. 8 | * Some scenes have been overridden so then Sprinter appears instead of Survivor. 9 | * To install, copy ExampleSlugcat.dll and SlugBase into the Mods folder. 10 | */ 11 | 12 | namespace ExampleSlugcat 13 | { 14 | // Your mod class 15 | // This does not have to be a PartialityMod 16 | internal class ExampleSlugcatMod : Partiality.Modloader.PartialityMod 17 | { 18 | public ExampleSlugcatMod() 19 | { 20 | ModID = "Example Slugcat"; 21 | Version = "1.2"; 22 | author = "Slime_Cubed"; 23 | } 24 | 25 | public override void OnEnable() 26 | { 27 | PlayerManager.RegisterCharacter(new SprinterSlugcat()); 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /ExampleSlugcat/SprinterSaveState.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using SlugBase; 3 | using System.Collections.Generic; 4 | using UnityEngine; 5 | 6 | namespace ExampleSlugcat 7 | { 8 | // Store extra information with the Sprinter's save file 9 | class SprinterSaveState : CustomSaveState 10 | { 11 | public bool isTurbo = false; 12 | 13 | public SprinterSaveState(PlayerProgression progression, SlugBaseCharacter character) : base(progression, character) 14 | { 15 | } 16 | 17 | public override void Load(Dictionary data) 18 | { 19 | isTurbo = data.TryGetValue("turbo", out string temp) ? bool.Parse(temp) : false; 20 | } 21 | 22 | public override void Save(Dictionary data) 23 | { 24 | data["turbo"] = isTurbo.ToString(); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /ExampleSlugcat/SprinterSlugcat.cs: -------------------------------------------------------------------------------- 1 | using SlugBase; 2 | using UnityEngine; 3 | 4 | namespace ExampleSlugcat 5 | { 6 | // Describes the character you want to add 7 | internal class SprinterSlugcat : SlugBaseCharacter 8 | { 9 | public SprinterSlugcat() : base("Sprinter", FormatVersion.V1, 0, true) { } 10 | 11 | // Custom // 12 | 13 | // Hooks are applied here 14 | protected override void Enable() 15 | { 16 | On.Player.MovementUpdate += Player_MovementUpdate; 17 | On.Player.ObjectEaten += Player_ObjectEaten; 18 | On.Player.Jump += Player_Jump; 19 | } 20 | 21 | // Hooks are disposed of here 22 | protected override void Disable() 23 | { 24 | On.Player.MovementUpdate -= Player_MovementUpdate; 25 | On.Player.ObjectEaten -= Player_ObjectEaten; 26 | On.Player.Jump -= Player_Jump; 27 | } 28 | 29 | // Attach some extra information to the Sprinter's save file 30 | public override CustomSaveState CreateNewSave(PlayerProgression progression) 31 | { 32 | return new SprinterSaveState(progression, this); 33 | } 34 | 35 | // Update stats when in turbo move 36 | private void Player_MovementUpdate(On.Player.orig_MovementUpdate orig, Player self, bool eu) 37 | { 38 | if (IsMe(self) && self.TryGetSave(out var save) && save.isTurbo) 39 | self.slugcatStats.runspeedFac = 5f; 40 | orig(self, eu); 41 | } 42 | 43 | // Go absolutely wild once a mushroom is eaten 44 | private void Player_ObjectEaten(On.Player.orig_ObjectEaten orig, Player self, IPlayerEdible edible) 45 | { 46 | if (IsMe(self) && edible is Mushroom && self.TryGetSave(out var save)) 47 | { 48 | save.isTurbo = true; 49 | } 50 | orig(self, edible); 51 | } 52 | 53 | // Add more height to all standard jumps 54 | private void Player_Jump(On.Player.orig_Jump orig, Player self) 55 | { 56 | orig(self); 57 | if (!IsMe(self)) return; 58 | 59 | if (self.TryGetSave(out var save) && save.isTurbo) 60 | self.jumpBoost += 9f; 61 | else 62 | self.jumpBoost += 3f; 63 | } 64 | 65 | 66 | // SlugBase // 67 | 68 | public override string DisplayName => "The Sprinter"; 69 | public override string Description => 70 | @"A lightspeed rodent whose supernatural speed stems from chillidogs and a curious glowing fungus. 71 | This is an example slugcat for the SlugBase framework."; 72 | 73 | public override Color? SlugcatColor(int slugcatCharacter, Color baseColor) 74 | { 75 | Color col = new Color(0.37f, 0.36f, 0.91f); 76 | 77 | if (slugcatCharacter == -1) 78 | return col; 79 | else 80 | return Color.Lerp(baseColor, col, 0.75f); 81 | } 82 | 83 | public override bool HasGuideOverseer => false; 84 | 85 | public override string StartRoom => "UW_I01"; 86 | 87 | protected override void GetStats(SlugcatStats stats) 88 | { 89 | stats.runspeedFac *= 1.5f; 90 | stats.poleClimbSpeedFac *= 1.5f; 91 | stats.corridorClimbSpeedFac *= 1.5f; 92 | stats.loudnessFac *= 2f; 93 | } 94 | 95 | public override void GetFoodMeter(out int maxFood, out int foodToSleep) 96 | { 97 | maxFood = 8; 98 | foodToSleep = 5; 99 | } 100 | 101 | // Play a short "cutscene", forcing the player to climb a pole when starting a new game 102 | public override void StartNewGame(Room room) 103 | { 104 | base.StartNewGame(room); 105 | 106 | // Make sure this is the right room 107 | if (room.abstractRoom.name != StartRoom) return; 108 | 109 | room.AddObject(new SprinterStart(room)); 110 | } 111 | 112 | public override CustomScene BuildScene(string sceneName) 113 | { 114 | RainWorld rw = Object.FindObjectOfType(); 115 | 116 | var scene = base.BuildScene(sceneName); 117 | 118 | // If not in turbo mode, hide some scene images 119 | if(sceneName == "SelectMenu") 120 | { 121 | bool turbo = false; 122 | try 123 | { 124 | turbo = bool.Parse(GetSaveSummary(rw).CustomData["turbo"]); 125 | } 126 | catch { } 127 | 128 | if (!turbo) 129 | scene.ApplyFilter(img => !img.HasTag("turboonly")); 130 | } 131 | 132 | return scene; 133 | } 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /ExampleSlugcat/SprinterStart.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | 3 | namespace ExampleSlugcat 4 | { 5 | // Plays a small "cutscene" at the start of the game 6 | internal class SprinterStart : UpdatableAndDeletable 7 | { 8 | private Player Sprinter => (room.game.Players.Count <= 0) ? null : (room.game.Players[0].realizedCreature as Player); 9 | private int timer = 0; 10 | private StartController startController; 11 | 12 | public SprinterStart(Room room) 13 | { 14 | this.room = room; 15 | } 16 | 17 | public override void Update(bool eu) 18 | { 19 | Player ply = Sprinter; 20 | if (ply == null) return; 21 | if (room.game.cameras[0].room != room) return; 22 | 23 | // Spawn the player at the correct place 24 | if (timer == 0) 25 | { 26 | room.game.cameras[0].MoveCamera(4); 27 | 28 | room.game.cameras[0].followAbstractCreature = null; 29 | 30 | if (room.game.cameras[0].hud == null) 31 | room.game.cameras[0].FireUpSinglePlayerHUD(ply); 32 | 33 | for (int i = 0; i < 2; i++) 34 | { 35 | ply.bodyChunks[i].HardSetPosition(room.MiddleOfTile(68, 30)); 36 | } 37 | 38 | ply.graphicsModule?.Reset(); 39 | 40 | startController = new StartController(this); 41 | ply.controller = startController; 42 | ply.playerState.foodInStomach = 5; 43 | 44 | ply.eatCounter = 15; 45 | AbstractPhysicalObject shroom = new AbstractConsumable(room.world, AbstractPhysicalObject.AbstractObjectType.Mushroom, null, new WorldCoordinate(room.abstractRoom.index, 68, 30, 0), room.game.GetNewID(), -1, -1, null); 46 | room.abstractRoom.AddEntity(shroom); 47 | shroom.RealizeInRoom(); 48 | shroom.realizedObject.firstChunk.HardSetPosition(ply.mainBodyChunk.pos + new Vector2(-30f, 0f)); 49 | ply.SlugcatGrab(shroom.realizedObject, 0); 50 | 51 | room.game.cameras[0].hud.foodMeter.NewShowCount(ply.FoodInStomach); 52 | room.game.cameras[0].hud.foodMeter.visibleCounter = 0; 53 | room.game.cameras[0].hud.foodMeter.fade = 0f; 54 | room.game.cameras[0].hud.foodMeter.lastFade = 0f; 55 | } 56 | 57 | // End the cutscene 58 | if (timer == 180) 59 | { 60 | ply.controller = null; 61 | ply.room.game.cameras[0].followAbstractCreature = ply.abstractCreature; 62 | Destroy(); 63 | } 64 | 65 | timer++; 66 | } 67 | 68 | // Makes Sprinter climb a pole without player input 69 | public class StartController : Player.PlayerController 70 | { 71 | public SprinterStart owner; 72 | 73 | public StartController(SprinterStart owner) 74 | { 75 | this.owner = owner; 76 | } 77 | 78 | public override Player.InputPackage GetInput() 79 | { 80 | int y; 81 | if (owner.timer < 5) y = 1; 82 | else if (owner.timer < 40) y = 0; 83 | else if (owner.timer < 165) y = 1; 84 | else y = 0; 85 | 86 | return new Player.InputPackage(false, 0, y, false, false, false, false, false); 87 | } 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # SlugBase 2 | A framework that simplifies adding new characters to Rain World. 3 | 4 | This mod targets Rain World 1.5, and will not be receiving further updates. Please use [SlugBase 2](https://github.com/SlimeCubed/SlugBaseRemix) for Rain World 1.9 and up. 5 | 6 | ## For Users 7 | Downloads can be found in the [Releases](https://github.com/SlimeCubed/SlugBase/releases/latest) tab.
Make sure to download the one called `SlugBase.dll`. 8 | SlugBase is a Partiality mod, so it may be applied with any mod loader that supports Partiality mods. This includes BepInEx. 9 | 10 | If you find any bugs or incompatibilities, first check the [Issues](https://github.com/SlimeCubed/SlugBase/issues) page. If the issue you are encountering isn't listed there, you can either open up a new issue or send me (Slime_Cubed#5880) a message on Discord. 11 | 12 | ## For Developers 13 | See the [Developer Guide](https://github.com/SlimeCubed/SlugBase/wiki/Developer-Guide). 14 | -------------------------------------------------------------------------------- /SlugBase.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.30804.86 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SlugBase", "SlugBase\SlugBase.csproj", "{E7B1AABF-4BE5-49D7-847D-24883F06261A}" 7 | EndProject 8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ExampleSlugcat", "ExampleSlugcat\ExampleSlugcat.csproj", "{87055141-6463-4F6F-B072-FB85F52B5A48}" 9 | EndProject 10 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BareMinimum", "BareMinimum\BareMinimum.csproj", "{C462D418-374C-4B99-BAF7-925248899242}" 11 | EndProject 12 | Global 13 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 14 | Debug|Any CPU = Debug|Any CPU 15 | Release|Any CPU = Release|Any CPU 16 | EndGlobalSection 17 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 18 | {E7B1AABF-4BE5-49D7-847D-24883F06261A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 19 | {E7B1AABF-4BE5-49D7-847D-24883F06261A}.Debug|Any CPU.Build.0 = Debug|Any CPU 20 | {E7B1AABF-4BE5-49D7-847D-24883F06261A}.Release|Any CPU.ActiveCfg = Release|Any CPU 21 | {E7B1AABF-4BE5-49D7-847D-24883F06261A}.Release|Any CPU.Build.0 = Release|Any CPU 22 | {87055141-6463-4F6F-B072-FB85F52B5A48}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 23 | {87055141-6463-4F6F-B072-FB85F52B5A48}.Debug|Any CPU.Build.0 = Debug|Any CPU 24 | {87055141-6463-4F6F-B072-FB85F52B5A48}.Release|Any CPU.ActiveCfg = Release|Any CPU 25 | {87055141-6463-4F6F-B072-FB85F52B5A48}.Release|Any CPU.Build.0 = Release|Any CPU 26 | {C462D418-374C-4B99-BAF7-925248899242}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 27 | {C462D418-374C-4B99-BAF7-925248899242}.Debug|Any CPU.Build.0 = Debug|Any CPU 28 | {C462D418-374C-4B99-BAF7-925248899242}.Release|Any CPU.ActiveCfg = Release|Any CPU 29 | {C462D418-374C-4B99-BAF7-925248899242}.Release|Any CPU.Build.0 = Release|Any CPU 30 | EndGlobalSection 31 | GlobalSection(SolutionProperties) = preSolution 32 | HideSolutionNode = FALSE 33 | EndGlobalSection 34 | GlobalSection(ExtensibilityGlobals) = postSolution 35 | SolutionGuid = {8485C5DD-03B8-43A9-91B4-9ED2AA80A302} 36 | EndGlobalSection 37 | EndGlobal 38 | -------------------------------------------------------------------------------- /SlugBase/AttachedField.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | /// 5 | /// A collection that attaches values to objects using . 6 | /// 7 | /// 8 | /// This is like ConditionalWeakTable, but with one major drawback: 9 | /// values that reference the key will stop the key from being garbage collected. 10 | /// Make sure that each instance of contains 11 | /// no references to the key, otherwise a memory leak may occur! 12 | /// 13 | /// The type to attach the value to. 14 | /// The type the the attached value. 15 | public class AttachedField 16 | { 17 | private static IEqualityComparer _comparer = new KeyComparer(); 18 | 19 | /// 20 | /// Called after a key is garbage collected. 21 | /// 22 | public event Action OnCulled; 23 | private Dictionary _dict = new Dictionary(_comparer); 24 | private int _lastGCCount = 0; 25 | 26 | /// 27 | /// Updates or attaches a value to an object. 28 | /// 29 | /// The object to attach to. 30 | /// The value to set. 31 | public void Set(TKey obj, TValue value) 32 | { 33 | if (obj == null) return; 34 | if (_dict.ContainsKey(obj)) 35 | _dict[obj] = value; 36 | else 37 | { 38 | Attach(obj, value); 39 | } 40 | } 41 | 42 | /// 43 | /// Detaches a value from an object. 44 | /// 45 | /// The object to remove the attached value from. 46 | public void Unset(TKey obj) 47 | { 48 | if (obj == null) return; 49 | _dict.Remove(obj); 50 | } 51 | 52 | private void Attach(TKey key, TValue value) 53 | { 54 | _dict[new WeakRefWithHash(key)] = value; 55 | // Only bother performing garbage collection when the dictionary increases in size 56 | CullDead(); 57 | } 58 | 59 | /// 60 | /// Retrieves a stored value for a given object. 61 | /// 62 | /// The object to get from. 63 | /// The previously set value for this object, or default() if unset. 64 | public TValue Get(TKey obj) 65 | { 66 | if (obj == null) throw new ArgumentNullException(nameof(obj)); 67 | if (_dict.TryGetValue(obj, out TValue value)) return value; 68 | return default(TValue); 69 | } 70 | 71 | /// 72 | /// Checks for and retrieves a stored value for a given object. 73 | /// 74 | /// The object to get from. 75 | /// The previously set value for this obejct. 76 | /// True if a value exists for , false otherwise. 77 | public bool TryGet(TKey obj, out TValue value) 78 | { 79 | if (obj == null) 80 | { 81 | value = default(TValue); 82 | return false; 83 | } 84 | return _dict.TryGetValue(obj, out value); 85 | } 86 | 87 | /// 88 | /// Sets or retrieves the value attached to object. 89 | /// 90 | /// The object key. 91 | /// The attached value, or default() if the value has not been set. 92 | public TValue this[TKey obj] 93 | { 94 | get => Get(obj); 95 | set => Set(obj, value); 96 | } 97 | 98 | /// 99 | /// Clears all entries. 100 | /// 101 | public void Clear() => _dict.Clear(); 102 | 103 | /// 104 | /// The number of entries currently stored. 105 | /// 106 | public int Count => _dict.Count; 107 | 108 | private List> _toRemove = new List>(); 109 | /// 110 | /// Removes entries for which the key has been garbage collected. 111 | /// 112 | public void CullDead() 113 | { 114 | // Assume the referenced objects are long-lived 115 | // Only cull dead refs when the garbage collector runs 116 | int gcCount = GC.CollectionCount(2); 117 | if (gcCount != _lastGCCount) 118 | _lastGCCount = gcCount; 119 | else return; 120 | 121 | // Search for dead references 122 | foreach (KeyValuePair pair in _dict) 123 | { 124 | if (!(pair.Key is WeakReference wr) || (!wr.IsAlive)) 125 | _toRemove.Add(pair); 126 | } 127 | // Remove them from the dictionary 128 | for (int i = _toRemove.Count - 1; i >= 0; i--) 129 | { 130 | OnCulled?.Invoke(_toRemove[i].Key as WeakReference, _toRemove[i].Value); 131 | _dict.Remove(_toRemove[i].Key); 132 | } 133 | _toRemove.Clear(); 134 | } 135 | 136 | private class WeakRefWithHash : WeakReference 137 | { 138 | public int hash; 139 | public WeakRefWithHash(object target) : base(target) 140 | { 141 | hash = target.GetHashCode(); 142 | } 143 | 144 | public override int GetHashCode() => hash; 145 | } 146 | 147 | private class KeyComparer : IEqualityComparer 148 | { 149 | public new bool Equals(object x, object y) 150 | { 151 | // Treat WeakReferences and the objects they reference as equal 152 | // ... and treat WeakReferences that reference the same object as equal 153 | if (x is WeakReference wrx) 154 | { 155 | x = wrx.Target; 156 | if (!wrx.IsAlive) x = wrx; 157 | } 158 | if (y is WeakReference wry) 159 | { 160 | y = wry.Target; 161 | if (!wry.IsAlive) y = wry; 162 | } 163 | return x == y; 164 | } 165 | 166 | public int GetHashCode(object obj) 167 | { 168 | // WeakRefWithHash already overrides GetHashCode, so no change needs to be made 169 | return obj.GetHashCode(); 170 | } 171 | } 172 | } -------------------------------------------------------------------------------- /SlugBase/Compatibility/FancySlugcats.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Reflection; 5 | using UnityEngine; 6 | using Partiality; 7 | using Partiality.Modloader; 8 | 9 | namespace SlugBase.Compatibility 10 | { 11 | internal static class FancySlugcats 12 | { 13 | public static void Apply() 14 | { 15 | try 16 | { 17 | Type fsMain = Type.GetType("FancySlugcats.Main, FancySlugcats"); 18 | if (fsMain == null) return; 19 | 20 | Debug.Log("Applying SlugBase compatibility changes for FancySlugcats..."); 21 | 22 | // FancySlugcats.Main.ShortcutColor (from PlayerGraphics.SlugcatColor) throws with indices above 4 23 | On.PlayerGraphics.SlugcatColor += FSPatch_SlugcatColor; 24 | 25 | // FancySlugcats.FancyPlayerGraphics.ctor throws with slugcat indices above 4 26 | On.Player.InitiateGraphicsModule += FSPatch_InitiateGraphicsModule; 27 | } 28 | catch(Exception e) 29 | { 30 | Debug.Log("Failed to apply compatibility changes. This shouldn't be fatal, but may cause compatibility issues."); 31 | Debug.Log(e); 32 | } 33 | } 34 | 35 | private static void FSPatch_InitiateGraphicsModule(On.Player.orig_InitiateGraphicsModule orig, Player self) 36 | { 37 | try 38 | { 39 | // First, try running it as normal 40 | orig(self); 41 | } 42 | catch 43 | { 44 | // If that fails, try running it with a modified character index 45 | int oldChar = self.playerState.slugcatCharacter; 46 | try 47 | { 48 | SlugBaseCharacter ply = PlayerManager.GetCustomPlayer(self.playerState.slugcatCharacter); 49 | if (ply != null) 50 | { 51 | // For SlugBase characters, use the character that it copies from 52 | self.playerState.slugcatCharacter = ply.InheritWorldFromSlugcat; 53 | } 54 | else 55 | { 56 | // For other characters, use the player number 57 | self.playerState.slugcatCharacter = self.playerState.playerNumber; 58 | } 59 | orig(self); 60 | } 61 | finally 62 | { 63 | self.playerState.slugcatCharacter = oldChar; 64 | } 65 | } 66 | } 67 | 68 | private static Color FSPatch_SlugcatColor(On.PlayerGraphics.orig_SlugcatColor orig, int i) 69 | { 70 | try { return orig(i); } 71 | catch 72 | { 73 | SlugBaseCharacter ply = PlayerManager.GetCustomPlayer(i); 74 | if(ply != null) 75 | { 76 | try 77 | { 78 | // Bypass SlugBase colors when using FancySlugcats 79 | PlayerManager.useOriginalColor = true; 80 | return orig(ply.InheritWorldFromSlugcat); 81 | } 82 | catch { } 83 | PlayerManager.useOriginalColor = false; 84 | } 85 | return Color.white; 86 | } 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /SlugBase/Compatibility/FlatmodeFix.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using UnityEngine; 6 | using RWCustom; 7 | 8 | namespace SlugBase.Compatibility 9 | { 10 | internal static class FlatmodeFix 11 | { 12 | public static void Apply() 13 | { 14 | On.Menu.SleepAndDeathScreen.Update += SleepAndDeathScreen_Update; 15 | } 16 | 17 | private static void SleepAndDeathScreen_Update(On.Menu.SleepAndDeathScreen.orig_Update orig, Menu.SleepAndDeathScreen self) 18 | { 19 | // Catch exception thrown by the vanilla game trying to access depth illustrations 20 | try 21 | { 22 | orig(self); 23 | } 24 | catch(ArgumentOutOfRangeException) 25 | { 26 | foreach(var illust in self.scene.depthIllustrations) 27 | illust.setAlpha = new float?(Mathf.Lerp(0.85f, 0.4f, self.fadeOutIllustration)); 28 | } 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /SlugBase/Compatibility/HookGenFix.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using UnityEngine; 5 | using System.Reflection; 6 | using MonoMod.RuntimeDetour; 7 | using System.Runtime.CompilerServices; 8 | using Debug = UnityEngine.Debug; 9 | 10 | namespace SlugBase.Compatibility 11 | { 12 | internal static class HookGenFix 13 | { 14 | private static Dictionary> hookMap = new Dictionary>(); 15 | 16 | public static void Apply() 17 | { 18 | try 19 | { 20 | // Find HookManager 21 | // This should return null if the user isn't running Partiality 22 | Type t = Type.GetType("MonoMod.RuntimeDetour.HookManager, MonoMod.RuntimeDetour, Version=18.6.0.33006, Culture=neutral, PublicKeyToken=null"); 23 | if (t == null) return; 24 | 25 | Debug.Log("Applying SlugBase compatibility changes for Partiality's HookGen..."); 26 | 27 | ApplyToPartiality(); 28 | 29 | } 30 | catch (Exception e) 31 | { 32 | Debug.Log("Failed to apply compatibility changes. This shouldn't be fatal, but may cause compatibility issues."); 33 | Debug.Log(e); 34 | } 35 | } 36 | 37 | private static void ApplyToPartiality() 38 | { 39 | // This is sequestered here since HookManager.Add might not exist 40 | new Hook((Action)HookManager.Add, (Action, MethodBase, Delegate>)HookManager_Add); 41 | new Hook((Action)HookManager.Remove, (Action, MethodBase, Delegate>)HookManager_Remove); 42 | } 43 | 44 | public static void HookManager_Add(Action orig, MethodBase method, Delegate hookDelegate) 45 | { 46 | HookInfo info = new HookInfo(method, hookDelegate); 47 | Stack stack; 48 | if (!hookMap.TryGetValue(info, out stack)) 49 | stack = hookMap[info] = new Stack(); 50 | Hook t = new Hook(method, hookDelegate); 51 | stack.Push(t); 52 | } 53 | 54 | public static void HookManager_Remove(Action orig, MethodBase method, Delegate hookDelegate) 55 | { 56 | HookInfo key = new HookInfo(method, hookDelegate); 57 | Stack stack; 58 | if (!hookMap.TryGetValue(key, out stack)) 59 | return; 60 | Hook hook = stack.Pop(); 61 | hook.Undo(); 62 | hook.Free(); 63 | if (stack.Count == 0) 64 | hookMap.Remove(key); 65 | } 66 | 67 | /// 68 | /// Everything needed to identify a hook 69 | /// 70 | private struct HookInfo 71 | { 72 | public HookInfo(MethodBase from, Delegate to) 73 | { 74 | this.from = from; 75 | this.to = to.Method; 76 | target = to.Target; 77 | } 78 | 79 | public MethodBase from; 80 | public MethodBase to; 81 | public object target; 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /SlugBase/Compatibility/JollyCoop.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using UnityEngine; 6 | using JollyCoop; 7 | using MonoMod.RuntimeDetour; 8 | using System.Reflection; 9 | 10 | namespace SlugBase.Compatibility 11 | { 12 | internal static class JollyCoop 13 | { 14 | private static int[] savedPlayerCharacters; 15 | 16 | public static void Apply() 17 | { 18 | try 19 | { 20 | ApplyLegacyFixes(); 21 | } 22 | catch 23 | { 24 | // This will throw if Jolly is not installed 25 | } 26 | 27 | try 28 | { 29 | ApplyIntegrations(); 30 | } 31 | catch 32 | { 33 | // This will throw if Jolly is not installed 34 | } 35 | } 36 | 37 | private static void ApplyIntegrations() 38 | { 39 | new Hook( 40 | Type.GetType("JollyCoop.HUDHK, JollyCoop").GetMethod("GetPlayerColor", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static), 41 | new Func, Player, Color>(HUDHK_GetPlayerColor) 42 | ); 43 | 44 | var playerIconType = Type.GetType("JollyCoop.PlayerMeter+PlayerIcon, JollyCoop"); 45 | _PlayerIcon_color = playerIconType.GetField("color", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); 46 | _PlayerIcon_playerNumber = playerIconType.GetField("playerNumber", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); 47 | _PlayerIcon_meter = playerIconType.GetField("meter", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); 48 | new Hook( 49 | playerIconType.GetMethod("Update", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance), 50 | new Action, object>(PlayerIcon_Update) 51 | ); 52 | } 53 | 54 | private static FieldInfo _PlayerIcon_color; 55 | private static FieldInfo _PlayerIcon_playerNumber; 56 | private static FieldInfo _PlayerIcon_meter; 57 | // The original method sets the HUD color without using HUDHK.GetPlayerColor 58 | private static void PlayerIcon_Update(Action orig, object self) 59 | { 60 | orig(self); 61 | HUD.HudPart meter = (HUD.HudPart)_PlayerIcon_meter.GetValue(self); 62 | if (meter.hud.owner is Player ply1) 63 | { 64 | var game = ply1.abstractPhysicalObject.world.game; 65 | int plyNum = (int)_PlayerIcon_playerNumber.GetValue(self); 66 | if (game.IsStorySession && plyNum >= 0 && plyNum < game.Players.Count) 67 | { 68 | var iconPly = game.Players[plyNum]?.realizedObject as Player; 69 | if (iconPly != null && PlayerManager.GetCustomPlayer(iconPly) != null) 70 | _PlayerIcon_color.SetValue(self, PlayerManager.GetSlugcatColor(iconPly)); 71 | } 72 | } 73 | } 74 | 75 | // Jolly passes in SlugcatStats.name to GetPlayerColor when it should pass in PlayerState.slugcatCharacter 76 | // These will always be the same for Jolly, but SlugBase can cause them to be misaligned 77 | private static Color HUDHK_GetPlayerColor(Func orig, Player self) 78 | { 79 | var cha = PlayerManager.GetCustomPlayer(self); 80 | return cha != null ? PlayerManager.GetSlugcatColor(self) : orig(self); 81 | } 82 | 83 | private static void ApplyLegacyFixes() 84 | { 85 | var jolly = (JollyMod)Partiality.PartialityManager.Instance.modManager.loadedMods.First(mod => mod is JollyMod); 86 | if (jolly.Version == "1.6.6" || jolly.Version == "1.6.7") 87 | { 88 | Debug.Log("Detected Jolly version 1.6.6 or 1.6.7! Applying hotfixes..."); 89 | if (jolly.Version == "1.6.6") 90 | { 91 | new Hook( 92 | typeof(JollyOption).GetMethod(nameof(JollyOption.ConfigOnChange), BindingFlags.Public | BindingFlags.Instance), 93 | new Action, JollyOption>(JollyOption_ConfigOnChange) 94 | ); 95 | } 96 | new Hook( 97 | typeof(Player).GetProperty(nameof(Player.slugcatStats), BindingFlags.Instance | BindingFlags.Public).GetGetMethod(), 98 | new Func(get_SlugcatStatsHK) 99 | ); 100 | On.Player.ctor += Player_ctor; 101 | On.RainWorldGame.ctor += RainWorldGame_ctor; 102 | On.RainWorldGame.ShutDownProcess += RainWorldGame_ShutDownProcess; 103 | } 104 | } 105 | 106 | // Player 2 never properly inherits stats from player 0 since it isn't spawned by Jolly 107 | private static void RainWorldGame_ctor(On.RainWorldGame.orig_ctor orig, RainWorldGame self, ProcessManager manager) 108 | { 109 | var chars = JollyMod.config.playerCharacters; 110 | savedPlayerCharacters = (int[])chars.Clone(); 111 | for (int i = 1; i < chars.Length; i++) 112 | { 113 | if (chars[i] == -1) 114 | chars[i] = chars[0]; 115 | } 116 | orig(self, manager); 117 | } 118 | 119 | // Ensure that modifications to the player character array (e.g., changing -1s to player 1's character) are reverted when leaving the game 120 | private static void RainWorldGame_ShutDownProcess(On.RainWorldGame.orig_ShutDownProcess orig, RainWorldGame self) 121 | { 122 | orig(self); 123 | 124 | if (savedPlayerCharacters != null) 125 | { 126 | JollyMod.config.playerCharacters = savedPlayerCharacters; 127 | savedPlayerCharacters = null; 128 | } 129 | } 130 | 131 | 132 | // A player character of 2 is impossible under normal circumstances, so make it possible 133 | // Jolly has some checks for character == 3 that re-implement Hunter's abilities. Is this a bug or a feature? 134 | // Obsolete with version 1.6.7 135 | private static void JollyOption_ConfigOnChange(Action orig, JollyOption self) 136 | { 137 | orig(self); 138 | for (int i = 0; i < JollyMod.config.playerCharacters.Length; i++) 139 | { 140 | if (JollyMod.config.playerCharacters[i] == 3) 141 | JollyMod.config.playerCharacters[i]--; 142 | } 143 | } 144 | 145 | // Player stats are never cleared and used cached values when possible, leading to stats carrying over between games 146 | private static void Player_ctor(On.Player.orig_ctor orig, Player self, AbstractCreature abstractCreature, World world) 147 | { 148 | var playerState = (PlayerState)abstractCreature.state; 149 | PlayerHK.stats[playerState.playerNumber] = null; 150 | orig(self, abstractCreature, world); 151 | } 152 | 153 | // Copied from PlayerHK.get_SlugcatStatsHK 154 | // The original hook is removed immediately since it is in a using statement 155 | // foodToHibernate is only written to when it won't be read from and only read from when it hasn't been written to 156 | // Player character 3 (Nightcat, or Hunter without the index fix) always inherits from the save's stats 157 | public static SlugcatStats get_SlugcatStatsHK(PlayerHK.orig_slugcatStats orig_slugcatStats, Player self) 158 | { 159 | SlugcatStats result; 160 | try 161 | { 162 | var globalStats = self.abstractPhysicalObject.world.game.session.characterStats; 163 | 164 | int playerNumber = self.playerState.playerNumber; 165 | int playerChar = JollyMod.config.playerCharacters[playerNumber]; 166 | if (playerNumber == 0) 167 | { 168 | result = orig_slugcatStats(self); 169 | } 170 | else 171 | { 172 | if (PlayerHK.stats[playerNumber] == null) 173 | { 174 | PlayerHK.stats[playerNumber] = new SlugcatStats(playerChar, self.Malnourished); 175 | PlayerHK.stats[playerNumber].foodToHibernate = globalStats.foodToHibernate; 176 | PlayerHK.stats[playerNumber].maxFood = globalStats.maxFood; 177 | } 178 | result = PlayerHK.stats[playerNumber]; 179 | } 180 | } 181 | catch (Exception e) 182 | { 183 | Debug.Log("slugcatStats hook failed!"); 184 | Debug.Log(e); 185 | result = orig_slugcatStats(self); 186 | } 187 | return result; 188 | } 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /SlugBase/Config/CharacterSelectButton.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using Menu; 4 | using RWCustom; 5 | using UnityEngine; 6 | using System.Linq; 7 | using MenuColors = Menu.Menu.MenuColors; 8 | using PlayerDescriptor = SlugBase.ArenaAdditions.PlayerDescriptor; 9 | 10 | namespace SlugBase.Config 11 | { 12 | /// 13 | /// A menu object that selects from all available player characters. 14 | /// 15 | public class CharacterSelectButton : ButtonTemplate 16 | { 17 | const float selectorHeight = 30f; 18 | const float buttonHeight = 22f; 19 | const float buttonMargin = 1f; 20 | const float buttonXOffset = 3f; 21 | 22 | private float childOffset; 23 | private bool expand; 24 | private readonly int playerNumber; 25 | private readonly RoundedRect back; 26 | private readonly RoundedRect selectRect; 27 | private readonly List labels; 28 | private readonly float expandedHeight; 29 | 30 | /// 31 | /// The this button belongs to. 32 | /// 33 | public CharacterSelectGroup Group { get; } 34 | 35 | /// 36 | /// The character this player has selected. 37 | /// 38 | public PlayerDescriptor SelectedCharacter => Group.GetPlayer(playerNumber); 39 | 40 | /// 41 | /// True if the button's menu is opening or fully open, false otherwise. 42 | /// 43 | public bool Active => expand; 44 | 45 | /// 46 | /// True if the button's menu is open or in its opening or closing animation. 47 | /// 48 | /// 49 | /// Menu objects that could be covered by this button's menu should be disabled while this is true. 50 | /// 51 | public bool Expanded => expand || size.y > selectorHeight; 52 | 53 | private int SelectedLabel 54 | { 55 | get 56 | { 57 | var character = Group.GetPlayer(playerNumber); 58 | return Math.Max(labels.FindIndex(l => l.player == character), 0); 59 | } 60 | } 61 | 62 | /// 63 | /// Creates a new character select button. 64 | /// 65 | /// The menu this button belongs to. 66 | /// The menu object this button is a sub-object of. 67 | /// The position of this button relative to its . 68 | /// The width of this button in pixels. 69 | /// The this button belongs to. may be passed in to create a new group. 70 | /// The player number this button should display. 71 | public CharacterSelectButton(Menu.Menu menu, MenuObject owner, Vector2 pos, float width, CharacterSelectGroup group, int playerNumber) : base(menu, owner, pos, new Vector2(width, selectorHeight)) 72 | { 73 | if (menu == null) throw new ArgumentNullException(nameof(menu)); 74 | if (owner == null) throw new ArgumentNullException(nameof(owner)); 75 | 76 | this.playerNumber = playerNumber; 77 | Group = group ?? new CharacterSelectGroup(); 78 | 79 | back = new RoundedRect(menu, this, new Vector2(), size, true); 80 | subObjects.Add(back); 81 | selectRect = new RoundedRect(menu, this, new Vector2(), size, false); 82 | subObjects.Add(selectRect); 83 | 84 | labels = new List(); 85 | 86 | float y = 0f; 87 | int i = 0; 88 | 89 | // Add non-SlugBase slugcats 90 | foreach(var name in Enum.GetValues(typeof(SlugcatStats.Name))) 91 | { 92 | if (PlayerManager.GetCustomPlayer((int)name) != null) continue; 93 | 94 | var label = new CharacterLabel(menu, this, new PlayerDescriptor((int)name), i++); 95 | labels.Add(label); 96 | y += buttonHeight; 97 | } 98 | 99 | // Add SlugBase slugcats 100 | foreach (var cha in PlayerManager.GetCustomPlayers()) 101 | { 102 | Color baseColor = PlayerGraphics.SlugcatColor(cha.SlugcatIndex); 103 | var label = new CharacterLabel(menu, this, new PlayerDescriptor(cha), i++); 104 | labels.Add(label); 105 | y += buttonHeight; 106 | } 107 | 108 | subObjects.AddRange(labels.Select(l => (MenuObject)l)); 109 | 110 | // This sets the height to fit the buttons exactly 111 | expandedHeight = labels.Count * buttonHeight + selectorHeight - buttonHeight; 112 | } 113 | 114 | /// 115 | /// Executed by the menu system when this button is clicked. This should not be called normally. 116 | /// 117 | public override void Clicked() 118 | { 119 | base.Clicked(); 120 | 121 | if (!expand) 122 | { 123 | expand = true; 124 | menu.PlaySound(SoundID.MENU_Button_Standard_Button_Pressed); 125 | } 126 | } 127 | 128 | /// 129 | /// Executed by the menu system when a signal is sent from a parent object. This should not be called normally. 130 | /// 131 | public override void Singal(MenuObject sender, string message) 132 | { 133 | switch(message) 134 | { 135 | case "SELECT": 136 | expand = !expand; 137 | break; 138 | default: 139 | base.Singal(sender, message); 140 | break; 141 | } 142 | } 143 | 144 | /// 145 | /// Executed by the menu system every fixed-timestep update. This should not be called normally. 146 | /// 147 | public override void Update() 148 | { 149 | // Updating the children before updaing the size of this element is important to keep them in sync 150 | // Without it, the children are delayed by a small amount 151 | Vector2 nextSize = size; 152 | nextSize.y = Custom.LerpAndTick(nextSize.y, expand ? expandedHeight : selectorHeight, 0.1f, 10f); 153 | 154 | back.fillAlpha = Custom.LerpMap(nextSize.y, selectorHeight, 35f, Mathf.Lerp(0.3f, 0.6f, buttonBehav.col), 1f); 155 | if (buttonBehav.clicked) 156 | { 157 | back.addSize = new Vector2(0f, 0f); 158 | selectRect.addSize = new Vector2(0f, 0f); 159 | } 160 | else 161 | { 162 | back.addSize = new Vector2(10f, 6f) * (buttonBehav.sizeBump + 0.5f * Mathf.Sin(buttonBehav.extraSizeBump * Mathf.PI)); 163 | selectRect.addSize = new Vector2(2f, -2f) * (buttonBehav.sizeBump + 0.5f * Mathf.Sin(buttonBehav.extraSizeBump * Mathf.PI)); 164 | } 165 | 166 | 167 | float deltaY = nextSize.y - size.y; 168 | 169 | float targetOffset = (selectorHeight - buttonHeight) / 2f - (expand ? 0f : SelectedLabel * buttonHeight); 170 | if(Mathf.Sign(deltaY) == Mathf.Sign(targetOffset - childOffset) && deltaY != 0f) 171 | { 172 | childOffset = Mathf.MoveTowards(childOffset, targetOffset, Mathf.Abs(deltaY)); 173 | } 174 | else 175 | { 176 | childOffset = targetOffset; 177 | } 178 | 179 | base.Update(); 180 | 181 | size = nextSize; 182 | back.size = size; 183 | selectRect.size = size; 184 | buttonBehav.greyedOut = size.y > selectorHeight; 185 | 186 | // Stop the selector from going offscreen 187 | float heightAboveScreen = ScreenPos.y + size.y - Futile.screen.height; 188 | if (heightAboveScreen > 0f) 189 | pos.y -= heightAboveScreen; 190 | } 191 | 192 | /// 193 | /// Executed by the menu system every frame. This should not be called normally. 194 | /// 195 | public override void GrafUpdate(float timeStacker) 196 | { 197 | base.GrafUpdate(timeStacker); 198 | 199 | Color fillColor = Color.Lerp(Menu.Menu.MenuRGB(MenuColors.Black), Menu.Menu.MenuRGB(MenuColors.White), Mathf.Lerp(buttonBehav.lastFlash, buttonBehav.flash, timeStacker)); 200 | for (int i = 0; i < 9; i++) 201 | { 202 | back.sprites[i].color = fillColor; 203 | } 204 | float selectAlpha = 0.5f + 0.5f * Mathf.Sin(Mathf.Lerp(buttonBehav.lastSin, buttonBehav.sin, timeStacker) / 30f * Mathf.PI * 2f); 205 | selectAlpha *= buttonBehav.sizeBump; 206 | 207 | for (int j = 0; j < 8; j++) 208 | { 209 | selectRect.sprites[j].color = MyColor(timeStacker); 210 | selectRect.sprites[j].alpha = selectAlpha; 211 | } 212 | } 213 | 214 | /// 215 | /// Used by the menu system to determine what color this button should be. This should not be called normally. 216 | /// 217 | public override Color MyColor(float timeStacker) 218 | { 219 | if (expand || buttonBehav.greyedOut) 220 | return Menu.Menu.MenuRGB(MenuColors.MediumGrey); 221 | else 222 | return base.MyColor(timeStacker); 223 | } 224 | 225 | // An icon and name label 226 | private class CharacterLabel : ButtonTemplate 227 | { 228 | public readonly PlayerDescriptor player; 229 | private readonly int order; 230 | private readonly CreatureSymbol icon; 231 | private readonly FLabel name; 232 | private CharacterSelectButton Owner => (CharacterSelectButton)owner; 233 | 234 | public CharacterLabel(Menu.Menu menu, CharacterSelectButton owner, PlayerDescriptor player, int order) : base(menu, owner, new Vector2(), new Vector2(owner.size.x, buttonHeight - buttonMargin * 2f)) 235 | { 236 | this.player = player; 237 | this.order = order; 238 | 239 | icon = new CreatureSymbol(MultiplayerUnlocks.SymbolDataForSandboxUnlock(MultiplayerUnlocks.SandboxUnlockID.Slugcat), Container); 240 | icon.Show(false); 241 | icon.lastShowFlash = 0f; 242 | icon.showFlash = 0f; 243 | icon.myColor = player.Color; 244 | 245 | // Remove "The" from the start of names 246 | var prettyName = player.name; 247 | if (prettyName.StartsWith("The ", StringComparison.OrdinalIgnoreCase)) 248 | prettyName = prettyName.Substring(4).TrimStart(); 249 | 250 | name = new FLabel("font", prettyName); 251 | name.anchorX = 0f; 252 | name.anchorY = 0.5f; 253 | Container.AddChild(name); 254 | } 255 | 256 | public override void RemoveSprites() 257 | { 258 | base.RemoveSprites(); 259 | 260 | icon.RemoveSprites(); 261 | name.RemoveFromContainer(); 262 | } 263 | 264 | public override void Update() 265 | { 266 | base.Update(); 267 | icon.Update(); 268 | 269 | pos.x = buttonMargin; 270 | pos.y = buttonMargin + buttonHeight * order + Owner.childOffset; 271 | buttonBehav.greyedOut = pos.y < 0f || pos.y + size.y > Owner.size.y; 272 | } 273 | 274 | public override void Clicked() 275 | { 276 | base.Clicked(); 277 | 278 | Owner.Group.SetPlayer(Owner.playerNumber, player); 279 | 280 | menu.PlaySound(SoundID.MENU_Button_Successfully_Assigned); 281 | 282 | Singal(this, "SELECT"); 283 | } 284 | 285 | public override void GrafUpdate(float timeStacker) 286 | { 287 | base.GrafUpdate(timeStacker); 288 | 289 | var drawPos = DrawPos(timeStacker) + new Vector2(0.1f, 0.1f); 290 | var relDrawPos = Vector2.Lerp(lastPos, pos, timeStacker); 291 | var drawSize = DrawSize(timeStacker); 292 | 293 | var iconPos = drawPos; 294 | iconPos.x += buttonXOffset; 295 | iconPos.y += drawSize.y / 2f; 296 | iconPos.x += drawSize.y / 2f; 297 | icon.Draw(timeStacker, iconPos); 298 | 299 | name.SetPosition(iconPos + new Vector2(drawSize.y / 2f + 5f, 0f)); 300 | 301 | bool visible = relDrawPos.y >= 0f && relDrawPos.y + drawSize.y <= Owner.DrawSize(timeStacker).y; 302 | icon.symbolSprite.isVisible = visible; 303 | name.isVisible = visible; 304 | name.color = Color.Lerp(Menu.Menu.MenuColor(MenuColors.MediumGrey).rgb, Color.white, Mathf.Lerp(buttonBehav.lastCol, buttonBehav.col, timeStacker)); 305 | } 306 | } 307 | } 308 | } 309 | -------------------------------------------------------------------------------- /SlugBase/Config/CharacterSelectGroup.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using PlayerDescriptor = SlugBase.ArenaAdditions.PlayerDescriptor; 6 | 7 | namespace SlugBase.Config 8 | { 9 | /// 10 | /// Represents a group of character selections. 11 | /// This may include any number of multi-instance or vanilla characters or one repeated single-instance character. 12 | /// 13 | public class CharacterSelectGroup 14 | { 15 | private readonly Dictionary characters = new Dictionary(); 16 | private PlayerDescriptor defaultCharacter = new PlayerDescriptor(0); 17 | 18 | /// 19 | /// Creates an empty character group. 20 | /// 21 | public CharacterSelectGroup() 22 | { 23 | } 24 | 25 | /// 26 | /// Sets a player, specified by index, to use a the given . 27 | /// 28 | /// 29 | /// This may change other selections in the same group. 30 | /// 31 | /// The index of the player. 32 | /// The character that player should use. 33 | public void SetPlayer(int playerNumber, PlayerDescriptor character) 34 | { 35 | if (character == null) throw new ArgumentNullException(nameof(character)); 36 | 37 | if (character == defaultCharacter) 38 | { 39 | characters.Remove(playerNumber); 40 | } 41 | else 42 | { 43 | if (character.MultiInstance) 44 | { 45 | // Clear all single-instance characters when any others are selected 46 | if (!defaultCharacter.MultiInstance) 47 | SetAllPlayers(new PlayerDescriptor(0)); 48 | 49 | characters[playerNumber] = character; 50 | } 51 | else 52 | { 53 | // Multi-instance characters will always override others 54 | SetAllPlayers(character); 55 | } 56 | } 57 | } 58 | 59 | /// 60 | /// Sets all players to use the given 61 | /// 62 | /// The character that all players should use. 63 | public void SetAllPlayers(PlayerDescriptor character) 64 | { 65 | defaultCharacter = character; 66 | characters.Clear(); 67 | } 68 | 69 | /// 70 | /// Retrieves the characer a player, specified by index, has selected. 71 | /// 72 | /// The index of the player to check. 73 | public PlayerDescriptor GetPlayer(int playerNumber) 74 | { 75 | if (characters.TryGetValue(playerNumber, out var res)) 76 | return res; 77 | else 78 | return defaultCharacter; 79 | } 80 | 81 | /// 82 | /// Creates a string representation of this group of characters. 83 | /// 84 | public override string ToString() 85 | { 86 | StringBuilder sb = new StringBuilder(); 87 | 88 | // Header 89 | sb.AppendLine("V2"); 90 | 91 | // Default 92 | sb.AppendLine(defaultCharacter.ToString()); 93 | 94 | // Key/value pairs for all individual selections 95 | foreach (var pair in characters) 96 | sb.AppendLine($"{pair.Key}:{pair.Value}"); 97 | 98 | return sb.ToString(); 99 | } 100 | 101 | /// 102 | /// Loads a group of characters from a string created with . 103 | /// 104 | /// 105 | /// The output may differ from the original group if any of the constituent characters have been uninstalled or have changed to true. 106 | /// It does this to ensure that it is always a valid group, meaning that it will contain either one single-instance character or many multi-instance characters. 107 | /// 108 | /// The string to parse. 109 | internal static CharacterSelectGroup FromString(string s) 110 | { 111 | var group = new CharacterSelectGroup(); 112 | group.SetFromString(s); 113 | return group; 114 | } 115 | 116 | /// 117 | public void SetFromString(string s) 118 | { 119 | SetAllPlayers(new PlayerDescriptor(0)); 120 | 121 | string[] lines = s.Split(new string[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries); 122 | 123 | int i; 124 | switch (lines[0]) 125 | { 126 | // V2: Header and default character, then key/value pairs for the rest 127 | case "V2": 128 | SetAllPlayers(PlayerDescriptor.FromString(lines[1])); 129 | for (i = 2; i < lines.Length; i++) 130 | { 131 | var line = lines[i]; 132 | int split = line.IndexOf(':'); 133 | 134 | int playerNum = int.Parse(line.Substring(0, split)); 135 | SetPlayer(playerNum, PlayerDescriptor.FromString(line.Substring(split + 1))); 136 | } 137 | break; 138 | 139 | // V1: No header, just a list of player descriptors 140 | default: 141 | for (i = 0; i < lines.Length; i++) 142 | { 143 | SetPlayer(i, PlayerDescriptor.FromString(lines[i])); 144 | } 145 | break; 146 | } 147 | } 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /SlugBase/CustomSaveState.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | 6 | namespace SlugBase 7 | { 8 | /// 9 | /// Contains the save data for any characters added through SlugBase. 10 | /// 11 | /// 12 | /// If you need to save extra data for your character, you should create a class that inherits from this. 13 | /// 14 | public class CustomSaveState : SaveState 15 | { 16 | private static bool appliedHooks = false; 17 | 18 | /// 19 | /// Creates a new representation of a SlugBase character's save state. 20 | /// 21 | /// The instance to attach this save state to. 22 | /// The SlugBase character that owns this save state. 23 | /// Thrown when is null. 24 | public CustomSaveState(PlayerProgression progression, SlugBaseCharacter character) : base(character.SlugcatIndex, progression) 25 | { 26 | if (character == null) throw new ArgumentException("Character may not be null.", nameof(character)); 27 | Character = character; 28 | 29 | if (!appliedHooks) { 30 | appliedHooks = true; 31 | ApplyHooks(); 32 | } 33 | } 34 | 35 | /// 36 | /// The that owns this save state. 37 | /// 38 | public SlugBaseCharacter Character { get; private set; } 39 | 40 | /// 41 | /// Converts save data for this character to strings to enter into a dictionary. 42 | /// This data is not saved when the player dies. 43 | /// 44 | /// 45 | /// should be overridden to handle the same values. 46 | /// 47 | /// The empty dictionary to add your data to. 48 | public virtual void Save(Dictionary data) 49 | { 50 | } 51 | 52 | /// 53 | /// Loads saved data for this character from entries in a dictionary. 54 | /// This data is loaded from the last time the player survived a cycle. 55 | /// 56 | /// 57 | /// should be overridden to handle the same values. 58 | /// 59 | /// The dictionary to read your data from. 60 | public virtual void Load(Dictionary data) 61 | { 62 | } 63 | 64 | /// 65 | /// Converts death-persistant save data for this character to strings to enter into a dictionary. 66 | /// This data is not reset when the player dies. 67 | /// 68 | /// 69 | /// Saved values that change on quit or death should be changed here. 70 | /// This is saved as a death once when starting the cycle, so quitting the game 71 | /// early counts as a death if it is not overwritten by the end of the cycle. 72 | /// should be overridden to handle the same values. 73 | /// 74 | /// The empty dictionary to add your data to. 75 | /// True if the player has quit or died. 76 | /// True if the player has quit. 77 | public virtual void SavePermanent(Dictionary data, bool asDeath, bool asQuit) 78 | { 79 | } 80 | 81 | /// 82 | /// Loads death-persistant saved data for this character from entries in a dictionary. 83 | /// This data is not reset when the player dies. 84 | /// 85 | /// 86 | /// should be overridden to handle the same values. 87 | /// 88 | /// The dictionary to read your data from. 89 | public virtual void LoadPermanent(Dictionary data) 90 | { 91 | } 92 | 93 | #region Hooks 94 | 95 | // Use hooks to simulate inheritance 96 | private static void ApplyHooks() 97 | { 98 | On.SaveState.SaveToString += SaveState_SaveToString; 99 | On.SaveState.LoadGame += SaveState_LoadGame; 100 | } 101 | 102 | private static string SaveState_SaveToString(On.SaveState.orig_SaveToString orig, SaveState self) 103 | { 104 | if(!(self is CustomSaveState css)) 105 | return orig(self); 106 | 107 | StringBuilder sb = new StringBuilder(); 108 | string customData = css.SaveCustomToString(); 109 | sb.Append("SLUGBASE"); 110 | sb.Append(customData ?? ""); 111 | sb.Append(""); 112 | 113 | customData = css.SaveCustomPermanentToString(false, false); 114 | sb.Append("SLUGBASEPERSISTENT"); 115 | sb.Append(customData ?? ""); 116 | sb.Append(""); 117 | 118 | return orig(self) + sb.ToString(); 119 | } 120 | 121 | private static void SaveState_LoadGame(On.SaveState.orig_LoadGame orig, SaveState self, string str, RainWorldGame game) 122 | { 123 | if(!(self is CustomSaveState css)) 124 | { 125 | orig(self, str, game); 126 | return; 127 | } 128 | 129 | // This will produce extraneous debugging logs - consider somehow suppressing them 130 | orig(self, str, game); 131 | 132 | string customStartRoom = css.Character.StartRoom; 133 | if (str == string.Empty && customStartRoom != null) 134 | self.denPosition = customStartRoom; 135 | 136 | var data = DataFromString(SearchForSavePair(str, "SLUGBASE", "", "")); 137 | var persistData = DataFromString(SearchForSavePair(str, "SLUGBASEPERSISTENT", "", "")); 138 | 139 | css.Load(data); 140 | css.LoadPermanent(persistData); 141 | } 142 | 143 | private static string SearchForSavePair(string input, string key, string separator, string cap) 144 | { 145 | int start = input.IndexOf(key + separator); 146 | if (start == -1) return null; 147 | start += key.Length + separator.Length; 148 | int end = input.IndexOf(cap, start); 149 | if (end == -1) return input.Substring(start); 150 | return input.Substring(start, end - start); 151 | } 152 | 153 | #endregion Hooks 154 | 155 | 156 | internal string SaveCustomToString() 157 | { 158 | var data = new Dictionary(); 159 | Save(data); 160 | return DataToString(data); 161 | } 162 | 163 | internal string SaveCustomPermanentToString(bool asDeath, bool asQuit) 164 | { 165 | var data = new Dictionary(); 166 | SavePermanent(data, asDeath, asQuit); 167 | return DataToString(data); 168 | } 169 | 170 | internal static Dictionary DataFromString(string dataString) 171 | { 172 | var data = new Dictionary(); 173 | if (string.IsNullOrEmpty(dataString)) return data; 174 | string[] entries = dataString.Split(','); 175 | for(int i = 0; i < entries.Length; i++) 176 | { 177 | string entry = entries[i]; 178 | int split = entry.IndexOf(':'); 179 | if (split == -1) continue; 180 | 181 | string key = Unescape(entry.Substring(0, split)); 182 | string value = Unescape(entry.Substring(split + 1)); 183 | data[key] = value; 184 | } 185 | return data; 186 | } 187 | 188 | internal static string DataToString(Dictionary data) 189 | { 190 | StringBuilder sb = new StringBuilder(); 191 | foreach(var pair in data) 192 | { 193 | sb.Append(Escape(pair.Key)); 194 | sb.Append(':'); 195 | sb.Append(Escape(pair.Value)); 196 | sb.Append(','); 197 | } 198 | return sb.ToString(); 199 | } 200 | 201 | // Because of how the save file is formatted, string values such as will permanently corrupt a save state. 202 | // Although this is unlikely to happen, it shouldn't be allowed to happen in the first place. 203 | // Escape "<" to "\L", ">" to "\G", and "\" to "\\" 204 | private static string Escape(string value) 205 | { 206 | value = value.Replace("\\", "\\\\"); 207 | value = value.Replace("<", "\\L"); 208 | value = value.Replace(">", "\\G"); 209 | value = value.Replace(":", "\\C"); 210 | value = value.Replace(",", "\\c"); 211 | return value; 212 | } 213 | 214 | private static string Unescape(string value) 215 | { 216 | StringBuilder sb = new StringBuilder(value.Length); 217 | int i = 0; 218 | bool escape = false; 219 | while(i < value.Length) 220 | { 221 | char c = value[i]; 222 | if (escape) 223 | { 224 | escape = false; 225 | switch(c) 226 | { 227 | case '\\': sb.Append('\\'); break; 228 | case 'L' : sb.Append('<' ); break; 229 | case 'G' : sb.Append('>' ); break; 230 | case 'C' : sb.Append(':' ); break; 231 | case 'c' : sb.Append(',' ); break; 232 | default : sb.Append(c ); break; 233 | } 234 | } 235 | else 236 | { 237 | if (c == '\\') escape = true; 238 | else sb.Append(c); 239 | } 240 | i++; 241 | } 242 | return sb.ToString(); 243 | } 244 | } 245 | } -------------------------------------------------------------------------------- /SlugBase/MultiplayerTweaks.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | 6 | namespace SlugBase 7 | { 8 | // Various mod-agnostic tweaks to multiplayer modes 9 | internal static class MultiplayerTweaks 10 | { 11 | public static void ApplyHooks() 12 | { 13 | On.Player.ctor += Player_ctor; 14 | } 15 | 16 | // Change the slugcat character of players 2 and on if they would look the same as player 1 17 | private static void Player_ctor(On.Player.orig_ctor orig, Player self, AbstractCreature abstractCreature, World world) 18 | { 19 | orig(self, abstractCreature, world); 20 | var state = self.playerState; 21 | if (!state.isGhost 22 | && state.playerNumber > 0 23 | && state.slugcatCharacter == (int)self.slugcatStats.name 24 | && PlayerManager.GetCustomPlayer(state.slugcatCharacter) != null) 25 | { 26 | state.slugcatCharacter = state.playerNumber; 27 | } 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /SlugBase/PlayerColors.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using UnityEngine; 3 | using MonoMod.RuntimeDetour; 4 | 5 | namespace SlugBase 6 | { 7 | // PlayerGraphics.SlugcatColor is ambiguous when using multi-instance characters since different characters have different colors for each slugcat index 8 | // This class contains hooks that find the slugcat associated with calls to SlugcatColor to correct that ambiguity 9 | internal static class PlayerColors 10 | { 11 | internal static SlugBaseCharacter drawingCharacter; 12 | 13 | public static void ApplyHooks() 14 | { 15 | On.ArenaBehaviors.SandboxEditor.EditCursor.DrawSprites += (orig, self, a, b, c, d) => DrawingPlayer(self.room.game, self.playerNumber, () => orig(self, a, b, c, d)); 16 | On.HUD.PlayerSpecificMultiplayerHud.Update += (orig, self) => DrawingPlayer(self.RealizedPlayer, () => orig(self)); 17 | On.HUD.PlayerSpecificMultiplayerHud.ctor += (orig, self, a, b, absPlayer) => DrawingPlayer(absPlayer.realizedObject as Player, () => orig(self, a, b, absPlayer)); 18 | On.HUD.PlayerSpecificMultiplayerHud.Update += (orig, self) => DrawingPlayer(self.RealizedPlayer, () => orig(self)); 19 | On.HUD.PlayerSpecificMultiplayerHud.Draw += (orig, self, a) => DrawingPlayer(self.RealizedPlayer, () => orig(self, a)); 20 | //On.Menu.PlayerJoinButton.GrafUpdate += (orig, self, a) => DrawingPlayer(null, () => orig(self, a)); 21 | //On.Menu.PlayerResultBox.GrafUpdate += (orig, self, a) => DrawingPlayer(null, () => orig(self, a)); 22 | On.Menu.SandboxEditorSelector.ButtonCursor.GrafUpdate += (orig, self, a) => DrawingPlayer(self.roomCursor.room.game, self.roomCursor.playerNumber, () => orig(self, a)); 23 | On.Player.Update += (orig, self, a) => DrawingPlayer(self, () => orig(self, a)); 24 | On.Player.ShortCutColor += (orig, self) => DrawingPlayer(self, () => orig(self)); 25 | On.PlayerGraphics.ApplyPalette += (orig, self, a, b, c) => DrawingPlayer(self.player, () => orig(self, a, b, c)); 26 | On.PlayerGraphics.DrawSprites += (orig, self, a, b, c, d) => DrawingPlayer(self.player, () => orig(self, a, b, c, d)); 27 | On.PlayerGraphics.Update += (orig, self) => DrawingPlayer(self.player, () => orig(self)); 28 | On.Player.ShortCutColor += (orig, self) => DrawingPlayer(self, () => orig(self)); 29 | 30 | new Hook( 31 | typeof(OverseerGraphics).GetProperty(nameof(OverseerGraphics.MainColor)).GetGetMethod(), 32 | new Func, OverseerGraphics, Color>((orig, self) => { 33 | if (self.overseer.editCursor != null) 34 | return DrawingPlayer(self.overseer.room.game, self.overseer.editCursor.playerNumber, () => orig(self)); 35 | else 36 | return orig(self); 37 | }) 38 | ); 39 | } 40 | 41 | internal static void DrawingPlayer(RainWorldGame game, int playerNumber, Action orig) 42 | { 43 | var lastDrawing = drawingCharacter; 44 | 45 | if (playerNumber >= 0 && playerNumber < game.Players.Count) 46 | drawingCharacter = PlayerManager.GetCustomPlayer(game.Players[playerNumber]?.realizedObject as Player); 47 | else if (game.IsArenaSession) 48 | drawingCharacter = ArenaAdditions.GetSelectedArenaCharacter(game.manager.arenaSetup, playerNumber).player; 49 | else 50 | drawingCharacter = PlayerManager.GetCustomPlayer(game); 51 | 52 | try 53 | { 54 | orig(); 55 | } 56 | finally 57 | { 58 | drawingCharacter = lastDrawing; 59 | } 60 | } 61 | 62 | internal static T DrawingPlayer(RainWorldGame game, int playerNumber, Func orig) 63 | { 64 | var lastDrawing = drawingCharacter; 65 | 66 | if (playerNumber >= 0 && playerNumber < game.Players.Count) 67 | drawingCharacter = PlayerManager.GetCustomPlayer(game.Players[playerNumber]?.realizedObject as Player); 68 | else if (game.IsArenaSession) 69 | drawingCharacter = ArenaAdditions.GetSelectedArenaCharacter(game.manager.arenaSetup, playerNumber).player; 70 | else 71 | drawingCharacter = PlayerManager.GetCustomPlayer(game); 72 | 73 | try 74 | { 75 | return orig(); 76 | } 77 | finally 78 | { 79 | drawingCharacter = lastDrawing; 80 | } 81 | } 82 | 83 | internal static void DrawingPlayer(Player player, Action orig) 84 | { 85 | var lastDrawing = drawingCharacter; 86 | if (player != null) 87 | drawingCharacter = PlayerManager.GetCustomPlayer(player); 88 | try 89 | { 90 | orig(); 91 | } 92 | finally 93 | { 94 | drawingCharacter = lastDrawing; 95 | } 96 | } 97 | 98 | internal static T DrawingPlayer(Player player, Func orig) 99 | { 100 | var lastDrawing = drawingCharacter; 101 | if (player != null) 102 | drawingCharacter = PlayerManager.GetCustomPlayer(player); 103 | try 104 | { 105 | return orig(); 106 | } 107 | finally 108 | { 109 | drawingCharacter = lastDrawing; 110 | } 111 | } 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /SlugBase/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | // General Information about an assembly is controlled through the following 6 | // set of attributes. Change these attribute values to modify the information 7 | // associated with an assembly. 8 | [assembly: AssemblyTitle("SlugBase")] 9 | [assembly: AssemblyDescription("A framework that simplifies adding new characters to Rain World.")] 10 | [assembly: AssemblyConfiguration("")] 11 | [assembly: AssemblyCompany("Slime_Cubed")] 12 | [assembly: AssemblyProduct("SlugBase")] 13 | [assembly: AssemblyCopyright("Copyright © 2021")] 14 | [assembly: AssemblyTrademark("")] 15 | [assembly: AssemblyCulture("")] 16 | 17 | // Setting ComVisible to false makes the types in this assembly not visible 18 | // to COM components. If you need to access a type in this assembly from 19 | // COM, set the ComVisible attribute to true on that type. 20 | [assembly: ComVisible(false)] 21 | 22 | // The following GUID is for the ID of the typelib if this project is exposed to COM 23 | [assembly: Guid("e7b1aabf-4be5-49d7-847d-24883f06261a")] 24 | 25 | // Version information for an assembly consists of the following four values: 26 | // 27 | // Major Version 28 | // Minor Version 29 | // Build Number 30 | // Revision 31 | // 32 | // You can specify all the values or you can default the Build and Revision Numbers 33 | // by using the '*' as shown below: 34 | // [assembly: AssemblyVersion("1.0.*")] 35 | [assembly: AssemblyVersion(SlugBase.SlugBaseMod.assemblyVersion)] 36 | [assembly: AssemblyFileVersion(SlugBase.SlugBaseMod.assemblyVersion)] 37 | -------------------------------------------------------------------------------- /SlugBase/Scenes/CustomScene.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | using UnityEngine; 5 | using Menu; 6 | using RWCustom; 7 | using JsonObj = System.Collections.Generic.Dictionary; 8 | using JsonList = System.Collections.Generic.List; 9 | 10 | namespace SlugBase 11 | { 12 | /// 13 | /// Represents a scene added by a SlugBase character. 14 | /// 15 | public class CustomScene 16 | { 17 | internal JsonObj properties = new JsonObj(); 18 | internal bool dirty; 19 | 20 | /// 21 | /// The images this scene contains. 22 | /// 23 | public List Images { get; private set; } 24 | 25 | /// 26 | /// The character that this scene was loaded from. 27 | /// 28 | public SlugBaseCharacter Owner { get; private set; } 29 | 30 | /// 31 | /// This scene's name. 32 | /// 33 | public string Name { get; private set; } 34 | 35 | /// 36 | /// Creates an empty scene. 37 | /// 38 | public CustomScene(SlugBaseCharacter owner, string name) 39 | { 40 | Images = new List(); 41 | Owner = owner; 42 | Name = name; 43 | } 44 | 45 | /// 46 | /// Creates a scene from a JSON string. 47 | /// 48 | public CustomScene(SlugBaseCharacter owner, string name, string json) : this(owner, name, json?.dictionaryFromJson()) { } 49 | 50 | /// 51 | /// Creates a scene from a JSON object. 52 | /// 53 | public CustomScene(SlugBaseCharacter owner, string name, JsonObj data) : this(owner, name) 54 | { 55 | if (owner == null) throw new ArgumentNullException(nameof(name)); 56 | if (name == null) throw new ArgumentNullException(nameof(name)); 57 | if (data == null) throw new ArgumentNullException(nameof(data), "Scene JSON data is null! This may be due to invalid JSON."); 58 | 59 | foreach(var pair in data) 60 | LoadValue(pair.Key, pair.Value); 61 | } 62 | 63 | private void LoadValue(string name, object value) 64 | { 65 | try 66 | { 67 | switch (name) 68 | { 69 | case "images": 70 | foreach (JsonObj imageData in (JsonList)value) 71 | Images.Add(new SceneImage(this, imageData)); 72 | break; 73 | default: 74 | properties[name.ToLower()] = value; 75 | break; 76 | } 77 | } 78 | catch (Exception e) 79 | { 80 | Debug.Log($"Scene property \"{name}\" cannot hold a value of type \"{value.GetType().Name}\"!"); 81 | Debug.LogException(e); 82 | } 83 | } 84 | 85 | /// 86 | /// Inserts an image into the list according to its depth. 87 | /// 88 | public void InsertImage(SceneImage newImage) 89 | { 90 | for (int i = 0; i < Images.Count; i++) 91 | { 92 | if (Images[i].depth < newImage.depth) 93 | { 94 | Images.Insert(i, newImage); 95 | return; 96 | } 97 | } 98 | Images.Add(newImage); 99 | } 100 | 101 | internal static T GetPropFromDict(string key, JsonObj dict) 102 | { 103 | if (!dict.TryGetValue(key.ToLower(), out object o)) return default(T); 104 | if (o == null) return default(T); 105 | 106 | try 107 | { 108 | // Allow nullables 109 | Type nullableType = Nullable.GetUnderlyingType(typeof(T)); 110 | if (nullableType != null) 111 | return (T)Convert.ChangeType(o, nullableType); 112 | 113 | return (T)Convert.ChangeType(o, typeof(T)); 114 | } 115 | catch (Exception) 116 | { 117 | return default(T); 118 | } 119 | } 120 | 121 | /// 122 | /// Retrieves a named value from this scene's JSON, or the type's default value if does not exist. 123 | /// These may be defined by the engine (see remarks), or by another mod. 124 | /// 125 | /// 126 | /// This may defined by your mod, or be one of several built-in tags: 127 | /// 128 | /// 129 | /// MarkX (float) 130 | /// The X position of the default mark of communication. 131 | /// 132 | /// 133 | /// MarkY (float) 134 | /// The Y position of the default mark of communication. 135 | /// 136 | /// 137 | /// GlowX (float) 138 | /// The X position of the default player glow. 139 | /// 140 | /// 141 | /// GlowY (float) 142 | /// The Y position of the default player glow. 143 | /// 144 | /// 145 | /// SlugcatDepth (float) 146 | /// The depth of the slugcat in the select screen. This is used when positioning the mark and glow. 147 | /// 148 | /// 149 | /// 150 | public T GetProperty(string key) => GetPropFromDict(key, properties); 151 | 152 | /// 153 | /// Attaches a named value to this scene. 154 | /// 155 | public void SetProperty(string key, object value) 156 | { 157 | SetDirty(); 158 | InternalSetProperty(key, value); 159 | } 160 | 161 | private void InternalSetProperty(string key, object value) 162 | { 163 | properties[key.ToLower()] = value; 164 | } 165 | 166 | internal void SetDirty() 167 | { 168 | dirty = true; 169 | } 170 | 171 | private JsonList SaveImages() 172 | { 173 | JsonList obj = new JsonList(); 174 | foreach (SceneImage image in Images) 175 | if (image.ShouldBeSaved) 176 | obj.Add(image.ToJsonObj()); 177 | return obj; 178 | } 179 | 180 | internal JsonObj ToJsonObj() 181 | { 182 | JsonObj obj = new JsonObj(); 183 | obj["images"] = SaveImages(); 184 | return obj; 185 | } 186 | 187 | /// 188 | /// Creates a JSON string that represents this object. 189 | /// 190 | /// A JSON string. 191 | public override string ToString() 192 | { 193 | return ToJsonObj().toJson(); 194 | } 195 | 196 | /// 197 | /// Disables images in this scene based on a filter. 198 | /// 199 | /// A delegate that returns false for any images that should be hidden. 200 | public void ApplyFilter(SceneImageFilter filter) 201 | { 202 | foreach (var img in Images) 203 | if (!filter(img)) 204 | img.Enabled = false; 205 | } 206 | } 207 | 208 | /// 209 | /// Represents a single image in a . 210 | /// 211 | /// 212 | /// This class may be inherited if a scene image requires more functionality than can provide. 213 | /// Consider using to add illustrations manually. 214 | /// 215 | public class SceneImage 216 | { 217 | internal string assetName; 218 | internal Vector2 pos; 219 | internal float depth = -1f; 220 | internal JsonObj properties = new JsonObj(); 221 | internal JsonObj tempProperties; 222 | internal bool dirty = false; 223 | 224 | /// 225 | /// The name of the file containing the image. 226 | /// 227 | public string AssetName 228 | { 229 | get => assetName; 230 | set { SetDirty(); assetName = value; } 231 | } 232 | 233 | /// 234 | /// The scene this image is a part of. 235 | /// 236 | public CustomScene Owner { get; private set; } 237 | 238 | /// 239 | /// The position of the bottom left of this image 240 | /// 241 | public Vector2 Pos 242 | { 243 | get => pos; 244 | set { SetDirty(); pos = value; } 245 | } 246 | 247 | /// 248 | /// How far into the screen this illustration should be drawn. 249 | /// The higher this value, the less this image moves when the mouse is moved. 250 | /// This should be less than zero if the image is flat. 251 | /// 252 | public float Depth 253 | { 254 | get => depth; 255 | set { SetDirty(); depth = value; } 256 | } 257 | 258 | /// 259 | /// True if this image should be shown in the scene, false otherwise. 260 | /// This has no effect once the scene's illustrations have been loaded. 261 | /// 262 | public bool Enabled { get; set; } 263 | 264 | /// 265 | /// True if this image should be saved to the scene. This does not affect . 266 | /// Defaults to true. 267 | /// 268 | public virtual bool ShouldBeSaved => true; 269 | 270 | /// 271 | /// The name to display when showing this image in the scene editor. 272 | /// 273 | public virtual string DisplayName => System.IO.Path.GetFileName(AssetName); 274 | 275 | /// 276 | /// A list of alpha keyframes. This may be null. 277 | /// 278 | /// 279 | /// X values correspond to time, Y values correspond to this image's alpha at this time. 280 | /// Time values range from 0 to 1. 281 | /// Keyframes will be linearly interpolated between during a slideshow. 282 | /// 283 | public List AlphaKeys { get; set; } 284 | 285 | /// 286 | /// Creates a blank scene image. 287 | /// 288 | public SceneImage(CustomScene owner) 289 | { 290 | Enabled = true; 291 | Owner = owner; 292 | } 293 | 294 | /// 295 | /// Creates a scene image from a JSON string. 296 | /// 297 | public SceneImage(CustomScene owner, string data) : this(owner, data.dictionaryFromJson()) { } 298 | 299 | /// 300 | /// Creates a scene image from a JSON object. 301 | /// 302 | public SceneImage(CustomScene owner, JsonObj json) : this(owner) 303 | { 304 | foreach (var pair in json) 305 | LoadValue(pair.Key, pair.Value); 306 | } 307 | 308 | private void LoadValue(string name, object value) 309 | { 310 | try 311 | { 312 | switch (name) 313 | { 314 | case "x": pos.x = Convert.ToSingle(value); break; 315 | case "y": pos.y = Convert.ToSingle(value); break; 316 | case "depth": depth = Convert.ToSingle(value); break; 317 | case "name": assetName = (string)value; break; 318 | case "alphakeys": 319 | { 320 | AlphaKeys = new List(); 321 | JsonList list = (JsonList)value; 322 | for (int i = 0; i < list.Count - 1; i += 2) 323 | { 324 | AlphaKeys.Add(new Vector2( 325 | Convert.ToSingle(list[i + 0]), 326 | Convert.ToSingle(list[i + 1]) 327 | )); 328 | } 329 | } 330 | break; 331 | default: InternalSetProperty(name, value); break; 332 | } 333 | } catch(Exception e) 334 | { 335 | Debug.Log($"Image property \"{name}\" cannot hold a value of type \"{value.GetType().Name}\"!"); 336 | Debug.LogException(e); 337 | } 338 | } 339 | 340 | /// 341 | /// Converts this image into a JSON string. 342 | /// 343 | /// A JSON string. 344 | public override string ToString() => ToJsonObj().toJson(); 345 | 346 | /// 347 | /// Finds the alpha of this image at by sampling from . 348 | /// 349 | /// The time to sample at, typically between 0 and 1. 350 | /// The alpha that this image should have at this point. This does not take other alpha modifiers into account. 351 | public float AlphaAtTime(float t) 352 | { 353 | var keys = AlphaKeys; 354 | 355 | if (keys.Count == 0) return 1f; 356 | if (keys.Count == 1) return keys[0].y; 357 | 358 | for (int i = 0; i < keys.Count - 1; i++) 359 | { 360 | if (keys[i + 1].x >= t) 361 | { 362 | return Custom.LerpMap(t, keys[i].x, keys[i + 1].x, keys[i].y, keys[i + 1].y); 363 | } 364 | } 365 | 366 | return keys[keys.Count - 1].y; 367 | } 368 | 369 | /// 370 | /// Called before a scene is saved. 371 | /// 372 | /// A JSON object representing the scene to be saved. 373 | public virtual void OnSave(JsonObj scene) 374 | { 375 | } 376 | 377 | /// 378 | /// Called when this image's graphics should be added to a scene. 379 | /// Defaults to true. 380 | /// 381 | /// 382 | /// This may be overridden to add graphics to the scene without using a . 383 | /// 384 | /// True if this image should create a . 385 | protected internal virtual bool OnBuild(MenuScene scene) 386 | { 387 | return true; 388 | } 389 | 390 | /// 391 | /// Retrieves a named value from this image's JSON, or the type's default value if does not exist. 392 | /// These may be defined by the engine (see remarks), or by another mod. 393 | /// 394 | /// 395 | /// This may defined by your mod, or be one of several built-in tags: 396 | /// 397 | /// 398 | /// Mark (bool) 399 | /// This image only appears on the select screen if the player has the mark of communication. 400 | /// 401 | /// 402 | /// Glow (bool) 403 | /// True if this image should only appear on the select screen if the player is glowing. 404 | /// 405 | /// 406 | /// Shader (string) 407 | /// Set this image to use the specified shader. 408 | /// These are first checked against . 409 | /// If the image is flat or there is no corresponding MenuShader, then the shader with this name is used. 410 | /// 411 | /// 412 | /// Flatmode (bool) 413 | /// True if this image should only appear in flat mode, and all others without it will be hidden in flat mode. 414 | /// This should be used in conjunction with a less than zero. 415 | /// 416 | /// 417 | /// Crisp (bool) 418 | /// True to disable antialiasing for this image. 419 | /// 420 | /// 421 | /// Focus (bool) 422 | /// Marks this image as a candidate for camera focus. 423 | /// If no images are marked as such, then the camera will occasionally focus at the depth of a random image. 424 | /// 425 | /// 426 | /// Alpha (float) 427 | /// Sets this image's opacity. This value should be between 0 and 1, inclusive. 428 | /// 429 | /// 430 | /// Fade (float) 431 | /// This image will fade out when the map is opened on the sleep and death screens. 432 | /// 433 | /// 434 | /// 435 | /// An instance of , or 's default value if no matching key was found. 436 | public T GetProperty(string key) => CustomScene.GetPropFromDict(key, properties); 437 | 438 | /// 439 | /// Attaches a named value to this image. 440 | /// 441 | public void SetProperty(string key, object value) 442 | { 443 | dirty = true; 444 | InternalSetProperty(key, value); 445 | } 446 | 447 | private void InternalSetProperty(string key, object value) 448 | { 449 | properties[key.ToLower()] = value; 450 | } 451 | 452 | /// 453 | /// Like , but only for temporary values. 454 | /// 455 | /// An instance of , or 's default value if no matching key was found. 456 | public T GetTempProperty(string key) 457 | { 458 | if (tempProperties == null) return default(T); 459 | return CustomScene.GetPropFromDict(key, tempProperties); 460 | } 461 | 462 | /// 463 | /// Attaches a temporary value to this image. This will not be saved with the scene. 464 | /// 465 | public void SetTempProperty(string key, object value) 466 | { 467 | if (tempProperties == null) tempProperties = new JsonObj(); 468 | tempProperties[key.ToLower()] = value; 469 | } 470 | 471 | private void SetDirty() 472 | { 473 | dirty = true; 474 | Owner?.SetDirty(); 475 | } 476 | 477 | internal JsonObj ToJsonObj() 478 | { 479 | JsonObj obj = new JsonObj(); 480 | foreach (var pair in properties) 481 | obj.Add(pair.Key, pair.Value); 482 | obj["x"] = pos.x; 483 | obj["x"] = pos.y; 484 | obj["depth"] = depth; 485 | obj["name"] = assetName; 486 | return obj; 487 | } 488 | 489 | /// 490 | /// Check if this image has the specified property, and that the property's value is not false. 491 | /// 492 | public bool HasTag(string tagName) 493 | { 494 | object prop = GetProperty(tagName); 495 | return prop != null && ((prop is bool) ? (bool)prop : true); 496 | } 497 | } 498 | } 499 | -------------------------------------------------------------------------------- /SlugBase/Scenes/CustomSceneManager.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Text; 4 | using System.Collections.Generic; 5 | using System.Collections.ObjectModel; 6 | using UnityEngine; 7 | using Menu; 8 | using RWCustom; 9 | 10 | namespace SlugBase 11 | { 12 | internal static class CustomSceneManager 13 | { 14 | // Passed in as a folder to some functions to mark that they should load a custom resource instead 15 | internal const string resourceFolderName = "SlugBase Resources"; 16 | 17 | private static ResourceOverride sceneOverride; 18 | private static ResourceOverride slideshowOverride; 19 | 20 | internal static AttachedField customRep = new AttachedField(); 21 | internal static AttachedField customSceneRep = new AttachedField(); 22 | internal static AttachedField customSlideshowRep = new AttachedField(); 23 | 24 | internal static void ApplyHooks() 25 | { 26 | On.Menu.SlideShowMenuScene.ApplySceneSpecificAlphas += SlideShowMenuScene_ApplySceneSpecificAlphas; 27 | On.RainWorldGame.ExitToVoidSeaSlideShow += RainWorldGame_ExitToVoidSeaSlideShow; 28 | On.Menu.SlugcatSelectMenu.StartGame += SlugcatSelectMenu_StartGame; 29 | On.Menu.SlideShow.ctor += SlideShow_ctor; 30 | On.Menu.MenuScene.BuildScene += MenuScene_BuildScene; 31 | On.Menu.MenuIllustration.LoadFile_1 += MenuIllustration_LoadFile_1; 32 | } 33 | 34 | #region Hooks 35 | 36 | private static void SlideShowMenuScene_ApplySceneSpecificAlphas(On.Menu.SlideShowMenuScene.orig_ApplySceneSpecificAlphas orig, SlideShowMenuScene self) 37 | { 38 | orig(self); 39 | 40 | foreach(MenuObject obj in self.subObjects) 41 | { 42 | if (!(obj is MenuIllustration illust)) continue; 43 | if (!customRep.TryGet(illust, out SceneImage img)) continue; 44 | 45 | if(img.AlphaKeys != null) 46 | illust.setAlpha = img.AlphaAtTime(self.displayTime); 47 | } 48 | } 49 | 50 | // Automatically override outro cutscenes 51 | private static void RainWorldGame_ExitToVoidSeaSlideShow(On.RainWorldGame.orig_ExitToVoidSeaSlideShow orig, RainWorldGame self) 52 | { 53 | orig(self); 54 | 55 | // Override the outro if this character has the corresponding slideshow 56 | SlugBaseCharacter ply = PlayerManager.GetCustomPlayer(self.StoryCharacter); 57 | if (ply == null) return; 58 | if (ply.HasSlideshow("Outro")) 59 | OverrideNextSlideshow(ply, "Outro"); 60 | } 61 | 62 | // Automatically override intro cutscenes 63 | private static void SlugcatSelectMenu_StartGame(On.Menu.SlugcatSelectMenu.orig_StartGame orig, SlugcatSelectMenu self, int storyGameCharacter) 64 | { 65 | orig(self, storyGameCharacter); 66 | 67 | if (!self.restartChecked && self.manager.rainWorld.progression.IsThereASavedGame(storyGameCharacter)) return; 68 | 69 | // Only continue to the slideshow if this character has an intro slideshow 70 | SlugBaseCharacter ply = PlayerManager.GetCustomPlayer(storyGameCharacter); 71 | if (ply == null) return; 72 | if(ply.HasSlideshow("Intro") && !Input.GetKey("s")) 73 | { 74 | OverrideNextSlideshow(ply, "Intro"); 75 | self.manager.upcomingProcess = null; 76 | self.manager.RequestMainProcessSwitch(ProcessManager.ProcessID.SlideShow); 77 | } else 78 | { 79 | self.manager.upcomingProcess = null; 80 | self.manager.RequestMainProcessSwitch(ProcessManager.ProcessID.Game); 81 | } 82 | } 83 | 84 | // Same as below, but for slideshows 85 | private static void SlideShow_ctor(On.Menu.SlideShow.orig_ctor orig, SlideShow self, ProcessManager manager, SlideShow.SlideShowID slideShowID) 86 | { 87 | // Automatically override slideshows if the current character has a slideshow by the same name 88 | SlugBaseCharacter currentPlayer = PlayerManager.GetCustomPlayer(manager.rainWorld.progression.PlayingAsSlugcat); 89 | 90 | if (currentPlayer != null) 91 | { 92 | string slideshowName = self.slideShowID.ToString(); 93 | if (slideshowOverride == null && currentPlayer.HasSlideshow(slideshowName)) 94 | OverrideNextSlideshow(currentPlayer, slideshowName); 95 | } 96 | 97 | if (slideshowOverride == null) 98 | { 99 | orig(self, manager, slideShowID); 100 | return; 101 | } 102 | 103 | try 104 | { 105 | // Call the original constructor, save a reference to the loading label 106 | // This will always be empty, due to the ID of -1 107 | FLabel loadingLabel = manager.loadingLabel; 108 | orig(self, manager, (SlideShow.SlideShowID)(-1)); 109 | 110 | // Undo RemoveLoadingLabel and NextScene 111 | manager.loadingLabel = loadingLabel; 112 | Futile.stage.AddChild(loadingLabel); 113 | self.current = -1; 114 | 115 | // Load a custom scene 116 | 117 | SlugBaseCharacter owner = slideshowOverride.Character; 118 | CustomSlideshow builtSlideshow; 119 | try 120 | { 121 | builtSlideshow = owner.BuildSlideshow(slideshowOverride.ResourceName); 122 | } 123 | catch(Exception e) 124 | { 125 | Debug.LogError($"Failed to load slideshow: {slideshowOverride.ResourceName}"); 126 | Debug.LogException(e); 127 | return; 128 | } 129 | slideshowOverride.Load(builtSlideshow); 130 | customSlideshowRep[self] = builtSlideshow; 131 | List slides = builtSlideshow.Slides; 132 | 133 | // Chose a destination process 134 | if (builtSlideshow.NextProcess == null) 135 | { 136 | switch (slideShowID) 137 | { 138 | case SlideShow.SlideShowID.WhiteIntro: 139 | case SlideShow.SlideShowID.YellowIntro: 140 | self.nextProcess = ProcessManager.ProcessID.Game; 141 | break; 142 | case SlideShow.SlideShowID.WhiteOutro: 143 | case SlideShow.SlideShowID.YellowOutro: 144 | case SlideShow.SlideShowID.RedOutro: 145 | self.nextProcess = ProcessManager.ProcessID.Credits; 146 | break; 147 | default: 148 | // Take a best guess 149 | // Accidentally going to the game is better than accidentally going to the credits 150 | self.nextProcess = ProcessManager.ProcessID.Game; 151 | break; 152 | } 153 | } 154 | else 155 | { 156 | self.nextProcess = builtSlideshow.NextProcess.Value; 157 | } 158 | 159 | // Custom music 160 | if (manager.musicPlayer != null && !string.IsNullOrEmpty(builtSlideshow.Music)) 161 | { 162 | self.waitForMusic = builtSlideshow.Music; 163 | self.stall = true; 164 | manager.musicPlayer.MenuRequestsSong(self.waitForMusic, 1.5f, 40f); 165 | } 166 | 167 | // Custom playlist 168 | float time = 0f; 169 | float endTime; 170 | self.playList.Clear(); 171 | foreach (SlideshowSlide slide in slides) 172 | { 173 | if (!slide.Enabled) continue; 174 | endTime = time + slide.Duration; 175 | self.playList.Add(new SlideShow.Scene(MenuScene.SceneID.Empty, time, time + slide.FadeIn, endTime - slide.FadeOut)); 176 | time = endTime; 177 | } 178 | 179 | // Preload the scenes 180 | self.preloadedScenes = new SlideShowMenuScene[self.playList.Count]; 181 | try 182 | { 183 | for (int i = 0; i < self.preloadedScenes.Length; i++) 184 | { 185 | MenuScene.SceneID id = MenuScene.SceneID.Empty; 186 | if (builtSlideshow.Owner.HasScene(slides[i].SceneName)) 187 | { 188 | // Prioritize this character's scenes 189 | OverrideNextScene(builtSlideshow.Owner, builtSlideshow.Slides[i].SceneName, slideshowOverride.Filter); 190 | } 191 | else 192 | { 193 | ClearSceneOverride(); 194 | try 195 | { 196 | // ... then try existing scenes 197 | id = Custom.ParseEnum(slides[i].SceneName); 198 | } 199 | catch (Exception) 200 | { 201 | // ... and default to Empty 202 | id = MenuScene.SceneID.Empty; 203 | } 204 | } 205 | self.preloadedScenes[i] = new SlideShowMenuScene(self, self.pages[0], id); 206 | self.preloadedScenes[i].Hide(); 207 | 208 | List camPath = self.preloadedScenes[i].cameraMovementPoints; 209 | camPath.Clear(); 210 | camPath.AddRange(slides[i].CameraPath); 211 | } 212 | } 213 | finally 214 | { 215 | ClearSceneOverride(); 216 | } 217 | } 218 | finally 219 | { 220 | ClearSlideshowOverride(); 221 | } 222 | manager.RemoveLoadingLabel(); 223 | self.NextScene(); 224 | } 225 | 226 | // Add SlugBase character resources as a "virtual file" that illustrations may be read from 227 | // Folder: "SlugBase Resources" 228 | // File: "PlayerName\Dir1\Dir2\...\DirN\Image.png" 229 | private static void MenuIllustration_LoadFile_1(On.Menu.MenuIllustration.orig_LoadFile_1 orig, Menu.MenuIllustration self, string folder) 230 | { 231 | Texture2D customTex; 232 | if (folder == resourceFolderName && ((customTex = LoadTextureFromResources(self.fileName)) != null)) 233 | { 234 | self.texture = customTex; 235 | self.texture.wrapMode = TextureWrapMode.Clamp; 236 | if (self.crispPixels) 237 | { 238 | self.texture.anisoLevel = 0; 239 | self.texture.filterMode = FilterMode.Point; 240 | } 241 | HeavyTexturesCache.LoadAndCacheAtlasFromTexture(self.fileName, self.texture); 242 | return; 243 | } 244 | else orig(self, folder); 245 | } 246 | 247 | private static void MenuScene_BuildScene(On.Menu.MenuScene.orig_BuildScene orig, MenuScene self) 248 | { 249 | // Automatically override scenes if the current character has a scene by the same name 250 | SlugBaseCharacter currentPlayer = PlayerManager.GetCustomPlayer(self.menu.manager.rainWorld.progression.PlayingAsSlugcat); 251 | 252 | if (currentPlayer != null) 253 | { 254 | string sceneName = self.sceneID.ToString(); 255 | if (sceneOverride == null && currentPlayer.HasScene(sceneName)) 256 | OverrideNextScene(currentPlayer, sceneName); 257 | } 258 | 259 | if (sceneOverride != null) 260 | { 261 | try 262 | { 263 | self.sceneFolder = resourceFolderName; 264 | 265 | var owner = sceneOverride.Character; 266 | var builtScene = owner.BuildScene(sceneOverride.ResourceName); 267 | sceneOverride.Load(builtScene); 268 | customSceneRep[self] = builtScene; 269 | if (sceneOverride.Filter != null) 270 | builtScene.ApplyFilter(sceneOverride.Filter); 271 | 272 | // Check for flatmode support 273 | bool hasFlatmode = false; 274 | foreach (var img in builtScene.Images) 275 | { 276 | if (img.HasTag("FLATMODE")) 277 | { 278 | hasFlatmode = true; 279 | break; 280 | } 281 | } 282 | 283 | // Load all images into the scene 284 | for (int imgIndex = 0; imgIndex < builtScene.Images.Count; imgIndex++) 285 | { 286 | var img = builtScene.Images[imgIndex]; 287 | 288 | // Hide disabled images 289 | if (!img.Enabled) continue; 290 | 291 | // Allow images to use their own sprites 292 | if (!img.OnBuild(self)) continue; 293 | 294 | // Skip this image if it is flatmode only and flatmode is disabled, and vice versa 295 | bool flat = img.depth < 0f; 296 | bool flatmodeOnly = hasFlatmode && img.HasTag("flatmode"); 297 | if (hasFlatmode && (self.flatMode != flatmodeOnly)) continue; 298 | 299 | // Parse alpha 300 | float alpha = img.GetProperty("alpha") ?? 1f; 301 | 302 | string assetPath = $"{owner.Name}\\Scenes\\{builtScene.Name}\\{img.assetName}"; 303 | Vector2 pos = img.Pos; 304 | bool crisp = img.HasTag("CRISP"); 305 | string shaderName = img.GetProperty("shader"); 306 | FShader shader = null; 307 | 308 | MenuIllustration illust; 309 | if (flat) 310 | { 311 | // It's Friday 312 | 313 | // Parse shader 314 | if (shaderName != null) 315 | { 316 | if (!self.menu.manager.rainWorld.Shaders.TryGetValue(shaderName, out shader)) shader = null; 317 | } 318 | 319 | // Add a flat illustration 320 | illust = new MenuIllustration(self.menu, self, self.sceneFolder, assetPath, pos, crisp, false); 321 | if (shader != null) 322 | illust.sprite.shader = shader; 323 | } 324 | else 325 | { 326 | // Parse shader 327 | MenuDepthIllustration.MenuShader menuShader = MenuDepthIllustration.MenuShader.Normal; 328 | if (shaderName != null) 329 | { 330 | try 331 | { 332 | menuShader = Custom.ParseEnum(shaderName); 333 | shader = null; 334 | } 335 | catch 336 | { 337 | if (!self.menu.manager.rainWorld.Shaders.TryGetValue(shaderName, out shader)) shader = null; 338 | menuShader = MenuDepthIllustration.MenuShader.Normal; 339 | } 340 | } 341 | 342 | // Add an illustration with depth 343 | illust = new MenuDepthIllustration(self.menu, self, self.sceneFolder, assetPath, pos, img.Depth, menuShader); 344 | 345 | // Apply crisp pixels 346 | if (crisp) illust.sprite.element.atlas.texture.filterMode = FilterMode.Point; 347 | } 348 | 349 | // Apply tags 350 | if (shader != null) illust.sprite.shader = shader; 351 | illust.setAlpha = alpha; 352 | self.AddIllustration(illust); 353 | 354 | // Link back to the custom scene image 355 | customRep[illust] = img; 356 | } 357 | 358 | // Add idle depths 359 | if (self is InteractiveMenuScene ims) 360 | { 361 | ims.idleDepths = new List(); 362 | List depths = builtScene.GetProperty>("idledepths"); 363 | if (depths != null) 364 | { 365 | for (int i = 0; i < depths.Count; i++) 366 | { 367 | if (depths[i] is double depth) 368 | ims.idleDepths.Add((float)depth); 369 | } 370 | } 371 | } 372 | 373 | } 374 | finally { ClearSceneOverride(); } 375 | } 376 | else 377 | { 378 | orig(self); 379 | } 380 | } 381 | 382 | #endregion Hooks 383 | 384 | internal static ResourceOverride OverrideNextScene(SlugBaseCharacter ply, string customSceneName, SceneImageFilter filter = null) 385 | { 386 | return sceneOverride = new ResourceOverride(ply, customSceneName, filter); 387 | } 388 | 389 | internal static void ClearSceneOverride() 390 | { 391 | sceneOverride = null; 392 | } 393 | 394 | internal static ResourceOverride OverrideNextSlideshow(SlugBaseCharacter ply, string customSlideshowName, SceneImageFilter filter = null) 395 | { 396 | return slideshowOverride = new ResourceOverride(ply, customSlideshowName, filter); 397 | } 398 | 399 | internal static void ClearSlideshowOverride() 400 | { 401 | slideshowOverride = null; 402 | } 403 | 404 | private static Texture2D LoadTextureFromResources(string fileName) 405 | { 406 | string[] args = fileName.Split('\\'); 407 | if (args.Length < 2) return null; 408 | SlugBaseCharacter ply = PlayerManager.GetCustomPlayer(args[0]); 409 | if (ply == null) return null; 410 | 411 | string[] resourcePath = new string[args.Length - 1]; 412 | for (int i = 0; i < resourcePath.Length; i++) 413 | resourcePath[i] = args[i + 1]; 414 | 415 | if (resourcePath.Length > 0) 416 | resourcePath[resourcePath.Length - 1] = Path.ChangeExtension(resourcePath[resourcePath.Length - 1], "png"); 417 | 418 | // Load the image resource from disk 419 | Texture2D tex = new Texture2D(1, 1); 420 | using (Stream imageData = ply.GetResource(resourcePath)) 421 | { 422 | 423 | if (imageData == null) 424 | { 425 | Debug.LogException(new FileNotFoundException($"Could not find image for SlugBase character: \"{ply.Name}:{string.Join("\\", resourcePath)}\".")); 426 | return null; 427 | } 428 | 429 | if (imageData.Length > int.MaxValue) 430 | throw new FormatException($"Image resource may not be more than {int.MaxValue} bytes!"); 431 | 432 | BinaryReader br = new BinaryReader(imageData); 433 | byte[] buffer = br.ReadBytes((int)imageData.Length); 434 | 435 | tex.LoadImage(buffer); 436 | } 437 | 438 | return tex; 439 | } 440 | 441 | internal class ResourceOverride 442 | { 443 | public SlugBaseCharacter Character { get; } 444 | public string ResourceName { get; } 445 | public SceneImageFilter Filter { get; } 446 | 447 | public event LoadHandler OnLoad; 448 | 449 | public ResourceOverride(SlugBaseCharacter character, string resource, SceneImageFilter filter = null) 450 | { 451 | Character = character; 452 | ResourceName = resource; 453 | Filter = filter; 454 | } 455 | 456 | public delegate void LoadHandler(T loadedResource); 457 | 458 | internal void Load(T builtScene) 459 | { 460 | OnLoad?.Invoke(builtScene); 461 | } 462 | } 463 | } 464 | 465 | /// 466 | /// Determines if this image should be included in the scene. 467 | /// 468 | /// 469 | /// This is intended to filter items based on . 470 | /// 471 | /// The instance to check. 472 | /// True if this image should be in the scene, false otherwise. 473 | public delegate bool SceneImageFilter(SceneImage image); 474 | } 475 | -------------------------------------------------------------------------------- /SlugBase/Scenes/CustomSlideshow.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using UnityEngine; 4 | using RWCustom; 5 | using JsonObj = System.Collections.Generic.Dictionary; 6 | using JsonList = System.Collections.Generic.List; 7 | 8 | namespace SlugBase 9 | { 10 | using static CustomScene; 11 | 12 | /// 13 | /// Represents a slideshow added by a SlugBase character. 14 | /// 15 | public class CustomSlideshow 16 | { 17 | /// 18 | /// The SlugBase character that owns this slideshow. 19 | /// 20 | public SlugBaseCharacter Owner { get; private set; } 21 | 22 | /// 23 | /// The name of this slideshow. 24 | /// 25 | public string Name { get; private set; } 26 | 27 | /// 28 | /// A list of slides to display during this slideshow. 29 | /// 30 | public List Slides { get; private set; } 31 | 32 | /// 33 | /// The name of the music track to play during this slideshow. 34 | /// 35 | public string Music { get; set; } 36 | 37 | /// 38 | /// The ID of the process to move to move to after the slideshow. 39 | /// If this is null, an ID will be chosen based on the of the 40 | /// slideshow that this replaced. 41 | /// 42 | public ProcessManager.ProcessID? NextProcess { get; set; } 43 | 44 | /// 45 | /// Creates an empty slideshow. 46 | /// 47 | /// 48 | /// 49 | public CustomSlideshow(SlugBaseCharacter owner, string name) 50 | { 51 | NextProcess = null; 52 | Owner = owner; 53 | Name = name; 54 | Slides = new List(); 55 | } 56 | 57 | /// 58 | /// Creates a slideshow from a JSON string. 59 | /// 60 | public CustomSlideshow(SlugBaseCharacter owner, string name, string json) : this(owner, name, json.dictionaryFromJson()) { } 61 | 62 | /// 63 | /// Creates a slideshow from a JSON object. 64 | /// 65 | public CustomSlideshow(SlugBaseCharacter owner, string name, JsonObj data) : this(owner, name) 66 | { 67 | foreach(var pair in data) 68 | LoadValue(pair.Key, pair.Value); 69 | } 70 | 71 | private void LoadValue(string name, object value) 72 | { 73 | try 74 | { 75 | switch (name) 76 | { 77 | case "slides": 78 | foreach (JsonObj slideData in (JsonList)value) 79 | Slides.Add(new SlideshowSlide(this, slideData)); 80 | break; 81 | case "music": 82 | Music = Convert.ToString(value); 83 | break; 84 | } 85 | } 86 | catch (Exception e) 87 | { 88 | Debug.Log($"Slideshow property \"{name}\" cannot hold a value of type \"{value.GetType().Name}\"!"); 89 | Debug.LogException(e); 90 | } 91 | } 92 | } 93 | 94 | /// 95 | /// Represents a single slide of a . 96 | /// 97 | public class SlideshowSlide 98 | { 99 | /// 100 | /// The that this slide is a part of. 101 | /// 102 | public CustomSlideshow Owner { get; private set; } 103 | 104 | /// 105 | /// The name of the scene that this slide displays. 106 | /// This is first checked against the owner's scenes, then against . 107 | /// 108 | public string SceneName { get; set; } 109 | 110 | /// 111 | /// The path that the camera takes while viewing this scene. 112 | /// X and Y are the camera's position, Z is the camera's focal depth. 113 | /// 114 | public List CameraPath { get; private set; } 115 | 116 | /// 117 | /// The time in seconds this image remains on screen, including both fades. 118 | /// 119 | public float Duration { get; set; } 120 | 121 | /// 122 | /// The time in seconds this image takes to fade in. 123 | /// 124 | public float FadeIn { get; set; } 125 | 126 | /// 127 | /// The time in seconds this image takes to fade out. 128 | /// 129 | public float FadeOut { get; set; } 130 | 131 | /// 132 | /// True if this slide should appear in the slideshow. 133 | /// 134 | public bool Enabled { get; set; } 135 | 136 | /// 137 | /// Creates an empty slideshow slide. 138 | /// 139 | public SlideshowSlide(CustomSlideshow owner) 140 | { 141 | Enabled = true; 142 | Owner = owner; 143 | CameraPath = new List(); 144 | Duration = 0f; 145 | FadeIn = 0.75f; 146 | FadeOut = 0.75f; 147 | } 148 | 149 | /// 150 | /// Creates a single slide from a JSON string. 151 | /// 152 | public SlideshowSlide(CustomSlideshow owner, string json) : this(owner, json.dictionaryFromJson()) { } 153 | 154 | /// 155 | /// Creates a single slide from a JSON object. 156 | /// 157 | public SlideshowSlide(CustomSlideshow owner, JsonObj data) : this(owner) 158 | { 159 | Duration = -1f; 160 | 161 | foreach (var pair in data) 162 | LoadValue(pair.Key, pair.Value); 163 | 164 | if (SceneName == null) throw new ArgumentException("Missing \"name\"!", nameof(data)); 165 | if (Duration < 0f) throw new ArgumentException("Missing or invalid \"duration\" value!", nameof(data)); 166 | if(CameraPath.Count == 0) 167 | { 168 | CameraPath.Add(new Vector3(0f, 0f, 3f)); 169 | } 170 | } 171 | 172 | private void LoadValue(string name, object value) 173 | { 174 | try 175 | { 176 | switch (name) 177 | { 178 | case "name": SceneName = Convert.ToString(value); break; 179 | case "duration": Duration = Convert.ToSingle(value); break; 180 | case "fadein": FadeIn = Convert.ToSingle(value); break; 181 | case "fadeout": FadeOut = Convert.ToSingle(value); break; 182 | case "campath": 183 | { 184 | JsonList list = (JsonList)value; 185 | for (int i = 0; i < list.Count - 2; i += 3) 186 | { 187 | CameraPath.Add(new Vector3( 188 | Convert.ToSingle(list[i + 0]), 189 | Convert.ToSingle(list[i + 1]), 190 | Convert.ToSingle(list[i + 2]) 191 | )); 192 | } 193 | } 194 | break; 195 | } 196 | } 197 | catch (Exception e) 198 | { 199 | Debug.Log($"Slide property \"{name}\" cannot hold a value of type \"{value.GetType().Name}\"!"); 200 | Debug.LogException(e); 201 | } 202 | } 203 | } 204 | } 205 | -------------------------------------------------------------------------------- /SlugBase/Scenes/SceneEditor.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using Menu; 6 | using UnityEngine; 7 | using RWCustom; 8 | using System.IO; 9 | 10 | namespace SlugBase 11 | { 12 | using static CustomSceneManager; 13 | 14 | // Do not store any references to the owner MenuScene! 15 | internal class SceneEditor 16 | { 17 | // STATIC // 18 | 19 | private static AttachedField editor; 20 | 21 | public static void ApplyHooks() { 22 | On.Menu.MenuScene.GrafUpdate += MenuScene_GrafUpdate; 23 | On.Menu.MenuObject.RemoveSprites += MenuObject_RemoveSprites; 24 | On.Menu.MenuScene.Show += MenuScene_Show; 25 | On.Menu.MenuScene.Hide += MenuScene_Hide; 26 | 27 | editor = new AttachedField(); 28 | editor.OnCulled += (key, val) => 29 | { 30 | val.Remove(); 31 | }; 32 | } 33 | 34 | private static void MenuScene_Hide(On.Menu.MenuScene.orig_Hide orig, MenuScene self) 35 | { 36 | orig(self); 37 | editor[self]?.Hide(); 38 | } 39 | 40 | private static void MenuScene_Show(On.Menu.MenuScene.orig_Show orig, MenuScene self) 41 | { 42 | orig(self); 43 | editor[self]?.Show(); 44 | } 45 | 46 | private static void MenuObject_RemoveSprites(On.Menu.MenuObject.orig_RemoveSprites orig, MenuObject self) 47 | { 48 | orig(self); 49 | if (self is MenuScene scene) 50 | editor[scene]?.Remove(); 51 | } 52 | 53 | private static void MenuScene_GrafUpdate(On.Menu.MenuScene.orig_GrafUpdate orig, MenuScene self, float timeStacker) 54 | { 55 | orig(self, timeStacker); 56 | 57 | if (editor.TryGet(self, out SceneEditor se)) 58 | { 59 | se.Update(self); 60 | if(Input.GetKeyDown(KeyCode.RightBracket)) 61 | { 62 | se.Remove(); 63 | editor.Unset(self); 64 | } 65 | } 66 | else 67 | { 68 | if (self.sceneFolder == resourceFolderName && Input.GetKeyDown(KeyCode.RightBracket)) 69 | { 70 | SlugBaseCharacter ply = null; 71 | foreach(MenuObject subObj in self.subObjects) 72 | { 73 | if(subObj is MenuIllustration illust) 74 | { 75 | ply = customRep[illust]?.Owner.Owner; 76 | if (ply != null) break; 77 | } 78 | } 79 | if (ply.DevMode) 80 | editor[self] = new SceneEditor(self); 81 | } 82 | } 83 | } 84 | 85 | // INSTANCE // 86 | 87 | private bool alive = true; 88 | private List handles; 89 | 90 | public SceneEditor(MenuScene owner) 91 | { 92 | handles = new List(); 93 | } 94 | 95 | public void Show() 96 | { 97 | foreach (MoveHandle handle in handles) 98 | handle.Show(); 99 | } 100 | 101 | public void Hide() 102 | { 103 | foreach (MoveHandle handle in handles) 104 | handle.Hide(); 105 | } 106 | 107 | public void Update(MenuScene owner) 108 | { 109 | if (!alive) return; 110 | int handle = 0; 111 | 112 | Vector2? mousePos = Input.mousePosition; 113 | 114 | // Fade out handles not close to the mouse 115 | MenuIllustration closestIllust = null; 116 | { 117 | float minIllustDist = 20f * 20f; 118 | for (int i = 0; i < owner.subObjects.Count; i++) 119 | { 120 | if (!(owner.subObjects[i] is MenuIllustration illust)) continue; 121 | float dist = Vector2.SqrMagnitude(illust.pos + illust.size / 2f - mousePos.Value); 122 | if(minIllustDist > dist) 123 | { 124 | closestIllust = illust; 125 | minIllustDist = dist; 126 | } 127 | } 128 | } 129 | 130 | // Update move handles 131 | for (int i = 0; i < owner.subObjects.Count; i++) 132 | { 133 | if (!(owner.subObjects[i] is MenuIllustration illust)) continue; 134 | if (!customRep.TryGet(illust, out SceneImage csi)) continue; 135 | 136 | Vector2 centerPos = illust.pos + illust.size / 2f; 137 | if (handles.Count <= handle) 138 | { 139 | handles.Add(new MoveHandle(csi.DisplayName)); 140 | } 141 | handles[handle].SetVisible(illust.sprite.concatenatedAlpha > 0f); 142 | handles[handle].Update(ref centerPos, ref mousePos, closestIllust != null && closestIllust != illust); 143 | illust.pos = centerPos - illust.size / 2f; 144 | csi.Pos = illust.pos; 145 | handle++; 146 | } 147 | 148 | // Save on request 149 | if (Input.GetKeyDown(KeyCode.LeftBracket)) 150 | { 151 | CustomScene sceneToSave = null; 152 | foreach (var subObj in owner.subObjects) 153 | { 154 | if (!(subObj is MenuIllustration illust)) continue; 155 | 156 | SceneImage csi = customRep[illust]; 157 | if (csi != null && csi.Owner.dirty) 158 | { 159 | sceneToSave = csi.Owner; 160 | break; 161 | } 162 | } 163 | 164 | if(sceneToSave != null) 165 | SaveEditedScene(sceneToSave); 166 | } 167 | } 168 | 169 | private static void SaveEditedScene(CustomScene scene) 170 | { 171 | // Write the scene to a file 172 | try 173 | { 174 | string outPath = string.Join(Path.DirectorySeparatorChar.ToString(), new string[] { 175 | scene.Owner.DefaultResourcePath, 176 | "Scenes", 177 | scene.Name, 178 | "scene.json" 179 | }); 180 | 181 | // Save all images to a JSON object 182 | Dictionary jsonObj = scene.ToJsonObj(); 183 | foreach (var img in scene.Images) 184 | img.OnSave(jsonObj); 185 | 186 | // Write to a file 187 | Directory.CreateDirectory(Path.GetDirectoryName(outPath)); 188 | File.WriteAllText(outPath, jsonObj.toJson()); 189 | 190 | // Unset all dirty flags 191 | foreach(var img in scene.Images) 192 | img.dirty = false; 193 | scene.dirty = false; 194 | } catch(Exception e) 195 | { 196 | Debug.Log("Failed to save scene to file!"); 197 | Debug.LogException(e); 198 | } 199 | } 200 | 201 | public void Remove() 202 | { 203 | if (!alive) return; 204 | alive = false; 205 | 206 | foreach(MoveHandle handle in handles) 207 | { 208 | handle.Remove(); 209 | } 210 | handles = null; 211 | } 212 | 213 | internal class MoveHandle 214 | { 215 | private FSprite handle; 216 | private FLabel name; 217 | private FLabel nameShadow; 218 | private bool dragging; 219 | private bool hidden; 220 | 221 | public MoveHandle(string name) 222 | { 223 | handle = new FSprite("buttonCircleA") { anchorX = 0.5f, anchorY = 0.5f, color = Color.red }; 224 | this.name = new FLabel("font", name) { anchorX = 0f, anchorY = 0.5f }; 225 | nameShadow = new FLabel("font", name) { anchorX = 0f, anchorY = 0.5f, color = Color.black }; 226 | Futile.stage.AddChild(handle); 227 | Futile.stage.AddChild(nameShadow); 228 | Futile.stage.AddChild(this.name); 229 | } 230 | 231 | public void Update(ref Vector2 handlePos, ref Vector2? mousePos, bool dark) 232 | { 233 | if (hidden) 234 | { 235 | dragging = false; 236 | return; 237 | } 238 | handle.alpha = dark ? 0.4f : 1f; 239 | name.alpha = dark ? 0.2f : 1f; 240 | nameShadow.alpha = dark ? 0.1f : 0.75f; 241 | if(mousePos.HasValue && Input.GetMouseButton(0)) 242 | { 243 | if (dragging) 244 | { 245 | handlePos = mousePos.Value; 246 | mousePos = null; 247 | } 248 | else 249 | { 250 | if(PointOverHandle(mousePos.Value, handlePos) && Input.GetMouseButtonDown(0)) 251 | { 252 | dragging = true; 253 | mousePos = null; 254 | } 255 | } 256 | } else 257 | { 258 | dragging = false; 259 | } 260 | 261 | Vector2 drawPos = new Vector2(Mathf.Floor(handlePos.x) + 0.1f, Mathf.Floor(handlePos.y) + 0.1f); 262 | handle.SetPosition(drawPos); 263 | drawPos.x += 12f; 264 | name.SetPosition(drawPos); 265 | drawPos.x += 1f; 266 | drawPos.y -= 1f; 267 | nameShadow.SetPosition(drawPos); 268 | } 269 | 270 | public void SetVisible(bool visible) 271 | { 272 | if(visible == hidden) 273 | { 274 | if (visible) Show(); 275 | else Hide(); 276 | } 277 | } 278 | 279 | public void Hide() 280 | { 281 | hidden = true; 282 | handle.isVisible = false; 283 | name.isVisible = false; 284 | nameShadow.isVisible = false; 285 | } 286 | 287 | public void Show() 288 | { 289 | hidden = false; 290 | handle.isVisible = true; 291 | name.isVisible = true; 292 | nameShadow.isVisible = true; 293 | } 294 | 295 | public void Remove() 296 | { 297 | handle.RemoveFromContainer(); 298 | name.RemoveFromContainer(); 299 | nameShadow.RemoveFromContainer(); 300 | } 301 | 302 | private bool PointOverHandle(Vector2 pos, Vector2 handlePos) 303 | { 304 | return Custom.DistLess(pos, handlePos, 8.5f); 305 | } 306 | } 307 | } 308 | } 309 | -------------------------------------------------------------------------------- /SlugBase/Scenes/SelectMenu.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Security; 4 | using System.Security.Permissions; 5 | using MonoMod.RuntimeDetour; 6 | using System.Reflection; 7 | using UnityEngine; 8 | using System.IO; 9 | using Menu; 10 | using HUD; 11 | using System.Runtime.CompilerServices; 12 | using System.Linq; 13 | using RWCustom; 14 | 15 | namespace SlugBase 16 | { 17 | using static CustomSceneManager; 18 | 19 | internal static class SelectMenu 20 | { 21 | private static bool selectMenuShimActive = false; 22 | private static float altRestartUp = 0f; 23 | 24 | public static void ApplyHooks() 25 | { 26 | On.Menu.SlugcatSelectMenu.UpdateStartButtonText += SlugcatSelectMenu_UpdateStartButtonText; 27 | new Hook( 28 | typeof(SlugcatSelectMenu.SlugcatPageContinue).GetProperty("saveGameData", BindingFlags.Public | BindingFlags.Instance).GetGetMethod(), 29 | typeof(SelectMenu).GetMethod(nameof(SlugcatPageContinue_get_saveGameData), BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static) 30 | ); 31 | On.Menu.SlugcatSelectMenu.GetSaveGameData += SlugcatSelectMenu_GetSaveGameData; 32 | On.Menu.SlugcatSelectMenu.MineForSaveData += SlugcatSelectMenu_MineForSaveData; 33 | On.Menu.SlugcatSelectMenu.Update += SlugcatSelectMenu_Update; 34 | On.Menu.SlugcatSelectMenu.ctor += SlugcatSelectMenu_ctor; 35 | On.Menu.HoldButton.ctor += HoldButton_ctor; 36 | On.Menu.SlugcatSelectMenu.SlugcatPage.AddImage += SlugcatPage_AddImage; 37 | On.Menu.SlugcatSelectMenu.SlugcatPage.ctor += SlugcatPage_ctor; 38 | On.Menu.SlugcatSelectMenu.SlugcatPageNewGame.ctor += SlugcatPageNewGame_ctor; 39 | On.HUD.KarmaMeter.Draw += KarmaMeter_Draw; 40 | } 41 | 42 | // Stop GetSaveGameData from inlining 43 | private static void SlugcatSelectMenu_UpdateStartButtonText(On.Menu.SlugcatSelectMenu.orig_UpdateStartButtonText orig, SlugcatSelectMenu self) 44 | { 45 | self.startButton.fillTime = (!self.restartChecked) ? 40f : 120f; 46 | if (self.saveGameData[self.slugcatPageIndex] == null || self.restartChecked) 47 | self.startButton.menuLabel.text = self.Translate("NEW GAME"); 48 | else if (self.slugcatPages[self.slugcatPageIndex].slugcatNumber == 2 && self.redIsDead) 49 | self.startButton.menuLabel.text = self.Translate("STATISTICS"); 50 | else 51 | self.startButton.menuLabel.text = self.Translate("CONTINUE"); 52 | } 53 | 54 | // Stop GetSaveGameData from inlining 55 | private static SlugcatSelectMenu.SaveGameData SlugcatPageContinue_get_saveGameData(Func orig, SlugcatSelectMenu.SlugcatPageContinue self) 56 | { 57 | if(self.menu is SlugcatSelectMenu ssm) 58 | return ssm.saveGameData[self.SlugcatPageIndex]; 59 | else 60 | return orig(self); 61 | } 62 | 63 | // The original took a slugcat index as an input, indexed into slugcatColorOrder, and returned the save game at that slot 64 | // slugcatColorOrder is a list of slugcat indices, indexed by page number 65 | // Indexing into this list is not guaranteed to return the correct output, but it works for the vanilla slugcats 66 | private static SlugcatSelectMenu.SaveGameData SlugcatSelectMenu_GetSaveGameData(On.Menu.SlugcatSelectMenu.orig_GetSaveGameData orig, SlugcatSelectMenu self, int slugcatColor) 67 | { 68 | int i = Array.IndexOf(self.slugcatColorOrder, slugcatColor); 69 | if (i < 0 || i >= self.saveGameData.Length) return null; 70 | return self.saveGameData[i]; 71 | } 72 | 73 | // The select menu relies on a manifest of save information 74 | // In vanilla, this is either pulled from the current game or mined from the progression file 75 | // If the indicated slugcat is added by slugbase, instead mine from the custom save file 76 | private static SlugcatSelectMenu.SaveGameData SlugcatSelectMenu_MineForSaveData(On.Menu.SlugcatSelectMenu.orig_MineForSaveData orig, ProcessManager manager, int slugcat) 77 | { 78 | SlugBaseCharacter ply = PlayerManager.GetCustomPlayer(slugcat); 79 | if(ply != null) 80 | { 81 | SaveState save = manager.rainWorld.progression.currentSaveState; 82 | if (save != null && save.saveStateNumber == slugcat) 83 | { 84 | return new SlugcatSelectMenu.SaveGameData 85 | { 86 | karmaCap = save.deathPersistentSaveData.karmaCap, 87 | karma = save.deathPersistentSaveData.karma, 88 | karmaReinforced = save.deathPersistentSaveData.reinforcedKarma, 89 | shelterName = save.denPosition, 90 | cycle = save.cycleNumber, 91 | hasGlow = save.theGlow, 92 | hasMark = save.deathPersistentSaveData.theMark, 93 | redsExtraCycles = save.redExtraCycles, 94 | food = save.food, 95 | redsDeath = save.deathPersistentSaveData.redsDeath, 96 | ascended = save.deathPersistentSaveData.ascended 97 | }; 98 | } 99 | return SaveManager.GetCustomSaveData(manager.rainWorld, ply.Name, manager.rainWorld.options.saveSlot); 100 | } 101 | return orig(manager, slugcat); 102 | } 103 | 104 | // Lock the select button when necessary 105 | private static void SlugcatSelectMenu_Update(On.Menu.SlugcatSelectMenu.orig_Update orig, SlugcatSelectMenu self) 106 | { 107 | // This is before orig because it must fetch the character before scrolling logic applies 108 | // Otherwise, there's a single frame where the button flashes 109 | SlugBaseCharacter ply = PlayerManager.GetCustomPlayer(self.slugcatColorOrder[self.slugcatPageIndex]); 110 | 111 | if (ply == null) 112 | { 113 | altRestartUp = 0f; 114 | orig(self); 115 | } 116 | else 117 | { 118 | var state = ply.GetSelectMenuState(self); 119 | 120 | if (state == SlugBaseCharacter.SelectMenuAccessibility.MustRestart && !self.restartAvailable) 121 | { 122 | altRestartUp = Mathf.Max(self.restartUp, Custom.LerpAndTick(altRestartUp, 1f, 0.07f, 0.025f)); 123 | self.restartUp = altRestartUp; 124 | if (altRestartUp == 1f) 125 | self.restartAvailable = true; 126 | } 127 | else 128 | altRestartUp = 0f; 129 | 130 | orig(self); 131 | 132 | bool locked = false; 133 | switch (state) 134 | { 135 | case SlugBaseCharacter.SelectMenuAccessibility.Locked: locked = true; break; 136 | case SlugBaseCharacter.SelectMenuAccessibility.Hidden: locked = true; break; 137 | case SlugBaseCharacter.SelectMenuAccessibility.MustRestart: locked = !self.restartChecked; break; 138 | } 139 | 140 | if (locked) self.startButton.GetButtonBehavior.greyedOut = true; 141 | 142 | 143 | } 144 | } 145 | 146 | // Change some data associated with custom slugcat pages 147 | private static void SlugcatPage_ctor(On.Menu.SlugcatSelectMenu.SlugcatPage.orig_ctor orig, SlugcatSelectMenu.SlugcatPage self, Menu.Menu menu, MenuObject owner, int pageIndex, int slugcatNumber) 148 | { 149 | orig(self, menu, owner, pageIndex, slugcatNumber); 150 | SlugBaseCharacter ply = PlayerManager.GetCustomPlayer(pageIndex); 151 | if (ply != null) { 152 | self.colorName = ply.Name; 153 | self.effectColor = ply.SlugcatColorInternal(slugcatNumber) ?? Color.white; 154 | } 155 | } 156 | 157 | // Override select scenes for SlugBase characters 158 | private static void SlugcatPage_AddImage(On.Menu.SlugcatSelectMenu.SlugcatPage.orig_AddImage orig, SlugcatSelectMenu.SlugcatPage self, bool ascended) 159 | { 160 | SlugBaseCharacter ply = PlayerManager.GetCustomPlayer(self.slugcatNumber); 161 | 162 | // Do not modify scenes for any non-SlugBase slugcats 163 | if(ply == null) 164 | { 165 | orig(self, ascended); 166 | return; 167 | } 168 | 169 | // Use Survivor's default scenes on the select menu 170 | string sceneName = ascended ? "SelectMenuAscended" : "SelectMenu"; 171 | if (!ply.HasScene(sceneName)) 172 | { 173 | orig(self, ascended); 174 | 175 | // Fix the scene position being off 176 | if(self.sceneOffset == default(Vector2)) 177 | self.sceneOffset = new Vector2(-10f, 100f); 178 | 179 | // Fix the wrong scene loading in when ascended 180 | if (ascended && self.slugcatImage.sceneID == MenuScene.SceneID.Slugcat_White) 181 | { 182 | self.slugcatImage.RemoveSprites(); 183 | self.RemoveSubObject(self.slugcatImage); 184 | 185 | self.slugcatImage = new InteractiveMenuScene(self.menu, self, MenuScene.SceneID.Ghost_White); 186 | self.subObjects.Add(self.slugcatImage); 187 | } 188 | 189 | return; 190 | } 191 | 192 | // Make sure it doesn't crash if the mark or glow is missing 193 | self.markSquare = new FSprite("pixel") { isVisible = false }; 194 | self.markGlow = new FSprite("pixel") { isVisible = false }; 195 | self.glowSpriteA = new FSprite("pixel") { isVisible = false }; 196 | self.glowSpriteB = new FSprite("pixel") { isVisible = false }; 197 | 198 | 199 | // This function intentionally does not call the original 200 | // If this mod has claimed a slot, it seems best to not let other mods try to change this screen 201 | 202 | // Taken from SlugcatPage.AddImage 203 | self.imagePos = new Vector2(683f, 484f); 204 | self.sceneOffset = new Vector2(0f, 0f); 205 | 206 | 207 | // Load a custom character's select screen from resources 208 | var sceneOverride = OverrideNextScene(ply, sceneName, img => 209 | { 210 | if (img.HasTag("MARK") && !self.HasMark) return false; 211 | if (img.HasTag("GLOW") && !self.HasGlow) return false; 212 | return true; 213 | }); 214 | 215 | MarkImage mark = null; 216 | GlowImage glow = null; 217 | sceneOverride.OnLoad += (scene) => 218 | { 219 | // Parse selectmenux and selectmenuy 220 | self.sceneOffset.x = scene.GetProperty("selectmenux") ?? 0f; 221 | self.sceneOffset.y = scene.GetProperty("selectmenuy") ?? 0f; 222 | Debug.Log($"Scene offset for {ply.Name}: {self.sceneOffset}"); 223 | 224 | // Slugcat depth, used for positioning the glow and mark 225 | self.slugcatDepth = scene.GetProperty("slugcatdepth") ?? 3f; 226 | 227 | // Add mark 228 | mark = new MarkImage(scene, self.slugcatDepth + 0.1f); 229 | scene.InsertImage(mark); 230 | 231 | // Add glow 232 | glow = new GlowImage(scene, self.slugcatDepth + 0.1f); 233 | scene.InsertImage(glow); 234 | }; 235 | 236 | try 237 | { 238 | self.slugcatImage = new InteractiveMenuScene(self.menu, self, MenuScene.SceneID.Slugcat_White); // This scene will be immediately overwritten 239 | } 240 | finally { ClearSceneOverride(); } 241 | self.subObjects.Add(self.slugcatImage); 242 | 243 | // Find the relative mark and glow positions 244 | self.markOffset = mark.Pos - new Vector2(self.MidXpos, self.imagePos.y + 150f) + self.sceneOffset; 245 | self.glowOffset = glow.Pos - new Vector2(self.MidXpos, self.imagePos.y) + self.sceneOffset; 246 | } 247 | 248 | // Add custom slugcat select screens 249 | private static void SlugcatSelectMenu_ctor(On.Menu.SlugcatSelectMenu.orig_ctor orig, SlugcatSelectMenu self, ProcessManager manager) 250 | { 251 | try 252 | { 253 | selectMenuShimActive = true; 254 | orig(self, manager); 255 | } 256 | finally 257 | { 258 | selectMenuShimActive = false; 259 | } 260 | } 261 | 262 | // Shim to add the slugcat pages at the correct layer 263 | // The hold button is the first object that needs to be layed above the scenes 264 | private static void HoldButton_ctor(On.Menu.HoldButton.orig_ctor orig, HoldButton self, Menu.Menu menu, MenuObject owner, string displayText, string singalText, Vector2 pos, float fillTime) 265 | { 266 | if (selectMenuShimActive && singalText == "START" && menu is SlugcatSelectMenu ssm) 267 | { 268 | AddSlugBaseScenes(ssm); 269 | selectMenuShimActive = false; 270 | } 271 | orig(self, menu, owner, displayText, singalText, pos, fillTime); 272 | } 273 | 274 | // Add all SlugBase characters to the select screen 275 | private static void AddSlugBaseScenes(SlugcatSelectMenu self) 276 | { 277 | int selectedSlugcat = self.manager.rainWorld.progression.miscProgressionData.currentlySelectedSinglePlayerSlugcat; 278 | 279 | List plys = PlayerManager.customPlayers; 280 | List visiblePlys = plys.Where(c => c.GetSelectMenuState(self) != SlugBaseCharacter.SelectMenuAccessibility.Hidden).ToList(); 281 | 282 | int origLength = self.slugcatColorOrder.Length; 283 | 284 | // First, try to find the highest taken slugcat index 285 | for (int i = 0; i < self.slugcatColorOrder.Length; i++) 286 | SlugBaseMod.FirstCustomIndex = Math.Max(self.slugcatColorOrder[i] + 1, SlugBaseMod.FirstCustomIndex); 287 | 288 | // Assign each character a unique index 289 | int nextCustomIndex = SlugBaseMod.FirstCustomIndex; 290 | for (int i = 0; i < plys.Count; i++) 291 | plys[i].SlugcatIndex = nextCustomIndex++; 292 | 293 | // Add SlugBase characters to the page order 294 | Array.Resize(ref self.slugcatColorOrder, origLength + visiblePlys.Count); 295 | for (int i = origLength; i < self.slugcatColorOrder.Length; i++) 296 | self.slugcatColorOrder[i] = visiblePlys[i - origLength].SlugcatIndex; 297 | 298 | // Retrieve save data 299 | Array.Resize(ref self.saveGameData, origLength + visiblePlys.Count); 300 | 301 | for (int i = 0; i < visiblePlys.Count; i++) 302 | { 303 | self.saveGameData[origLength + i] = SlugcatSelectMenu.MineForSaveData(self.manager, visiblePlys[i].SlugcatIndex); 304 | } 305 | 306 | // Add a new page to the menu 307 | Array.Resize(ref self.slugcatPages, origLength + visiblePlys.Count); 308 | 309 | for (int i = 0; i < visiblePlys.Count; i++) 310 | { 311 | int o = origLength + i; 312 | if (self.saveGameData[o] != null) 313 | { 314 | self.slugcatPages[o] = new SlugcatSelectMenu.SlugcatPageContinue(self, null, o + 1, self.slugcatColorOrder[o]); 315 | } 316 | else 317 | { 318 | self.slugcatPages[o] = new SlugcatSelectMenu.SlugcatPageNewGame(self, null, o + 1, self.slugcatColorOrder[o]); 319 | } 320 | 321 | // Select the correct page 322 | if (selectedSlugcat == self.slugcatColorOrder[o]) 323 | self.slugcatPageIndex = o; 324 | 325 | self.pages.Add(self.slugcatPages[o]); 326 | } 327 | } 328 | 329 | // Change select screen name and description 330 | private static void SlugcatPageNewGame_ctor(On.Menu.SlugcatSelectMenu.SlugcatPageNewGame.orig_ctor orig, SlugcatSelectMenu.SlugcatPageNewGame self, Menu.Menu menu, MenuObject owner, int pageIndex, int slugcatNumber) 331 | { 332 | orig(self, menu, owner, pageIndex, slugcatNumber); 333 | 334 | SlugBaseCharacter ply = PlayerManager.GetCustomPlayer(slugcatNumber); 335 | if(ply != null) 336 | { 337 | self.difficultyLabel.text = ply.DisplayName.ToUpper(); 338 | self.infoLabel.text = ply.Description.Replace("", Environment.NewLine); 339 | } 340 | } 341 | 342 | // Fix position calculation of the karma meter to use timeStacker instead of timer 343 | // This results in a smooth animation 344 | private static void KarmaMeter_Draw(On.HUD.KarmaMeter.orig_Draw orig, KarmaMeter self, float timeStacker) 345 | { 346 | orig(self, timeStacker); 347 | if (self.hud?.owner?.GetOwnerType() == HUD.HUD.OwnerType.CharacterSelect) 348 | { 349 | if (self.karmaSprite == null) return; 350 | Vector2 pos = self.DrawPos(timeStacker); 351 | self.karmaSprite.x = pos.x; 352 | self.karmaSprite.y = pos.y; 353 | if (self.showAsReinforced) 354 | { 355 | if (self.ringSprite != null) 356 | { 357 | self.ringSprite.x = pos.x; 358 | self.ringSprite.y = pos.y; 359 | } 360 | if (self.vectorRingSprite != null) 361 | { 362 | self.vectorRingSprite.x = pos.x; 363 | self.vectorRingSprite.y = pos.y; 364 | } 365 | } 366 | } 367 | } 368 | 369 | private class MarkImage : SceneImage 370 | { 371 | public MarkImage(CustomScene owner, float depth) : base(owner) 372 | { 373 | Pos = new Vector2(owner.GetProperty("markx") ?? 683f, owner.GetProperty("marky") ?? 484f); 374 | Depth = depth; 375 | } 376 | 377 | public override string DisplayName => "MARK"; 378 | 379 | public override bool ShouldBeSaved => false; 380 | 381 | protected internal override bool OnBuild(MenuScene scene) 382 | { 383 | if (!(scene.owner is SlugcatSelectMenu.SlugcatPage sp)) return false; 384 | 385 | if (sp.HasMark) 386 | { 387 | sp.markSquare = new FSprite("pixel", true); 388 | sp.markSquare.scale = 14f; 389 | sp.markSquare.color = Color.Lerp(sp.effectColor, Color.white, 0.7f); 390 | sp.Container.AddChild(sp.markSquare); 391 | sp.markGlow = new FSprite("Futile_White", true); 392 | sp.markGlow.shader = sp.menu.manager.rainWorld.Shaders["FlatLight"]; 393 | sp.markGlow.color = sp.effectColor; 394 | sp.Container.AddChild(sp.markGlow); 395 | } else 396 | { 397 | sp.markSquare = new FSprite("pixel") { isVisible = false }; 398 | sp.markGlow = new FSprite("pixel") { isVisible = false }; 399 | } 400 | return false; 401 | } 402 | 403 | public override void OnSave(Dictionary scene) 404 | { 405 | scene["markx"] = Pos.x; 406 | scene["marky"] = Pos.y; 407 | } 408 | } 409 | 410 | private class GlowImage : SceneImage 411 | { 412 | public GlowImage(CustomScene owner, float depth) : base(owner) 413 | { 414 | Pos = new Vector2(owner.GetProperty("glowx") ?? 683f, owner.GetProperty("glowy") ?? 484f); 415 | Debug.Log($"Glow is at {Pos}"); 416 | Depth = depth; 417 | } 418 | 419 | public override string DisplayName => "GLOW"; 420 | 421 | public override bool ShouldBeSaved => false; 422 | 423 | protected internal override bool OnBuild(MenuScene scene) 424 | { 425 | if (!(scene.owner is SlugcatSelectMenu.SlugcatPage sp)) return false; 426 | 427 | if (sp.HasMark) 428 | { 429 | sp.glowSpriteB = new FSprite("Futile_White"); 430 | sp.glowSpriteB.shader = sp.menu.manager.rainWorld.Shaders["FlatLightNoisy"]; 431 | sp.Container.AddChild(sp.glowSpriteB); 432 | sp.glowSpriteA = new FSprite("Futile_White"); 433 | sp.glowSpriteA.shader = sp.menu.manager.rainWorld.Shaders["FlatLightNoisy"]; 434 | sp.Container.AddChild(sp.glowSpriteA); 435 | } else 436 | { 437 | sp.glowSpriteA = new FSprite("pixel") { isVisible = false }; 438 | sp.glowSpriteB = new FSprite("pixel") { isVisible = false }; 439 | } 440 | 441 | return false; 442 | } 443 | 444 | public override void OnSave(Dictionary scene) 445 | { 446 | scene["glowx"] = Pos.x; 447 | scene["glowy"] = Pos.y; 448 | } 449 | } 450 | } 451 | } 452 | -------------------------------------------------------------------------------- /SlugBase/Scenes/ShelterScreens.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using UnityEngine; 4 | using Menu; 5 | using MonoMod.RuntimeDetour; 6 | using RWCustom; 7 | using System.Reflection; 8 | using System.IO; 9 | 10 | namespace SlugBase 11 | { 12 | using static CustomSceneManager; 13 | 14 | internal static class ShelterScreens 15 | { 16 | private static Dictionary updateDelegates = new Dictionary(); 17 | 18 | public static void ApplyHooks() 19 | { 20 | On.Menu.MenuScene.BuildScene += MenuScene_BuildScene; 21 | On.Menu.MenuScene.AddIllustration += MenuScene_AddIllustration; 22 | On.MainLoopProcess.ShutDownProcess += MainLoopProcess_ShutDownProcess; 23 | On.Menu.SleepAndDeathScreen.AddBkgIllustration += SleepAndDeathScreen_AddBkgIllustration; 24 | On.Menu.SleepAndDeathScreen.Update += SleepAndDeathScreen_Update; 25 | } 26 | 27 | // Image positions are loaded from positions.txt at the end of BuildScene 28 | // This gets around that by moving them after 29 | private static void MenuScene_BuildScene(On.Menu.MenuScene.orig_BuildScene orig, MenuScene self) 30 | { 31 | orig(self); 32 | if(moveImages.Count > 0) 33 | { 34 | foreach(var pair in moveImages) 35 | { 36 | pair.Key.lastPos = pair.Value; 37 | pair.Key.pos = pair.Value; 38 | } 39 | moveImages.Clear(); 40 | } 41 | } 42 | 43 | // The default sleep screen is Hunter, which doesn't line up with the default select screen 44 | // Change Hunter to Survivor for the sleep screen 45 | private static List> moveImages = new List>(); 46 | private static void MenuScene_AddIllustration(On.Menu.MenuScene.orig_AddIllustration orig, MenuScene self, MenuIllustration newIllu) 47 | { 48 | SlugBaseCharacter chara = PlayerManager.GetCustomPlayer(self.menu.manager.rainWorld.progression.miscProgressionData.currentlySelectedSinglePlayerSlugcat); 49 | if (newIllu.fileName == "Sleep - 2 - Red" 50 | && chara != null 51 | && !chara.HasScene("SleepScreen") 52 | && ((self.menu as SleepAndDeathScreen)?.IsSleepScreen ?? false) 53 | && newIllu is MenuDepthIllustration mdi) 54 | { 55 | string folder = string.Concat(new object[] 56 | { 57 | "Scenes", 58 | Path.DirectorySeparatorChar, 59 | "Sleep Screen - White", 60 | }); 61 | newIllu.RemoveSprites(); 62 | newIllu = new MenuDepthIllustration(newIllu.menu, newIllu.owner, folder, "Sleep - 2 - White", new Vector2(677f, 63f), mdi.depth, mdi.shader); 63 | moveImages.Add(new KeyValuePair((MenuDepthIllustration)newIllu, new Vector2(677f, 63f))); 64 | } 65 | 66 | orig(self, newIllu); 67 | } 68 | 69 | // Plug a memory leak 70 | private static void MainLoopProcess_ShutDownProcess(On.MainLoopProcess.orig_ShutDownProcess orig, MainLoopProcess self) 71 | { 72 | orig(self); 73 | updateDelegates.Clear(); 74 | } 75 | 76 | // The original method may crash when called, since it assumes that there are 3+ images in the scene 77 | // Replace the method entirely for modded slugcats 78 | private static void SleepAndDeathScreen_Update(On.Menu.SleepAndDeathScreen.orig_Update orig, SleepAndDeathScreen self) 79 | { 80 | if(self.scene.sceneFolder != resourceFolderName) 81 | { 82 | orig(self); 83 | return; 84 | } 85 | 86 | if (self.starvedWarningCounter >= 0) 87 | { 88 | self.starvedWarningCounter++; 89 | } 90 | 91 | // base.Update(); 92 | if(!updateDelegates.TryGetValue(self, out Action baseUpdate)) 93 | { 94 | MethodInfo m = typeof(KarmaLadderScreen).GetMethod("Update", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); 95 | baseUpdate = (Action)Activator.CreateInstance(typeof(Action), self, m.MethodHandle.GetFunctionPointer()); 96 | updateDelegates[self] = baseUpdate; 97 | } 98 | baseUpdate(); 99 | 100 | if (self.exitButton != null) 101 | { 102 | self.exitButton.buttonBehav.greyedOut = self.ButtonsGreyedOut; 103 | } 104 | if (self.passageButton != null) 105 | { 106 | self.passageButton.buttonBehav.greyedOut = (self.ButtonsGreyedOut || self.goalMalnourished); 107 | self.passageButton.black = Mathf.Max(0f, self.passageButton.black - 0.0125f); 108 | } 109 | if (self.endGameSceneCounter >= 0) 110 | { 111 | self.endGameSceneCounter++; 112 | if (self.endGameSceneCounter > 140) 113 | { 114 | self.manager.RequestMainProcessSwitch(ProcessManager.ProcessID.CustomEndGameScreen); 115 | } 116 | } 117 | if (self.RevealMap) 118 | { 119 | self.fadeOutIllustration = Custom.LerpAndTick(self.fadeOutIllustration, 1f, 0.02f, 0.025f); 120 | } 121 | else 122 | { 123 | self.fadeOutIllustration = Custom.LerpAndTick(self.fadeOutIllustration, 0f, 0.02f, 0.025f); 124 | } 125 | 126 | for(int i = 0; i < self.scene.subObjects.Count; i++) 127 | { 128 | ImageSettings settings = null; 129 | 130 | if (!(self.scene.subObjects[i] is MenuIllustration illust)) continue; 131 | if(customRep.TryGet(illust, out SceneImage csi)) { 132 | settings = csi.GetTempProperty("ShelterSettings"); 133 | } 134 | 135 | illust.setAlpha = Mathf.Lerp(settings?.baseAlpha ?? 1f, settings?.fadeAlpha ?? 1f, self.fadeOutIllustration); 136 | } 137 | } 138 | 139 | // Parse FADE tag 140 | private static void SleepAndDeathScreen_AddBkgIllustration(On.Menu.SleepAndDeathScreen.orig_AddBkgIllustration orig, SleepAndDeathScreen self) 141 | { 142 | orig(self); 143 | 144 | for(int i = 0; i < self.scene.subObjects.Count; i++) 145 | { 146 | if (!(self.scene.subObjects[i] is MenuIllustration illust)) continue; 147 | 148 | if (!customRep.TryGet(illust, out SceneImage csi)) continue; 149 | 150 | ImageSettings settings = new ImageSettings(); 151 | settings.baseAlpha = illust.setAlpha ?? illust.alpha; 152 | settings.fadeAlpha = settings.baseAlpha; 153 | 154 | // Fade property 155 | // The image's alpha will lerp to this when the map is open 156 | settings.fadeAlpha = csi.GetProperty("FADE") ?? settings.baseAlpha; 157 | 158 | csi.SetTempProperty("ShelterSettings", settings); 159 | } 160 | } 161 | 162 | private class ImageSettings 163 | { 164 | public float baseAlpha; 165 | public float fadeAlpha; 166 | } 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /SlugBase/SlugBase.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | {E7B1AABF-4BE5-49D7-847D-24883F06261A} 8 | Library 9 | Properties 10 | SlugBase 11 | SlugBase 12 | v3.5 13 | 512 14 | true 15 | 16 | 17 | true 18 | full 19 | false 20 | bin\Debug\ 21 | DEBUG;TRACE 22 | prompt 23 | 4 24 | 25 | 26 | pdbonly 27 | true 28 | bin\Release\ 29 | TRACE 30 | prompt 31 | 4 32 | bin\Release\SlugBase.xml 33 | 34 | 35 | 36 | ..\..\rwmodlibspublic\Assembly-CSharp.dll 37 | False 38 | 39 | 40 | ..\..\rwmodlibspublic\ConfigMachine.dll 41 | False 42 | 43 | 44 | ..\..\rwmodlibspublic\HOOKS-Assembly-CSharp.dll 45 | False 46 | 47 | 48 | ..\..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Rain World\Mods\JollyCoop.dll 49 | False 50 | 51 | 52 | ..\..\rwmodlibspublic\MonoMod.RuntimeDetour.dll 53 | False 54 | 55 | 56 | ..\..\rwmodlibspublic\Partiality.dll 57 | False 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | ..\..\rwmodlibspublic\UnityEngine.dll 67 | False 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 | if defined RWMods (copy /Y "$(TargetPath)" "%25RWMods%25" & copy /Y "$(TargetDir)$(TargetName).xml" "%25RWMods%25") 100 | 101 | -------------------------------------------------------------------------------- /SlugBase/SlugBaseEx.cs: -------------------------------------------------------------------------------- 1 | using Menu; 2 | 3 | namespace SlugBase 4 | { 5 | /// 6 | /// Extension helper methods for SlugBase mods. 7 | /// 8 | public static class SlugBaseEx 9 | { 10 | /// 11 | /// Finds the save file associated with this player. 12 | /// 13 | /// The save file type to search for. This will likely be a child of . 14 | /// The player that owns the save file. 15 | /// The save file that was found. 16 | /// True if a save file of the appropriate type was found, false otherwise. 17 | public static bool TryGetSave(this Player player, out T save) where T : SaveState 18 | { 19 | save = null; 20 | return player.room?.game?.TryGetSave(out save) ?? false; 21 | } 22 | 23 | /// 24 | /// Finds the save file associated with this session. 25 | /// 26 | /// The save file type to search for. This will likely be a child of . 27 | /// The current instance. 28 | /// The save file that was found. 29 | /// A save file or null if it was not found or did not match the given type. 30 | public static bool TryGetSave(this RainWorldGame game, out T save) where T : SaveState 31 | { 32 | if (game?.GetStorySession?.saveState is T t) 33 | { 34 | save = t; 35 | return true; 36 | } 37 | else 38 | { 39 | save = null; 40 | return false; 41 | } 42 | } 43 | 44 | /// 45 | /// Gets the associated with this illustration if it was built from a 's resources. 46 | /// 47 | /// The menu illustration to check. 48 | /// The associated with this illustration or null if it was not built from a 's resources. 49 | public static SceneImage GetCustomImage(this MenuIllustration illustration) 50 | { 51 | return CustomSceneManager.customRep[illustration]; 52 | } 53 | 54 | /// 55 | /// Gets the associated with this scene if it was built from a 's resources. 56 | /// 57 | /// 58 | /// New elements should not be added after the scene is built. 59 | /// 60 | /// The scene to check. 61 | /// The associated with this scene or null if it was not built from a 's resources. 62 | public static CustomScene GetCustomScene(this MenuScene scene) 63 | { 64 | return CustomSceneManager.customSceneRep[scene]; 65 | } 66 | 67 | /// 68 | /// Gets the associated with this slideshow if it was built from a 's resources. 69 | /// 70 | /// 71 | /// New slides should not be added after the slideshow is built. 72 | /// 73 | /// The slideshow to check. 74 | /// The associated with this slideshow or null if it was not built from a 's resources. 75 | public static CustomSlideshow GetCustomSlideshow(this SlideShow slideshow) 76 | { 77 | return CustomSceneManager.customSlideshowRep[slideshow]; 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /SlugBase/SlugBaseMod.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Collections.Generic; 4 | using Partiality.Modloader; 5 | using UnityEngine; 6 | using System.Security; 7 | using System.Security.Permissions; 8 | 9 | [module: UnverifiableCode] 10 | [assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)] 11 | 12 | namespace SlugBase 13 | { 14 | internal class SlugBaseMod : PartialityMod 15 | { 16 | public const string versionString = "1.3.0"; 17 | public const string assemblyVersion = "1.3.0.0"; 18 | 19 | // AutoUpdate support 20 | public string updateURL = "http://beestuff.pythonanywhere.com/audb/api/mods/6/0"; 21 | public int version = 8; 22 | public string keyE = "AQAB"; 23 | public string keyN = "szjz4lkR8G9JuQ4Jt2DEk7h5hRcvpX0LfHWXp203VrsSwWenj2xho0zl8m6gsSYNVaBFm3WXbqkj7snI+DuheYfvSLpfLZsHCOF2XdIO2FCyOFSUmQ7T4Jvd/ap5jFMofXu6geBf0hl0H4VJ1/D2SpDg7rkAi+hAbHBd1d7o1mfON1ZdzDKIeTeFCstw5w+ImfE83sg1OspLmrrec3UNyXlNzc5x+r5gHwgOfMMTWLfI1fUVRd3o43U+zV7PHsyOjPGzHfLVLS3IO6va3Pc7sng+bxifchP9IWS4RTps4qmGA6AcQE2qaI1oH0Ql9EzAfBeIhvNXica0nlTHBJQ8tZxewA1igdHl2deSgszpKseAPPxsg9+njoaq4rvqcEys3/KfJImxyS3W49U+GxGmoPx298GMSUlfyw3zY3Ytlbb7/7tbHfP71G4/ISwkn+WyhufE3SLYWX/6uR//0aMGNe/zoH8AOvnPtepX4Mwy3HYnETzc5WsCgetmCViEI0YdAKl3FClgtuhsYRXmEXDy7yeVpTSsAzoUdkqnzFSG5ykm1mh1ISCpBiQ9prB2inCaWMc6DALWsFUElOV6yVbmWorfX2EiNesDhoFmAxz6pt6CADVBoxewDTFUtT103jYVkROKe4oNUr2W0Sj1sEv6kURHfjE5+3OLfbrk3OLJrnU="; 24 | 25 | internal static int FirstCustomIndex = 4; 26 | 27 | public SlugBaseMod() 28 | { 29 | ModID = "SlugBase"; 30 | Version = versionString; 31 | author = "Slime_Cubed"; 32 | 33 | /* 34 | * This mod aims to reduce the boilerplate needed to add custom slugcats. 35 | * This should cover everything that currently varies between characters, such as: 36 | * - Stats 37 | * - Scenes 38 | * - Diets 39 | * - Save slots 40 | * 41 | * This does not cover anything that is unchanged between characters, such as: 42 | * - Adding new abilities 43 | * - Adding new regions 44 | * 45 | * This mod should aim to limit the functionality of added characters as little 46 | * as possible - even though it doesn't implement something, it should still 47 | * expect the mod to implement it itself. 48 | * Generally, this should only change methods from their orig when the modder 49 | * requests it, such as with CustomPlayer.QuarterFood. 50 | */ 51 | 52 | /* 53 | * TODO: 54 | * 55 | * Scene editor (maybe) 56 | * 57 | */ 58 | } 59 | 60 | public override void OnLoad() 61 | { 62 | // Compatibility fixes 63 | Compatibility.FlatmodeFix.Apply(); 64 | Compatibility.HookGenFix.Apply(); 65 | 66 | // Core changes 67 | CustomSceneManager.ApplyHooks(); 68 | MultiplayerTweaks.ApplyHooks(); 69 | PlayerColors.ApplyHooks(); 70 | PlayerManager.ApplyHooks(); 71 | SaveManager.ApplyHooks(); 72 | //SceneEditor.ApplyHooks(); 73 | SelectMenu.ApplyHooks(); 74 | ShelterScreens.ApplyHooks(); 75 | RegionTools.ApplyHooks(); 76 | WorldFixes.ApplyHooks(); 77 | 78 | // Changes that must be applied late for compatibility 79 | On.RainWorld.Start += (orig, self) => 80 | { 81 | Compatibility.FancySlugcats.Apply(); 82 | Compatibility.JollyCoop.Apply(); 83 | ArenaAdditions.ApplyHooks(); 84 | WorldFixes.LateApply(); 85 | 86 | orig(self); 87 | }; 88 | 89 | // Guess an appropriate index to assign to SlugBase characters 90 | // This should make them more resistant to skipping the select screen 91 | foreach (SlugcatStats.Name name in Enum.GetValues(typeof(SlugcatStats.Name))) 92 | FirstCustomIndex = Math.Max((int)name + 1, FirstCustomIndex); 93 | } 94 | 95 | public object GetReloadState() 96 | { 97 | return FirstCustomIndex; 98 | } 99 | 100 | public void Reload(object state) 101 | { 102 | FirstCustomIndex = (int)state; 103 | } 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /SlugBase/WorldFixes.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using UnityEngine; 6 | 7 | namespace SlugBase 8 | { 9 | // The player index is sometimes used to do things that don't work with more than 3 characters 10 | // This mostly fixes them by copying the index of the character world spawns are copied from 11 | internal static class WorldFixes 12 | { 13 | public static void ApplyHooks() 14 | { 15 | On.Menu.FastTravelScreen.InitiateRegionSwitch += FastTravelScreen_InitiateRegionSwitch; 16 | On.Menu.FastTravelScreen.ctor += FastTravelScreen_ctor; 17 | 18 | //On.WorldLoader.ctor += WorldLoader_ctor; 19 | // deferred, moved to start 20 | 21 | On.CollectToken.CollectTokenData.ctor += CollectTokenData_ctor; 22 | On.CollectToken.CollectTokenData.FromString += CollectTokenData_FromString; 23 | 24 | On.EventTrigger.ctor += EventTrigger_ctor; 25 | On.EventTrigger.FromString += EventTrigger_FromString; 26 | } 27 | 28 | internal static void LateApply() 29 | { 30 | On.WorldLoader.ctor += WorldLoader_ctor; 31 | } 32 | 33 | // FastTravelScreen 34 | // To avoid having to replace the entire method body of FastTravelScreen.ctor 35 | private static bool checkFastTravelForCustom = false; 36 | private static void FastTravelScreen_InitiateRegionSwitch(On.Menu.FastTravelScreen.orig_InitiateRegionSwitch orig, Menu.FastTravelScreen self, int switchToRegion) 37 | { 38 | if(!checkFastTravelForCustom) 39 | { 40 | orig(self, switchToRegion); 41 | return; 42 | } 43 | 44 | int maxIndex = -1; 45 | foreach (SlugBaseCharacter ply in PlayerManager.customPlayers) 46 | maxIndex = Mathf.Max(maxIndex, ply.SlugcatIndex); 47 | 48 | // Add custom characterse to the shelter list 49 | int oldLen = self.playerShelters.Length; 50 | if (oldLen < maxIndex) 51 | { 52 | ResizeToFit(ref self.playerShelters, maxIndex + 1, null); 53 | for (int i = oldLen; i < self.playerShelters.Length; i++) 54 | { 55 | self.playerShelters[i] = self.manager.rainWorld.progression.ShelterOfSaveGame(i); 56 | } 57 | } 58 | 59 | var prog = self.manager.rainWorld.progression; 60 | 61 | // Check the current slugcat for a shelter 62 | if (prog.PlayingAsSlugcat >= 0 && prog.PlayingAsSlugcat < self.playerShelters.Length && self.playerShelters[prog.PlayingAsSlugcat] != null) 63 | self.currentShelter = self.playerShelters[prog.PlayingAsSlugcat]; 64 | 65 | // Find the region that this shelter is in 66 | if (self.currentShelter != null) 67 | { 68 | string regionName = self.currentShelter.Substring(0, 2); 69 | for (int regionInd = 0; regionInd < self.accessibleRegions.Count; regionInd++) 70 | { 71 | if (self.allRegions[self.accessibleRegions[regionInd]].name == regionName) 72 | { 73 | Debug.Log(self.currentShelter); 74 | Debug.Log(string.Concat(new object[] 75 | { 76 | "actually found start region (including SlugBase saves): ", 77 | regionInd, 78 | " ", 79 | self.allRegions[self.accessibleRegions[regionInd]].name 80 | })); 81 | self.currentRegion = regionInd; 82 | break; 83 | } 84 | } 85 | } 86 | 87 | orig(self, self.currentRegion); 88 | } 89 | 90 | private static void FastTravelScreen_ctor(On.Menu.FastTravelScreen.orig_ctor orig, Menu.FastTravelScreen self, ProcessManager manager, ProcessManager.ProcessID ID) 91 | { 92 | if(PlayerManager.GetCustomPlayer(self.PlayerCharacter) != null) 93 | checkFastTravelForCustom = true; 94 | try 95 | { 96 | orig(self, manager, ID); 97 | } 98 | finally 99 | { 100 | checkFastTravelForCustom = false; 101 | } 102 | } 103 | 104 | // WorldLoader 105 | // Copy the creatures from another character 106 | private static void WorldLoader_ctor(On.WorldLoader.orig_ctor orig, WorldLoader self, RainWorldGame game, int playerCharacter, bool singleRoomWorld, string worldName, Region region, RainWorldGame.SetupValues setupValues) 107 | { 108 | orig(self, game, PlayerManager.GetCustomPlayer(game)?.InheritWorldFromSlugcat ?? playerCharacter, singleRoomWorld, worldName, region, setupValues); 109 | } 110 | 111 | // CollectToken 112 | private static void CollectTokenData_ctor(On.CollectToken.CollectTokenData.orig_ctor orig, CollectToken.CollectTokenData self, PlacedObject owner, bool isBlue) 113 | { 114 | orig(self, owner, isBlue); 115 | 116 | ResizeToFitPlayers(ref self.availableToPlayers, true); 117 | } 118 | 119 | private static void CollectTokenData_FromString(On.CollectToken.CollectTokenData.orig_FromString orig, CollectToken.CollectTokenData self, string s) 120 | { 121 | orig(self, s); 122 | 123 | var availability = self.availableToPlayers; 124 | foreach (var cha in PlayerManager.GetCustomPlayers()) 125 | { 126 | var from = cha.InheritWorldFromSlugcat; 127 | var to = cha.SlugcatIndex; 128 | if (from >= 0 && to >= 0 && from < availability.Length && to < availability.Length) 129 | availability[to] = availability[from]; 130 | } 131 | } 132 | 133 | // EventTrigger 134 | private static void EventTrigger_ctor(On.EventTrigger.orig_ctor orig, EventTrigger self, EventTrigger.TriggerType type) 135 | { 136 | orig(self, type); 137 | 138 | ResizeToFitPlayers(ref self.slugcats, true); 139 | } 140 | 141 | private static void EventTrigger_FromString(On.EventTrigger.orig_FromString orig, EventTrigger self, string[] s) 142 | { 143 | orig(self, s); 144 | 145 | var slugcats = self.slugcats; 146 | foreach(var cha in PlayerManager.GetCustomPlayers()) 147 | { 148 | var from = cha.InheritWorldFromSlugcat; 149 | var to = cha.SlugcatIndex; 150 | if (from >= 0 && to >= 0 && from < slugcats.Length && to < slugcats.Length) 151 | slugcats[to] = slugcats[from]; 152 | } 153 | } 154 | 155 | private static void ResizeToFit(ref T[] array, int length, T initValue) 156 | { 157 | int origLength = array.Length; 158 | if (length > origLength) 159 | { 160 | Array.Resize(ref array, length); 161 | for (int i = origLength; i < length; i++) 162 | array[i] = initValue; 163 | } 164 | } 165 | 166 | private static void ResizeToFitPlayers(ref T[] array, T initValue) 167 | { 168 | if (PlayerManager.GetCustomPlayers().Count == 0) return; 169 | 170 | var maxIndex = PlayerManager.GetCustomPlayers().Max(cha => cha.SlugcatIndex); 171 | ResizeToFit(ref array, maxIndex + 1, initValue); 172 | } 173 | } 174 | } 175 | --------------------------------------------------------------------------------