├── .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 | [](https://github.com/evaisadev/lethallib/actions/workflows/build.yml)
4 | [](https://thunderstore.io/c/lethal-company/p/Evaisa/LethalLib/)
5 | [](https://thunderstore.io/c/lethal-company/p/Evaisa/LethalLib/)
6 | [](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
--------------------------------------------------------------------------------