├── .config └── dotnet-tools.json ├── .editorconfig ├── .gitattributes ├── .github └── workflows │ ├── build.yml │ └── publish.yml ├── .gitignore ├── CHANGELOG.md ├── Directory.Build.props ├── LICENSE ├── LethalLib.sln ├── LethalLib ├── Compats │ └── LethalLevelLoaderCompat.cs ├── Extras │ ├── DungeonDef.cs │ ├── DungeonGraphLineDef.cs │ ├── Extensions.cs │ ├── GameObjectChanceDef.cs │ ├── SpawnableMapObjectDef.cs │ ├── SpawnableOutsideObjectDef.cs │ ├── UnlockableItemDef.cs │ └── WeatherDef.cs ├── LethalLib.csproj ├── Modules │ ├── ContentLoader.cs │ ├── Dungeon.cs │ ├── Enemies.cs │ ├── Items.cs │ ├── Levels.cs │ ├── MapObjects.cs │ ├── NetworkPrefabs.cs │ ├── Player.cs │ ├── PrefabUtils.cs │ ├── Shaders.cs │ ├── TerminalUtils.cs │ ├── Unlockables.cs │ ├── Utilities.cs │ └── Weathers.cs ├── Plugin.cs └── assets │ ├── bundles │ └── lethallib │ ├── icons │ └── lethal-lib.png │ └── thunderstore.toml ├── NuGet.Config ├── README.md ├── hooks └── post-checkout └── lib └── MMHOOK_Assembly-CSharp.dll /.config/dotnet-tools.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "isRoot": true, 4 | "tools": { 5 | "tcli": { 6 | "version": "0.2.3", 7 | "commands": [ 8 | "tcli" 9 | ] 10 | }, 11 | "evaisa.netcodepatcher.cli": { 12 | "version": "3.3.3", 13 | "commands": [ 14 | "netcode-patch" 15 | ] 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: https://EditorConfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | trim_trailing_whitespace = true 7 | end_of_line = lf 8 | insert_final_newline = true 9 | indent_style = space 10 | 11 | [*.{csproj,props,targets}.user] 12 | indent_size = 4 13 | 14 | [*.{csproj,props,targets}] 15 | indent_size = 4 16 | 17 | [*.cs] 18 | indent_size = 4 19 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | # Trigger the workflow on pushes to only the 'main' branch (this avoids duplicate checks being run e.g., for dependabot pull requests) 5 | push: 6 | branches: [ main ] 7 | # Trigger the workflow on any pull request 8 | pull_request: 9 | 10 | jobs: 11 | build: 12 | name: Build 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Fetch Sources 16 | uses: actions/checkout@v4 17 | with: 18 | fetch-depth: 0 19 | filter: tree:0 20 | 21 | - name: Setup .NET environment 22 | uses: actions/setup-dotnet@v3 23 | with: 24 | dotnet-version: "8.0.100" 25 | 26 | - name: Restore project 27 | run: | 28 | dotnet restore 29 | dotnet tool restore 30 | 31 | - name: Build solution 32 | run: | 33 | dotnet build -c Release 34 | 35 | - name: Upload Artifacts 36 | uses: actions/upload-artifact@v4 37 | with: 38 | name: build-artifacts 39 | path: ./*/dist/*.zip 40 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | 3 | on: 4 | release: 5 | types: [prereleased, released] 6 | 7 | jobs: 8 | build: 9 | name: Build 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Fetch Sources 13 | uses: actions/checkout@v4 14 | with: 15 | ref: ${{ github.event.release.tag_name }} 16 | fetch-depth: 0 17 | filter: tree:0 18 | 19 | - name: Setup .NET environment 20 | uses: actions/setup-dotnet@v3 21 | with: 22 | dotnet-version: "8.0.100" 23 | 24 | - name: Restore project 25 | run: | 26 | dotnet restore 27 | dotnet tool restore 28 | 29 | - name: Build and pack solution 30 | run: | 31 | dotnet pack -c Release 32 | 33 | - name: Upload Thunderstore artifact 34 | uses: actions/upload-artifact@v4 35 | with: 36 | name: thunderstore-build 37 | path: ./*/dist/*.zip 38 | 39 | - name: Upload nupkg artifact 40 | uses: actions/upload-artifact@v4 41 | with: 42 | name: nupkg-build 43 | path: ./*/bin/Release/*.nupkg 44 | 45 | upload-release-artifacts: 46 | name: Upload Release Artifacts 47 | needs: build 48 | runs-on: ubuntu-latest 49 | steps: 50 | - name: Fetch Sources 51 | uses: actions/checkout@v4 52 | 53 | - name: Download all artifacts 54 | uses: actions/download-artifact@v4 55 | 56 | - name: Upload artifacts to Release 57 | env: 58 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 59 | run: gh release upload ${{ github.event.release.tag_name }} thunderstore-build/*/dist/*.zip nupkg-build/*/bin/Release/*.nupkg 60 | 61 | deploy-nuget: 62 | name: Deploy to NuGet 63 | needs: build 64 | runs-on: ubuntu-latest 65 | steps: 66 | - name: Fetch Sources 67 | uses: actions/checkout@v4 68 | 69 | - name: Download nupkg artifact 70 | uses: actions/download-artifact@v4 71 | with: 72 | name: nupkg-build 73 | 74 | - name: Setup .NET environment 75 | uses: actions/setup-dotnet@v3 76 | with: 77 | dotnet-version: "8.0.100" 78 | 79 | - name: Publish to NuGet.org 80 | run: | 81 | dotnet nuget push ./*/bin/Release/*.nupkg --api-key ${{ secrets.NUGET_API_TOKEN }} --source https://api.nuget.org/v3/index.json 82 | 83 | deploy-thunderstore: 84 | name: Deploy to Thunderstore 85 | needs: build 86 | runs-on: ubuntu-latest 87 | steps: 88 | - name: Fetch Sources 89 | uses: actions/checkout@v4 90 | 91 | - name: Download Thunderstore artifact 92 | uses: actions/download-artifact@v4 93 | with: 94 | name: thunderstore-build 95 | 96 | - name: Setup .NET environment 97 | uses: actions/setup-dotnet@v3 98 | with: 99 | dotnet-version: "8.0.100" 100 | 101 | - name: Restore dotnet tools 102 | run: | 103 | dotnet tool restore 104 | 105 | - name: Publish to Thunderstore 106 | env: 107 | TCLI_AUTH_TOKEN: ${{ secrets.THUNDERSTORE_API_TOKEN }} 108 | run: | 109 | dotnet build -target:PublishThunderstore 110 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Exclude everything 2 | /* 3 | 4 | # Specific includes 5 | 6 | ## Build System/Scripts 7 | !/.config/ 8 | !NuGet.Config 9 | !Directory.Build.props 10 | !build.sh 11 | !build.ps1 12 | !build.cmd 13 | 14 | ## Markdowns 15 | !*.md 16 | !LICENSE 17 | 18 | ## Editorconfig 19 | !.editorconfig 20 | 21 | ## important Git files 22 | !.gitmodules 23 | !.gitignore 24 | !.gitattributes 25 | !.git-blame-ignore-revs 26 | 27 | ### Git Hooks 28 | !/hooks 29 | 30 | ## GitHub specific 31 | !.github/ 32 | 33 | ## Solution 34 | !LethalLib.sln 35 | 36 | ### LethalLib project 37 | !/LethalLib 38 | LethalLib/[Bb]in 39 | LethalLib/[Oo]bj 40 | LethalLib/[Dd]ist 41 | 42 | # Explicit exceptions/ignores 43 | 44 | ## Any `.user` (e.g. `.csproj.user`) 45 | **.user 46 | 47 | ## allow MMHook lib, but nothing else 48 | !/lib 49 | lib/* 50 | !lib/MMHOOK_Assembly-CSharp.dll 51 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## LethalLib [1.1.1] 9 | 10 | ### Added 11 | 12 | - MonoDetour added as a Thunderstore dependency to provide CIL analysis in stack traces when any ILHook (includes HarmonyX transpilers) manipulation target method throws on compilation. LethalLib has no real dependency on it. This change is purely made to make debugging ILHooks/transpilers easier for modders who happen to have LethalLib installed. 13 | 14 | ## LethalLib [1.1.0] 15 | 16 | ### Fixed 17 | 18 | - Updated mod version to work with V70 Lethal. 19 | 20 | ## LethalLib [1.0.3] 21 | 22 | ### Fixed 23 | 24 | - Changed the validation to be a clamp and not forced to be 1. 25 | 26 | ## LethalLib [1.0.2] 27 | 28 | ### Added 29 | 30 | - Validation to scrap and shop items registered with invalid weight values (above 4 and under 1). 31 | 32 | ## LethalLib [1.0.1] 33 | 34 | ### Added 35 | 36 | - LethalLib NuGet package now ships with xml docs (wow!!) 37 | - Enabled embedded debug symbols for easier to read stacktraces for when LethalLib explodes 38 | 39 | ### Fixed 40 | 41 | - Null checks to avoid errors with loading into lobby with empty MapObjects 42 | 43 | ## LethalLib [1.0.0] 44 | 45 | > [!NOTE] 46 | > Despite the major version jump from 0.16.4 to 1.0.0, no major or breaking changes were made. 47 | > This change was made to properly follow SemVer to show that LethalLib's public API is stable. 48 | 49 | ### Added 50 | 51 | - Ability for items, levels, outside and inside mapobjects to register to levels through their LethalLevelLoader content tag. 52 | 53 | ### Fixed 54 | 55 | - mapobjects maybe having the same issues that items and enemies had the previous two versions with case sensitivity and leveltype validation. 56 | 57 | ## LethalLib [0.16.4] 58 | 59 | ### Fixed 60 | - `AddEnemyToLevel` needing a `LevelType` to validate custom moon enemy rarities. 61 | - `AddScrapItemToLevel` having the same issue as above. 62 | 63 | ## LethalLib [0.16.3] 64 | 65 | ### Fixed 66 | - `GetLLLNameOfLevel` function now returns a lowercase level name so input is no longer case sensitive. 67 | 68 | ## LethalLib [0.16.2] 69 | 70 | ### Fixed 71 | - The last return value of `spawnRateFunction` of MapObjects no longer overwrites a map object's spawn curve for each moon. 72 | 73 | ## LethalLib [0.16.1] 74 | 75 | ### Fixed 76 | - `Levels.LevelTypes.Vanilla` now works for registering enemies and items on moons. 77 | 78 | ## LethalLib [0.16.0] 79 | 80 | ### Added 81 | - Version 50 moons were finally added to the `LevelTypes` enum. 82 | - LethalLib weathers now also get added to LethalLevelLoader moons. 83 | 84 | ### Changed 85 | - Use `TryGotoNext` instead of `GotoNext` for `StackFrame.AddFrames` ILHook so it doesn't throw if sequence was not found due to another mod patching the method first ([#74](https://github.com/EvaisaDev/LethalLib/pull/74)) 86 | - Added a reference to a `ToString` weather enum Hook ([#81](https://github.com/EvaisaDev/LethalLib/pull/81)) 87 | 88 | ### Fixed 89 | - `RemoveWeather`'s first argument was named "levelName", now it is "weatherName". 90 | 91 | ## LethalLib [0.15.1] 92 | 93 | ### Fixed 94 | - Custom DungeonFlow registration has been disabled to prevent issues when using mod in current v50 beta versions. 95 | 96 | ## LethalLib [0.15.0] 97 | 98 | ### Added 99 | - LethalLib will now also register enemies and items for when LethalLevelLoader adds its moons. 100 | 101 | ### Changed 102 | - customLevelRarities will now accept the original level name or the level name modified by LethalLevelLoader, meaning enemies and items can target a custom moon using either name 103 | 104 | ### Fixed 105 | - Enemy and item spawn weights now get applied as one would expect 106 | - `Levels.LevelTypes.All` no longer overrides all spawn weights 107 | - `Levels.LevelTypes.Modded` now applies its spawn weights 108 | - this used to only apply its weight if customLevelRarities contained the level's name 109 | - customLevelRarities now applies its weights 110 | 111 | ## LethalLib [0.14.4] 112 | 113 | ### Fixed 114 | - Added various null checks to prevent crashes and to give better feedback to developers when using custom enemy API. 115 | 116 | ## LethalLib [0.14.3] 117 | 118 | ### Fixed 119 | - API for enemy registration with rarity tables works now. 120 | 121 | ## LethalLib [0.14.2] 122 | 123 | ### Changed 124 | - Added config option: Extended Logging. 125 | - Reduced the amount of logging LethalLib does by default. 126 | 127 | ## LethalLib [0.14.1] 128 | 129 | ### Fixed 130 | - Last update broke the network registry API 💀 131 | 132 | ## LethalLib [0.14.0] 133 | 134 | ### Added 135 | - Added enemies to debug menu 136 | - https://github.com/EvaisaDev/LethalLib/pull/53 137 | 138 | ## LethalLib [0.13.2] 139 | 140 | ### Fixed 141 | - Disabled decor was still showing in the shop, added some horrific hax to prevent this. 142 | 143 | ## LethalLib [0.13.1] 144 | 145 | ### Fixed 146 | - Map objects were being added every time a lobby was loaded, causing too many to spawn. 147 | 148 | ## LethalLib [0.13.0] 149 | 150 | ### Added 151 | - Ability to pass rarity dictionaries for registering enemies. 152 | - "Modded" LevelTypes flag 153 | 154 | ## LethalLib [0.12.1] 155 | 156 | ### Fixed 157 | 158 | - Reverted function signature changes for backwards compatibility reasons. 159 | - Readded some removed properties (These do not do anything now but they are there to prevent old mods from dying.) 160 | 161 | ## LethalLib [0.12.0] 162 | 163 | > [!WARNING] 164 | > Includes potentially breaking changes! 165 | 166 | ### Added 167 | - Ability to pass rarity dictionaries for registering scrap items. 168 | 169 | ### Changed 170 | - Cleaned up git repo slightly. 171 | - Internal changes to the way scrap items are added to levels. 172 | - When registering the same scrap item multiple times it will be merged with the previous ones. 173 | 174 | ## LethalLib [0.11.2] 175 | 176 | ### Fixed 177 | 178 | - (to verify) Issue with Terminal, where when a mod was disabling a shop item, 179 | all the shop items after it would mess up their orders. 180 | 181 | ## LethalLib [0.11.1] 182 | 183 | ### Changed 184 | 185 | - RegisterNetworkPrefab now checks prefabs to avoid registering duplicates 186 | 187 | ## LethalLib [0.11.0] 188 | 189 | ### Added 190 | 191 | - Module: PrefabUtils 192 | - Method: ClonePrefab() 193 | - Method: CreatePrefab() 194 | - Method: NetworkPrefabs.CreateNetworkPrefab() 195 | - Creates a network prefab programmatically and registers it with the network manager. 196 | - Method: NetworkPrefabs.CloneNetworkPrefab() 197 | - Clones a network prefab programmatically and registers it with the network manager. 198 | 199 | ### Changed 200 | 201 | - Behaviour for Items module 202 | - When a scrap item is registered as a shop item, the LethalLib 203 | will now automatically create a copy and switch the IsScrap value. 204 | - When a shop item is registered as a scrap, the LethalLib will now 205 | automatically create a copy, assign sell values, set IsScrap to true, and add a scan node. 206 | 207 | ## LethalLib [0.10.4] 208 | 209 | ### Added 210 | 211 | - Additional error logging and prevented an exception when 212 | a custom dungeon RandomMapObject had an invalid prefab assigned. 213 | 214 | ### Removed 215 | 216 | - LethalExpansion soft dependency as it caused more issues than it was worth. 217 | 218 | ## LethalLib [0.10.3] 219 | 220 | ### Added 221 | 222 | - Soft dependency to LethalExpansion which might help compatibility(?) 223 | 224 | ### Fixed 225 | 226 | - Fixed custom dungeon generation breaking because of Lethal Company update. 227 | 228 | ## LethalLib [0.10.1] 229 | 230 | ### Fixed 231 | 232 | - Fixed issue with Ragdolls system where ragdolls got registered multiple times. 233 | 234 | ## LethalLib [0.10.0] 235 | 236 | > [!WARNING] 237 | > Includes potentially breaking changes! 238 | 239 | ### Added 240 | 241 | - Save system patch which attempts to keep the items array in the same order, 242 | so that items don't change when you load an old save after mods have updated. 243 | - This will likely break all existing saves. 244 | - Intellisense comments to all API functions. 245 | - Method: Enemies.RemoveEnemyFromLevels() 246 | - Method: Items.RemoveScrapFromLevels() 247 | - Method: Items.RemoveShopItem() 248 | - Method: Items.UpdateShopItemPrice() 249 | - Method: Unlockables.DisableUnlockable() 250 | - Method: Unlockables.UpdateUnlockablePrice() 251 | - Method: Weathers.RemoveWeather() 252 | - Method: MapObjects.RemoveMapObject() 253 | - Method: MapObjects.RemoveOutsideObject() 254 | - Added Module: ContentLoader 255 | - This acts as an alternative way to register content, abstracting 256 | some extra stuff away such as network registry and asset loading. 257 | - Added Module: Player 258 | - Method: RegisterPlayerRagdoll() 259 | - Method: GetRagdollIndex() 260 | - Method: GetRagdoll() 261 | -------------------------------------------------------------------------------- /Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | Evaisa 4 | latest 5 | enable 6 | false 7 | true 8 | MIT 9 | https://github.com/EvaisaDev/LethalLib 10 | https://github.com/EvaisaDev/LethalLib 11 | git 12 | 13 | 14 | 15 | 16 | dev 17 | v 18 | 19 | 20 | 21 | runtime; build; native; contentfiles; analyzers; buildtransitive 22 | all 23 | 24 | 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Evaisa 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /LethalLib.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LethalLib", "LethalLib\LethalLib.csproj", "{2DC60A9D-F44B-4B06-8A37-9B05D6D2CAC8}" 4 | EndProject 5 | Global 6 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 7 | Debug|Any CPU = Debug|Any CPU 8 | Release|Any CPU = Release|Any CPU 9 | EndGlobalSection 10 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 11 | {2DC60A9D-F44B-4B06-8A37-9B05D6D2CAC8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 12 | {2DC60A9D-F44B-4B06-8A37-9B05D6D2CAC8}.Debug|Any CPU.Build.0 = Debug|Any CPU 13 | {2DC60A9D-F44B-4B06-8A37-9B05D6D2CAC8}.Release|Any CPU.ActiveCfg = Release|Any CPU 14 | {2DC60A9D-F44B-4B06-8A37-9B05D6D2CAC8}.Release|Any CPU.Build.0 = Release|Any CPU 15 | EndGlobalSection 16 | EndGlobal 17 | -------------------------------------------------------------------------------- /LethalLib/Compats/LethalLevelLoaderCompat.cs: -------------------------------------------------------------------------------- 1 | using BepInEx.Bootstrap; 2 | using System.Collections.Generic; 3 | using System.Runtime.CompilerServices; 4 | 5 | namespace LethalLib.Compats; 6 | internal static class LethalLevelLoaderCompat 7 | { 8 | public static bool LethalLevelLoaderExists => Chainloader.PluginInfos.ContainsKey("imabatby.lethallevelloader"); 9 | 10 | [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)] 11 | public static List TryGetLLLTagsFromLevels(SelectableLevel level) 12 | { 13 | if (LethalLevelLoaderExists) 14 | { 15 | return GetLLLTagsFromLevel(level); // do i need to make another method? i forgor 16 | } 17 | return new(); 18 | } 19 | 20 | [MethodImpl(MethodImplOptions.NoInlining)] 21 | internal static List GetLLLTagsFromLevel(SelectableLevel level) 22 | { 23 | List tagsForLevel = []; 24 | foreach (LethalLevelLoader.ExtendedLevel extendedLevel in LethalLevelLoader.PatchedContent.CustomExtendedLevels) 25 | { 26 | if (extendedLevel.SelectableLevel != level) continue; 27 | foreach (LethalLevelLoader.ContentTag tag in extendedLevel.ContentTags) 28 | { 29 | tagsForLevel.Add(tag.contentTagName.Trim().ToLowerInvariant()); 30 | } 31 | break; 32 | } 33 | 34 | foreach (LethalLevelLoader.ExtendedLevel extendedLevel in LethalLevelLoader.PatchedContent.VanillaExtendedLevels) 35 | { 36 | if (extendedLevel.SelectableLevel != level) continue; 37 | foreach (LethalLevelLoader.ContentTag tag in extendedLevel.ContentTags) 38 | { 39 | tagsForLevel.Add(tag.contentTagName.Trim().ToLowerInvariant()); 40 | } 41 | break; 42 | } 43 | 44 | return tagsForLevel; 45 | } 46 | } -------------------------------------------------------------------------------- /LethalLib/Extras/DungeonDef.cs: -------------------------------------------------------------------------------- 1 | #region 2 | 3 | using DunGen.Graph; 4 | using UnityEngine; 5 | 6 | #endregion 7 | 8 | namespace LethalLib.Extras; 9 | 10 | [CreateAssetMenu(menuName = "ScriptableObjects/DungeonDef")] 11 | public class DungeonDef : ScriptableObject 12 | { 13 | public DungeonFlow dungeonFlow; 14 | [Range(0f, 300f)] 15 | public int rarity; 16 | public AudioClip firstTimeDungeonAudio; 17 | } -------------------------------------------------------------------------------- /LethalLib/Extras/DungeonGraphLineDef.cs: -------------------------------------------------------------------------------- 1 | #region 2 | 3 | using DunGen.Graph; 4 | using UnityEngine; 5 | 6 | #endregion 7 | 8 | namespace LethalLib.Extras; 9 | 10 | [CreateAssetMenu(menuName = "ScriptableObjects/DungeonGraphLine")] 11 | public class DungeonGraphLineDef : ScriptableObject 12 | { 13 | public GraphLine graphLine; 14 | } -------------------------------------------------------------------------------- /LethalLib/Extras/Extensions.cs: -------------------------------------------------------------------------------- 1 | #region 2 | 3 | using UnityEngine; 4 | using Object = UnityEngine.Object; 5 | 6 | #endregion 7 | 8 | namespace LethalLib.Extras; 9 | 10 | public static class ScriptableObjectExtension 11 | { 12 | /// 13 | /// Creates and returns a clone of any given scriptable object. 14 | /// 15 | public static T Clone(this T scriptableObject) where T : ScriptableObject 16 | { 17 | if (scriptableObject == null) 18 | { 19 | Debug.LogError($"ScriptableObject was null. Returning default {typeof(T)} object."); 20 | return (T)ScriptableObject.CreateInstance(typeof(T)); 21 | } 22 | 23 | T instance = Object.Instantiate(scriptableObject); 24 | instance.name = scriptableObject.name; // remove (Clone) from name 25 | return instance; 26 | } 27 | } -------------------------------------------------------------------------------- /LethalLib/Extras/GameObjectChanceDef.cs: -------------------------------------------------------------------------------- 1 | #region 2 | 3 | using DunGen; 4 | using UnityEngine; 5 | 6 | #endregion 7 | 8 | namespace LethalLib.Extras; 9 | 10 | [CreateAssetMenu(menuName = "ScriptableObjects/GameObjectChance")] 11 | public class GameObjectChanceDef : ScriptableObject 12 | { 13 | public GameObjectChance gameObjectChance; 14 | } 15 | -------------------------------------------------------------------------------- /LethalLib/Extras/SpawnableMapObjectDef.cs: -------------------------------------------------------------------------------- 1 | #region 2 | 3 | using UnityEngine; 4 | 5 | #endregion 6 | 7 | namespace LethalLib.Extras; 8 | 9 | [CreateAssetMenu(menuName = "ScriptableObjects/SpawnableMapObject")] 10 | public class SpawnableMapObjectDef : ScriptableObject 11 | { 12 | public SpawnableMapObject spawnableMapObject; 13 | } -------------------------------------------------------------------------------- /LethalLib/Extras/SpawnableOutsideObjectDef.cs: -------------------------------------------------------------------------------- 1 | #region 2 | 3 | using UnityEngine; 4 | 5 | #endregion 6 | 7 | namespace LethalLib.Extras; 8 | 9 | [CreateAssetMenu(menuName = "ScriptableObjects/SpawnableOutsideObject")] 10 | public class SpawnableOutsideObjectDef : ScriptableObject 11 | { 12 | public SpawnableOutsideObjectWithRarity spawnableMapObject; 13 | } -------------------------------------------------------------------------------- /LethalLib/Extras/UnlockableItemDef.cs: -------------------------------------------------------------------------------- 1 | #region 2 | 3 | using LethalLib.Modules; 4 | using UnityEngine; 5 | 6 | #endregion 7 | 8 | namespace LethalLib.Extras; 9 | 10 | [CreateAssetMenu(menuName = "ScriptableObjects/UnlockableItem")] 11 | public class UnlockableItemDef : ScriptableObject 12 | { 13 | // storeType is not really used, but it is still here for compatibility. 14 | public StoreType storeType = StoreType.None; 15 | public UnlockableItem unlockable; 16 | } -------------------------------------------------------------------------------- /LethalLib/Extras/WeatherDef.cs: -------------------------------------------------------------------------------- 1 | #region 2 | 3 | using LethalLib.Modules; 4 | using UnityEngine; 5 | 6 | #endregion 7 | 8 | namespace LethalLib.Extras; 9 | 10 | [CreateAssetMenu(menuName = "ScriptableObjects/WeatherDef")] 11 | public class WeatherDef : ScriptableObject 12 | { 13 | public string weatherName; 14 | public Levels.LevelTypes levels = Levels.LevelTypes.None; 15 | public string[] levelOverrides; 16 | public int weatherVariable1; 17 | public int weatherVariable2; 18 | public WeatherEffect weatherEffect; 19 | 20 | } -------------------------------------------------------------------------------- /LethalLib/LethalLib.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Library 5 | netstandard2.1 6 | LethalLib 7 | LethalLib 8 | Content-addition API for Lethal Company 9 | 10 | Evaisa.LethalLib 11 | README.md 12 | true 13 | true 14 | 15 | true 16 | embedded 17 | $([System.IO.Path]::GetFullPath('$(MSBuildThisFileDirectory)'))=./ 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | all 30 | runtime; build; native; contentfiles; analyzers; buildtransitive 31 | 32 | 33 | 34 | 35 | all 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | $(LethalCompanyDir)Lethal Company_Data/Managed/Assembly-CSharp.dll 45 | 46 | 47 | $(LethalCompanyDir)Lethal Company_Data/Managed/Assembly-CSharp-firstpass.dll 48 | 49 | 50 | $(LethalCompanyDir)Lethal Company_Data/Managed/Unity.InputSystem.dll 51 | 52 | 53 | $(LethalCompanyDir)Lethal Company_Data/Managed/Unity.Netcode.Runtime.dll 54 | 55 | 56 | $(TestProfileDir)BepInEx/plugins/MMHOOK/MMHOOK_Assembly-CSharp.dll 57 | 58 | 59 | 60 | 61 | 62 | 63 | $(ProjectDir)../lib/MMHOOK_Assembly-CSharp.dll 64 | 65 | 66 | 67 | 68 | 69 | $(MinVerMajor).$(MinVerMinor).$(MinVerPatch) 70 | $(PlainVersion) 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | -------------------------------------------------------------------------------- /LethalLib/Modules/ContentLoader.cs: -------------------------------------------------------------------------------- 1 | #region 2 | 3 | using System; 4 | using System.Collections.Generic; 5 | using BepInEx; 6 | using LethalLib.Extras; 7 | using UnityEngine; 8 | 9 | #endregion 10 | 11 | namespace LethalLib.Modules; 12 | 13 | /// 14 | /// Content loading module, for easier content loading. 15 | /// Not feature complete. If someone wants to add to this feel free to do so. 16 | /// 17 | public class ContentLoader 18 | { 19 | // getsetter 20 | public Dictionary LoadedContent { get; } = new(); 21 | 22 | // Main stuff 23 | public PluginInfo modInfo; 24 | AssetBundle modBundle; 25 | public string modName => modInfo.Metadata.Name; 26 | 27 | public string modVersion => modInfo.Metadata.Version.ToString(); 28 | 29 | public string modGUID => modInfo.Metadata.GUID; 30 | 31 | public Action prefabCallback = (content, prefab) => { }; 32 | 33 | public ContentLoader(PluginInfo modInfo, AssetBundle modBundle, Action prefabCallback = null) 34 | { 35 | this.modInfo = modInfo; 36 | this.modBundle = modBundle; 37 | 38 | if (prefabCallback != null) 39 | { 40 | this.prefabCallback = prefabCallback; 41 | } 42 | } 43 | public ContentLoader Create(PluginInfo modInfo, AssetBundle modBundle, Action prefabCallback = null) 44 | { 45 | return new ContentLoader(modInfo, modBundle, prefabCallback); 46 | } 47 | 48 | 49 | /// 50 | /// Loads and registers custom content. 51 | /// Handles everything for you including network registry. 52 | /// 53 | public void Register(CustomContent content) 54 | { 55 | if(LoadedContent.ContainsKey(content.ID)) 56 | { 57 | Debug.LogError($"[LethalLib] {modName} tried to register content with ID {content.ID} but it already exists!"); 58 | return; 59 | } 60 | 61 | if(content is CustomItem item) 62 | { 63 | var itemAsset = modBundle.LoadAsset(item.contentPath); 64 | item.item = itemAsset; 65 | NetworkPrefabs.RegisterNetworkPrefab(itemAsset.spawnPrefab); 66 | Utilities.FixMixerGroups(itemAsset.spawnPrefab); 67 | prefabCallback(item, itemAsset.spawnPrefab); 68 | item.registryCallback(itemAsset); 69 | 70 | if(content is ShopItem shopItem) 71 | { 72 | TerminalNode buyNode1 = null; 73 | TerminalNode buyNode2 = null; 74 | TerminalNode itemInfo = null; 75 | if(shopItem.buyNode1Path != null) 76 | { 77 | buyNode1 = modBundle.LoadAsset(shopItem.buyNode1Path); 78 | } 79 | if(shopItem.buyNode2Path != null) 80 | { 81 | buyNode2 = modBundle.LoadAsset(shopItem.buyNode2Path); 82 | } 83 | if(shopItem.itemInfoPath != null) 84 | { 85 | itemInfo = modBundle.LoadAsset(shopItem.itemInfoPath); 86 | } 87 | 88 | Items.RegisterShopItem(itemAsset, buyNode1, buyNode2, itemInfo, shopItem.initPrice); 89 | } 90 | else if(content is ScrapItem scrapItem) 91 | { 92 | Items.RegisterScrap(itemAsset, scrapItem.levelRarities, scrapItem.customLevelRarities); 93 | } 94 | else 95 | { 96 | Items.RegisterItem(itemAsset); 97 | } 98 | 99 | 100 | } 101 | else if (content is Unlockable unlockable) 102 | { 103 | var unlockableAsset = modBundle.LoadAsset(unlockable.contentPath); 104 | if(unlockableAsset.unlockable.prefabObject != null) 105 | { 106 | NetworkPrefabs.RegisterNetworkPrefab(unlockableAsset.unlockable.prefabObject); 107 | prefabCallback(content, unlockableAsset.unlockable.prefabObject); 108 | Utilities.FixMixerGroups(unlockableAsset.unlockable.prefabObject); 109 | } 110 | unlockable.unlockable = unlockableAsset.unlockable; 111 | unlockable.registryCallback(unlockableAsset.unlockable); 112 | 113 | 114 | TerminalNode buyNode1 = null; 115 | TerminalNode buyNode2 = null; 116 | TerminalNode itemInfo = null; 117 | if(unlockable.buyNode1Path != null) 118 | { 119 | buyNode1 = modBundle.LoadAsset(unlockable.buyNode1Path); 120 | } 121 | if(unlockable.buyNode2Path != null) 122 | { 123 | buyNode2 = modBundle.LoadAsset(unlockable.buyNode2Path); 124 | } 125 | if(unlockable.itemInfoPath != null) 126 | { 127 | itemInfo = modBundle.LoadAsset(unlockable.itemInfoPath); 128 | } 129 | 130 | Unlockables.RegisterUnlockable(unlockableAsset, unlockable.storeType, buyNode1, buyNode2, itemInfo, unlockable.initPrice); 131 | 132 | } 133 | else if (content is CustomEnemy enemy) 134 | { 135 | var enemyAsset = modBundle.LoadAsset(enemy.contentPath); 136 | NetworkPrefabs.RegisterNetworkPrefab(enemyAsset.enemyPrefab); 137 | Utilities.FixMixerGroups(enemyAsset.enemyPrefab); 138 | enemy.enemy = enemyAsset; 139 | prefabCallback(content, enemyAsset.enemyPrefab); 140 | enemy.registryCallback(enemyAsset); 141 | 142 | TerminalNode infoNode = null; 143 | TerminalKeyword infoKeyword = null; 144 | if(enemy.infoNodePath != null) 145 | { 146 | infoNode = modBundle.LoadAsset(enemy.infoNodePath); 147 | } 148 | if(enemy.infoKeywordPath != null) 149 | { 150 | infoKeyword = modBundle.LoadAsset(enemy.infoKeywordPath); 151 | } 152 | 153 | if ((int)(enemy.spawnType) == -1) 154 | { 155 | Enemies.RegisterEnemy(enemyAsset, enemy.rarity, enemy.LevelTypes, enemy.levelOverrides, infoNode, infoKeyword); 156 | } 157 | else 158 | { 159 | Enemies.RegisterEnemy(enemyAsset, enemy.rarity, enemy.LevelTypes, enemy.spawnType, enemy.levelOverrides, infoNode, infoKeyword); 160 | } 161 | 162 | } 163 | else if (content is MapHazard mapObject) 164 | { 165 | var mapObjectAsset = modBundle.LoadAsset(mapObject.contentPath); 166 | mapObject.hazard = mapObjectAsset; 167 | NetworkPrefabs.RegisterNetworkPrefab(mapObjectAsset.spawnableMapObject.prefabToSpawn); 168 | Utilities.FixMixerGroups(mapObjectAsset.spawnableMapObject.prefabToSpawn); 169 | prefabCallback(content, mapObjectAsset.spawnableMapObject.prefabToSpawn); 170 | mapObject.registryCallback(mapObjectAsset); 171 | 172 | MapObjects.RegisterMapObject(mapObjectAsset, mapObject.LevelTypes, mapObject.levelOverrides, mapObject.spawnRateFunction); 173 | 174 | } 175 | else if (content is OutsideObject outsideObject) 176 | { 177 | var mapObjectAsset = modBundle.LoadAsset(outsideObject.contentPath); 178 | outsideObject.mapObject = mapObjectAsset; 179 | NetworkPrefabs.RegisterNetworkPrefab(mapObjectAsset.spawnableMapObject.spawnableObject.prefabToSpawn); 180 | Utilities.FixMixerGroups(mapObjectAsset.spawnableMapObject.spawnableObject.prefabToSpawn); 181 | prefabCallback(content, mapObjectAsset.spawnableMapObject.spawnableObject.prefabToSpawn); 182 | outsideObject.registryCallback(mapObjectAsset); 183 | 184 | MapObjects.RegisterOutsideObject(mapObjectAsset, outsideObject.LevelTypes, outsideObject.levelOverrides, outsideObject.spawnRateFunction); 185 | 186 | } 187 | 188 | LoadedContent.Add(content.ID, content); 189 | } 190 | 191 | /// 192 | /// Loads and registers an entire array of custom content. 193 | /// Handles everything for you including network registry. 194 | /// 195 | public void RegisterAll(CustomContent[] content) 196 | { 197 | Plugin.logger.LogInfo($"[LethalLib] {modName} is registering {content.Length} content items!"); 198 | foreach(CustomContent c in content) 199 | { 200 | Register(c); 201 | } 202 | } 203 | 204 | /// 205 | /// Loads and registers an entire list of custom content. 206 | /// Handles everything for you including network registry. 207 | /// 208 | public void RegisterAll(List content) 209 | { 210 | Plugin.logger.LogInfo($"[LethalLib] {modName} is registering {content.Count} content items!"); 211 | foreach (CustomContent c in content) 212 | { 213 | Register(c); 214 | } 215 | } 216 | 217 | // Content classes 218 | public class CustomContent 219 | { 220 | private string id = ""; 221 | public string ID => id; 222 | 223 | public CustomContent(string id) 224 | { 225 | this.id = id; 226 | } 227 | } 228 | 229 | public class CustomItem : CustomContent 230 | { 231 | public Action registryCallback = (item) => { }; 232 | public string contentPath = ""; 233 | internal Item item; 234 | public Item Item => item; 235 | 236 | public CustomItem(string id, string contentPath, Action registryCallback = null) : base(id) 237 | { 238 | this.contentPath = contentPath; 239 | if(registryCallback != null) 240 | { 241 | this.registryCallback = registryCallback; 242 | } 243 | } 244 | } 245 | 246 | public class ShopItem : CustomItem 247 | { 248 | public void RemoveFromShop() 249 | { 250 | Items.RemoveShopItem(Item); 251 | } 252 | 253 | public void SetPrice(int price) 254 | { 255 | Items.UpdateShopItemPrice(Item, price); 256 | } 257 | 258 | public int initPrice = 0; 259 | public string buyNode1Path = null; 260 | public string buyNode2Path = null; 261 | public string itemInfoPath = null; 262 | 263 | public ShopItem(string id, string contentPath, int price = 0, string buyNode1Path = null, string buyNode2Path = null, string itemInfoPath = null, Action registryCallback = null) : base(id, contentPath, registryCallback) 264 | { 265 | this.initPrice = price; 266 | this.buyNode1Path = buyNode1Path; 267 | this.buyNode2Path = buyNode2Path; 268 | this.itemInfoPath = itemInfoPath; 269 | } 270 | } 271 | 272 | public class ScrapItem : CustomItem 273 | { 274 | public void RemoveFromLevels(Levels.LevelTypes levelFlags) 275 | { 276 | Items.RemoveScrapFromLevels(Item, levelFlags); 277 | } 278 | 279 | /// 280 | /// THIS IS NEVER USED, ONLY HERE FOR COMPAT 281 | /// 282 | public int Rarity { 283 | get => 0; 284 | } 285 | 286 | public Dictionary levelRarities = new Dictionary(); 287 | public Dictionary customLevelRarities = new Dictionary(); 288 | 289 | public ScrapItem(string id, string contentPath, int rarity, Levels.LevelTypes levelFlags = Levels.LevelTypes.None, string[] levelOverrides = null, Action registryCallback = null) : base(id, contentPath, registryCallback) 290 | { 291 | // assign level rarities 292 | if(levelFlags != Levels.LevelTypes.None) 293 | { 294 | levelRarities.Add(levelFlags, rarity); 295 | } 296 | else if(levelOverrides != null) 297 | { 298 | foreach(string s in levelOverrides) 299 | { 300 | customLevelRarities.Add(s, rarity); 301 | } 302 | } 303 | } 304 | 305 | public ScrapItem(string id, string contentPath, Dictionary? levelRarities = null, Dictionary? customLevelRarities = null, Action registryCallback = null) : base(id, contentPath, registryCallback) 306 | { 307 | if(levelRarities != null) 308 | { 309 | this.levelRarities = levelRarities; 310 | } 311 | if(customLevelRarities != null) 312 | { 313 | this.customLevelRarities = customLevelRarities; 314 | } 315 | } 316 | } 317 | 318 | public class Unlockable : CustomContent 319 | { 320 | public Action registryCallback = (unlockable) => { }; 321 | internal UnlockableItem unlockable; 322 | public UnlockableItem UnlockableItem => unlockable; 323 | public string contentPath = ""; 324 | public int initPrice = 0; 325 | public string buyNode1Path = null; 326 | public string buyNode2Path = null; 327 | public string itemInfoPath = null; 328 | public StoreType storeType = StoreType.None; 329 | 330 | public void RemoveFromShop() 331 | { 332 | Unlockables.DisableUnlockable(UnlockableItem); 333 | } 334 | 335 | public void SetPrice(int price) 336 | { 337 | Unlockables.UpdateUnlockablePrice(UnlockableItem, price); 338 | } 339 | 340 | public Unlockable(string id, string contentPath, int price = 0, string buyNode1Path = null, string buyNode2Path = null, string itemInfoPath = null, StoreType storeType = StoreType.None, Action registryCallback = null) : base(id) 341 | { 342 | this.contentPath = contentPath; 343 | if(registryCallback != null) 344 | { 345 | this.registryCallback = registryCallback; 346 | } 347 | this.initPrice = price; 348 | this.buyNode1Path = buyNode1Path; 349 | this.buyNode2Path = buyNode2Path; 350 | this.itemInfoPath = itemInfoPath; 351 | this.storeType = storeType; 352 | } 353 | } 354 | 355 | public class CustomEnemy : CustomContent 356 | { 357 | public Action registryCallback = (enemy) => { }; 358 | public string contentPath = ""; 359 | internal EnemyType enemy; 360 | public EnemyType Enemy => enemy; 361 | public string infoNodePath = null; 362 | public string infoKeywordPath = null; 363 | public int rarity = 0; 364 | 365 | public void RemoveFromLevels(Levels.LevelTypes levelFlags) 366 | { 367 | Enemies.RemoveEnemyFromLevels(Enemy, levelFlags); 368 | } 369 | 370 | public Levels.LevelTypes LevelTypes = Levels.LevelTypes.None; 371 | public string[] levelOverrides = null; 372 | 373 | public Enemies.SpawnType spawnType = (Enemies.SpawnType)(-1); 374 | 375 | public CustomEnemy(string id, string contentPath, int rarity = 0, Levels.LevelTypes levelFlags = Levels.LevelTypes.None, Enemies.SpawnType spawnType = (Enemies.SpawnType)(-1), string[] levelOverrides = null, string infoNodePath = null, string infoKeywordPath = null, Action registryCallback = null) : base(id) 376 | { 377 | this.contentPath = contentPath; 378 | if(registryCallback != null) 379 | { 380 | this.registryCallback = registryCallback; 381 | } 382 | this.infoNodePath = infoNodePath; 383 | this.infoKeywordPath = infoKeywordPath; 384 | this.rarity = rarity; 385 | this.LevelTypes = levelFlags; 386 | this.levelOverrides = levelOverrides; 387 | this.spawnType = spawnType; 388 | } 389 | } 390 | 391 | 392 | public class MapHazard : CustomContent 393 | { 394 | public Action registryCallback = (hazard) => { }; 395 | public string contentPath = ""; 396 | internal SpawnableMapObjectDef hazard; 397 | public SpawnableMapObjectDef Hazard => hazard; 398 | public Func spawnRateFunction; 399 | 400 | public Levels.LevelTypes LevelTypes = Levels.LevelTypes.None; 401 | public string[] levelOverrides = null; 402 | 403 | public void RemoveFromLevels(Levels.LevelTypes levelFlags = Levels.LevelTypes.None, string[] levelOverrides = null) 404 | { 405 | MapObjects.RemoveMapObject(Hazard, levelFlags, levelOverrides); 406 | } 407 | 408 | public MapHazard(string id, string contentPath, Levels.LevelTypes levelFlags = Levels.LevelTypes.None, string[] levelOverrides = null, Func spawnRateFunction = null, Action registryCallback = null) : base(id) 409 | { 410 | this.contentPath = contentPath; 411 | if(registryCallback != null) 412 | { 413 | this.registryCallback = registryCallback; 414 | } 415 | this.LevelTypes = levelFlags; 416 | this.levelOverrides = levelOverrides; 417 | this.spawnRateFunction = spawnRateFunction; 418 | } 419 | } 420 | 421 | public class OutsideObject : CustomContent 422 | { 423 | public Action registryCallback = (hazard) => { }; 424 | public string contentPath = ""; 425 | internal SpawnableOutsideObjectDef mapObject; 426 | public SpawnableOutsideObjectDef MapObject => mapObject; 427 | public Func spawnRateFunction; 428 | 429 | public Levels.LevelTypes LevelTypes = Levels.LevelTypes.None; 430 | public string[] levelOverrides = null; 431 | 432 | public void RemoveFromLevels(Levels.LevelTypes levelFlags = Levels.LevelTypes.None, string[] levelOverrides = null) 433 | { 434 | MapObjects.RemoveOutsideObject(MapObject, levelFlags, levelOverrides); 435 | } 436 | 437 | public OutsideObject(string id, string contentPath, Levels.LevelTypes levelFlags = Levels.LevelTypes.None, string[] levelOverrides = null, Func spawnRateFunction = null, Action registryCallback = null) : base(id) 438 | { 439 | this.contentPath = contentPath; 440 | if (registryCallback != null) 441 | { 442 | this.registryCallback = registryCallback; 443 | } 444 | this.LevelTypes = levelFlags; 445 | this.levelOverrides = levelOverrides; 446 | this.spawnRateFunction = spawnRateFunction; 447 | } 448 | } 449 | 450 | } 451 | -------------------------------------------------------------------------------- /LethalLib/Modules/Dungeon.cs: -------------------------------------------------------------------------------- 1 | #region 2 | 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using DunGen; 7 | using DunGen.Graph; 8 | using LethalLib.Extras; 9 | using Unity.Netcode; 10 | using UnityEngine; 11 | 12 | #endregion 13 | 14 | namespace LethalLib.Modules; 15 | 16 | public class Dungeon 17 | { 18 | public static void Init() 19 | { 20 | On.RoundManager.GenerateNewFloor += RoundManager_GenerateNewFloor; 21 | On.RoundManager.Start += RoundManager_Start; 22 | // On.StartOfRound.Start += StartOfRound_Start; 23 | } 24 | /* 25 | private static void StartOfRound_Start(On.StartOfRound.orig_Start orig, StartOfRound self) 26 | { 27 | 28 | 29 | Plugin.logger.LogInfo("Added custom dungeons to levels"); 30 | orig(self); 31 | } 32 | */ 33 | 34 | private static void RoundManager_Start(On.RoundManager.orig_Start orig, RoundManager self) 35 | { 36 | /*foreach(var dungeon in customDungeons) 37 | { 38 | if (!self.dungeonFlowTypes.Contains(dungeon.dungeonFlow)) 39 | { 40 | var flowTypes = self.dungeonFlowTypes.ToList(); 41 | flowTypes.Add(dungeon.dungeonFlow); 42 | self.dungeonFlowTypes = flowTypes.ToArray(); 43 | 44 | var newDungeonIndex = self.dungeonFlowTypes.Length - 1; 45 | dungeon.dungeonIndex = newDungeonIndex; 46 | 47 | var firstTimeDungeonAudios = self.firstTimeDungeonAudios.ToList(); 48 | // check if the indexes match 49 | if (firstTimeDungeonAudios.Count != self.dungeonFlowTypes.Length - 1) 50 | { 51 | // add nulls until they do 52 | while (firstTimeDungeonAudios.Count < self.dungeonFlowTypes.Length - 1) 53 | { 54 | firstTimeDungeonAudios.Add(null); 55 | } 56 | } 57 | firstTimeDungeonAudios.Add(dungeon.firstTimeDungeonAudio); 58 | self.firstTimeDungeonAudios = firstTimeDungeonAudios.ToArray(); 59 | } 60 | } 61 | 62 | 63 | var startOfRound = StartOfRound.Instance; 64 | foreach (var dungeon in customDungeons) 65 | { 66 | foreach (var level in startOfRound.levels) 67 | { 68 | var name = level.name; 69 | var alwaysValid = dungeon.LevelTypes.HasFlag(Levels.LevelTypes.All) || (dungeon.levelOverrides != null && dungeon.levelOverrides.Any(item => item.ToLowerInvariant() == name.ToLowerInvariant())); 70 | var isModded = dungeon.LevelTypes.HasFlag(Levels.LevelTypes.Modded) && !Enum.IsDefined(typeof(Levels.LevelTypes), name); 71 | 72 | if (isModded) 73 | { 74 | alwaysValid = true; 75 | } 76 | 77 | if (Enum.IsDefined(typeof(Levels.LevelTypes), name) || alwaysValid) 78 | { 79 | var levelEnum = alwaysValid ? Levels.LevelTypes.All : (Levels.LevelTypes)Enum.Parse(typeof(Levels.LevelTypes), name); 80 | 81 | if ((alwaysValid || dungeon.LevelTypes.HasFlag(levelEnum)) && !level.dungeonFlowTypes.Any(rarityInt => rarityInt.id == dungeon.dungeonIndex)) 82 | { 83 | var flowTypes = level.dungeonFlowTypes.ToList(); 84 | flowTypes.Add(new IntWithRarity { id = dungeon.dungeonIndex, rarity = dungeon.rarity }); 85 | level.dungeonFlowTypes = flowTypes.ToArray(); 86 | } 87 | } 88 | } 89 | } 90 | */ 91 | orig(self); 92 | } 93 | 94 | public class CustomDungeonArchetype 95 | { 96 | public DungeonArchetype archeType; 97 | public Levels.LevelTypes LevelTypes; 98 | public int lineIndex = -1; 99 | } 100 | 101 | public class CustomGraphLine 102 | { 103 | public GraphLine graphLine; 104 | public Levels.LevelTypes LevelTypes; 105 | } 106 | 107 | public class CustomDungeon 108 | { 109 | public int rarity; 110 | public DungeonFlow dungeonFlow; 111 | public Levels.LevelTypes LevelTypes; 112 | public string[] levelOverrides; 113 | public int dungeonIndex = -1; 114 | public AudioClip firstTimeDungeonAudio; 115 | } 116 | 117 | public static List customDungeonArchetypes = new List(); 118 | public static List customGraphLines = new List(); 119 | public static Dictionary extraTileSets = new Dictionary(); 120 | public static Dictionary extraRooms = new Dictionary(); 121 | public static List customDungeons = new List(); 122 | 123 | private static void RoundManager_GenerateNewFloor(On.RoundManager.orig_GenerateNewFloor orig, RoundManager self) 124 | { 125 | var name = self.currentLevel.name; 126 | if (Enum.IsDefined(typeof(Levels.LevelTypes), name)) 127 | { 128 | var levelEnum = (Levels.LevelTypes)Enum.Parse(typeof(Levels.LevelTypes), name); 129 | 130 | var index = 0; 131 | self.dungeonGenerator.Generator.DungeonFlow.Lines.ForEach((line) => 132 | { 133 | foreach (var dungeonArchetype in customDungeonArchetypes) 134 | { 135 | if (dungeonArchetype.LevelTypes.HasFlag(levelEnum)) 136 | { 137 | if (!line.DungeonArchetypes.Contains(dungeonArchetype.archeType) && (dungeonArchetype.lineIndex == -1 || dungeonArchetype.lineIndex == index)) { 138 | line.DungeonArchetypes.Add(dungeonArchetype.archeType); 139 | if (Plugin.extendedLogging.Value) 140 | Plugin.logger.LogInfo($"Added {dungeonArchetype.archeType.name} to {name}"); 141 | } 142 | } 143 | } 144 | 145 | foreach (var archetype in line.DungeonArchetypes) 146 | { 147 | var archetypeName = archetype.name; 148 | if (extraTileSets.ContainsKey(archetypeName)) 149 | { 150 | var tileSet = extraTileSets[archetypeName]; 151 | 152 | if (!archetype.TileSets.Contains(tileSet)) 153 | { 154 | archetype.TileSets.Add(tileSet); 155 | if (Plugin.extendedLogging.Value) 156 | Plugin.logger.LogInfo($"Added {tileSet.name} to {name}"); 157 | } 158 | } 159 | foreach (var tileSet in archetype.TileSets) 160 | { 161 | var tileSetName = tileSet.name; 162 | if (extraRooms.ContainsKey(tileSetName)) 163 | { 164 | var room = extraRooms[tileSetName]; 165 | if (!tileSet.TileWeights.Weights.Contains(room)) 166 | { 167 | tileSet.TileWeights.Weights.Add(room); 168 | } 169 | } 170 | } 171 | } 172 | 173 | index++; 174 | }); 175 | 176 | 177 | foreach (var graphLine in customGraphLines) 178 | { 179 | if (graphLine.LevelTypes.HasFlag(levelEnum)) 180 | { 181 | if (!self.dungeonGenerator.Generator.DungeonFlow.Lines.Contains(graphLine.graphLine)) 182 | { 183 | self.dungeonGenerator.Generator.DungeonFlow.Lines.Add(graphLine.graphLine); 184 | // Plugin.logger.LogInfo($"Added {graphLine.graphLine.name} to {name}"); 185 | } 186 | } 187 | } 188 | } 189 | 190 | orig(self); 191 | 192 | // debug copy of GenerateNewFloor 193 | /* 194 | if (self.currentLevel.dungeonFlowTypes != null && self.currentLevel.dungeonFlowTypes.Length != 0) 195 | { 196 | List list = new List(); 197 | for (int i = 0; i < self.currentLevel.dungeonFlowTypes.Length; i++) 198 | { 199 | list.Add(self.currentLevel.dungeonFlowTypes[i].rarity); 200 | } 201 | int id = self.currentLevel.dungeonFlowTypes[self.GetRandomWeightedIndex(list.ToArray(), self.LevelRandom)].id; 202 | 203 | Plugin.logger.LogInfo($"Dungeon flow id: {id}"); 204 | Plugin.logger.LogInfo($"Dungeon flow count: {self.dungeonFlowTypes.Length}"); 205 | Plugin.logger.LogInfo($"Dungeon flow name: {self.dungeonFlowTypes[id].name}"); 206 | 207 | self.dungeonGenerator.Generator.DungeonFlow = self.dungeonFlowTypes[id]; 208 | if (id < self.firstTimeDungeonAudios.Length && self.firstTimeDungeonAudios[id] != null) 209 | { 210 | EntranceTeleport[] array = UnityEngine.Object.FindObjectsOfType(); 211 | if (array != null && array.Length != 0) 212 | { 213 | for (int j = 0; j < array.Length; j++) 214 | { 215 | if (array[j].isEntranceToBuilding) 216 | { 217 | array[j].firstTimeAudio = self.firstTimeDungeonAudios[id]; 218 | array[j].dungeonFlowId = id; 219 | } 220 | } 221 | } 222 | } 223 | } 224 | self.dungeonGenerator.Generator.ShouldRandomizeSeed = false; 225 | self.dungeonGenerator.Generator.Seed = self.LevelRandom.Next(); 226 | Debug.Log($"GenerateNewFloor(). Map generator's random seed: {self.dungeonGenerator.Generator.Seed}"); 227 | float lengthMultiplier = self.currentLevel.factorySizeMultiplier * self.mapSizeMultiplier; 228 | self.dungeonGenerator.Generator.LengthMultiplier = lengthMultiplier; 229 | self.dungeonGenerator.Generate();*/ 230 | 231 | 232 | // register prefabs 233 | 234 | var networkManager = UnityEngine.Object.FindObjectOfType(); 235 | 236 | RandomMapObject[] objarray = UnityEngine.Object.FindObjectsOfType(); 237 | 238 | foreach (RandomMapObject randomMapObject in objarray) 239 | { 240 | // loop through 241 | for(int i = 0; i < randomMapObject.spawnablePrefabs.Count; i++) 242 | { 243 | // get prefab name 244 | var prefabName = randomMapObject.spawnablePrefabs[i].name; 245 | 246 | var prefab = networkManager.NetworkConfig.Prefabs.m_Prefabs.FirstOrDefault(x => x.Prefab.name == prefabName); 247 | 248 | if (prefab != default(NetworkPrefab) && prefab.Prefab != randomMapObject.spawnablePrefabs[i]) 249 | { 250 | randomMapObject.spawnablePrefabs[i] = prefab.Prefab; 251 | //Plugin.logger.LogInfo($"DungeonGeneration - Remapped prefab ({prefabName})!"); 252 | } 253 | else if(prefab == default(NetworkPrefab)) 254 | { 255 | //Plugin.logger.LogInfo($"DungeonGeneration - Could not find network prefab ({prefabName})!"); 256 | Plugin.logger.LogError($"DungeonGeneration - Could not find network prefab ({prefabName})! Make sure your assigned prefab is registered with the network manager, or named identically to the vanilla prefab you are referencing."); 257 | } 258 | /*else 259 | { 260 | Plugin.logger.LogInfo($"DungeonGeneration - Prefab ({prefabName}) was already correctly mapped!"); 261 | }*/ 262 | } 263 | } 264 | 265 | } 266 | 267 | /// 268 | /// Registers a custom archetype to a level. 269 | /// 270 | public static void AddArchetype(DungeonArchetype archetype, Levels.LevelTypes levelFlags, int lineIndex = -1) 271 | { 272 | var customArchetype = new CustomDungeonArchetype(); 273 | customArchetype.archeType = archetype; 274 | customArchetype.LevelTypes = levelFlags; 275 | customArchetype.lineIndex = lineIndex; 276 | customDungeonArchetypes.Add(customArchetype); 277 | } 278 | 279 | /// 280 | /// Registers a dungeon graphline to a level. 281 | /// 282 | public static void AddLine(GraphLine line, Levels.LevelTypes levelFlags) 283 | { 284 | var customLine = new CustomGraphLine(); 285 | customLine.graphLine = line; 286 | customLine.LevelTypes = levelFlags; 287 | customGraphLines.Add(customLine); 288 | } 289 | 290 | /// 291 | /// Registers a dungeon graphline to a level. 292 | /// 293 | public static void AddLine(DungeonGraphLineDef line, Levels.LevelTypes levelFlags) 294 | { 295 | AddLine(line.graphLine, levelFlags); 296 | } 297 | 298 | /// 299 | /// Adds a tileset to a dungeon archetype 300 | /// 301 | public static void AddTileSet(TileSet set, string archetypeName) 302 | { 303 | extraTileSets.Add(archetypeName, set); 304 | } 305 | 306 | /// 307 | /// Adds a room to a tileset with the given name. 308 | /// 309 | public static void AddRoom(GameObjectChance room, string tileSetName) 310 | { 311 | extraRooms.Add(tileSetName, room); 312 | } 313 | 314 | /// 315 | /// Adds a room to a tileset with the given name. 316 | /// 317 | public static void AddRoom(GameObjectChanceDef room, string tileSetName) 318 | { 319 | AddRoom(room.gameObjectChance, tileSetName); 320 | } 321 | 322 | /// 323 | /// Adds a dungeon to the given levels. 324 | /// 325 | public static void AddDungeon(DungeonDef dungeon, Levels.LevelTypes levelFlags) 326 | { 327 | AddDungeon(dungeon.dungeonFlow, dungeon.rarity, levelFlags, dungeon.firstTimeDungeonAudio); 328 | } 329 | 330 | /// 331 | /// Adds a dungeon to the given levels. 332 | /// 333 | public static void AddDungeon(DungeonDef dungeon, Levels.LevelTypes levelFlags, string[] levelOverrides) 334 | { 335 | AddDungeon(dungeon.dungeonFlow, dungeon.rarity, levelFlags, levelOverrides, dungeon.firstTimeDungeonAudio); 336 | } 337 | 338 | /// 339 | /// Adds a dungeon to the given levels. 340 | /// 341 | public static void AddDungeon(DungeonFlow dungeon, int rarity, Levels.LevelTypes levelFlags, AudioClip firstTimeDungeonAudio = null) 342 | { 343 | customDungeons.Add(new CustomDungeon 344 | { 345 | dungeonFlow = dungeon, 346 | rarity = rarity, 347 | LevelTypes = levelFlags, 348 | firstTimeDungeonAudio = firstTimeDungeonAudio 349 | }); 350 | } 351 | 352 | /// 353 | /// Adds a dungeon to the given levels. 354 | /// 355 | public static void AddDungeon(DungeonFlow dungeon, int rarity, Levels.LevelTypes levelFlags, string[] levelOverrides = null, AudioClip firstTimeDungeonAudio = null) 356 | { 357 | customDungeons.Add(new CustomDungeon 358 | { 359 | dungeonFlow = dungeon, 360 | rarity = rarity, 361 | LevelTypes = levelFlags, 362 | firstTimeDungeonAudio = firstTimeDungeonAudio, 363 | levelOverrides = levelOverrides 364 | }); 365 | } 366 | 367 | // TODO: Allow runtime removal to let people have synced configs (I do not want to implement this because it is a hassle.) 368 | } 369 | -------------------------------------------------------------------------------- /LethalLib/Modules/Enemies.cs: -------------------------------------------------------------------------------- 1 | #region 2 | 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using System.Reflection; 7 | using UnityEngine; 8 | 9 | #endregion 10 | 11 | namespace LethalLib.Modules; 12 | 13 | public class Enemies 14 | { 15 | private static List levelsAlreadyAddedTo = new(); 16 | public static void Init() 17 | { 18 | On.StartOfRound.Awake += RegisterLevelEnemies; 19 | On.Terminal.Start += Terminal_Start; 20 | On.QuickMenuManager.Start += QuickMenuManager_Start; 21 | } 22 | 23 | static bool addedToDebug = false; // This method of initializing can be changed to your liking. 24 | private static void QuickMenuManager_Start(On.QuickMenuManager.orig_Start orig, QuickMenuManager self) 25 | { 26 | if (addedToDebug) 27 | { 28 | orig(self); 29 | return; 30 | } 31 | var testLevel = self.testAllEnemiesLevel; 32 | var inside = testLevel.Enemies; 33 | var daytime = testLevel.DaytimeEnemies; 34 | var outside = testLevel.OutsideEnemies; 35 | foreach (SpawnableEnemy spawnableEnemy in spawnableEnemies) 36 | { 37 | if (inside.All(x => x.enemyType == spawnableEnemy.enemy)) continue; 38 | SpawnableEnemyWithRarity spawnableEnemyWithRarity = new SpawnableEnemyWithRarity 39 | { 40 | enemyType = spawnableEnemy.enemy, 41 | rarity = spawnableEnemy.rarity 42 | }; 43 | switch (spawnableEnemy.spawnType) 44 | { 45 | case SpawnType.Default: 46 | if (!inside.Any(x => x.enemyType == spawnableEnemy.enemy)) 47 | inside.Add(spawnableEnemyWithRarity); 48 | break; 49 | case SpawnType.Daytime: 50 | if (!daytime.Any(x => x.enemyType == spawnableEnemy.enemy)) 51 | daytime.Add(spawnableEnemyWithRarity); 52 | break; 53 | case SpawnType.Outside: 54 | if (!outside.Any(x => x.enemyType == spawnableEnemy.enemy)) 55 | outside.Add(spawnableEnemyWithRarity); 56 | break; 57 | } 58 | if (Plugin.extendedLogging.Value) 59 | Plugin.logger.LogInfo($"Added {spawnableEnemy.enemy.enemyName} to DebugList [{spawnableEnemy.spawnType}]"); 60 | } 61 | addedToDebug = true; 62 | orig(self); 63 | } 64 | 65 | public struct EnemyAssetInfo 66 | { 67 | public EnemyType EnemyAsset; 68 | public TerminalKeyword keyword; 69 | } 70 | public static Terminal terminal; 71 | 72 | public static List enemyAssetInfos = new List(); 73 | 74 | private static void Terminal_Start(On.Terminal.orig_Start orig, Terminal self) 75 | { 76 | terminal = self; 77 | var infoKeyword = self.terminalNodes.allKeywords.First(keyword => keyword.word == "info"); 78 | var addedEnemies = new List(); 79 | foreach (SpawnableEnemy spawnableEnemy in spawnableEnemies) 80 | { 81 | // if terminal node is null, create one 82 | if (addedEnemies.Contains(spawnableEnemy.enemy.enemyName)) 83 | { 84 | Plugin.logger.LogInfo($"Skipping {spawnableEnemy.enemy.enemyName} because it was already added"); 85 | continue; 86 | } 87 | 88 | if (spawnableEnemy.terminalNode == null) 89 | { 90 | spawnableEnemy.terminalNode = ScriptableObject.CreateInstance(); 91 | spawnableEnemy.terminalNode.displayText = $"{spawnableEnemy.enemy.enemyName}\n\nDanger level: Unknown\n\n[No information about this creature was found.]\n\n"; 92 | spawnableEnemy.terminalNode.clearPreviousText = true; 93 | spawnableEnemy.terminalNode.maxCharactersToType = 35; 94 | spawnableEnemy.terminalNode.creatureName = spawnableEnemy.enemy.enemyName; 95 | } 96 | 97 | // if spawnableEnemy terminalnode is already in enemyfiles, skip 98 | if (self.enemyFiles.Any(x => x.creatureName == spawnableEnemy.terminalNode.creatureName)) 99 | { 100 | Plugin.logger.LogInfo($"Skipping {spawnableEnemy.enemy.enemyName} because it was already added"); 101 | continue; 102 | } 103 | 104 | var keyword = spawnableEnemy.infoKeyword != null ? spawnableEnemy.infoKeyword : TerminalUtils.CreateTerminalKeyword(spawnableEnemy.terminalNode.creatureName.ToLowerInvariant().Replace(" ", "-"), defaultVerb: infoKeyword); 105 | 106 | keyword.defaultVerb = infoKeyword; 107 | 108 | var allKeywords = self.terminalNodes.allKeywords.ToList(); 109 | 110 | // if doesn't contain keyword, add it 111 | if (!allKeywords.Any(x => x.word == keyword.word)) 112 | { 113 | allKeywords.Add(keyword); 114 | self.terminalNodes.allKeywords = allKeywords.ToArray(); 115 | } 116 | 117 | var itemInfoNouns = infoKeyword.compatibleNouns.ToList(); 118 | // if doesn't contain noun, add it 119 | if (!itemInfoNouns.Any(x => x.noun.word == keyword.word)) 120 | { 121 | itemInfoNouns.Add(new CompatibleNoun() 122 | { 123 | noun = keyword, 124 | result = spawnableEnemy.terminalNode 125 | }); 126 | } 127 | infoKeyword.compatibleNouns = itemInfoNouns.ToArray(); 128 | 129 | 130 | 131 | spawnableEnemy.terminalNode.creatureFileID = self.enemyFiles.Count; 132 | 133 | self.enemyFiles.Add(spawnableEnemy.terminalNode); 134 | 135 | var scanNodePropertyComponents = spawnableEnemy.enemy.enemyPrefab.GetComponentsInChildren(); 136 | for (int i = 0; i < scanNodePropertyComponents.Length; i++) 137 | { 138 | scanNodePropertyComponents[i].creatureScanID = spawnableEnemy.terminalNode.creatureFileID; 139 | } 140 | 141 | var enemyAssetInfo = new EnemyAssetInfo() 142 | { 143 | EnemyAsset = spawnableEnemy.enemy, 144 | keyword = keyword 145 | }; 146 | 147 | enemyAssetInfos.Add(enemyAssetInfo); 148 | } 149 | orig(self); 150 | } 151 | 152 | private static void RegisterLevelEnemies(On.StartOfRound.orig_Awake orig, StartOfRound self) 153 | { 154 | orig(self); 155 | 156 | RegisterLethalLibEnemiesForAllLevels(); 157 | 158 | if(BepInEx.Bootstrap.Chainloader.PluginInfos.ContainsKey("imabatby.lethallevelloader") || // currently has typo 159 | BepInEx.Bootstrap.Chainloader.PluginInfos.ContainsKey("iambatby.lethallevelloader")) // might be changed to this 160 | On.RoundManager.Start += RegisterLevelEnemiesforLLL_RoundManager_Start; 161 | 162 | if(BepInEx.Bootstrap.Chainloader.PluginInfos.ContainsKey("LethalExpansion")) 163 | On.Terminal.Start += RegisterLevelEnemiesforLE_Terminal_Start; 164 | } 165 | 166 | private static void RegisterLevelEnemiesforLLL_RoundManager_Start(On.RoundManager.orig_Start orig, RoundManager self) 167 | { 168 | orig(self); 169 | RegisterLethalLibEnemiesForAllLevels(); 170 | } 171 | 172 | private static void RegisterLevelEnemiesforLE_Terminal_Start(On.Terminal.orig_Start orig, Terminal self) 173 | { 174 | orig(self); 175 | RegisterLethalLibEnemiesForAllLevels(); 176 | } 177 | 178 | private static void RegisterLethalLibEnemiesForAllLevels() 179 | { 180 | 181 | foreach (SelectableLevel level in StartOfRound.Instance.levels) 182 | { 183 | if (levelsAlreadyAddedTo.Contains(level)) 184 | continue; 185 | foreach (SpawnableEnemy spawnableEnemy in spawnableEnemies) 186 | { 187 | AddEnemyToLevel(spawnableEnemy, level); 188 | } 189 | levelsAlreadyAddedTo.Add(level); 190 | } 191 | } 192 | 193 | private static void AddEnemyToLevel(SpawnableEnemy spawnableEnemy, SelectableLevel level) 194 | { 195 | string name = level.name; 196 | string customName = Levels.Compatibility.GetLLLNameOfLevel(name); 197 | Levels.LevelTypes currentLevelType = Levels.LevelTypes.None; 198 | bool isCurrentLevelFromVanilla = false; 199 | 200 | if (Enum.TryParse(name, true, out currentLevelType)) // It'd be weird if a level was called "Modded" or "All" so I think im good to not check that lol 201 | { 202 | isCurrentLevelFromVanilla = true; 203 | } 204 | else 205 | { 206 | name = customName; 207 | } 208 | 209 | string tagName = string.Empty; 210 | bool enemyValidToAdd = spawnableEnemy.levelRarities.ContainsKey(Levels.LevelTypes.All) 211 | || (spawnableEnemy.customLevelRarities != null && spawnableEnemy.customLevelRarities.Keys != null && Levels.Compatibility.ContentIncludedToLevelViaTag(spawnableEnemy.customLevelRarities.Keys.ToArray(), level, out tagName)) 212 | || (isCurrentLevelFromVanilla && spawnableEnemy.levelRarities.ContainsKey(Levels.LevelTypes.Vanilla)) 213 | || (!isCurrentLevelFromVanilla && spawnableEnemy.levelRarities.ContainsKey(Levels.LevelTypes.Modded)) 214 | || (isCurrentLevelFromVanilla && spawnableEnemy.levelRarities.ContainsKey(currentLevelType)) 215 | || (!isCurrentLevelFromVanilla && spawnableEnemy.customLevelRarities != null && spawnableEnemy.customLevelRarities.ContainsKey(customName)); 216 | 217 | if (Plugin.extendedLogging.Value) 218 | Plugin.logger.LogInfo($"{name} for enemy: {spawnableEnemy.enemy.enemyName}, isCurrentLevelFromVanilla: {isCurrentLevelFromVanilla}, Found valid: {enemyValidToAdd}"); 219 | 220 | if (!enemyValidToAdd) return; 221 | // find rarity 222 | int rarity = 0; 223 | 224 | if (spawnableEnemy.levelRarities.ContainsKey(currentLevelType)) 225 | { 226 | rarity = spawnableEnemy.levelRarities[currentLevelType]; 227 | } 228 | else if (spawnableEnemy.customLevelRarities != null && spawnableEnemy.customLevelRarities.ContainsKey(name)) 229 | { 230 | rarity = spawnableEnemy.customLevelRarities[name]; 231 | } 232 | else if (spawnableEnemy.customLevelRarities != null && tagName != string.Empty && spawnableEnemy.customLevelRarities.ContainsKey(tagName)) 233 | { 234 | rarity = spawnableEnemy.customLevelRarities[tagName]; 235 | } 236 | else if (isCurrentLevelFromVanilla && spawnableEnemy.levelRarities.ContainsKey(Levels.LevelTypes.Vanilla)) 237 | { 238 | rarity = spawnableEnemy.levelRarities[Levels.LevelTypes.Vanilla]; 239 | } 240 | else if (!isCurrentLevelFromVanilla && spawnableEnemy.levelRarities.ContainsKey(Levels.LevelTypes.Modded)) 241 | { 242 | rarity = spawnableEnemy.levelRarities[Levels.LevelTypes.Modded]; 243 | } 244 | else if (spawnableEnemy.levelRarities.ContainsKey(Levels.LevelTypes.All)) 245 | { 246 | rarity = spawnableEnemy.levelRarities[Levels.LevelTypes.All]; 247 | } 248 | 249 | var spawnableEnemyWithRarity = new SpawnableEnemyWithRarity() 250 | { 251 | enemyType = spawnableEnemy.enemy, 252 | rarity = rarity 253 | }; 254 | 255 | // make sure spawnableScrap does not already contain item 256 | //Plugin.logger.LogInfo($"Checking if {spawnableEnemy.enemy.name} is already in {name}"); 257 | 258 | /* 259 | if (!level.spawnableEnemies.Any(x => x.spawnableEnemy == spawnableEnemy.enemy)) 260 | { 261 | level.spawnableEnemies.Add(spawnableEnemyWithRarity); 262 | Logger.LogInfo($"Added {spawnableEnemy.enemy.name} to {name}"); 263 | }*/ 264 | 265 | switch (spawnableEnemy.spawnType) 266 | { 267 | case SpawnType.Default: 268 | if (!level.Enemies.Any(x => x.enemyType == spawnableEnemy.enemy)) 269 | { 270 | level.Enemies.Add(spawnableEnemyWithRarity); 271 | if (Plugin.extendedLogging.Value) 272 | Plugin.logger.LogInfo($"{name} added {spawnableEnemy.enemy.enemyName} with weight {rarity} and SpawnType [Inside/Default]"); 273 | } 274 | break; 275 | case SpawnType.Daytime: 276 | if (!level.DaytimeEnemies.Any(x => x.enemyType == spawnableEnemy.enemy)) 277 | { 278 | level.DaytimeEnemies.Add(spawnableEnemyWithRarity); 279 | if (Plugin.extendedLogging.Value) 280 | Plugin.logger.LogInfo($"{name} added {spawnableEnemy.enemy.enemyName} with weight {rarity} andSpawnType [Daytime]"); 281 | } 282 | break; 283 | case SpawnType.Outside: 284 | if (!level.OutsideEnemies.Any(x => x.enemyType == spawnableEnemy.enemy)) 285 | { 286 | level.OutsideEnemies.Add(spawnableEnemyWithRarity); 287 | if (Plugin.extendedLogging.Value) 288 | Plugin.logger.LogInfo($"{name} added {spawnableEnemy.enemy.enemyName} with weight {rarity} and SpawnType [Outside]"); 289 | } 290 | break; 291 | default: 292 | break; 293 | } 294 | } 295 | 296 | public enum SpawnType 297 | { 298 | Default, 299 | Daytime, 300 | Outside 301 | } 302 | 303 | public class SpawnableEnemy 304 | { 305 | public EnemyType enemy; 306 | public SpawnType spawnType; 307 | public TerminalNode terminalNode; 308 | public TerminalKeyword infoKeyword; 309 | public string modName; 310 | 311 | /// 312 | /// Deprecated 313 | /// This is never set or used, use levelRarities and customLevelRarities instead. 314 | /// 315 | public int rarity; 316 | 317 | /// 318 | /// Deprecated 319 | /// This is never set or used, use levelRarities and customLevelRarities instead. 320 | /// 321 | public Levels.LevelTypes spawnLevels; 322 | 323 | /// 324 | /// Deprecated 325 | /// This is never set or used, use levelRarities and customLevelRarities instead. 326 | /// 327 | public string[] spawnLevelOverrides; 328 | 329 | public Dictionary customLevelRarities = new Dictionary(); 330 | public Dictionary levelRarities = new Dictionary(); 331 | public SpawnableEnemy(EnemyType enemy, int rarity, Levels.LevelTypes spawnLevels, SpawnType spawnType, string[] spawnLevelOverrides = null) 332 | { 333 | this.enemy = enemy; 334 | //this.rarity = rarity; 335 | this.spawnLevels = spawnLevels; 336 | this.spawnType = spawnType; 337 | //this.spawnLevelOverrides = spawnLevelOverrides; 338 | 339 | if (spawnLevelOverrides != null) 340 | { 341 | foreach (var level in spawnLevelOverrides) 342 | { 343 | customLevelRarities.Add(Levels.Compatibility.GetLLLNameOfLevel(level), rarity); 344 | } 345 | } 346 | 347 | if (spawnLevels != Levels.LevelTypes.None) 348 | { 349 | foreach (Levels.LevelTypes level in Enum.GetValues(typeof(Levels.LevelTypes))) 350 | { 351 | if (spawnLevels.HasFlag(level)) 352 | { 353 | levelRarities.Add(level, rarity); 354 | } 355 | } 356 | } 357 | } 358 | 359 | public SpawnableEnemy(EnemyType enemy, SpawnType spawnType, Dictionary? levelRarities = null, Dictionary? customLevelRarities = null) 360 | { 361 | this.enemy = enemy; 362 | this.spawnType = spawnType; 363 | 364 | if (levelRarities != null) 365 | { 366 | this.levelRarities = levelRarities; 367 | } 368 | 369 | if (customLevelRarities != null) 370 | { 371 | this.customLevelRarities = Levels.Compatibility.LLLifyLevelRarityDictionary(customLevelRarities); 372 | } 373 | } 374 | } 375 | 376 | public static List spawnableEnemies = new List(); 377 | 378 | /// 379 | /// Registers a enemy to be added to the given levels. 380 | /// 381 | public static void RegisterEnemy(EnemyType enemy, int rarity, Levels.LevelTypes levelFlags, SpawnType spawnType, TerminalNode infoNode = null, TerminalKeyword infoKeyword = null) 382 | { 383 | RegisterEnemy(enemy, rarity, levelFlags, spawnType, null, infoNode, infoKeyword); 384 | } 385 | 386 | /// 387 | /// Registers a enemy to be added to the given levels. 388 | /// 389 | public static void RegisterEnemy(EnemyType enemy, int rarity, Levels.LevelTypes levelFlags, SpawnType spawnType, string[] spawnLevelOverrides = null, TerminalNode infoNode = null, TerminalKeyword infoKeyword = null) 390 | { 391 | EnemyNullCheck(enemy); 392 | // if already registered, add rarity to levelRarities 393 | var spawnableEnemy = spawnableEnemies.FirstOrDefault(x => x.enemy == enemy && x.spawnType == spawnType); 394 | 395 | if (spawnableEnemy != null) 396 | { 397 | if (levelFlags != Levels.LevelTypes.None) 398 | { 399 | spawnableEnemy.levelRarities.Add(levelFlags, rarity); 400 | } 401 | 402 | if (spawnLevelOverrides != null) 403 | { 404 | foreach (var level in spawnLevelOverrides) 405 | { 406 | spawnableEnemy.customLevelRarities.Add(Levels.Compatibility.GetLLLNameOfLevel(level), rarity); 407 | } 408 | } 409 | return; 410 | } 411 | 412 | spawnableEnemy = new SpawnableEnemy(enemy, rarity, levelFlags, spawnType, spawnLevelOverrides); 413 | 414 | spawnableEnemy.terminalNode = infoNode; 415 | spawnableEnemy.infoKeyword = infoKeyword; 416 | 417 | FinalizeRegisterEnemy(spawnableEnemy); 418 | } 419 | 420 | /// 421 | /// Registers a enemy to be added to the given levels, However it allows you to pass rarity tables, instead of just a single rarity 422 | /// 423 | public static void RegisterEnemy(EnemyType enemy, SpawnType spawnType, Dictionary? levelRarities = null, Dictionary? customLevelRarities = null, TerminalNode infoNode = null, TerminalKeyword infoKeyword = null) 424 | { 425 | EnemyNullCheck(enemy); 426 | // if already registered, add rarity to levelRarities 427 | var spawnableEnemy = spawnableEnemies.FirstOrDefault(x => x.enemy == enemy && x.spawnType == spawnType); 428 | 429 | if (spawnableEnemy != null) 430 | { 431 | if (levelRarities != null) 432 | { 433 | foreach (var level in levelRarities) 434 | { 435 | spawnableEnemy.levelRarities.Add(level.Key, level.Value); 436 | } 437 | } 438 | 439 | if (customLevelRarities != null) 440 | { 441 | foreach (var level in customLevelRarities) 442 | { 443 | spawnableEnemy.customLevelRarities.Add(Levels.Compatibility.GetLLLNameOfLevel(level.Key), level.Value); 444 | } 445 | } 446 | return; 447 | } 448 | 449 | spawnableEnemy = new SpawnableEnemy(enemy, spawnType, levelRarities, customLevelRarities); 450 | 451 | spawnableEnemy.terminalNode = infoNode; 452 | spawnableEnemy.infoKeyword = infoKeyword; 453 | 454 | FinalizeRegisterEnemy(spawnableEnemy); 455 | } 456 | 457 | private static void FinalizeRegisterEnemy(SpawnableEnemy spawnableEnemy) 458 | { 459 | var callingAssembly = Assembly.GetCallingAssembly(); 460 | var modDLL = callingAssembly.GetName().Name; 461 | spawnableEnemy.modName = modDLL; 462 | 463 | if (spawnableEnemy.enemy.enemyPrefab is null) { 464 | throw new NullReferenceException($"Cannot register enemy '{spawnableEnemy.enemy.enemyName}', because enemy.enemyPrefab is null!"); 465 | } 466 | 467 | var collisionDetectComponents = spawnableEnemy.enemy.enemyPrefab.GetComponentsInChildren(); 468 | foreach (var collisionDetectComponent in collisionDetectComponents) 469 | { 470 | if (collisionDetectComponent.mainScript is null) { 471 | Plugin.logger.LogWarning($"An Enemy AI Collision Detect Script on GameObject '{collisionDetectComponent.gameObject.name}' of enemy '{spawnableEnemy.enemy.enemyName}' does not reference a 'Main Script', and could cause Null Reference Exceptions."); 472 | } 473 | } 474 | 475 | spawnableEnemies.Add(spawnableEnemy); 476 | } 477 | 478 | private static void EnemyNullCheck(EnemyType enemy) 479 | { 480 | if (enemy is null) { 481 | throw new ArgumentNullException(nameof(enemy), $"The first argument of {nameof(RegisterEnemy)} was null!"); 482 | } 483 | } 484 | 485 | /// 486 | /// Registers a enemy to be added to the given levels. 487 | /// Automatically sets the spawnType based on the enemy's isDaytimeEnemy and isOutsideEnemy properties. 488 | /// 489 | public static void RegisterEnemy(EnemyType enemy, int rarity, Levels.LevelTypes levelFlags, TerminalNode infoNode = null, TerminalKeyword infoKeyword = null) 490 | { 491 | EnemyNullCheck(enemy); 492 | var spawnType = enemy.isDaytimeEnemy ? SpawnType.Daytime : enemy.isOutsideEnemy ? SpawnType.Outside : SpawnType.Default; 493 | 494 | RegisterEnemy(enemy, rarity, levelFlags, spawnType, null, infoNode, infoKeyword); 495 | } 496 | 497 | /// 498 | /// Registers a enemy to be added to the given levels. 499 | /// Automatically sets the spawnType based on the enemy's isDaytimeEnemy and isOutsideEnemy properties. 500 | /// 501 | public static void RegisterEnemy(EnemyType enemy, int rarity, Levels.LevelTypes levelFlags, string[] spawnLevelOverrides = null, TerminalNode infoNode = null, TerminalKeyword infoKeyword = null) 502 | { 503 | EnemyNullCheck(enemy); 504 | var spawnType = enemy.isDaytimeEnemy ? SpawnType.Daytime : enemy.isOutsideEnemy ? SpawnType.Outside : SpawnType.Default; 505 | 506 | RegisterEnemy(enemy, rarity, levelFlags, spawnType, spawnLevelOverrides, infoNode, infoKeyword); 507 | } 508 | 509 | /// 510 | /// Registers a enemy to be added to the given levels, However it allows you to pass rarity tables, instead of just a single rarity 511 | /// Automatically sets the spawnType based on the enemy's isDaytimeEnemy and isOutsideEnemy properties. 512 | /// 513 | public static void RegisterEnemy(EnemyType enemy, Dictionary? levelRarities = null, Dictionary? customLevelRarities = null, TerminalNode infoNode = null, TerminalKeyword infoKeyword = null) 514 | { 515 | EnemyNullCheck(enemy); 516 | var spawnType = enemy.isDaytimeEnemy ? SpawnType.Daytime : enemy.isOutsideEnemy ? SpawnType.Outside : SpawnType.Default; 517 | 518 | RegisterEnemy(enemy, spawnType, levelRarities, customLevelRarities, infoNode, infoKeyword); 519 | } 520 | 521 | /// 522 | ///Removes a enemy from the given levels. 523 | ///This needs to be called after StartOfRound.Awake, can be used for config sync. 524 | ///Only works for enemies added by LethalLib. 525 | /// 526 | public static void RemoveEnemyFromLevels(EnemyType enemyType, Levels.LevelTypes levelFlags = Levels.LevelTypes.None, string[] levelOverrides = null) 527 | { 528 | if (StartOfRound.Instance != null) 529 | { 530 | foreach (SelectableLevel level in StartOfRound.Instance.levels) 531 | { 532 | var name = level.name; 533 | 534 | if(!Enum.IsDefined(typeof(Levels.LevelTypes), name)) 535 | name = Levels.Compatibility.GetLLLNameOfLevel(name); 536 | 537 | var alwaysValid = levelFlags.HasFlag(Levels.LevelTypes.All) || (levelOverrides != null && levelOverrides.Any(item => Levels.Compatibility.GetLLLNameOfLevel(item).ToLowerInvariant() == name.ToLowerInvariant())); 538 | var isModded = levelFlags.HasFlag(Levels.LevelTypes.Modded) && !Enum.IsDefined(typeof(Levels.LevelTypes), name); 539 | 540 | if (isModded) 541 | { 542 | alwaysValid = true; 543 | } 544 | 545 | if (Enum.IsDefined(typeof(Levels.LevelTypes), name) || alwaysValid) 546 | { 547 | var levelEnum = alwaysValid ? Levels.LevelTypes.All : (Levels.LevelTypes)Enum.Parse(typeof(Levels.LevelTypes), name); 548 | if (alwaysValid || levelFlags.HasFlag(levelEnum)) 549 | { 550 | 551 | var enemies = level.Enemies; 552 | var daytimeEnemies = level.DaytimeEnemies; 553 | var outsideEnemies = level.OutsideEnemies; 554 | 555 | 556 | enemies.RemoveAll(x => x.enemyType == enemyType); 557 | daytimeEnemies.RemoveAll(x => x.enemyType == enemyType); 558 | outsideEnemies.RemoveAll(x => x.enemyType == enemyType); 559 | if (Plugin.extendedLogging.Value) 560 | Plugin.logger.LogInfo("Removed Enemy " + enemyType.name + " from Level " + name); 561 | } 562 | } 563 | } 564 | } 565 | } 566 | } 567 | -------------------------------------------------------------------------------- /LethalLib/Modules/Items.cs: -------------------------------------------------------------------------------- 1 | #region 2 | 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using System.Reflection; 7 | using BepInEx.Configuration; 8 | using LethalLib.Extras; 9 | using UnityEngine; 10 | using UnityEngine.Rendering; 11 | using Object = UnityEngine.Object; 12 | 13 | #endregion 14 | 15 | namespace LethalLib.Modules; 16 | 17 | public class Items 18 | { 19 | public static ConfigEntry useSavedataFix; 20 | public static GameObject scanNodePrefab; 21 | private static List levelsAlreadyAddedTo = new(); 22 | 23 | public static void Init() 24 | { 25 | useSavedataFix = Plugin.config.Bind("Items", "EnableItemSaveFix", false, "Allow for LethalLib to store/reorder the item list, which should fix issues where items get reshuffled when loading an old save. This is experimental and may cause save corruptions occasionally."); 26 | 27 | scanNodePrefab = Plugin.MainAssets.LoadAsset("Assets/Custom/ItemScanNode.prefab"); 28 | 29 | On.StartOfRound.Start += StartOfRound_Start; 30 | On.Terminal.Awake += Terminal_Awake; 31 | On.Terminal.TextPostProcess += Terminal_TextPostProcess; 32 | } 33 | 34 | 35 | 36 | private static string Terminal_TextPostProcess(On.Terminal.orig_TextPostProcess orig, Terminal self, string modifiedDisplayText, TerminalNode node) 37 | { 38 | var oldItemList = self.buyableItemsList.ToList(); 39 | var itemList = self.buyableItemsList.ToList(); 40 | 41 | // remove any disabled items, this is horrific for performance but i do not have a better solution rn 42 | itemList.RemoveAll(x => { 43 | var actualItem = shopItems.FirstOrDefault(item => item.origItem == x || item.item == x); 44 | if (actualItem != null) 45 | { 46 | return actualItem.wasRemoved; 47 | } 48 | return false; 49 | }); 50 | 51 | self.buyableItemsList = itemList.ToArray(); 52 | var output = orig(self, modifiedDisplayText, node); 53 | 54 | self.buyableItemsList = oldItemList.ToArray(); 55 | 56 | return output; 57 | } 58 | 59 | public struct ItemSaveOrderData 60 | { 61 | public int itemId; 62 | public string itemName; 63 | public string assetName; 64 | } 65 | 66 | public struct BuyableItemAssetInfo 67 | { 68 | public Item itemAsset; 69 | public TerminalKeyword keyword; 70 | } 71 | 72 | public static List LethalLibItemList = new List(); 73 | public static List buyableItemAssetInfos = new List(); 74 | public static Terminal terminal; 75 | 76 | private static void StartOfRound_Start(On.StartOfRound.orig_Start orig, StartOfRound self) 77 | { 78 | // Savedata fix, not sure if this works properly because my savegames have been randomly getting corrupted. 79 | if (useSavedataFix.Value && self.IsHost) 80 | { 81 | Plugin.logger.LogInfo($"Fixing Item savedata!!"); 82 | 83 | List itemList = new List(); 84 | 85 | StartOfRound.Instance.allItemsList.itemsList.ForEach(item => 86 | { 87 | itemList.Add(new ItemSaveOrderData() 88 | { 89 | itemId = item.itemId, 90 | itemName = item.itemName, 91 | assetName = item.name 92 | }); 93 | }); 94 | 95 | 96 | 97 | // load itemlist from es3 98 | if (ES3.KeyExists("LethalLibAllItemsList", GameNetworkManager.Instance.currentSaveFileName)) 99 | { 100 | // load itemsList 101 | itemList = ES3.Load>("LethalLibAllItemsList", GameNetworkManager.Instance.currentSaveFileName); 102 | } 103 | 104 | // sort so that items are in the same order as they were when the game was saved 105 | // if item is not in list, add it at the end 106 | List list = StartOfRound.Instance.allItemsList.itemsList; 107 | 108 | List newList = new List(); 109 | 110 | foreach (ItemSaveOrderData item in itemList) 111 | { 112 | var itemInList = list.FirstOrDefault(x => x.itemId == item.itemId && x.itemName == item.itemName && item.assetName == x.name); 113 | 114 | // add in correct place, if there is a gap, we want to add an empty Item scriptable object 115 | if (itemInList != null) 116 | { 117 | newList.Add(itemInList); 118 | } 119 | else 120 | { 121 | newList.Add(ScriptableObject.CreateInstance()); 122 | } 123 | } 124 | 125 | foreach (Item item in list) 126 | { 127 | if (!newList.Contains(item)) 128 | { 129 | newList.Add(item); 130 | } 131 | } 132 | 133 | StartOfRound.Instance.allItemsList.itemsList = newList; 134 | 135 | // save itemlist to es3 136 | ES3.Save>("LethalLibAllItemsList", itemList, GameNetworkManager.Instance.currentSaveFileName); 137 | 138 | // loop and print 139 | /*for (int i = 0; i < StartOfRound.Instance.allItemsList.itemsList.Count; i++) 140 | { 141 | var item = StartOfRound.Instance.allItemsList.itemsList[i]; 142 | Plugin.logger.LogInfo($"Item {i}: Name: {item.itemName} - ItemID: {item.itemId} - AssetName: {item.name}"); 143 | }*/ 144 | } 145 | 146 | orig(self); 147 | } 148 | 149 | private static void RegisterLevelScrapforLLL_RoundManager_Start(On.RoundManager.orig_Start orig, RoundManager self) 150 | { 151 | orig(self); 152 | RegisterLethalLibScrapItemsForAllLevels(); 153 | } 154 | 155 | private static void RegisterLevelScrapforLE_Terminal_Start(On.Terminal.orig_Start orig, Terminal self) 156 | { 157 | orig(self); 158 | RegisterLethalLibScrapItemsForAllLevels(); 159 | } 160 | 161 | private static void RegisterLethalLibScrapItemsForAllLevels() 162 | { 163 | 164 | foreach (SelectableLevel level in StartOfRound.Instance.levels) 165 | { 166 | if(levelsAlreadyAddedTo.Contains(level)) 167 | continue; 168 | foreach (ScrapItem scrapItem in scrapItems) 169 | { 170 | AddScrapItemToLevel(scrapItem, level); 171 | } 172 | levelsAlreadyAddedTo.Add(level); 173 | } 174 | } 175 | 176 | private static void AddScrapItemToLevel(ScrapItem scrapItem, SelectableLevel level) 177 | { 178 | string name = level.name; 179 | string customName = Levels.Compatibility.GetLLLNameOfLevel(name); 180 | Levels.LevelTypes currentLevelType = Levels.LevelTypes.None; 181 | bool isCurrentLevelFromVanilla = false; 182 | 183 | if (Enum.TryParse(name, true, out currentLevelType)) // It'd be weird if a level was called "Modded" or "All" so I think im good to not check that lol 184 | { 185 | isCurrentLevelFromVanilla = true; 186 | } 187 | else 188 | { 189 | name = customName; 190 | } 191 | 192 | string tagName = string.Empty; 193 | bool itemValidToAdd = scrapItem.levelRarities.ContainsKey(Levels.LevelTypes.All) 194 | || (scrapItem.customLevelRarities != null && scrapItem.customLevelRarities.Keys != null && Levels.Compatibility.ContentIncludedToLevelViaTag(scrapItem.customLevelRarities.Keys.ToArray(), level, out tagName)) 195 | || (isCurrentLevelFromVanilla && scrapItem.levelRarities.ContainsKey(Levels.LevelTypes.Vanilla)) 196 | || (!isCurrentLevelFromVanilla && scrapItem.levelRarities.ContainsKey(Levels.LevelTypes.Modded)) 197 | || (isCurrentLevelFromVanilla && scrapItem.levelRarities.ContainsKey(currentLevelType)) 198 | || (!isCurrentLevelFromVanilla && scrapItem.customLevelRarities != null && scrapItem.customLevelRarities.ContainsKey(customName)); 199 | 200 | if (Plugin.extendedLogging.Value) 201 | Plugin.logger.LogInfo($"{name} for item: {scrapItem.item.itemName}, isCurrentLevelFromVanilla: {isCurrentLevelFromVanilla}, Found valid: {itemValidToAdd}"); 202 | 203 | if (!itemValidToAdd) return; 204 | // find rarity 205 | int rarity = 0; 206 | 207 | if (scrapItem.levelRarities.ContainsKey(currentLevelType)) 208 | { 209 | rarity = scrapItem.levelRarities[currentLevelType]; 210 | } 211 | else if (scrapItem.customLevelRarities != null && scrapItem.customLevelRarities.ContainsKey(name)) 212 | { 213 | rarity = scrapItem.customLevelRarities[name]; 214 | } 215 | else if (scrapItem.customLevelRarities != null && tagName != string.Empty && scrapItem.customLevelRarities.ContainsKey(tagName)) 216 | { 217 | rarity = scrapItem.customLevelRarities[tagName]; 218 | } 219 | else if (isCurrentLevelFromVanilla && scrapItem.levelRarities.ContainsKey(Levels.LevelTypes.Vanilla)) 220 | { 221 | rarity = scrapItem.levelRarities[Levels.LevelTypes.Vanilla]; 222 | } 223 | else if (!isCurrentLevelFromVanilla && scrapItem.levelRarities.ContainsKey(Levels.LevelTypes.Modded)) 224 | { 225 | rarity = scrapItem.levelRarities[Levels.LevelTypes.Modded]; 226 | } 227 | else if (scrapItem.levelRarities.ContainsKey(Levels.LevelTypes.All)) 228 | { 229 | rarity = scrapItem.levelRarities[Levels.LevelTypes.All]; 230 | } 231 | 232 | var spawnableÍtemWithRarity = new SpawnableItemWithRarity() 233 | { 234 | spawnableItem = scrapItem.item, 235 | rarity = rarity 236 | }; 237 | 238 | // make sure spawnableScrap does not already contain item 239 | //Plugin.logger.LogInfo($"Checking if {scrapItem.item.name} is already in {name}"); 240 | 241 | if (!level.spawnableScrap.Any(x => x.spawnableItem == scrapItem.item)) 242 | { 243 | level.spawnableScrap.Add(spawnableÍtemWithRarity); 244 | 245 | if (Plugin.extendedLogging.Value) 246 | Plugin.logger.LogInfo($"{name} added {scrapItem.item.name} with rarity: {rarity}"); 247 | 248 | } 249 | } 250 | 251 | private static void RegisterScrapAsItem(StartOfRound startOfRound) 252 | { 253 | foreach (ScrapItem scrapItem in scrapItems) 254 | { 255 | if (!startOfRound.allItemsList.itemsList.Contains(scrapItem.item)) 256 | { 257 | if (Plugin.extendedLogging.Value) 258 | { 259 | if (scrapItem.modName != "LethalLib") 260 | { 261 | Plugin.logger.LogInfo($"{scrapItem.modName} registered scrap item: {scrapItem.item.itemName}"); 262 | } 263 | else 264 | { 265 | Plugin.logger.LogInfo($"Registered scrap item: {scrapItem.item.itemName}"); 266 | } 267 | } 268 | 269 | LethalLibItemList.Add(scrapItem.item); 270 | 271 | startOfRound.allItemsList.itemsList.Add(scrapItem.item); 272 | } 273 | } 274 | } 275 | 276 | private static void Terminal_Awake(On.Terminal.orig_Awake orig, Terminal self) 277 | { 278 | var startOfRound = StartOfRound.Instance; 279 | 280 | RegisterLethalLibScrapItemsForAllLevels(); 281 | 282 | if(BepInEx.Bootstrap.Chainloader.PluginInfos.ContainsKey("imabatby.lethallevelloader") || // currently has typo 283 | BepInEx.Bootstrap.Chainloader.PluginInfos.ContainsKey("iambatby.lethallevelloader")) // might be changed to this 284 | On.RoundManager.Start += RegisterLevelScrapforLLL_RoundManager_Start; 285 | 286 | if(BepInEx.Bootstrap.Chainloader.PluginInfos.ContainsKey("LethalExpansion")) 287 | On.Terminal.Start += RegisterLevelScrapforLE_Terminal_Start; 288 | 289 | // startOfRound.allItemsList.itemsList.RemoveAll(x => LethalLibItemList.Contains(x)); 290 | 291 | RegisterScrapAsItem(startOfRound); 292 | 293 | foreach (ShopItem shopItem in shopItems) 294 | { 295 | if (!startOfRound.allItemsList.itemsList.Contains(shopItem.item)) 296 | { 297 | if (Plugin.extendedLogging.Value) 298 | { 299 | if (shopItem.modName != "LethalLib") 300 | { 301 | Plugin.logger.LogInfo($"{shopItem.modName} registered shop item: {shopItem.item.itemName}"); 302 | } 303 | else 304 | { 305 | Plugin.logger.LogInfo($"Registered shop item: {shopItem.item.itemName}"); 306 | } 307 | } 308 | 309 | LethalLibItemList.Add(shopItem.item); 310 | 311 | startOfRound.allItemsList.itemsList.Add(shopItem.item); 312 | } 313 | } 314 | 315 | foreach (PlainItem plainItem in plainItems) 316 | { 317 | if (!startOfRound.allItemsList.itemsList.Contains(plainItem.item)) 318 | { 319 | if (Plugin.extendedLogging.Value) 320 | { 321 | if (plainItem.modName != "LethalLib") 322 | { 323 | Plugin.logger.LogInfo($"{plainItem.modName} registered item: {plainItem.item.itemName}"); 324 | } 325 | else 326 | { 327 | Plugin.logger.LogInfo($"Registered item: {plainItem.item.itemName}"); 328 | } 329 | } 330 | 331 | LethalLibItemList.Add(plainItem.item); 332 | 333 | startOfRound.allItemsList.itemsList.Add(plainItem.item); 334 | } 335 | } 336 | 337 | 338 | 339 | terminal = self; 340 | var itemList = self.buyableItemsList.ToList(); 341 | 342 | var buyKeyword = self.terminalNodes.allKeywords.First(keyword => keyword.word == "buy"); 343 | var cancelPurchaseNode = buyKeyword.compatibleNouns[0].result.terminalOptions[1].result; 344 | var infoKeyword = self.terminalNodes.allKeywords.First(keyword => keyword.word == "info"); 345 | 346 | 347 | 348 | Plugin.logger.LogInfo($"Adding {shopItems.Count} items to terminal"); 349 | foreach (ShopItem item in shopItems) 350 | { 351 | if (itemList.Any((Item x) => x.itemName == item.item.itemName) && !item.wasRemoved) 352 | { 353 | Plugin.logger.LogInfo((object)("Item " + item.item.itemName + " already exists in terminal, skipping")); 354 | continue; 355 | } 356 | 357 | item.wasRemoved = false; 358 | 359 | if (item.price == -1) 360 | { 361 | item.price = item.item.creditsWorth; 362 | } 363 | else 364 | { 365 | item.item.creditsWorth = item.price; 366 | } 367 | 368 | var oldIndex = -1; 369 | 370 | if (!itemList.Any((Item x) => x == item.item)) 371 | { 372 | itemList.Add(item.item); 373 | } 374 | else 375 | { 376 | oldIndex = itemList.IndexOf(item.item); 377 | } 378 | 379 | var newIndex = oldIndex == -1 ? itemList.Count - 1 : oldIndex; 380 | 381 | var itemName = item.item.itemName; 382 | var lastChar = itemName[itemName.Length - 1]; 383 | var itemNamePlural = itemName; 384 | 385 | //Plugin.logger.LogInfo($"Adding {itemName} to terminal"); 386 | 387 | var buyNode2 = item.buyNode2; 388 | 389 | if(buyNode2 == null) 390 | { 391 | buyNode2 = ScriptableObject.CreateInstance(); 392 | 393 | buyNode2.name = $"{itemName.Replace(" ", "-")}BuyNode2"; 394 | buyNode2.displayText = $"Ordered [variableAmount] {itemNamePlural}. Your new balance is [playerCredits].\n\nOur contractors enjoy fast, free shipping while on the job! Any purchased items will arrive hourly at your approximate location.\r\n\r\n"; 395 | buyNode2.clearPreviousText = true; 396 | buyNode2.maxCharactersToType = 15; 397 | } 398 | 399 | buyNode2.buyItemIndex = newIndex; 400 | buyNode2.isConfirmationNode = false; 401 | buyNode2.itemCost = item.price; 402 | buyNode2.playSyncedClip = 0; 403 | 404 | //Plugin.logger.LogInfo($"Item price: {buyNode2.itemCost}, Item index: {buyNode2.buyItemIndex}"); 405 | 406 | var buyNode1 = item.buyNode1; 407 | if (buyNode1 == null) 408 | { 409 | buyNode1 = ScriptableObject.CreateInstance(); 410 | buyNode1.name = $"{itemName.Replace(" ", "-")}BuyNode1"; 411 | buyNode1.displayText = $"You have requested to order {itemNamePlural}. Amount: [variableAmount].\nTotal cost of items: [totalCost].\n\nPlease CONFIRM or DENY.\r\n\r\n"; 412 | buyNode1.clearPreviousText = true; 413 | buyNode1.maxCharactersToType = 35; 414 | 415 | //Plugin.logger.LogInfo($"Generating buynode1"); 416 | 417 | } 418 | 419 | buyNode1.buyItemIndex = newIndex; 420 | buyNode1.isConfirmationNode = true; 421 | buyNode1.overrideOptions = true; 422 | buyNode1.itemCost = item.price; 423 | 424 | //Plugin.logger.LogInfo($"Item price: {buyNode1.itemCost}, Item index: {buyNode1.buyItemIndex}"); 425 | 426 | buyNode1.terminalOptions = new CompatibleNoun[2] 427 | { 428 | new CompatibleNoun() 429 | { 430 | noun = self.terminalNodes.allKeywords.First(keyword2 => keyword2.word == "confirm"), 431 | result = buyNode2 432 | }, 433 | new CompatibleNoun() 434 | { 435 | noun = self.terminalNodes.allKeywords.First(keyword2 => keyword2.word == "deny"), 436 | result = cancelPurchaseNode 437 | } 438 | }; 439 | 440 | var keyword = TerminalUtils.CreateTerminalKeyword(itemName.ToLowerInvariant().Replace(" ", "-"), defaultVerb: buyKeyword); 441 | 442 | //Plugin.logger.LogInfo($"Generated keyword: {keyword.word}"); 443 | 444 | //self.terminalNodes.allKeywords.AddItem(keyword); 445 | var allKeywords = self.terminalNodes.allKeywords.ToList(); 446 | allKeywords.Add(keyword); 447 | self.terminalNodes.allKeywords = allKeywords.ToArray(); 448 | 449 | var nouns = buyKeyword.compatibleNouns.ToList(); 450 | nouns.Add(new CompatibleNoun() 451 | { 452 | noun = keyword, 453 | result = buyNode1 454 | }); 455 | buyKeyword.compatibleNouns = nouns.ToArray(); 456 | 457 | 458 | var itemInfo = item.itemInfo; 459 | if (itemInfo == null) 460 | { 461 | itemInfo = ScriptableObject.CreateInstance(); 462 | itemInfo.name = $"{itemName.Replace(" ", "-")}InfoNode"; 463 | itemInfo.displayText = $"[No information about this object was found.]\n\n"; 464 | itemInfo.clearPreviousText = true; 465 | itemInfo.maxCharactersToType = 25; 466 | 467 | // Plugin.logger.LogInfo($"Generated item info!!"); 468 | } 469 | 470 | self.terminalNodes.allKeywords = allKeywords.ToArray(); 471 | 472 | var itemInfoNouns = infoKeyword.compatibleNouns.ToList(); 473 | itemInfoNouns.Add(new CompatibleNoun() 474 | { 475 | noun = keyword, 476 | result = itemInfo 477 | }); 478 | infoKeyword.compatibleNouns = itemInfoNouns.ToArray(); 479 | 480 | BuyableItemAssetInfo buyableItemAssetInfo = new BuyableItemAssetInfo() 481 | { 482 | itemAsset = item.item, 483 | keyword = keyword 484 | }; 485 | 486 | buyableItemAssetInfos.Add(buyableItemAssetInfo); 487 | 488 | if (Plugin.extendedLogging.Value) 489 | { 490 | Plugin.logger.LogInfo($"Added {itemName} to terminal (Item price: {buyNode1.itemCost}, Item Index: {buyNode1.buyItemIndex}, Terminal keyword: {keyword.word})"); 491 | } 492 | } 493 | 494 | self.buyableItemsList = itemList.ToArray(); 495 | 496 | orig(self); 497 | } 498 | 499 | public static List scrapItems = new List(); 500 | public static List shopItems = new List(); 501 | public static List plainItems = new List(); 502 | 503 | 504 | public class ScrapItem 505 | { 506 | public Item item; 507 | public Item origItem; 508 | 509 | /// 510 | /// Deprecated 511 | /// This is never set or used, use levelRarities and customLevelRarities instead. 512 | /// 513 | public int rarity = 0; 514 | /// 515 | /// Deprecated 516 | /// This is never set or used, use levelRarities and customLevelRarities instead. 517 | /// 518 | public Levels.LevelTypes spawnLevels; 519 | /// 520 | /// Deprecated 521 | /// This is never set or used, use levelRarities and customLevelRarities instead. 522 | /// 523 | public string[] spawnLevelOverrides; 524 | 525 | 526 | public string modName = "Unknown"; 527 | public Dictionary customLevelRarities = new Dictionary(); 528 | public Dictionary levelRarities = new Dictionary(); 529 | 530 | public ScrapItem(Item item, int rarity, Levels.LevelTypes spawnLevels = Levels.LevelTypes.None, string[] spawnLevelOverrides = null) 531 | { 532 | origItem = item; 533 | if (item.isScrap == false) 534 | { 535 | 536 | item = item.Clone(); 537 | item.isScrap = true; 538 | if(item.maxValue == 0 && item.minValue == 0) 539 | { 540 | item.minValue = 40; 541 | item.maxValue = 100; 542 | } 543 | else if(item.maxValue == 0) 544 | { 545 | item.maxValue = item.minValue * 2; 546 | } 547 | else if(item.minValue == 0) 548 | { 549 | item.minValue = item.maxValue / 2; 550 | } 551 | 552 | var newPrefab = NetworkPrefabs.CloneNetworkPrefab(item.spawnPrefab); 553 | 554 | if(newPrefab.GetComponent() != null) 555 | { 556 | newPrefab.GetComponent().itemProperties = item; 557 | } 558 | 559 | if(newPrefab.GetComponentInChildren() == null) 560 | { 561 | // add scan node 562 | var scanNode = Object.Instantiate(scanNodePrefab, newPrefab.transform); 563 | scanNode.name = "ScanNode"; 564 | scanNode.transform.localPosition = new Vector3(0, 0, 0); 565 | var properties = scanNode.GetComponent(); 566 | properties.headerText = item.itemName; 567 | } 568 | 569 | item.spawnPrefab = newPrefab; 570 | } 571 | this.item = item; 572 | /*this.rarity = rarity; 573 | this.spawnLevels = spawnLevels; 574 | this.spawnLevelOverrides = spawnLevelOverrides;*/ 575 | 576 | 577 | if (spawnLevelOverrides != null) 578 | { 579 | foreach (var level in spawnLevelOverrides) 580 | { 581 | customLevelRarities.Add(Levels.Compatibility.GetLLLNameOfLevel(level), rarity); 582 | } 583 | } 584 | 585 | if (spawnLevels != Levels.LevelTypes.None) 586 | { 587 | foreach (Levels.LevelTypes level in Enum.GetValues(typeof(Levels.LevelTypes))) 588 | { 589 | if (spawnLevels.HasFlag(level)) 590 | { 591 | levelRarities.Add(level, rarity); 592 | } 593 | } 594 | } 595 | } 596 | 597 | public ScrapItem(Item item, Dictionary? levelRarities = null, Dictionary? customLevelRarities = null) 598 | { 599 | origItem = item; 600 | if (item.isScrap == false) 601 | { 602 | item = item.Clone(); 603 | item.isScrap = true; 604 | if (item.maxValue == 0 && item.minValue == 0) 605 | { 606 | item.minValue = 40; 607 | item.maxValue = 100; 608 | } 609 | else if (item.maxValue == 0) 610 | { 611 | item.maxValue = item.minValue * 2; 612 | } 613 | else if (item.minValue == 0) 614 | { 615 | item.minValue = item.maxValue / 2; 616 | } 617 | 618 | var newPrefab = NetworkPrefabs.CloneNetworkPrefab(item.spawnPrefab); 619 | 620 | if (newPrefab.GetComponent() != null) 621 | { 622 | newPrefab.GetComponent().itemProperties = item; 623 | } 624 | 625 | if (newPrefab.GetComponentInChildren() == null) 626 | { 627 | // add scan node 628 | var scanNode = Object.Instantiate(scanNodePrefab, newPrefab.transform); 629 | scanNode.name = "ScanNode"; 630 | scanNode.transform.localPosition = new Vector3(0, 0, 0); 631 | var properties = scanNode.GetComponent(); 632 | properties.headerText = item.itemName; 633 | } 634 | 635 | item.spawnPrefab = newPrefab; 636 | } 637 | this.item = item; 638 | 639 | if (customLevelRarities != null) 640 | { 641 | this.customLevelRarities = Levels.Compatibility.LLLifyLevelRarityDictionary(customLevelRarities); 642 | } 643 | 644 | if (levelRarities != null) 645 | { 646 | this.levelRarities = levelRarities; 647 | } 648 | } 649 | } 650 | 651 | public class PlainItem 652 | { 653 | public Item item; 654 | public string modName; 655 | 656 | public PlainItem(Item item) 657 | { 658 | this.item = item; 659 | } 660 | } 661 | 662 | public class ShopItem 663 | { 664 | public Item item; 665 | public Item origItem; 666 | public TerminalNode buyNode1; 667 | public TerminalNode buyNode2; 668 | public TerminalNode itemInfo; 669 | public bool wasRemoved = false; 670 | public int price; 671 | public string modName; 672 | public ShopItem(Item item, TerminalNode buyNode1 = null, TerminalNode buyNode2 = null, TerminalNode itemInfo = null, int price = 0) 673 | { 674 | origItem = item; 675 | // removed until further notice... this confuses people who add their scrap to the shop for testing. 676 | /* 677 | if (item.isScrap) 678 | { 679 | item = item.Clone(); 680 | item.isScrap = false; 681 | 682 | var newPrefab = NetworkPrefabs.CloneNetworkPrefab(item.spawnPrefab); 683 | 684 | if (newPrefab.GetComponent() != null) 685 | { 686 | newPrefab.GetComponent().itemProperties = item; 687 | } 688 | 689 | if (newPrefab.GetComponentInChildren() != null) 690 | { 691 | Object.Destroy(newPrefab.GetComponentInChildren().gameObject); 692 | } 693 | 694 | item.spawnPrefab = newPrefab; 695 | }*/ 696 | this.item = item; 697 | this.price = price; 698 | if (buyNode1 != null) 699 | { 700 | this.buyNode1 = buyNode1; 701 | } 702 | if (buyNode2 != null) 703 | { 704 | this.buyNode2 = buyNode2; 705 | } 706 | if (itemInfo != null) 707 | { 708 | this.itemInfo = itemInfo; 709 | } 710 | } 711 | } 712 | 713 | /// 714 | ///This method registers a scrap item to the game, making it obtainable in the specified levels. 715 | /// 716 | public static void RegisterScrap(Item spawnableItem, int rarity, Levels.LevelTypes levelFlags) 717 | { 718 | // check if item is already registered, if it is we just want to add to the rarity table 719 | var scrapItem = scrapItems.FirstOrDefault(x => x.origItem == spawnableItem || x.item == spawnableItem); 720 | 721 | if (scrapItem != null) 722 | { 723 | if (levelFlags != Levels.LevelTypes.None) 724 | { 725 | scrapItem.levelRarities.Add(levelFlags, rarity); 726 | } 727 | return; 728 | } 729 | 730 | scrapItem = new ScrapItem(spawnableItem, rarity, levelFlags); 731 | ValidateItemProperties(scrapItem.item); 732 | 733 | var callingAssembly = Assembly.GetCallingAssembly(); 734 | var modDLL = callingAssembly.GetName().Name; 735 | scrapItem.modName = modDLL; 736 | 737 | 738 | scrapItems.Add(scrapItem); 739 | } 740 | 741 | 742 | /// 743 | ///This method registers a scrap item to the game, making it obtainable in the specified levels. With the option to add custom levels to the list. 744 | /// 745 | public static void RegisterScrap(Item spawnableItem, int rarity, Levels.LevelTypes levelFlags = Levels.LevelTypes.None, string[] levelOverrides = null) 746 | { 747 | // check if item is already registered, if it is we just want to add to the rarity table 748 | var scrapItem = scrapItems.FirstOrDefault(x => x.origItem == spawnableItem || x.item == spawnableItem); 749 | 750 | if (scrapItem != null) 751 | { 752 | if (levelFlags != Levels.LevelTypes.None) 753 | { 754 | scrapItem.levelRarities.Add(levelFlags, rarity); 755 | } 756 | 757 | 758 | if (levelOverrides != null) 759 | { 760 | foreach (var level in levelOverrides) 761 | { 762 | scrapItem.customLevelRarities.Add(Levels.Compatibility.GetLLLNameOfLevel(level), rarity); 763 | } 764 | } 765 | return; 766 | } 767 | 768 | scrapItem = new ScrapItem(spawnableItem, rarity, levelFlags, levelOverrides); 769 | ValidateItemProperties(scrapItem.item); 770 | 771 | var callingAssembly = Assembly.GetCallingAssembly(); 772 | var modDLL = callingAssembly.GetName().Name; 773 | scrapItem.modName = modDLL; 774 | 775 | 776 | scrapItems.Add(scrapItem); 777 | } 778 | 779 | /// 782 | public static void RegisterScrap(Item spawnableItem, Dictionary? levelRarities = null, Dictionary? customLevelRarities = null) 783 | { 784 | // check if item is already registered, if it is we just want to add to the rarity table 785 | var scrapItem = scrapItems.FirstOrDefault(x => x.origItem == spawnableItem || x.item == spawnableItem); 786 | 787 | if (scrapItem != null) 788 | { 789 | if (levelRarities != null) 790 | { 791 | foreach (var level in levelRarities) 792 | { 793 | scrapItem.levelRarities.Add(level.Key, level.Value); 794 | } 795 | } 796 | 797 | if (customLevelRarities != null) 798 | { 799 | foreach (var level in customLevelRarities) 800 | { 801 | scrapItem.customLevelRarities.Add(Levels.Compatibility.GetLLLNameOfLevel(level.Key), level.Value); 802 | } 803 | } 804 | return; 805 | } 806 | 807 | scrapItem = new ScrapItem(spawnableItem, levelRarities, customLevelRarities); 808 | ValidateItemProperties(scrapItem.item); 809 | 810 | var callingAssembly = Assembly.GetCallingAssembly(); 811 | var modDLL = callingAssembly.GetName().Name; 812 | scrapItem.modName = modDLL; 813 | 814 | scrapItems.Add(scrapItem); 815 | } 816 | 817 | /// 818 | ///This method registers a shop item to the game. 819 | /// 820 | public static void RegisterShopItem(Item shopItem, TerminalNode buyNode1 = null, TerminalNode buyNode2 = null, TerminalNode itemInfo = null, int price = -1) 821 | { 822 | var item = new ShopItem(shopItem, buyNode1, buyNode2, itemInfo, price); 823 | ValidateItemProperties(item.item); 824 | 825 | var callingAssembly = Assembly.GetCallingAssembly(); 826 | var modDLL = callingAssembly.GetName().Name; 827 | item.modName = modDLL; 828 | 829 | shopItems.Add(item); 830 | } 831 | 832 | /// 833 | ///This method registers a shop item to the game. 834 | /// 835 | public static void RegisterShopItem(Item shopItem, int price = -1) 836 | { 837 | var item = new ShopItem(shopItem, null, null, null, price); 838 | ValidateItemProperties(item.item); 839 | 840 | var callingAssembly = Assembly.GetCallingAssembly(); 841 | var modDLL = callingAssembly.GetName().Name; 842 | item.modName = modDLL; 843 | 844 | shopItems.Add(item); 845 | } 846 | 847 | /// 848 | ///This method registers an item to the game, without making it obtainable in any way. 849 | /// 850 | public static void RegisterItem(Item plainItem) 851 | { 852 | var item = new PlainItem(plainItem); 853 | ValidateItemProperties(item.item); 854 | 855 | var callingAssembly = Assembly.GetCallingAssembly(); 856 | var modDLL = callingAssembly.GetName().Name; 857 | item.modName = modDLL; 858 | 859 | plainItems.Add(item); 860 | } 861 | 862 | private static void ValidateItemProperties(Item item) 863 | { 864 | if (item == null) 865 | return; 866 | 867 | if (item.weight < 1 || item.weight > 4) 868 | { 869 | Plugin.logger.LogWarning($"Item {item.itemName} has an invalid weight of {item.weight}, resetting to weight of 1, please check the lethal.wiki for the weight calculation and give it a valid number, anything below 1 or above 4 gets forced to be 1 or 4."); 870 | item.weight = Mathf.Clamp(item.weight, 1, 4); 871 | } 872 | } 873 | /// 874 | ///Removes a scrap from the given levels. 875 | ///This needs to be called after StartOfRound.Awake. 876 | /// 877 | public static void RemoveScrapFromLevels(Item scrapItem, Levels.LevelTypes levelFlags = Levels.LevelTypes.None, string[] levelOverrides = null) 878 | { 879 | if (StartOfRound.Instance != null) 880 | { 881 | foreach (SelectableLevel level in StartOfRound.Instance.levels) 882 | { 883 | var name = level.name; 884 | 885 | if(!Enum.IsDefined(typeof(Levels.LevelTypes), name)) 886 | name = Levels.Compatibility.GetLLLNameOfLevel(name); 887 | 888 | var alwaysValid = levelFlags.HasFlag(Levels.LevelTypes.All) || (levelOverrides != null && levelOverrides.Any(item => Levels.Compatibility.GetLLLNameOfLevel(item).ToLowerInvariant() == name.ToLowerInvariant())); 889 | var isModded = levelFlags.HasFlag(Levels.LevelTypes.Modded) && !Enum.IsDefined(typeof(Levels.LevelTypes), name); 890 | 891 | if (isModded) 892 | { 893 | alwaysValid = true; 894 | } 895 | 896 | if (Enum.IsDefined(typeof(Levels.LevelTypes), name) || alwaysValid) 897 | { 898 | var levelEnum = alwaysValid ? Levels.LevelTypes.All : (Levels.LevelTypes)Enum.Parse(typeof(Levels.LevelTypes), name); 899 | if (alwaysValid || levelFlags.HasFlag(levelEnum)) 900 | { 901 | // find item in scrapItems 902 | var actualItem = scrapItems.FirstOrDefault(x => x.origItem == scrapItem || x.item == scrapItem); 903 | 904 | var spawnableItemWithRarity = level.spawnableScrap.FirstOrDefault(x => x.spawnableItem == actualItem.item); 905 | 906 | if (spawnableItemWithRarity != null) 907 | { 908 | if (Plugin.extendedLogging.Value) 909 | Plugin.logger.LogInfo("Removed Item " + spawnableItemWithRarity.spawnableItem.name + " from Level " + name); 910 | 911 | level.spawnableScrap.Remove(spawnableItemWithRarity); 912 | } 913 | 914 | } 915 | } 916 | } 917 | } 918 | } 919 | 920 | /// 921 | ///Removes a shop item from the game. 922 | ///This needs to be called after StartOfRound.Awake. 923 | ///Only works for items registered by LethalLib. 924 | /// 925 | public static void RemoveShopItem(Item shopItem) 926 | { 927 | if (StartOfRound.Instance != null) 928 | { 929 | var actualItem = shopItems.FirstOrDefault(x => x.origItem == shopItem || x.item == shopItem); 930 | 931 | // do not remove from list because it fucks up the indexes 932 | /* 933 | var itemList = terminal.buyableItemsList.ToList(); 934 | itemList.RemoveAll(x => x == actualItem.item); 935 | terminal.buyableItemsList = itemList.ToArray(); 936 | */ 937 | 938 | actualItem.wasRemoved = true; 939 | 940 | var allKeywords = terminal.terminalNodes.allKeywords.ToList(); 941 | var infoKeyword = terminal.terminalNodes.allKeywords.First(keyword => keyword.word == "info"); 942 | var buyKeyword = terminal.terminalNodes.allKeywords.First(keyword => keyword.word == "buy"); 943 | 944 | 945 | var nouns = buyKeyword.compatibleNouns.ToList(); 946 | var itemInfoNouns = infoKeyword.compatibleNouns.ToList(); 947 | if (buyableItemAssetInfos.Any(x => x.itemAsset == actualItem.item)) 948 | { 949 | var asset = buyableItemAssetInfos.First(x => x.itemAsset == actualItem.item); 950 | allKeywords.Remove(asset.keyword); 951 | 952 | nouns.RemoveAll(noun => noun.noun == asset.keyword); 953 | itemInfoNouns.RemoveAll(noun => noun.noun == asset.keyword); 954 | } 955 | terminal.terminalNodes.allKeywords = allKeywords.ToArray(); 956 | buyKeyword.compatibleNouns = nouns.ToArray(); 957 | infoKeyword.compatibleNouns = itemInfoNouns.ToArray(); 958 | } 959 | } 960 | 961 | /// 962 | ///Updates the price of an already registered shop item. 963 | ///This needs to be called after StartOfRound.Awake. 964 | ///Only works for items registered by LethalLib. 965 | /// 966 | public static void UpdateShopItemPrice(Item shopItem, int price) 967 | { 968 | if (StartOfRound.Instance != null) 969 | { 970 | var actualItem = shopItems.FirstOrDefault(x => x.origItem == shopItem || x.item == shopItem); 971 | 972 | actualItem.item.creditsWorth = price; 973 | var buyKeyword = terminal.terminalNodes.allKeywords.First(keyword => keyword.word == "buy"); 974 | var cancelPurchaseNode = buyKeyword.compatibleNouns[0].result.terminalOptions[1].result; 975 | var nouns = buyKeyword.compatibleNouns.ToList(); 976 | if (buyableItemAssetInfos.Any(x => x.itemAsset == actualItem.item)) 977 | { 978 | var asset = buyableItemAssetInfos.First(x => x.itemAsset == actualItem.item); 979 | 980 | // correct noun 981 | if (nouns.Any(noun => noun.noun == asset.keyword)) 982 | { 983 | var noun = nouns.First(noun => noun.noun == asset.keyword); 984 | var node = noun.result; 985 | node.itemCost = price; 986 | // get buynode 2 987 | if (node.terminalOptions.Length > 0) 988 | { 989 | // loop through terminal options 990 | foreach (var option in node.terminalOptions) { 991 | if(option.result != null && option.result.buyItemIndex != -1) 992 | { 993 | option.result.itemCost = price; 994 | } 995 | } 996 | } 997 | } 998 | } 999 | } 1000 | } 1001 | 1002 | } 1003 | -------------------------------------------------------------------------------- /LethalLib/Modules/Levels.cs: -------------------------------------------------------------------------------- 1 | #region 2 | 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Globalization; 6 | using System.Linq; 7 | 8 | #endregion 9 | 10 | namespace LethalLib.Modules; 11 | 12 | public class Levels 13 | { 14 | 15 | [Flags] 16 | public enum LevelTypes 17 | { 18 | None = 1 << 0, 19 | ExperimentationLevel = 1 << 2, 20 | AssuranceLevel = 1 << 3, 21 | VowLevel = 1 << 4, 22 | OffenseLevel = 1 << 5, 23 | MarchLevel = 1 << 6, 24 | RendLevel = 1 << 7, 25 | DineLevel = 1 << 8, 26 | TitanLevel = 1 << 9, 27 | AdamanceLevel = 1 << 11, 28 | ArtificeLevel = 1 << 12, 29 | EmbrionLevel = 1 << 13, 30 | Vanilla = ExperimentationLevel | AssuranceLevel | VowLevel | OffenseLevel | MarchLevel | RendLevel | DineLevel | TitanLevel | AdamanceLevel | ArtificeLevel | EmbrionLevel, 31 | 32 | /// 33 | /// Only modded levels 34 | /// 35 | Modded = 1 << 10, 36 | 37 | /// 38 | /// This includes modded levels! 39 | /// Acts as a global override 40 | /// 41 | All = ~0 42 | } 43 | 44 | internal static class Compatibility 45 | { 46 | /* 47 | // The following code is from LLL, but is copied here because we need to use it 48 | // even when LLL is not installed, because LLL alters LE(C) moon names to be 49 | // usable in e.g. BepInEx configuration files by removing illegal characters. 50 | // 51 | // https://github.com/IAmBatby/LethalLevelLoader 52 | */ 53 | 54 | // From LLL, class: ConfigHelper 55 | private const string illegalCharacters = ".,?!@#$%^&*()_+-=';:'\""; 56 | 57 | // From LLL, class: ExtendedLevel (modified to take a string as input) 58 | private static string GetNumberlessPlanetName(string planetName) 59 | { 60 | if (planetName != null) 61 | return new string(planetName.SkipWhile(c => !char.IsLetter(c)).ToArray()); 62 | else 63 | return string.Empty; 64 | } 65 | 66 | // From LLL, class: Extensions (modified: removed 'this' from string input) 67 | private static string StripSpecialCharacters(string input) 68 | { 69 | string returnString = string.Empty; 70 | 71 | foreach (char charmander in input) 72 | if ((!illegalCharacters.ToCharArray().Contains(charmander) && char.IsLetterOrDigit(charmander)) || charmander.ToString() == " ") 73 | returnString += charmander; 74 | 75 | return returnString; 76 | } 77 | 78 | // Helper Method for LethalLib 79 | internal static string GetLLLNameOfLevel(string levelName) 80 | { 81 | // -> 10 Example 82 | string newName = StripSpecialCharacters(GetNumberlessPlanetName(levelName)); 83 | // -> Example 84 | if (!newName.EndsWith("Level", true, CultureInfo.InvariantCulture)) 85 | newName += "Level"; 86 | // -> ExampleLevel 87 | newName = newName.ToLowerInvariant(); 88 | // -> examplelevel 89 | return newName; 90 | } 91 | 92 | // Helper Method for LethalLib 93 | internal static Dictionary LLLifyLevelRarityDictionary(Dictionary keyValuePairs) 94 | { 95 | // LethalLevelLoader changes LethalExpansion level names. By applying the LLL changes always, 96 | // we can make sure all enemies get added to their target levels whether or not LLL is installed. 97 | Dictionary LLLifiedCustomLevelRarities = new(); 98 | var clrKeys = keyValuePairs.Keys.ToList(); 99 | var clrValues = keyValuePairs.Values.ToList(); 100 | for (int i = 0; i < keyValuePairs.Count; i++) 101 | { 102 | LLLifiedCustomLevelRarities.Add(GetLLLNameOfLevel(clrKeys[i]), clrValues[i]); 103 | } 104 | return LLLifiedCustomLevelRarities; 105 | } 106 | 107 | 108 | internal static bool ContentIncludedToLevelViaTag(string[] potentialTags, SelectableLevel level, out string chosenTag) 109 | { 110 | chosenTag = string.Empty; 111 | List levelsCurrentTags = LethalLib.Compats.LethalLevelLoaderCompat.TryGetLLLTagsFromLevels(level); 112 | foreach (string levelTag in levelsCurrentTags) 113 | { 114 | foreach (string potentialTag in potentialTags) 115 | { 116 | string cleanedTag = potentialTag.Remove(potentialTag.Length - 5); 117 | if (levelTag == cleanedTag) 118 | { 119 | chosenTag = levelTag; 120 | if (Plugin.extendedLogging.Value) 121 | Plugin.logger.LogInfo($"Level {level.name} has valid tag {cleanedTag}"); 122 | return true; 123 | } 124 | } 125 | } 126 | return false; 127 | } 128 | } 129 | } -------------------------------------------------------------------------------- /LethalLib/Modules/MapObjects.cs: -------------------------------------------------------------------------------- 1 | #region 2 | 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using LethalLib.Extras; 7 | using UnityEngine; 8 | 9 | #endregion 10 | 11 | namespace LethalLib.Modules; 12 | 13 | public class MapObjects 14 | { 15 | public static void Init() 16 | { 17 | On.StartOfRound.Awake += StartOfRound_Awake; 18 | On.RoundManager.SpawnMapObjects += RoundManager_SpawnMapObjects; 19 | } 20 | 21 | private static void RoundManager_SpawnMapObjects(On.RoundManager.orig_SpawnMapObjects orig, RoundManager self) 22 | { 23 | RandomMapObject[] array = UnityEngine.Object.FindObjectsOfType(); 24 | 25 | foreach (RandomMapObject randomMapObject in array) 26 | { 27 | foreach (RegisteredMapObject mapObject in mapObjects) 28 | { 29 | if (mapObject.mapObject != null) 30 | { 31 | if (!randomMapObject.spawnablePrefabs.Any((prefab) => prefab == mapObject.mapObject.prefabToSpawn)) 32 | { 33 | randomMapObject.spawnablePrefabs.Add(mapObject.mapObject.prefabToSpawn); 34 | } 35 | } 36 | } 37 | } 38 | 39 | orig(self); 40 | /* 41 | if (self.currentLevel.spawnableMapObjects.Length == 0) 42 | { 43 | return; 44 | } 45 | self.mapPropsContainer = GameObject.FindGameObjectWithTag("MapPropsContainer"); 46 | 47 | List list = new List(); 48 | for (int i = 0; i < self.currentLevel.spawnableMapObjects.Length; i++) 49 | { 50 | var ran = (float)self.AnomalyRandom.NextDouble(); 51 | Plugin.logger.LogInfo($"Random value: {ran}"); 52 | var val = self.currentLevel.spawnableMapObjects[i].numberToSpawn.Evaluate(ran); 53 | Plugin.logger.LogInfo($"Evaluated value: {val}"); 54 | int num = (int)val; 55 | // print 56 | Plugin.logger.LogInfo($"Spawning {self.currentLevel.spawnableMapObjects[i].prefabToSpawn.name} {num} times"); 57 | if (num <= 0) 58 | { 59 | continue; 60 | } 61 | for (int j = 0; j < array.Length; j++) 62 | { 63 | if (array[j].spawnablePrefabs.Contains(self.currentLevel.spawnableMapObjects[i].prefabToSpawn)) 64 | { 65 | list.Add(array[j]); 66 | } 67 | } 68 | for (int k = 0; k < num; k++) 69 | { 70 | RandomMapObject randomMapObject = list[self.AnomalyRandom.Next(0, list.Count)]; 71 | Vector3 position = randomMapObject.transform.position; 72 | position = self.GetRandomNavMeshPositionInRadius(position, randomMapObject.spawnRange); 73 | GameObject gameObject = UnityEngine.Object.Instantiate(self.currentLevel.spawnableMapObjects[i].prefabToSpawn, position, Quaternion.identity, self.mapPropsContainer.transform); 74 | if (self.currentLevel.spawnableMapObjects[i].spawnFacingAwayFromWall) 75 | { 76 | gameObject.transform.eulerAngles = new Vector3(0f, self.YRotationThatFacesTheFarthestFromPosition(position + Vector3.up * 0.2f), 0f); 77 | } 78 | else 79 | { 80 | gameObject.transform.eulerAngles = new Vector3(gameObject.transform.eulerAngles.x, self.AnomalyRandom.Next(0, 360), gameObject.transform.eulerAngles.z); 81 | } 82 | gameObject.GetComponent().Spawn(destroyWithScene: true); 83 | } 84 | } 85 | for (int l = 0; l < array.Length; l++) 86 | { 87 | UnityEngine.Object.Destroy(array[l].gameObject); 88 | }*/ 89 | } 90 | 91 | private static void StartOfRound_Awake(On.StartOfRound.orig_Awake orig, StartOfRound self) 92 | { 93 | orig(self); 94 | foreach (RegisteredMapObject mapObject in mapObjects) 95 | { 96 | foreach (SelectableLevel level in self.levels) 97 | { 98 | AddMapObjectToLevel(mapObject, level); 99 | } 100 | } 101 | } 102 | 103 | private static void AddMapObjectToLevel(RegisteredMapObject mapObject, SelectableLevel level) 104 | { 105 | string name = level.name; 106 | string customName = Levels.Compatibility.GetLLLNameOfLevel(name); 107 | Levels.LevelTypes currentLevelType = Levels.LevelTypes.None; 108 | bool isCurrentLevelFromVanilla = false; 109 | 110 | if (Enum.TryParse(name, true, out currentLevelType)) // It'd be weird if a level was called "Modded" or "All" so I think im good to not check that lol 111 | { 112 | isCurrentLevelFromVanilla = true; 113 | } 114 | else 115 | { 116 | name = customName; 117 | } 118 | 119 | string tagName = string.Empty; 120 | bool mapObjectValidToAdd = mapObject.levels.HasFlag(Levels.LevelTypes.All) 121 | || (mapObject.spawnLevelOverrides != null && Levels.Compatibility.ContentIncludedToLevelViaTag(mapObject.spawnLevelOverrides, level, out tagName)) 122 | || (isCurrentLevelFromVanilla && mapObject.levels.HasFlag(Levels.LevelTypes.Vanilla)) 123 | || (!isCurrentLevelFromVanilla && mapObject.levels.HasFlag(Levels.LevelTypes.Modded)) 124 | || (isCurrentLevelFromVanilla && mapObject.levels.HasFlag(currentLevelType)) 125 | || (!isCurrentLevelFromVanilla && mapObject.spawnLevelOverrides != null && mapObject.spawnLevelOverrides.Contains(customName)); 126 | 127 | string mapObjectName = "invalid!"; 128 | if (mapObject.mapObject != null) 129 | { 130 | mapObjectName = mapObject.mapObject.prefabToSpawn.name; 131 | } 132 | else if (mapObject.outsideObject != null) 133 | { 134 | mapObjectName = mapObject.outsideObject.spawnableObject.prefabToSpawn.name; 135 | } 136 | if (Plugin.extendedLogging.Value) 137 | Plugin.logger.LogInfo($"{name} for mapObject: {mapObjectName}, isCurrentLevelFromVanilla: {isCurrentLevelFromVanilla}, Found valid: {mapObjectValidToAdd}"); 138 | 139 | if (!mapObjectValidToAdd) return; 140 | if (mapObject.mapObject != null) 141 | { 142 | // Remove existing object if it exists 143 | if (level.spawnableMapObjects.Any(x => x.prefabToSpawn == mapObject.mapObject.prefabToSpawn)) 144 | { 145 | var list = level.spawnableMapObjects.ToList(); 146 | list.RemoveAll(x => x.prefabToSpawn == mapObject.mapObject.prefabToSpawn); 147 | level.spawnableMapObjects = list.ToArray(); 148 | } 149 | 150 | // Create a new instance so it can have its own `numberToSpawn` value 151 | SpawnableMapObject spawnableMapObject = new() 152 | { 153 | prefabToSpawn = mapObject.mapObject.prefabToSpawn, 154 | spawnFacingAwayFromWall = mapObject.mapObject.spawnFacingAwayFromWall, 155 | spawnFacingWall = mapObject.mapObject.spawnFacingWall, 156 | spawnWithBackToWall = mapObject.mapObject.spawnWithBackToWall, 157 | spawnWithBackFlushAgainstWall = mapObject.mapObject.spawnWithBackFlushAgainstWall, 158 | requireDistanceBetweenSpawns = mapObject.mapObject.requireDistanceBetweenSpawns, 159 | disallowSpawningNearEntrances = mapObject.mapObject.disallowSpawningNearEntrances, 160 | }; 161 | 162 | if (mapObject.spawnRateFunction != null) 163 | { 164 | spawnableMapObject.numberToSpawn = mapObject.spawnRateFunction(level); 165 | } 166 | 167 | var mapObjectsList = level.spawnableMapObjects.ToList(); 168 | mapObjectsList.Add(spawnableMapObject); 169 | level.spawnableMapObjects = mapObjectsList.ToArray(); 170 | 171 | if (Plugin.extendedLogging.Value) 172 | Plugin.logger.LogInfo($"Added {spawnableMapObject.prefabToSpawn.name} to {name}"); 173 | } 174 | else if (mapObject.outsideObject != null) 175 | { 176 | if (level.spawnableOutsideObjects.Any(x => x.spawnableObject.prefabToSpawn == mapObject.outsideObject.spawnableObject.prefabToSpawn)) 177 | { 178 | var list = level.spawnableOutsideObjects.ToList(); 179 | list.RemoveAll(x => x.spawnableObject.prefabToSpawn == mapObject.outsideObject.spawnableObject.prefabToSpawn); 180 | level.spawnableOutsideObjects = list.ToArray(); 181 | } 182 | 183 | SpawnableOutsideObjectWithRarity spawnableOutsideObject = new() 184 | { 185 | spawnableObject = mapObject.outsideObject.spawnableObject 186 | }; 187 | 188 | if (mapObject.spawnRateFunction != null) 189 | { 190 | spawnableOutsideObject.randomAmount = mapObject.spawnRateFunction(level); 191 | } 192 | 193 | var mapObjectsList = level.spawnableOutsideObjects.ToList(); 194 | mapObjectsList.Add(spawnableOutsideObject); 195 | level.spawnableOutsideObjects = mapObjectsList.ToArray(); 196 | 197 | if (Plugin.extendedLogging.Value) 198 | Plugin.logger.LogInfo($"Added {spawnableOutsideObject.spawnableObject.prefabToSpawn.name} to {name}"); 199 | } 200 | } 201 | 202 | public class RegisteredMapObject 203 | { 204 | public SpawnableMapObject mapObject; 205 | public SpawnableOutsideObjectWithRarity outsideObject; 206 | public Levels.LevelTypes levels; 207 | public string[] spawnLevelOverrides; 208 | // this way of handling things is very inconsistent with the enemies and items modules. 209 | public Func spawnRateFunction; 210 | } 211 | 212 | public static List mapObjects = new List(); 213 | 214 | /// 215 | /// Register an inside map object to spawn in a level, these are things like turrets and mines 216 | /// 217 | public static void RegisterMapObject(SpawnableMapObjectDef mapObject, Levels.LevelTypes levels, Func spawnRateFunction = null) 218 | { 219 | RegisterMapObject(mapObject.spawnableMapObject, levels, spawnRateFunction); 220 | } 221 | 222 | /// 223 | /// Register an inside map object to spawn in a level, these are things like turrets and mines 224 | /// 225 | public static void RegisterMapObject(SpawnableMapObjectDef mapObject, Levels.LevelTypes levels = Levels.LevelTypes.None, string[] levelOverrides = null, Func spawnRateFunction = null) 226 | { 227 | RegisterMapObject(mapObject.spawnableMapObject, levels, levelOverrides, spawnRateFunction); 228 | } 229 | 230 | /// 231 | /// Register an inside map object to spawn in a level, these are things like turrets and mines 232 | /// 233 | public static void RegisterMapObject(SpawnableMapObject mapObject, Levels.LevelTypes levels, Func spawnRateFunction = null) 234 | { 235 | mapObjects.Add(new RegisteredMapObject 236 | { 237 | mapObject = mapObject, 238 | levels = levels, 239 | spawnRateFunction = spawnRateFunction 240 | }); 241 | } 242 | 243 | /// 244 | /// Register an inside map object to spawn in a level, these are things like turrets and mines 245 | /// 246 | public static void RegisterMapObject(SpawnableMapObject mapObject, Levels.LevelTypes levels = Levels.LevelTypes.None, string[] levelOverrides = null, Func spawnRateFunction = null) 247 | { 248 | mapObjects.Add(new RegisteredMapObject 249 | { 250 | mapObject = mapObject, 251 | levels = levels, 252 | spawnRateFunction = spawnRateFunction, 253 | spawnLevelOverrides = levelOverrides 254 | }); 255 | } 256 | 257 | /// 258 | /// Register an outside map object to spawn in a level, these are things like pumpkins and rocks. 259 | /// 260 | public static void RegisterOutsideObject(SpawnableOutsideObjectDef mapObject, Levels.LevelTypes levels, Func spawnRateFunction = null) 261 | { 262 | RegisterOutsideObject(mapObject.spawnableMapObject, levels, spawnRateFunction); 263 | } 264 | 265 | /// 266 | /// Register an outside map object to spawn in a level, these are things like pumpkins and rocks. 267 | /// 268 | public static void RegisterOutsideObject(SpawnableOutsideObjectDef mapObject, Levels.LevelTypes levels = Levels.LevelTypes.None, string[] levelOverrides = null, Func spawnRateFunction = null) 269 | { 270 | RegisterOutsideObject(mapObject.spawnableMapObject, levels, levelOverrides, spawnRateFunction); 271 | } 272 | 273 | /// 274 | /// Register an outside map object to spawn in a level, these are things like pumpkins and rocks. 275 | /// 276 | public static void RegisterOutsideObject(SpawnableOutsideObjectWithRarity mapObject, Levels.LevelTypes levels, Func spawnRateFunction = null) 277 | { 278 | mapObjects.Add(new RegisteredMapObject 279 | { 280 | outsideObject = mapObject, 281 | levels = levels, 282 | spawnRateFunction = spawnRateFunction 283 | }); 284 | } 285 | 286 | /// 287 | /// Register an outside map object to spawn in a level, these are things like pumpkins and rocks. 288 | /// 289 | public static void RegisterOutsideObject(SpawnableOutsideObjectWithRarity mapObject, Levels.LevelTypes levels = Levels.LevelTypes.None, string[] levelOverrides = null, Func spawnRateFunction = null) 290 | { 291 | mapObjects.Add(new RegisteredMapObject 292 | { 293 | outsideObject = mapObject, 294 | levels = levels, 295 | spawnRateFunction = spawnRateFunction, 296 | spawnLevelOverrides = levelOverrides 297 | }); 298 | } 299 | 300 | /// 301 | /// Remove a inside map object from a level 302 | /// 303 | public static void RemoveMapObject(SpawnableMapObjectDef mapObject, Levels.LevelTypes levelFlags, string[] levelOverrides = null) 304 | { 305 | RemoveMapObject(mapObject.spawnableMapObject, levelFlags, levelOverrides); 306 | } 307 | 308 | /// 309 | /// Remove a inside map object from a level 310 | /// 311 | public static void RemoveMapObject(SpawnableMapObject mapObject, Levels.LevelTypes levelFlags, string[] levelOverrides = null) 312 | { 313 | if (StartOfRound.Instance != null) 314 | { 315 | foreach (SelectableLevel level in StartOfRound.Instance.levels) 316 | { 317 | var name = level.name; 318 | 319 | var alwaysValid = levelFlags.HasFlag(Levels.LevelTypes.All) || (levelOverrides != null && levelOverrides.Any(item => item.ToLowerInvariant() == name.ToLowerInvariant())); 320 | var isModded = levelFlags.HasFlag(Levels.LevelTypes.Modded) && !Enum.IsDefined(typeof(Levels.LevelTypes), name); 321 | 322 | if (isModded) 323 | { 324 | alwaysValid = true; 325 | } 326 | if (Enum.IsDefined(typeof(Levels.LevelTypes), name) || alwaysValid) 327 | { 328 | var levelEnum = alwaysValid ? Levels.LevelTypes.All : (Levels.LevelTypes)Enum.Parse(typeof(Levels.LevelTypes), name); 329 | if (alwaysValid || levelFlags.HasFlag(levelEnum)) 330 | { 331 | 332 | level.spawnableMapObjects = level.spawnableMapObjects.Where(x => x.prefabToSpawn != mapObject.prefabToSpawn).ToArray(); 333 | } 334 | } 335 | } 336 | } 337 | } 338 | 339 | /// 340 | /// Remove a outside map object from a level 341 | /// 342 | public static void RemoveOutsideObject(SpawnableOutsideObjectDef mapObject, Levels.LevelTypes levelFlags, string[] levelOverrides = null) 343 | { 344 | RemoveOutsideObject(mapObject.spawnableMapObject, levelFlags, levelOverrides); 345 | } 346 | 347 | /// 348 | /// Remove a outside map object from a level 349 | /// 350 | public static void RemoveOutsideObject(SpawnableOutsideObjectWithRarity mapObject, Levels.LevelTypes levelFlags, string[] levelOverrides = null) 351 | { 352 | if (StartOfRound.Instance != null) 353 | { 354 | foreach (SelectableLevel level in StartOfRound.Instance.levels) 355 | { 356 | var name = level.name; 357 | 358 | var alwaysValid = levelFlags.HasFlag(Levels.LevelTypes.All) || (levelOverrides != null && levelOverrides.Any(item => item.ToLowerInvariant() == name.ToLowerInvariant())); 359 | var isModded = levelFlags.HasFlag(Levels.LevelTypes.Modded) && !Enum.IsDefined(typeof(Levels.LevelTypes), name); 360 | 361 | if (isModded) 362 | { 363 | alwaysValid = true; 364 | } 365 | if (Enum.IsDefined(typeof(Levels.LevelTypes), name) || alwaysValid) 366 | { 367 | var levelEnum = alwaysValid ? Levels.LevelTypes.All : (Levels.LevelTypes)Enum.Parse(typeof(Levels.LevelTypes), name); 368 | if (alwaysValid || levelFlags.HasFlag(levelEnum)) 369 | { 370 | 371 | level.spawnableOutsideObjects = level.spawnableOutsideObjects.Where(x => x.spawnableObject.prefabToSpawn != mapObject.spawnableObject.prefabToSpawn).ToArray(); 372 | } 373 | } 374 | } 375 | } 376 | } 377 | 378 | } 379 | -------------------------------------------------------------------------------- /LethalLib/Modules/NetworkPrefabs.cs: -------------------------------------------------------------------------------- 1 | #region 2 | 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Reflection; 6 | using System.Security.Cryptography; 7 | using System.Text; 8 | using Unity.Netcode; 9 | using UnityEngine; 10 | 11 | #endregion 12 | 13 | namespace LethalLib.Modules; 14 | 15 | public class NetworkPrefabs 16 | { 17 | 18 | 19 | private static List _networkPrefabs = new List(); 20 | internal static void Init() 21 | { 22 | On.GameNetworkManager.Start += GameNetworkManager_Start; 23 | } 24 | 25 | /// 26 | /// Registers a prefab to be added to the network manager. 27 | /// 28 | public static void RegisterNetworkPrefab(GameObject prefab) 29 | { 30 | if (prefab is null) 31 | throw new ArgumentNullException(nameof(prefab), $"The given argument for {nameof(RegisterNetworkPrefab)} is null!"); 32 | if (!_networkPrefabs.Contains(prefab)) 33 | _networkPrefabs.Add(prefab); 34 | } 35 | 36 | /// 37 | /// Creates a network prefab programmatically and registers it with the network manager. 38 | /// Credit to Day and Xilo. 39 | /// 40 | public static GameObject CreateNetworkPrefab(string name) 41 | { 42 | var prefab = PrefabUtils.CreatePrefab(name); 43 | prefab.AddComponent(); 44 | 45 | var hash = MD5.Create().ComputeHash(Encoding.UTF8.GetBytes(Assembly.GetCallingAssembly().GetName().Name + name)); 46 | 47 | prefab.GetComponent().GlobalObjectIdHash = BitConverter.ToUInt32(hash, 0); 48 | 49 | RegisterNetworkPrefab(prefab); 50 | return prefab; 51 | } 52 | 53 | /// 54 | /// Clones a network prefab programmatically and registers it with the network manager. 55 | /// Credit to Day and Xilo. 56 | /// 57 | public static GameObject CloneNetworkPrefab(GameObject prefabToClone, string newName = null) 58 | { 59 | var prefab = PrefabUtils.ClonePrefab(prefabToClone, newName); 60 | 61 | var hash = MD5.Create().ComputeHash(Encoding.UTF8.GetBytes(Assembly.GetCallingAssembly().GetName().Name + prefab.name)); 62 | 63 | prefab.GetComponent().GlobalObjectIdHash = BitConverter.ToUInt32(hash, 0); 64 | 65 | RegisterNetworkPrefab(prefab); 66 | return prefab; 67 | } 68 | 69 | 70 | private static void GameNetworkManager_Start(On.GameNetworkManager.orig_Start orig, GameNetworkManager self) 71 | { 72 | orig(self); 73 | 74 | foreach (GameObject obj in _networkPrefabs) 75 | { 76 | if (!NetworkManager.Singleton.NetworkConfig.Prefabs.Contains(obj)) 77 | NetworkManager.Singleton.AddNetworkPrefab(obj); 78 | } 79 | 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /LethalLib/Modules/Player.cs: -------------------------------------------------------------------------------- 1 | #region 2 | 3 | using System.Collections.Generic; 4 | using UnityEngine; 5 | 6 | #endregion 7 | 8 | namespace LethalLib.Modules; 9 | 10 | public class Player 11 | { 12 | public static Dictionary ragdollRefs = new Dictionary(); 13 | public static Dictionary ragdollIndexes = new Dictionary(); 14 | 15 | public static void Init() 16 | { 17 | On.StartOfRound.Awake += StartOfRound_Awake; 18 | } 19 | 20 | private static void StartOfRound_Awake(On.StartOfRound.orig_Awake orig, StartOfRound self) 21 | { 22 | orig(self); 23 | 24 | 25 | // loop through ragdollrefs 26 | foreach (KeyValuePair ragdollRef in ragdollRefs) 27 | { 28 | if (!self.playerRagdolls.Contains(ragdollRef.Value)) 29 | { 30 | self.playerRagdolls.Add(ragdollRef.Value); 31 | // get index 32 | int index = self.playerRagdolls.Count - 1; 33 | // add to ragdollIndexes 34 | if(ragdollIndexes.ContainsKey(ragdollRef.Key)) 35 | ragdollIndexes[ragdollRef.Key] = index; 36 | else 37 | ragdollIndexes.Add(ragdollRef.Key, index); 38 | } 39 | } 40 | } 41 | 42 | public static int GetRagdollIndex(string id) 43 | { 44 | return ragdollIndexes[id]; 45 | } 46 | 47 | public static GameObject GetRagdoll(string id) 48 | { 49 | return ragdollRefs[id]; 50 | } 51 | 52 | // custom player ragdolls for special deaths. 53 | public static void RegisterPlayerRagdoll(string id, GameObject ragdoll) 54 | { 55 | Plugin.logger.LogInfo($"Registering player ragdoll {id}"); 56 | ragdollRefs.Add(id, ragdoll); 57 | } 58 | } -------------------------------------------------------------------------------- /LethalLib/Modules/PrefabUtils.cs: -------------------------------------------------------------------------------- 1 | #region 2 | 3 | using System; 4 | using UnityEngine; 5 | using Object = UnityEngine.Object; 6 | 7 | #endregion 8 | 9 | namespace LethalLib.Modules; 10 | 11 | public class PrefabUtils 12 | { 13 | internal static Lazy _prefabParent; 14 | internal static GameObject prefabParent { get { return _prefabParent.Value; } } 15 | 16 | static PrefabUtils() 17 | { 18 | _prefabParent = new Lazy(() => 19 | { 20 | var parent = new GameObject("LethalLibGeneratedPrefabs"); 21 | parent.hideFlags = HideFlags.HideAndDontSave; 22 | parent.SetActive(false); 23 | 24 | return parent; 25 | }); 26 | } 27 | 28 | /// 29 | /// Clones a prefab and returns the clone. 30 | /// 31 | public static GameObject ClonePrefab(GameObject prefabToClone, string newName = null) 32 | { 33 | var prefab = Object.Instantiate(prefabToClone, prefabParent.transform); 34 | prefab.hideFlags = HideFlags.HideAndDontSave; 35 | 36 | if (newName != null) 37 | { 38 | prefab.name = newName; 39 | } 40 | else 41 | { 42 | prefab.name = prefabToClone.name; 43 | } 44 | 45 | return prefab; 46 | } 47 | 48 | /// 49 | /// Creates a prefab and returns it. 50 | /// 51 | public static GameObject CreatePrefab(string name) 52 | { 53 | var prefab = new GameObject(name); 54 | prefab.hideFlags = HideFlags.HideAndDontSave; 55 | 56 | prefab.transform.SetParent(prefabParent.transform); 57 | 58 | return prefab; 59 | } 60 | } -------------------------------------------------------------------------------- /LethalLib/Modules/Shaders.cs: -------------------------------------------------------------------------------- 1 | #region 2 | 3 | using UnityEngine; 4 | 5 | #endregion 6 | 7 | namespace LethalLib.Modules; 8 | 9 | public class Shaders 10 | { 11 | 12 | public static void FixShaders(GameObject gameObject) 13 | { 14 | foreach (var renderer in gameObject.GetComponentsInChildren()) 15 | { 16 | foreach (var material in renderer.materials) 17 | { 18 | if (material.shader.name.Contains("Standard")) 19 | { 20 | // ge 21 | material.shader = Shader.Find("HDRP/Lit"); 22 | } 23 | } 24 | } 25 | } 26 | } -------------------------------------------------------------------------------- /LethalLib/Modules/TerminalUtils.cs: -------------------------------------------------------------------------------- 1 | #region 2 | 3 | using UnityEngine; 4 | 5 | #endregion 6 | 7 | namespace LethalLib.Modules; 8 | 9 | public class TerminalUtils 10 | { 11 | /// 12 | /// This is only for creating terminal keywords, does not handle adding it to the actual terminal. 13 | /// 14 | public static TerminalKeyword CreateTerminalKeyword(string word, bool isVerb = false, CompatibleNoun[] compatibleNouns = null, TerminalNode specialKeywordResult = null, TerminalKeyword defaultVerb = null, bool accessTerminalObjects = false) 15 | { 16 | 17 | TerminalKeyword keyword = ScriptableObject.CreateInstance(); 18 | keyword.name = word; 19 | keyword.word = word; 20 | keyword.isVerb = isVerb; 21 | keyword.compatibleNouns = compatibleNouns; 22 | keyword.specialKeywordResult = specialKeywordResult; 23 | keyword.defaultVerb = defaultVerb; 24 | keyword.accessTerminalObjects = accessTerminalObjects; 25 | return keyword; 26 | } 27 | } -------------------------------------------------------------------------------- /LethalLib/Modules/Unlockables.cs: -------------------------------------------------------------------------------- 1 | #region 2 | 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Reflection; 6 | using LethalLib.Extras; 7 | using UnityEngine; 8 | using static LethalLib.Modules.Items; 9 | 10 | #endregion 11 | 12 | namespace LethalLib.Modules; 13 | 14 | public enum StoreType 15 | { 16 | None, 17 | ShipUpgrade, 18 | Decor 19 | } 20 | public class Unlockables 21 | { 22 | public class RegisteredUnlockable 23 | { 24 | public UnlockableItem unlockable; 25 | public StoreType StoreType; 26 | public TerminalNode buyNode1; 27 | public TerminalNode buyNode2; 28 | public TerminalNode itemInfo; 29 | public int price; 30 | public string modName; 31 | public bool disabled = false; 32 | public bool wasAlwaysInStock = false; 33 | 34 | public RegisteredUnlockable(UnlockableItem unlockable, TerminalNode buyNode1 = null, TerminalNode buyNode2 = null, TerminalNode itemInfo = null, int price = -1) 35 | { 36 | this.unlockable = unlockable; 37 | this.buyNode1 = buyNode1; 38 | this.buyNode2 = buyNode2; 39 | this.itemInfo = itemInfo; 40 | this.price = price; 41 | } 42 | } 43 | 44 | public static List registeredUnlockables = new List(); 45 | 46 | public static void Init() 47 | { 48 | On.Terminal.Awake += Terminal_Awake; 49 | On.Terminal.TextPostProcess += Terminal_TextPostProcess; 50 | On.Terminal.RotateShipDecorSelection += Terminal_RotateShipDecorSelection; 51 | } 52 | 53 | private static void Terminal_RotateShipDecorSelection(On.Terminal.orig_RotateShipDecorSelection orig, Terminal self) 54 | { 55 | // horrific hax to make sure disabled decor is not in the shop.. 56 | 57 | foreach (var unlockable in registeredUnlockables) 58 | { 59 | if (unlockable.StoreType == StoreType.Decor && unlockable.disabled) 60 | { 61 | unlockable.wasAlwaysInStock = unlockable.unlockable.alwaysInStock; 62 | unlockable.unlockable.alwaysInStock = true; 63 | } 64 | } 65 | 66 | orig(self); 67 | 68 | foreach (var unlockable in registeredUnlockables) 69 | { 70 | if (unlockable.StoreType == StoreType.Decor && unlockable.disabled) 71 | { 72 | unlockable.unlockable.alwaysInStock = unlockable.wasAlwaysInStock; 73 | } 74 | } 75 | } 76 | 77 | private static string Terminal_TextPostProcess(On.Terminal.orig_TextPostProcess orig, Terminal self, string modifiedDisplayText, TerminalNode node) 78 | { 79 | if (modifiedDisplayText.Contains("[buyableItemsList]") && modifiedDisplayText.Contains("[unlockablesSelectionList]")) 80 | { 81 | // create new line after first colon 82 | var index = modifiedDisplayText.IndexOf(@":"); 83 | 84 | // example: "* Loud horn // Price: $150" 85 | foreach (var unlockable in registeredUnlockables) 86 | { 87 | if (unlockable.StoreType == StoreType.ShipUpgrade && !unlockable.disabled) 88 | { 89 | 90 | var unlockableName = unlockable.unlockable.unlockableName; 91 | var unlockablePrice = unlockable.price; 92 | 93 | var newLine = $"\n* {unlockableName} // Price: ${unlockablePrice}"; 94 | 95 | modifiedDisplayText = modifiedDisplayText.Insert(index + 1, newLine); 96 | } 97 | 98 | } 99 | 100 | } 101 | 102 | 103 | return orig(self, modifiedDisplayText, node); 104 | } 105 | 106 | public struct BuyableUnlockableAssetInfo 107 | { 108 | public UnlockableItem itemAsset; 109 | public TerminalKeyword keyword; 110 | } 111 | 112 | public static List buyableUnlockableAssetInfos = new List(); 113 | 114 | private static void Terminal_Awake(On.Terminal.orig_Awake orig, Terminal self) 115 | { 116 | var startOfRound = StartOfRound.Instance; 117 | 118 | Plugin.logger.LogInfo($"Adding {registeredUnlockables.Count} unlockables to unlockables list"); 119 | foreach (var unlockable in registeredUnlockables) 120 | { 121 | if (startOfRound.unlockablesList.unlockables.Any((UnlockableItem x) => x.unlockableName == unlockable.unlockable.unlockableName)) 122 | { 123 | Plugin.logger.LogInfo((object)("Unlockable " + unlockable.unlockable.unlockableName + " already exists in unlockables list, skipping")); 124 | continue; 125 | } 126 | 127 | 128 | if (unlockable.unlockable.prefabObject != null) 129 | { 130 | var placeable = unlockable.unlockable.prefabObject.GetComponentInChildren(); 131 | if (placeable != null) 132 | { 133 | placeable.unlockableID = startOfRound.unlockablesList.unlockables.Count; 134 | } 135 | } 136 | startOfRound.unlockablesList.unlockables.Add(unlockable.unlockable); 137 | } 138 | 139 | var buyKeyword = self.terminalNodes.allKeywords.First(keyword => keyword.word == "buy"); 140 | var cancelPurchaseNode = buyKeyword.compatibleNouns[0].result.terminalOptions[1].result; 141 | var infoKeyword = self.terminalNodes.allKeywords.First(keyword => keyword.word == "info"); 142 | 143 | var shopItems = registeredUnlockables.FindAll(unlockable => unlockable.price != -1).ToList(); 144 | 145 | foreach (var item in shopItems) 146 | { 147 | string itemName = item.unlockable.unlockableName; 148 | 149 | var keyword = TerminalUtils.CreateTerminalKeyword(itemName.ToLowerInvariant().Replace(" ", "-"), defaultVerb: buyKeyword); 150 | 151 | 152 | if (self.terminalNodes.allKeywords.Any((TerminalKeyword kw) => kw.word == keyword.word)) 153 | { 154 | Plugin.logger.LogInfo((object)("Keyword " + keyword.word + " already registed, skipping.")); 155 | continue; 156 | } 157 | 158 | 159 | 160 | var itemIndex = StartOfRound.Instance.unlockablesList.unlockables.FindIndex(unlockable => unlockable.unlockableName == item.unlockable.unlockableName); 161 | 162 | var wah = StartOfRound.Instance; 163 | 164 | if(wah == null) 165 | { 166 | Debug.Log("STARTOFROUND INSTANCE NOT FOUND"); 167 | } 168 | 169 | item.disabled = false; 170 | 171 | if (item.price == -1 && item.buyNode1 != null) 172 | { 173 | item.price = item.buyNode1.itemCost; 174 | } 175 | 176 | var lastChar = itemName[itemName.Length - 1]; 177 | var itemNamePlural = itemName; 178 | 179 | var buyNode2 = item.buyNode2; 180 | 181 | if (buyNode2 == null) 182 | { 183 | buyNode2 = ScriptableObject.CreateInstance(); 184 | 185 | buyNode2.name = $"{itemName.Replace(" ", "-")}BuyNode2"; 186 | buyNode2.displayText = $"Ordered [variableAmount] {itemNamePlural}. Your new balance is [playerCredits].\n\nOur contractors enjoy fast, free shipping while on the job! Any purchased items will arrive hourly at your approximate location.\r\n\r\n"; 187 | buyNode2.clearPreviousText = true; 188 | buyNode2.maxCharactersToType = 15; 189 | 190 | 191 | } 192 | buyNode2.buyItemIndex = -1; 193 | buyNode2.shipUnlockableID = itemIndex; 194 | buyNode2.buyUnlockable = true; 195 | buyNode2.creatureName = itemName; 196 | buyNode2.isConfirmationNode = false; 197 | buyNode2.itemCost = item.price; 198 | buyNode2.playSyncedClip = 0; 199 | 200 | var buyNode1 = item.buyNode1; 201 | if (buyNode1 == null) 202 | { 203 | buyNode1 = ScriptableObject.CreateInstance(); 204 | buyNode1.name = $"{itemName.Replace(" ", "-")}BuyNode1"; 205 | buyNode1.displayText = $"You have requested to order {itemNamePlural}. Amount: [variableAmount].\nTotal cost of items: [totalCost].\n\nPlease CONFIRM or DENY.\r\n\r\n"; 206 | buyNode1.clearPreviousText = true; 207 | buyNode1.maxCharactersToType = 35; 208 | } 209 | 210 | buyNode1.buyItemIndex = -1; 211 | buyNode1.shipUnlockableID = itemIndex; 212 | buyNode1.creatureName = itemName; 213 | buyNode1.isConfirmationNode = true; 214 | buyNode1.overrideOptions = true; 215 | buyNode1.itemCost = item.price; 216 | buyNode1.terminalOptions = new CompatibleNoun[2] 217 | { 218 | new CompatibleNoun() 219 | { 220 | noun = self.terminalNodes.allKeywords.First(keyword2 => keyword2.word == "confirm"), 221 | result = buyNode2 222 | }, 223 | new CompatibleNoun() 224 | { 225 | noun = self.terminalNodes.allKeywords.First(keyword2 => keyword2.word == "deny"), 226 | result = cancelPurchaseNode 227 | } 228 | }; 229 | 230 | if (item.StoreType == StoreType.Decor) 231 | { 232 | item.unlockable.shopSelectionNode = buyNode1; 233 | } 234 | else 235 | { 236 | item.unlockable.shopSelectionNode = null; 237 | } 238 | 239 | //self.terminalNodes.allKeywords.AddItem(keyword); 240 | var allKeywords = self.terminalNodes.allKeywords.ToList(); 241 | allKeywords.Add(keyword); 242 | self.terminalNodes.allKeywords = allKeywords.ToArray(); 243 | 244 | var nouns = buyKeyword.compatibleNouns.ToList(); 245 | nouns.Add(new CompatibleNoun() 246 | { 247 | noun = keyword, 248 | result = buyNode1 249 | }); 250 | buyKeyword.compatibleNouns = nouns.ToArray(); 251 | 252 | 253 | var itemInfo = item.itemInfo; 254 | if (itemInfo == null) 255 | { 256 | itemInfo = ScriptableObject.CreateInstance(); 257 | itemInfo.name = $"{itemName.Replace(" ", "-")}InfoNode"; 258 | itemInfo.displayText = $"[No information about this object was found.]\n\n"; 259 | itemInfo.clearPreviousText = true; 260 | itemInfo.maxCharactersToType = 25; 261 | } 262 | 263 | self.terminalNodes.allKeywords = allKeywords.ToArray(); 264 | 265 | var itemInfoNouns = infoKeyword.compatibleNouns.ToList(); 266 | itemInfoNouns.Add(new CompatibleNoun() 267 | { 268 | noun = keyword, 269 | result = itemInfo 270 | }); 271 | infoKeyword.compatibleNouns = itemInfoNouns.ToArray(); 272 | 273 | var buyableItemAssetInfo = new BuyableUnlockableAssetInfo() 274 | { 275 | itemAsset = item.unlockable, 276 | keyword = keyword 277 | }; 278 | 279 | buyableUnlockableAssetInfos.Add(buyableItemAssetInfo); 280 | if (Plugin.extendedLogging.Value) 281 | Plugin.logger.LogInfo($"{item.modName} registered item: {item.unlockable.unlockableName}"); 282 | } 283 | 284 | orig(self); 285 | } 286 | 287 | 288 | 289 | /// 290 | ///Registers a unlockable. 291 | /// 292 | public static void RegisterUnlockable(UnlockableItemDef unlockable, int price = -1, StoreType storeType = StoreType.None) 293 | { 294 | RegisterUnlockable(unlockable.unlockable, storeType, null, null, null, price); 295 | } 296 | 297 | /// 298 | ///Registers a unlockable. 299 | /// 300 | public static void RegisterUnlockable(UnlockableItem unlockable, int price = -1, StoreType storeType = StoreType.None) 301 | { 302 | RegisterUnlockable(unlockable, storeType, null, null, null, price); 303 | } 304 | 305 | /// 306 | ///Registers a unlockable. 307 | /// 308 | public static void RegisterUnlockable(UnlockableItemDef unlockable, StoreType storeType = StoreType.None, TerminalNode buyNode1 = null, TerminalNode buyNode2 = null, TerminalNode itemInfo = null, int price = -1) 309 | { 310 | RegisterUnlockable(unlockable.unlockable, storeType, buyNode1, buyNode2, itemInfo, price); 311 | } 312 | 313 | /// 314 | ///Registers a unlockable. 315 | /// 316 | public static void RegisterUnlockable(UnlockableItem unlockable, StoreType storeType = StoreType.None, TerminalNode buyNode1 = null, TerminalNode buyNode2 = null, TerminalNode itemInfo = null, int price = -1) 317 | { 318 | var unlock = new RegisteredUnlockable(unlockable, buyNode1, buyNode2, itemInfo, price); 319 | var callingAssembly = Assembly.GetCallingAssembly(); 320 | var modDLL = callingAssembly.GetName().Name; 321 | unlock.modName = modDLL; 322 | unlock.StoreType = storeType; 323 | 324 | registeredUnlockables.Add(unlock); 325 | } 326 | 327 | /// 328 | ///Removes a unlockable. 329 | ///This needs to be called after StartOfRound.Awake, can be used for config sync. 330 | ///This is prone to breaking saves. 331 | /// 332 | public static void DisableUnlockable(UnlockableItemDef unlockable) 333 | { 334 | DisableUnlockable(unlockable.unlockable); 335 | } 336 | 337 | /// 338 | ///Removes a unlockable. 339 | ///This needs to be called after StartOfRound.Awake, can be used for config sync. 340 | ///This is prone to breaking saves. 341 | /// 342 | public static void DisableUnlockable(UnlockableItem unlockable) 343 | { 344 | if (StartOfRound.Instance != null) 345 | { 346 | var allKeywords = terminal.terminalNodes.allKeywords.ToList(); 347 | var infoKeyword = terminal.terminalNodes.allKeywords.First(keyword => keyword.word == "info"); 348 | var buyKeyword = terminal.terminalNodes.allKeywords.First(keyword => keyword.word == "buy"); 349 | var cancelPurchaseNode = buyKeyword.compatibleNouns[0].result.terminalOptions[1].result; 350 | var nouns = buyKeyword.compatibleNouns.ToList(); 351 | var itemInfoNouns = infoKeyword.compatibleNouns.ToList(); 352 | RegisteredUnlockable registeredUnlockable = registeredUnlockables.Find(unlock => unlock.unlockable == unlockable); 353 | 354 | registeredUnlockable.disabled = true; 355 | 356 | if (buyableUnlockableAssetInfos.Any(x => x.itemAsset == unlockable)) 357 | { 358 | 359 | 360 | var asset = buyableUnlockableAssetInfos.First(x => x.itemAsset == unlockable); 361 | allKeywords.Remove(asset.keyword); 362 | 363 | nouns.RemoveAll(noun => noun.noun == asset.keyword); 364 | itemInfoNouns.RemoveAll(noun => noun.noun == asset.keyword); 365 | } 366 | terminal.terminalNodes.allKeywords = allKeywords.ToArray(); 367 | buyKeyword.compatibleNouns = nouns.ToArray(); 368 | infoKeyword.compatibleNouns = itemInfoNouns.ToArray(); 369 | 370 | if (StartOfRound.Instance.IsServer) 371 | { 372 | // i lack a better way to prevent disabled decor from being in the shop because the ship decor is rotated too early. 373 | UnityEngine.Object.FindObjectOfType().RotateShipDecorSelection(); 374 | } 375 | } 376 | } 377 | 378 | /// 379 | ///Updates the price of an already registered unlockable 380 | ///This needs to be called after StartOfRound.Awake. 381 | ///Only works for items registered by LethalLib. 382 | /// 383 | public static void UpdateUnlockablePrice(UnlockableItem shopItem, int price) 384 | { 385 | if (StartOfRound.Instance != null) 386 | { 387 | var buyKeyword = terminal.terminalNodes.allKeywords.First(keyword => keyword.word == "buy"); 388 | var cancelPurchaseNode = buyKeyword.compatibleNouns[0].result.terminalOptions[1].result; 389 | var nouns = buyKeyword.compatibleNouns.ToList(); 390 | RegisteredUnlockable registeredUnlockable = registeredUnlockables.Find(unlock => unlock.unlockable == shopItem); 391 | 392 | if(registeredUnlockable != null && registeredUnlockable.price != -1) 393 | { 394 | registeredUnlockable.price = price; 395 | } 396 | 397 | if (buyableUnlockableAssetInfos.Any(x => x.itemAsset == shopItem)) 398 | { 399 | var asset = buyableUnlockableAssetInfos.First(x => x.itemAsset == shopItem); 400 | 401 | // correct noun 402 | if (nouns.Any(noun => noun.noun == asset.keyword)) 403 | { 404 | var noun = nouns.First(noun => noun.noun == asset.keyword); 405 | var node = noun.result; 406 | node.itemCost = price; 407 | // get buynode 2 408 | if (node.terminalOptions.Length > 0) 409 | { 410 | // loop through terminal options 411 | foreach (var option in node.terminalOptions) 412 | { 413 | if (option.result != null && option.result.buyItemIndex != -1) 414 | { 415 | option.result.itemCost = price; 416 | } 417 | } 418 | } 419 | } 420 | } 421 | } 422 | } 423 | } 424 | -------------------------------------------------------------------------------- /LethalLib/Modules/Utilities.cs: -------------------------------------------------------------------------------- 1 | #region 2 | 3 | using System.Collections.Generic; 4 | using UnityEngine; 5 | using UnityEngine.Audio; 6 | 7 | #endregion 8 | 9 | namespace LethalLib.Modules; 10 | 11 | public class Utilities 12 | { 13 | public static List prefabsToFix = new List(); 14 | public static List fixedPrefabs = new List(); 15 | public static void Init() 16 | { 17 | On.StartOfRound.Start += StartOfRound_Start; 18 | On.MenuManager.Start += MenuManager_Start; 19 | } 20 | 21 | private static void StartOfRound_Start(On.StartOfRound.orig_Start orig, StartOfRound self) 22 | { 23 | AudioMixer audioMixer = SoundManager.Instance.diageticMixer; 24 | 25 | // log 26 | if (Plugin.extendedLogging.Value) 27 | Plugin.logger.LogInfo($"Diagetic mixer is {audioMixer.name}"); 28 | 29 | Plugin.logger.LogInfo($"Found {prefabsToFix.Count} prefabs to fix"); 30 | 31 | List prefabsToRemove = new List(); 32 | 33 | for (int i = prefabsToFix.Count - 1; i >= 0; i--) 34 | { 35 | GameObject prefab = prefabsToFix[i]; 36 | // get audio sources and then use string matching to find the correct mixer group 37 | AudioSource[] audioSources = prefab.GetComponentsInChildren(); 38 | foreach (AudioSource audioSource in audioSources) 39 | { 40 | // check if any mixer group is assigned 41 | if (audioSource.outputAudioMixerGroup == null) 42 | { 43 | //Plugin.logger.LogInfo($"No mixer group for {audioSource.name} in {prefab.name}"); 44 | continue; 45 | } 46 | 47 | // log mixer group name 48 | //Plugin.logger.LogInfo($"Mixer group for {audioSource.name} in {prefab.name} is {audioSource.outputAudioMixerGroup.audioMixer.name}"); 49 | 50 | if (audioSource.outputAudioMixerGroup.audioMixer.name == "Diagetic") 51 | { 52 | 53 | var mixerGroup = audioMixer.FindMatchingGroups(audioSource.outputAudioMixerGroup.name)[0]; 54 | 55 | // check if group was found 56 | if (mixerGroup != null) 57 | { 58 | audioSource.outputAudioMixerGroup = mixerGroup; 59 | // log 60 | if (Plugin.extendedLogging.Value) 61 | Plugin.logger.LogInfo($"Set mixer group for {audioSource.name} in {prefab.name} to Diagetic:{mixerGroup.name}"); 62 | 63 | // remove from list 64 | prefabsToRemove.Add(prefab); 65 | } 66 | } 67 | } 68 | 69 | } 70 | 71 | // remove fixed prefabs from list 72 | foreach (GameObject prefab in prefabsToRemove) 73 | { 74 | prefabsToFix.Remove(prefab); 75 | } 76 | 77 | orig(self); 78 | } 79 | 80 | private static void MenuManager_Start(On.MenuManager.orig_Start orig, MenuManager self) 81 | { 82 | orig(self); 83 | 84 | if(self.GetComponent() == null) 85 | { 86 | return; 87 | } 88 | // non diagetic mixer 89 | AudioMixer audioMixer = self.GetComponent().outputAudioMixerGroup.audioMixer; 90 | 91 | List prefabsToRemove = new List(); 92 | // reverse loop so we can remove items 93 | for (int i = prefabsToFix.Count - 1; i >= 0; i--) 94 | { 95 | GameObject prefab = prefabsToFix[i]; 96 | // get audio sources and then use string matching to find the correct mixer group 97 | AudioSource[] audioSources = prefab.GetComponentsInChildren(); 98 | foreach (AudioSource audioSource in audioSources) 99 | { 100 | // check if any mixer group is assigned 101 | if (audioSource.outputAudioMixerGroup == null) 102 | { 103 | continue; 104 | } 105 | 106 | // log mixer group name 107 | //Plugin.logger.LogInfo($"Mixer group for {audioSource.name} in {prefab.name} is {audioSource.outputAudioMixerGroup.audioMixer.name}"); 108 | 109 | if (audioSource.outputAudioMixerGroup.audioMixer.name == "NonDiagetic") 110 | { 111 | 112 | var mixerGroup = audioMixer.FindMatchingGroups(audioSource.outputAudioMixerGroup.name)[0]; 113 | 114 | // check if group was found 115 | if (mixerGroup != null) 116 | { 117 | audioSource.outputAudioMixerGroup = mixerGroup; 118 | // log 119 | if (Plugin.extendedLogging.Value) 120 | Plugin.logger.LogInfo($"Set mixer group for {audioSource.name} in {prefab.name} to NonDiagetic:{mixerGroup.name}"); 121 | 122 | // remove from list 123 | prefabsToRemove.Add(prefab); 124 | } 125 | } 126 | } 127 | } 128 | 129 | // remove fixed prefabs from list 130 | foreach (GameObject prefab in prefabsToRemove) 131 | { 132 | prefabsToFix.Remove(prefab); 133 | } 134 | } 135 | 136 | public static void FixMixerGroups(GameObject prefab) 137 | { 138 | if(fixedPrefabs.Contains(prefab)) 139 | { 140 | return; 141 | } 142 | 143 | //Plugin.logger.LogInfo($"Fixing mixer groups for {prefab.name}"); 144 | 145 | fixedPrefabs.Add(prefab); 146 | 147 | prefabsToFix.Add(prefab); 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /LethalLib/Modules/Weathers.cs: -------------------------------------------------------------------------------- 1 | #region 2 | 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using LethalLib.Extras; 7 | using MonoMod.RuntimeDetour; 8 | 9 | #endregion 10 | 11 | namespace LethalLib.Modules; 12 | 13 | public class Weathers 14 | { 15 | public class CustomWeather 16 | { 17 | public string name; 18 | public int weatherVariable1; 19 | public int weatherVariable2; 20 | public WeatherEffect weatherEffect; 21 | public Levels.LevelTypes levels; 22 | public string[] spawnLevelOverrides; 23 | 24 | public CustomWeather(string name, WeatherEffect weatherEffect, Levels.LevelTypes levels = Levels.LevelTypes.None, string[] spawnLevelOverrides = null, int weatherVariable1 = 0, int weatherVariable2 = 0 ) 25 | { 26 | this.name = name; 27 | this.weatherVariable1 = weatherVariable1; 28 | this.weatherVariable2 = weatherVariable2; 29 | this.weatherEffect = weatherEffect; 30 | this.levels = levels; 31 | this.spawnLevelOverrides = spawnLevelOverrides; 32 | } 33 | } 34 | 35 | public static Dictionary customWeathers = new Dictionary(); 36 | private static List levelsAlreadyAddedTo = new(); 37 | 38 | public static int numCustomWeathers = 0; 39 | // public static Array newWeatherValuesArray; 40 | //public static string[] newWeatherNamesArray; 41 | private static Hook? weatherEnumHook ; 42 | 43 | public static void Init() 44 | { 45 | 46 | 47 | //public static Array GetValues(Type enumType); 48 | /*new Hook(typeof(Enum).GetMethod("GetValues", new Type[] 49 | { 50 | typeof(Type) 51 | }), typeof(Weathers).GetMethod("GetValuesHook")); 52 | 53 | //public static string[] GetNames(Type enumType); 54 | new Hook(typeof(Enum).GetMethod("GetNames", new Type[] 55 | { 56 | typeof(Type) 57 | }), typeof(Weathers).GetMethod("GetNamesHook"));*/ 58 | 59 | //public override string ToString(); 60 | weatherEnumHook = new Hook(typeof(Enum).GetMethod("ToString", new Type[] 61 | { 62 | }), typeof(Weathers).GetMethod("ToStringHook")); 63 | 64 | On.TimeOfDay.Awake += TimeOfDay_Awake; 65 | On.StartOfRound.Awake += RegisterLevelWeathers_StartOfRound_Awake; 66 | } 67 | 68 | private static void RegisterLevelWeathers_StartOfRound_Awake(On.StartOfRound.orig_Awake orig, StartOfRound self) 69 | { 70 | RegisterLethalLibWeathersForAllLevels(self); 71 | 72 | if(BepInEx.Bootstrap.Chainloader.PluginInfos.ContainsKey("imabatby.lethallevelloader") || // currently has typo 73 | BepInEx.Bootstrap.Chainloader.PluginInfos.ContainsKey("iambatby.lethallevelloader")) // might be changed to this 74 | { 75 | // LLL adds it's moons later, so we add the weathers for it also 76 | On.RoundManager.Start += (orig, selfRoundManager) => 77 | { 78 | orig(selfRoundManager); 79 | RegisterLethalLibWeathersForAllLevels(self); 80 | }; 81 | } 82 | 83 | orig(self); 84 | } 85 | 86 | private static void RegisterLethalLibWeathersForAllLevels(StartOfRound startOfRound) 87 | { 88 | foreach (SelectableLevel level in startOfRound.levels) 89 | { 90 | if(levelsAlreadyAddedTo.Contains(level)) 91 | continue; 92 | foreach (KeyValuePair entry in customWeathers) 93 | { 94 | AddWeatherToLevel(entry, level); 95 | } 96 | levelsAlreadyAddedTo.Add(level); 97 | } 98 | } 99 | 100 | private static void AddWeatherToLevel(KeyValuePair entry, SelectableLevel level) 101 | { 102 | var name = level.name; 103 | 104 | var alwaysValid = entry.Value.levels.HasFlag(Levels.LevelTypes.All) || (entry.Value.spawnLevelOverrides != null && entry.Value.spawnLevelOverrides.Any(item => item.ToLowerInvariant() == name.ToLowerInvariant())); 105 | var isModded = entry.Value.levels.HasFlag(Levels.LevelTypes.Modded) && !Enum.IsDefined(typeof(Levels.LevelTypes), name); 106 | 107 | if (isModded) 108 | { 109 | alwaysValid = true; 110 | } 111 | 112 | if (Enum.IsDefined(typeof(Levels.LevelTypes), name) || alwaysValid) 113 | { 114 | var levelEnum = alwaysValid ? Levels.LevelTypes.All : (Levels.LevelTypes)Enum.Parse(typeof(Levels.LevelTypes), name); 115 | var weathers = level.randomWeathers.ToList(); 116 | // loop through custom weathers 117 | 118 | // if the custom weather has the level 119 | if (alwaysValid || entry.Value.levels.HasFlag(levelEnum)) 120 | { 121 | // add it to the level 122 | weathers.Add(new RandomWeatherWithVariables() 123 | { 124 | weatherType = (LevelWeatherType)entry.Key, 125 | weatherVariable = entry.Value.weatherVariable1, 126 | weatherVariable2 = entry.Value.weatherVariable2 127 | }); 128 | if (Plugin.extendedLogging.Value) 129 | Plugin.logger.LogInfo($"To level {level.name} added weather {entry.Value.name} at weather index: {entry.Key}"); 130 | } 131 | 132 | level.randomWeathers = weathers.ToArray(); 133 | 134 | } 135 | } 136 | 137 | private static void TimeOfDay_Awake(On.TimeOfDay.orig_Awake orig, TimeOfDay self) 138 | { 139 | List weatherList = self.effects.ToList(); 140 | 141 | // we want to insert things at the right index, but there might be gaps, in which case we need to fill it with nulls 142 | // first we find our highest index 143 | int highestIndex = 0; 144 | foreach (KeyValuePair entry in customWeathers) 145 | { 146 | if (entry.Key > highestIndex) 147 | { 148 | highestIndex = entry.Key; 149 | } 150 | } 151 | 152 | // then we fill the list with nulls until we reach the highest index 153 | while (weatherList.Count <= highestIndex) 154 | { 155 | weatherList.Add(null); 156 | } 157 | 158 | // thne we set the custom weathers at their index 159 | foreach (KeyValuePair entry in customWeathers) 160 | { 161 | weatherList[entry.Key] = entry.Value.weatherEffect; 162 | } 163 | 164 | // then we set the list 165 | self.effects = weatherList.ToArray(); 166 | 167 | orig(self); 168 | } 169 | 170 | /* 171 | public static Array GetValuesHook(Func orig, Type enumType) 172 | { 173 | if(enumType == typeof(LevelWeatherType)) 174 | { 175 | 176 | 177 | } 178 | return orig(enumType); 179 | } 180 | 181 | 182 | public static string[] GetNamesHook(Func orig, Type enumType) 183 | { 184 | if (enumType == typeof(LevelWeatherType)) 185 | { 186 | 187 | } 188 | return orig(enumType); 189 | }*/ 190 | 191 | public static string ToStringHook(Func orig, Enum self) 192 | { 193 | if (self.GetType() == typeof(LevelWeatherType)) 194 | { 195 | if (customWeathers.ContainsKey((int)(LevelWeatherType)self)) 196 | { 197 | return customWeathers[(int)(LevelWeatherType)self].name; 198 | } 199 | } 200 | 201 | return orig(self); 202 | } 203 | 204 | 205 | /// 206 | ///Register a weather with the game. 207 | /// 208 | public static void RegisterWeather(WeatherDef weather) 209 | { 210 | RegisterWeather(weather.weatherName, weather.weatherEffect, weather.levels, weather.levelOverrides, weather.weatherVariable1, weather.weatherVariable2); 211 | } 212 | 213 | /// 214 | ///Register a weather with the game, which are able to show up on the specified levels. 215 | /// 216 | public static void RegisterWeather(string name, WeatherEffect weatherEffect, Levels.LevelTypes levels = Levels.LevelTypes.None, int weatherVariable1 = 0, int weatherVariable2 = 0) 217 | { 218 | var origValues = Enum.GetValues(typeof(LevelWeatherType)); 219 | int num = origValues.Length - 1; 220 | 221 | num += numCustomWeathers; 222 | 223 | // add our numcustomweathers 224 | numCustomWeathers++; 225 | 226 | Plugin.logger.LogInfo($"Registering weather {name} at index {num - 1}"); 227 | 228 | // add to dictionary at next value 229 | customWeathers.Add(num, new CustomWeather(name, weatherEffect, levels, null, weatherVariable1, weatherVariable2)); 230 | } 231 | 232 | /// 233 | ///Register a weather with the game, which are able to show up on the specified levels. 234 | /// 235 | public static void RegisterWeather(string name, WeatherEffect weatherEffect, Levels.LevelTypes levels = Levels.LevelTypes.None, string[] spawnLevelOverrides = null, int weatherVariable1 = 0, int weatherVariable2 = 0) 236 | { 237 | var origValues = Enum.GetValues(typeof(LevelWeatherType)); 238 | int num = origValues.Length - 1; 239 | 240 | num += numCustomWeathers; 241 | 242 | // add our numcustomweathers 243 | numCustomWeathers++; 244 | 245 | Plugin.logger.LogInfo($"Registering weather {name} at index {num - 1}"); 246 | 247 | // add to dictionary at next value 248 | customWeathers.Add(num, new CustomWeather(name, weatherEffect, levels, spawnLevelOverrides, weatherVariable1, weatherVariable2)); 249 | } 250 | 251 | /// 252 | ///Removes a weather from the specified levels. 253 | ///This needs to be called after StartOfRound.Awake. 254 | ///Only works for weathers registered by LethalLib. 255 | /// 256 | public static void RemoveWeather(string weatherName, Levels.LevelTypes levelFlags = Levels.LevelTypes.None, string[]? levelOverrides = null) 257 | { 258 | foreach (KeyValuePair entry in customWeathers) 259 | { 260 | if (entry.Value.name == weatherName && StartOfRound.Instance != null) 261 | { 262 | 263 | foreach (SelectableLevel level in StartOfRound.Instance.levels) 264 | { 265 | var name = level.name; 266 | 267 | var alwaysValid = levelFlags.HasFlag(Levels.LevelTypes.All) || (levelOverrides != null && levelOverrides.Any(item => item.ToLowerInvariant() == name.ToLowerInvariant())); 268 | var isModded = levelFlags.HasFlag(Levels.LevelTypes.Modded) && !Enum.IsDefined(typeof(Levels.LevelTypes), name); 269 | 270 | if (isModded) 271 | { 272 | alwaysValid = true; 273 | } 274 | if (Enum.IsDefined(typeof(Levels.LevelTypes), name) || alwaysValid) 275 | { 276 | var levelEnum = alwaysValid ? Levels.LevelTypes.All : (Levels.LevelTypes)Enum.Parse(typeof(Levels.LevelTypes), name); 277 | if (alwaysValid || levelFlags.HasFlag(levelEnum)) 278 | { 279 | var weathers = level.randomWeathers.ToList(); 280 | 281 | weathers.RemoveAll(item => item.weatherType == (LevelWeatherType)entry.Key); 282 | 283 | level.randomWeathers = weathers.ToArray(); 284 | } 285 | } 286 | } 287 | } 288 | } 289 | } 290 | 291 | } 292 | -------------------------------------------------------------------------------- /LethalLib/Plugin.cs: -------------------------------------------------------------------------------- 1 | #region 2 | 3 | using System; 4 | using System.Diagnostics; 5 | using System.IO; 6 | using System.Reflection; 7 | using System.Security.Permissions; 8 | using BepInEx; 9 | using BepInEx.Configuration; 10 | using LethalLib.Modules; 11 | using MonoMod.Cil; 12 | using MonoMod.RuntimeDetour; 13 | using UnityEngine; 14 | 15 | #endregion 16 | 17 | [assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)] 18 | namespace LethalLib; 19 | 20 | [BepInPlugin(ModGUID, ModName, ModVersion)] 21 | //[BepInDependency("LethalExpansion", BepInDependency.DependencyFlags.SoftDependency)] 22 | public class Plugin : BaseUnityPlugin 23 | { 24 | public const string ModGUID = "evaisa.lethallib";//MyPluginInfo.PLUGIN_GUID; 25 | public const string ModName = MyPluginInfo.PLUGIN_NAME; 26 | public const string ModVersion = MyPluginInfo.PLUGIN_VERSION; 27 | 28 | public static AssetBundle MainAssets; 29 | 30 | public static BepInEx.Logging.ManualLogSource logger; 31 | public static ConfigFile config; 32 | 33 | public static Plugin Instance; 34 | 35 | public static ConfigEntry extendedLogging; 36 | 37 | private void Awake() 38 | { 39 | Instance = this; 40 | config = Config; 41 | logger = Logger; 42 | 43 | Logger.LogInfo($"LethalLib loaded!!"); 44 | 45 | extendedLogging = Config.Bind("General", "ExtendedLogging", false, "Enable extended logging"); 46 | 47 | MainAssets = AssetBundle.LoadFromFile(Path.Combine(Path.GetDirectoryName(Info.Location)!, "lethallib")); 48 | 49 | new ILHook(typeof(StackTrace).GetMethod("AddFrames", BindingFlags.Instance | BindingFlags.NonPublic), IlHook); 50 | Enemies.Init(); 51 | Items.Init(); 52 | Unlockables.Init(); 53 | MapObjects.Init(); 54 | Dungeon.Init(); 55 | Weathers.Init(); 56 | Player.Init(); 57 | Utilities.Init(); 58 | NetworkPrefabs.Init(); 59 | } 60 | 61 | private void IlHook(ILContext il) 62 | { 63 | var cursor = new ILCursor(il); 64 | var getFileLineNumberMethod = typeof(StackFrame).GetMethod("GetFileLineNumber", BindingFlags.Instance | BindingFlags.Public); 65 | 66 | if (cursor.TryGotoNext(x => x.MatchCallvirt(getFileLineNumberMethod))) 67 | { 68 | cursor.RemoveRange(2); 69 | cursor.EmitDelegate>(GetLineOrIL); 70 | } 71 | } 72 | 73 | private static string GetLineOrIL(StackFrame instance) 74 | { 75 | var line = instance.GetFileLineNumber(); 76 | if (line == StackFrame.OFFSET_UNKNOWN || line == 0) 77 | { 78 | return "IL_" + instance.GetILOffset().ToString("X4"); 79 | } 80 | 81 | return line.ToString(); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /LethalLib/assets/bundles/lethallib: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EvaisaDev/LethalLib/261890e45db4febd8c7882148cb9186671f09606/LethalLib/assets/bundles/lethallib -------------------------------------------------------------------------------- /LethalLib/assets/icons/lethal-lib.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EvaisaDev/LethalLib/261890e45db4febd8c7882148cb9186671f09606/LethalLib/assets/icons/lethal-lib.png -------------------------------------------------------------------------------- /LethalLib/assets/thunderstore.toml: -------------------------------------------------------------------------------- 1 | [config] 2 | schemaVersion = "0.0.1" 3 | 4 | [general] 5 | repository = "https://thunderstore.io" 6 | 7 | [package] 8 | namespace = "Evaisa" 9 | name = "LethalLib" 10 | description = "Personal modding tools for Lethal Company" 11 | websiteUrl = "https://github.com/EvaisaDev/LethalLib" 12 | containsNsfwContent = false 13 | [package.dependencies] 14 | BepInEx-BepInExPack = "5.4.2100" 15 | Evaisa-HookGenPatcher = "0.0.5" 16 | MonoDetour-MonoDetour_BepInEx_5 = "0.6.3" 17 | 18 | [build] 19 | icon = "icons/lethal-lib.png" 20 | readme = "../../README.md" 21 | outdir = "../dist" 22 | 23 | [[build.copy]] 24 | source = "../bin/Release/netstandard2.1/LethalLib.dll" 25 | target = "plugins/LethalLib/" 26 | 27 | [[build.copy]] 28 | source = "bundles" 29 | target = "plugins/LethalLib/" 30 | 31 | [[build.copy]] 32 | source = "../../CHANGELOG.md" 33 | target = "/" 34 | 35 | [[build.copy]] 36 | source = "../../LICENSE" 37 | target = "/" 38 | 39 | [publish] 40 | communities = [ "lethal-company", ] 41 | [publish.categories] 42 | lethal-company = [ "libraries", "tools", "mods", "bepinex", ] 43 | 44 | 45 | -------------------------------------------------------------------------------- /NuGet.Config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # LethalLib 2 | 3 | [![GitHub Build Status](https://img.shields.io/github/actions/workflow/status/evaisadev/lethallib/build.yml?style=for-the-badge&logo=github)](https://github.com/evaisadev/lethallib/actions/workflows/build.yml) 4 | [![Thunderstore Version](https://img.shields.io/thunderstore/v/Evaisa/LethalLib?style=for-the-badge&logo=thunderstore&logoColor=white)](https://thunderstore.io/c/lethal-company/p/Evaisa/LethalLib/) 5 | [![Thunderstore Downloads](https://img.shields.io/thunderstore/dt/Evaisa/LethalLib?style=for-the-badge&logo=thunderstore&logoColor=white)](https://thunderstore.io/c/lethal-company/p/Evaisa/LethalLib/) 6 | [![NuGet Version](https://img.shields.io/nuget/v/evaisa.lethallib?style=for-the-badge&logo=nuget)](https://www.nuget.org/packages/Evaisa.LethalLib) 7 | 8 | **A library for adding new content to Lethal Company, mainly for personal use.** 9 | 10 | ## Features 11 | 12 | Currently includes: 13 | - Custom Scrap Item API 14 | - Custom Shop Item API 15 | - Unlockables API 16 | - Map Objects API 17 | - Dungeon API 18 | - Custom Enemy API 19 | - Network Prefab API 20 | - Prefab Utils 21 | - Weather API 22 | - ContentLoader 23 | 24 | ## Changes 25 | 26 | See the [changelog](https://github.com/EvaisaDev/LethalLib/blob/main/CHANGELOG.md) for changes by-version and unreleased changes. 27 | 28 | ## Contributing 29 | 30 | ### Fork & Clone 31 | 32 | Fork the repository on GitHub and clone your fork locally. 33 | 34 | ### Configure Git hooks & `post-checkout` 35 | 36 | Configure the Git hooks directory for your local copy of the repository: 37 | ```sh 38 | git config core.hooksPath hooks/ 39 | ``` 40 | 41 | Alternatively, you can create symbolic links in `.git/hooks/*` that point to `../hooks/*`. 42 | 43 | Then re-checkout to trigger the `post-checkout` hook: 44 | ```sh 45 | git checkout main 46 | ``` 47 | 48 | ### `LethalLib.csproj.user` 49 | You will need to create a `LethalLib/LethalLib.csproj.user` file to provide your Lethal Company game directory path. 50 | 51 | #### Template 52 | ```xml 53 | 54 | 55 | 56 | C:/Program Files (x86)/Steam/steamapps/common/Lethal Company/ 57 | $(APPDATA)/r2modmanPlus-local/LethalCompany/profiles/Test LethalLib/ 58 | 59 | 60 | 61 | 62 | 66 | 67 | 68 | 69 | ``` 70 | -------------------------------------------------------------------------------- /hooks/post-checkout: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | dotnet tool restore -------------------------------------------------------------------------------- /lib/MMHOOK_Assembly-CSharp.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EvaisaDev/LethalLib/261890e45db4febd8c7882148cb9186671f09606/lib/MMHOOK_Assembly-CSharp.dll --------------------------------------------------------------------------------