├── .github └── workflows │ ├── build.yml │ └── publish.yml ├── .gitignore ├── CHANGELOG.md ├── LC-API.sln ├── LC-API ├── BundleAPI │ ├── BundleLoader.cs │ └── LoadedAssetBundle.cs ├── CheatDatabase.cs ├── ClientAPI │ └── CommandHandler.cs ├── Comp │ └── LC_APIManager.cs ├── Data │ ├── NetworkBroadcastDataType.cs │ └── ShipState.cs ├── Exceptions │ └── NoAuthorityException.cs ├── Extensions │ └── DelegateExtensions.cs ├── GameInterfaceAPI │ ├── Events │ │ ├── Cache │ │ │ └── Player.cs │ │ ├── EventArgs │ │ │ └── Player │ │ │ │ ├── DiedEventArgs.cs │ │ │ │ ├── DroppingItemEventArgs.cs │ │ │ │ ├── DyingEventArgs.cs │ │ │ │ ├── GrabbedItemEventArgs.cs │ │ │ │ ├── GrabbingItemEventArgs.cs │ │ │ │ ├── HurtEventArgs.cs │ │ │ │ ├── HurtingEventArgs.cs │ │ │ │ ├── JoinedEventArgs.cs │ │ │ │ ├── LeftEventArgs.cs │ │ │ │ └── StartGrabbingItemEventArgs.cs │ │ ├── EventExtensions.cs │ │ ├── Events.cs │ │ ├── Handlers │ │ │ └── Player.cs │ │ └── Patches │ │ │ ├── Internal │ │ │ ├── DisplayTipPatch.cs │ │ │ ├── GameNetworkManagerStartPatch.cs │ │ │ └── PlayerControllerBStartPatch.cs │ │ │ └── Player │ │ │ ├── Die.cs │ │ │ ├── DropItem.cs │ │ │ ├── GrabItem.cs │ │ │ ├── Hurt.cs │ │ │ ├── Joined.cs │ │ │ └── Left.cs │ ├── Features │ │ ├── Item.cs │ │ ├── Player.cs │ │ └── Tip.cs │ ├── GameState.cs │ └── GameTips.cs ├── LC-API.csproj ├── ManualPatches │ └── ServerPatch.cs ├── Networking │ ├── Network.cs │ └── Serializers.cs ├── Plugin.cs ├── ServerAPI │ ├── ModdedServer.cs │ └── Networking.cs └── Utils.cs ├── LICENSE ├── README.md └── assets ├── bundles └── networking ├── icons └── icon.png └── thunderstore.toml /.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 solution 27 | run: | 28 | dotnet restore 29 | 30 | - name: Setup Netcode Patcher 31 | id: setup-netcode-patcher 32 | uses: Lordfirespeed/setup-netcode-patcher@v0 33 | with: 34 | netcode-patcher-version: 2.4.0 35 | deps-packages: '[{"id": "UnityEngine.Modules", "version": "2022.3.9"}, {"id": "LethalCompany.GameLibs.Steam", "version": "49.0.0-alpha.1"}]' 36 | target-framework: "netstandard2.1" 37 | 38 | - name: Install Thunderstore CLI 39 | run: | 40 | dotnet tool install -g tcli 41 | 42 | - name: Build solution 43 | run: | 44 | dotnet build -p:NETCODE_PATCHER_DIR="${{ steps.setup-netcode-patcher.outputs.netcode-patcher-directory }}" 45 | 46 | - name: Upload Artifacts 47 | uses: actions/upload-artifact@v4 48 | with: 49 | name: build-artifacts 50 | path: "dist/*.zip" 51 | -------------------------------------------------------------------------------- /.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 solution 25 | run: | 26 | dotnet restore 27 | 28 | - name: Setup Netcode Patcher 29 | id: setup-netcode-patcher 30 | uses: Lordfirespeed/setup-netcode-patcher@v0 31 | with: 32 | netcode-patcher-version: 2.4.0 33 | deps-packages: '[{"id": "UnityEngine.Modules", "version": "2022.3.9"}, {"id": "LethalCompany.GameLibs.Steam", "version": "49.0.0-alpha.1"}]' 34 | target-framework: "netstandard2.1" 35 | 36 | - name: Install Thunderstore CLI 37 | run: | 38 | dotnet tool install -g tcli 39 | 40 | - name: Build and pack solution 41 | run: | 42 | dotnet pack -p:NETCODE_PATCHER_DIR="${{ steps.setup-netcode-patcher.outputs.netcode-patcher-directory }}" 43 | 44 | - name: Upload Thunderstore artifact 45 | uses: actions/upload-artifact@v4 46 | with: 47 | name: thunderstore-build 48 | path: dist/*.zip 49 | 50 | - name: Upload nupkg artifact 51 | uses: actions/upload-artifact@v4 52 | with: 53 | name: nupkg-build 54 | path: ./*/bin/*.nupkg 55 | 56 | upload-release-artifacts: 57 | name: Upload Release Artifacts 58 | needs: build 59 | runs-on: ubuntu-latest 60 | steps: 61 | - name: Fetch Sources 62 | uses: actions/checkout@v4 63 | 64 | - name: Download all artifacts 65 | uses: actions/download-artifact@v4 66 | 67 | - name: Upload artifacts to Release 68 | env: 69 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 70 | run: gh release upload ${{ github.event.release.tag_name }} thunderstore-build/*.zip nupkg-build/*/bin/*.nupkg 71 | 72 | deploy-nuget: 73 | name: Deploy to NuGet 74 | needs: build 75 | runs-on: ubuntu-latest 76 | steps: 77 | - name: Fetch Sources 78 | uses: actions/checkout@v4 79 | 80 | - name: Download nupkg artifact 81 | uses: actions/download-artifact@v4 82 | with: 83 | name: nupkg-build 84 | 85 | - name: Setup .NET environment 86 | uses: actions/setup-dotnet@v3 87 | with: 88 | dotnet-version: "8.0.100" 89 | 90 | - name: Publish to NuGet.org 91 | run: | 92 | dotnet nuget push ./*/bin/*.nupkg --api-key ${{ secrets.NUGET_API_TOKEN }} --source https://api.nuget.org/v3/index.json 93 | 94 | deploy-thunderstore: 95 | name: Deploy to Thunderstore 96 | needs: build 97 | runs-on: ubuntu-latest 98 | steps: 99 | - name: Fetch Sources 100 | uses: actions/checkout@v4 101 | 102 | - name: Download Thunderstore artifact 103 | uses: actions/download-artifact@v4 104 | with: 105 | name: thunderstore-build 106 | path: ./dist 107 | 108 | - name: Validate artifact exists 109 | run: test -d ./dist 110 | 111 | - name: Setup .NET environment 112 | uses: actions/setup-dotnet@v3 113 | with: 114 | dotnet-version: "8.0.100" 115 | 116 | - name: Install Thunderstore CLI 117 | run: | 118 | dotnet tool install -g tcli 119 | 120 | - name: Publish to Thunderstore 121 | env: 122 | TCLI_AUTH_TOKEN: ${{ secrets.THUNDERSTORE_API_TOKEN }} 123 | run: | 124 | tcli publish --config-path ./assets/thunderstore.toml --file dist/*.zip 125 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Exclude everything 2 | /* 3 | 4 | # Specific includes 5 | 6 | ## Build System/Scripts 7 | !NuGet.Config 8 | !Directory.Build.props 9 | !build.sh 10 | !build.ps1 11 | !build.cmd 12 | 13 | ## Markdowns 14 | !*.md 15 | !LICENSE 16 | 17 | ## important Git files 18 | !.gitmodules 19 | !.gitignore 20 | !.gitattributes 21 | !.git-blame-ignore-revs 22 | 23 | ## GitHub specific 24 | !.github/ 25 | 26 | ## Assets 27 | !assets/ 28 | 29 | ## Thunderstore metadata 30 | !thunderstore.toml 31 | 32 | ## Thunderstore Assets 33 | !Thunderstore 34 | LC_API.dll 35 | 36 | ## Solution 37 | !LC-API.sln 38 | 39 | ### LC-API Project 40 | !LC-API/ 41 | LC-API/[Bb]in/ 42 | LC-API/[Oo]bj/ 43 | 44 | # Explicit exceptions/ignores 45 | **.user -------------------------------------------------------------------------------- /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 | ## [Unreleased] 9 | 10 | N/A 11 | 12 | ## Version [3.4.5] 13 | - Fixed an issue where bodies would not ragdoll after death when using vanilla support. 14 | - Fixed an issue where hurting sometimes did not work while using vanilla support. 15 | 16 | ## Version [3.4.4] 17 | - Fixed an issue where dropping/grabbing items would not work properly if vanilla support was enabled. 18 | 19 | ## Version [3.4.3] 20 | - Fixed an issue where intro tips would play regardless of if you've seen them or not. 21 | 22 | ## Version [3.4.2] 23 | - Fixed object placement not rotating with the player properly. 24 | 25 | ## Version [3.4.1] 26 | - Fixed players not taking damage or dying. 27 | 28 | ## Version [3.4.0] 29 | - Added `Player.Died` and `Player.Hurt` *past-tense* events. 30 | - Added `Player.StartGrabbingItem` and `Player.GrabbingItem` and `Player.GrabbedItem` events. 31 | - Added `Player.DroppingItem` event. 32 | - Added new `Player.ShowTip(string header, string message, float duration, bool isWarning, bool useSave, string prefsKey)` and `Player.QueueTip(string header, string message, float duration, int priority, bool isWarning, bool useSave, string prefsKey)` 33 | - `Player.ShowTip` bypasses the new tip queue. 34 | - Base game tips (or mods that use the `HUDManager.DisplayTip` method) will be treated as max priority tips for compatibility. 35 | - Tips now have a configurable duration in which they will stay on the screen. 36 | - The host can show tips to anyone, however local clients can only show tips to themselves. 37 | - The `int priority` parameter of the `QueueTip` method allows you to set a priority, where higher means it will be shown sooner. 38 | - Tips will always go to the end of their equivalent priority "list" meaning tips added after another that have the same priority will be shown after the ones added previously. 39 | - When a tip bypasses the queue for any reason, the currently showing tip will continue afterwards with whatever time left it had remaining as long as there were >= 1.5 seconds remaining. 40 | - This is the only "breaking" change as it will also affect tips shown by other plugins using other methods, which usually won't requeue the current tip. This will be monitored in case it needs to be removed. 41 | - With this, `GameTips` class has been deprecated, but will still work as expected. 42 | - Fixed `Player.Username` not updating on the radar or escape menu. 43 | - Added `Player.PlayerObjectId` which returns the player's specified player object index in places like `allPlayerScripts`. 44 | - Fixed an issue where players would be added twice to the player's dictionary causing an error. 45 | - Fixed an issue where giving an item to a player but not switching to it would cause it to not be held properly. 46 | 47 | ## Version [3.3.2] 48 | 49 | - Fixed `Player.Postion` setter for v47. 50 | 51 | ## Version [3.3.1] 52 | - Fixed `Player.Username` not updating on the radar or escape menu. 53 | - Because of this, `Player.PlayerObjectId` has been added which returns the player's specified player object index in places like `allPlayerScripts`. 54 | - Fixed `Player.ActiveList` not resetting. 55 | - Fixed `Player.Dictionary` not resetting. 56 | 57 | ## Version [3.3.0] 58 | 59 | - Fixed an issue where the old networking (`ServerAPI.Networking`) would not work properly. 60 | - Fixed issues with the build workflow. 61 | - Fixed the file structure in the README for manual installation. 62 | - Fixed an issue where network messages wouldn't properly be re-registered when rejoining. 63 | - Added a "Vanilla Support" config option that will disable installing the network bundle so you can play on vanilla servers. 64 | - The `Player` and `Item` classes will be completely disabled and this will probably break mods! Please just consider everyone in your game having LC API. 65 | 66 | ## Version [3.2.4] 67 | 68 | - Fixed an issue with the `Player.Joined` and `Player.Left` events where they wouldn't work on LAN due to not having a steam ID. 69 | 70 | ## Version [3.2.3] 71 | 72 | - Fixed package references being added as transitive dependencies when using the NuGet package. 73 | 74 | ## Version [3.2.2] 75 | 76 | - Remove directory validation when pushing to NuGet (dotnet validates for us) 77 | - Pack readme, changelog and license into nuget package 78 | 79 | ## Version [3.2.1] 80 | 81 | - Fix nupkg-build artifact pathing in Publish workflow 82 | - [Lordfirespeed](https://github.com/Lordfirespeed) sucks. 83 | 84 | ## Version [3.2.0] 85 | 86 | - Added `Networking` namespace 87 | - Provides much better networking that the previous `ServerAPI.Networking` class, which still exists for backwards compatibility. 88 | - See the [wiki](https://github.com/steven4547466/LC-API/wiki/Networking) for usage instructions. 89 | - Added CI/CD github actions. 90 | - Changed a hard-coded file location to be dynamically based off of where the plugin file is to prevent an issue with manual installation. 91 | - Significantly revamped project structure. 92 | 93 | ## Version [3.1.0] 94 | 95 | - Added `Item` class for interacting with grabbable objects easily. 96 | - The `Player` class now has multiple new properties for inventory management. 97 | - `Player.Inventory` will return a `PlayerInventory` for this. 98 | - The `Player.Joined` event should now work properly on LAN. 99 | 100 | ## Version [3.0.2] 101 | 102 | - Fixed the command handler "eating" messages if they started with the command prefix, but weren't registered as commands. 103 | 104 | ## Version [3.0.1] 105 | 106 | - Fixed `Player.HostPlayer`. 107 | 108 | ## Version [3.0.0] 109 | 110 | - Removed automated bundle loading. 111 | - Legacy loading will still automatically load bundles if wanted. 112 | - Added event system. 113 | - More events to be added in future. 114 | - Added `Player` class for interacting with players easily. 115 | - `ModdedServer.GameVersion` will now contain the base game version even if LC API modified the version to set modded only. 116 | 117 | ## Version [2.2.0] 118 | 119 | - Added a command handler. 120 | - The bundle loader will only attempt to load actual bundles. 121 | - Added a temporary fix for lethal expansion bundles. Will be looking into a long term solution in the next update. 122 | - The local player now will NOT receive data that they broadcast. The bool value on the receive delegates is also gone. If you were using Networking, you will need to ajust your code. 123 | 124 | ## Version [2.1.2] 125 | 126 | - Updated to game version 45. 127 | 128 | ## Version [2.1.1] 129 | 130 | - Actually fixed the BundleLodaer loading assets twice. 131 | - Added new CheatDatabase with the purpose of catching users trying to join non-modded servers with cheaty mods. The CheatDatabase also allows for the host to view all mods installed by people joining. (As long as they have LC_API installed). 132 | 133 | ## Version [2.1.0] 134 | 135 | - Fixed the BundleLodaer loading assets twice. 136 | 137 | ## Version [2.0.0] 138 | 139 | - Changes to the BundleLoader to stop conflicts with other plugins loading assets without the BundleLoader. 140 | - Changes to Networking and GameState events, plugins using these will need to be rebuilt. 141 | - Added GameTips to GameInterfaceAPI. GameTips uses a tip que system to ensure no popup tip messages overlap with eachother. 142 | - Changed the CheatDatabase to now (in theory) work for all players, not just the host. 143 | - Changes the CheatDatabase to use GameTips to display information. It will still output information to the logs. 144 | 145 | ## Version [1.4.2] 146 | - Fix for the new config option causing the API to fail to initialize. 147 | 148 | 149 | ## Version [1.4.1] 150 | 151 | - LC_API should now be able to load no matter if the Hide Manager GameObject option is on or off. 152 | - A config option has been added that will disable the BundleLoader. 153 | 154 | 155 | ## Version [1.4.0] 156 | 157 | - Changed how the BundleLoader in the BundleAPI loads assets to fix issues with certain languages. This will break some mods, but a config option is included to revert to the old system so you can still use older mods. 158 | - If you are a plugin developer, use GetLoadedAsset to get an asset, instead of using the asset dictionary. This ensures that your plugin will still work even when changes like this are made. 159 | 160 | 161 | ## Version [1.3.0] 162 | 163 | - Changed how the BundleLoader in the BundleAPI loads assets to fix issues caused by downloading mods from mod managers. The path BepInEx > Bundles is outdated and should not be used anymore. 164 | 165 | ## Version [1.2.1] 166 | 167 | - Adjusted README formatting. 168 | 169 | 170 | ## Version [1.2.0] 171 | 172 | - Added new GameInterfaceAPI. Documentation will be created soon. 173 | 174 | ## Version [1.1.1] 175 | 176 | - General bug fixes for Networking 177 | 178 | ## Version [1.1.0] 179 | 180 | - General bug fixes for Networking 181 | 182 | ## Version [1.0.0] 183 | 184 | - Release 185 | -------------------------------------------------------------------------------- /LC-API.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.6.33829.357 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LC-API", "LC-API/LC-API.csproj", "{D1BF6DDB-3AEE-4EF0-B6C8-4FBED65C75B9}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|Any CPU = Debug|Any CPU 11 | Release|Any CPU = Release|Any CPU 12 | EndGlobalSection 13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 14 | {D1BF6DDB-3AEE-4EF0-B6C8-4FBED65C75B9}.Debug|Any CPU.ActiveCfg = Release|Any CPU 15 | {D1BF6DDB-3AEE-4EF0-B6C8-4FBED65C75B9}.Debug|Any CPU.Build.0 = Release|Any CPU 16 | {D1BF6DDB-3AEE-4EF0-B6C8-4FBED65C75B9}.Release|Any CPU.ActiveCfg = Release|Any CPU 17 | {D1BF6DDB-3AEE-4EF0-B6C8-4FBED65C75B9}.Release|Any CPU.Build.0 = Release|Any CPU 18 | EndGlobalSection 19 | GlobalSection(SolutionProperties) = preSolution 20 | HideSolutionNode = FALSE 21 | EndGlobalSection 22 | GlobalSection(ExtensibilityGlobals) = postSolution 23 | SolutionGuid = {EB6BFF56-A2F5-4195-9F89-775EB386EDC2} 24 | EndGlobalSection 25 | EndGlobal 26 | -------------------------------------------------------------------------------- /LC-API/BundleAPI/BundleLoader.cs: -------------------------------------------------------------------------------- 1 | using BepInEx; 2 | using LC_API.Extensions; 3 | using System; 4 | using System.Collections.Concurrent; 5 | using System.Collections.Generic; 6 | using System.IO; 7 | using System.Linq; 8 | using System.Text; 9 | using UnityEngine; 10 | 11 | //todo: force all paths to lowercase after privating 'assets' to ensure all paths get normalized when accessing. 12 | 13 | #pragma warning disable CS0618 // Type or member is obsolete - we're the ones obsoleting things. 14 | namespace LC_API.BundleAPI 15 | { 16 | /// 17 | /// Use the BundleLoader to load and get custom assets. A folder for placing custom assets will be generated in BepInEx/Bundles. 18 | /// 19 | public static class BundleLoader 20 | { 21 | /// 22 | /// Is set to if there are any files not ending in '.manifest' in the legacy bundle dir. if there are no files or only .manifest files. 23 | /// 24 | public static bool AssetsInLegacyDirectory { get; private set; } 25 | 26 | /// 27 | /// Is set to if asset loading legacy bundle dir was enabled. if assets were not allowed to be loaded from the legacy bundle dir. 28 | /// 29 | public static bool LegacyLoadingEnabled { get; private set; } 30 | 31 | // i think eventually 'assets' should be privatized and changed to a Dictionary. Unity is comically thread-unsafe, so using a thread-safe ConcurrentDict is overkill. 32 | [Obsolete("Use GetLoadedAsset instead. This will be removed/private in a future update.")] 33 | public static ConcurrentDictionary assets = new ConcurrentDictionary(); 34 | 35 | [Obsolete("Use OnLoadedBundles instead. This will be removed/private in a future update.")] 36 | public delegate void OnLoadedAssetsDelegate(); 37 | /// 38 | /// This is called when the BundleLoader finishes loading assets. 39 | /// 40 | [Obsolete("Use OnLoadedBundles instead. This will be removed/private in a future update.")] 41 | public static OnLoadedAssetsDelegate OnLoadedAssets = LoadAssetsCompleted; 42 | 43 | /// 44 | /// This is called when the BundleLoader finishes loading assets. 45 | /// 46 | public static event Action OnLoadedBundles = LoadAssetsCompleted; 47 | 48 | private static Dictionary loadedAssetBundles = new Dictionary(); 49 | 50 | internal static void Load(bool legacyLoading) 51 | { 52 | LegacyLoadingEnabled = legacyLoading; 53 | if (LegacyLoadingEnabled) 54 | { 55 | Plugin.Log.LogMessage("BundleAPI will now load all asset bundles..."); 56 | string bundleDir = Path.Combine(Paths.BepInExRootPath, "Bundles"); 57 | if (!Directory.Exists(bundleDir)) 58 | { 59 | Directory.CreateDirectory(bundleDir); 60 | Plugin.Log.LogMessage("BundleAPI Created legacy bundle directory in BepInEx/Bundles"); 61 | } 62 | string[] bundles = Directory.GetFiles(bundleDir, "*", SearchOption.AllDirectories).Where(x => !x.EndsWith(".manifest", StringComparison.CurrentCultureIgnoreCase)).ToArray(); 63 | 64 | AssetsInLegacyDirectory = bundles.Length != 0; 65 | 66 | if (!AssetsInLegacyDirectory) 67 | { 68 | Plugin.Log.LogMessage("BundleAPI got no assets to load from legacy directory"); 69 | } 70 | 71 | if (AssetsInLegacyDirectory) 72 | { 73 | Plugin.Log.LogWarning("The path BepInEx > Bundles is outdated and should not be used anymore! Bundles will be loaded from BepInEx > plugins from now on"); 74 | LoadAllAssetsFromDirectory(bundles, legacyLoading); 75 | } 76 | 77 | string[] invalidEndings = { ".dll", ".json", ".png", ".md", ".old", ".txt", ".exe", ".lem" }; 78 | bundleDir = Path.Combine(Paths.BepInExRootPath, "plugins"); 79 | bundles = Directory.GetFiles(bundleDir, "*", SearchOption.AllDirectories).Where(file => !invalidEndings.Any(ending => file.EndsWith(ending, StringComparison.CurrentCultureIgnoreCase))).ToArray(); 80 | 81 | byte[] bundleStart = Encoding.ASCII.GetBytes("UnityFS"); 82 | 83 | List properBundles = new List(); 84 | 85 | foreach (string path in bundles) 86 | { 87 | byte[] buffer = new byte[bundleStart.Length]; 88 | 89 | try 90 | { 91 | using (FileStream fs = File.Open(path, FileMode.Open)) 92 | { 93 | fs.Read(buffer, 0, buffer.Length); 94 | } 95 | } 96 | catch (IOException ioException) 97 | { 98 | Plugin.Log.LogError($"BundleAPI failed to open file \"{path}\": {ioException.Message}\n{ioException.StackTrace}"); 99 | } 100 | 101 | if (buffer.SequenceEqual(bundleStart)) 102 | { 103 | properBundles.Add(path); 104 | } 105 | } 106 | 107 | bundles = properBundles.ToArray(); 108 | 109 | if (bundles.Length == 0) 110 | { 111 | Plugin.Log.LogMessage("BundleAPI got no assets to load from plugins folder"); 112 | } 113 | else 114 | { 115 | LoadAllAssetsFromDirectory(bundles, legacyLoading); 116 | } 117 | 118 | OnLoadedAssets.InvokeParameterlessDelegate(); 119 | OnLoadedBundles.InvokeActionSafe(); 120 | } 121 | } 122 | 123 | private static void LoadAllAssetsFromDirectory(string[] array, bool legacyLoading) 124 | { 125 | if (legacyLoading) 126 | { 127 | Plugin.Log.LogMessage("BundleAPI got " + array.Length.ToString() + " AssetBundles to load!"); 128 | 129 | for (int i = 0; i < array.Length; i++) 130 | { 131 | try 132 | { 133 | SaveAsset(array[i], legacyLoading); 134 | } 135 | catch (Exception ex) 136 | { 137 | Plugin.Log.LogError("Failed to load an assetbundle! Path: " + array[i]); 138 | } 139 | } 140 | } 141 | else 142 | { 143 | Plugin.Log.LogMessage("BundleAPI got " + array.Length.ToString() + " AssetBundles to load!"); 144 | 145 | for (int i = 0; i < array.Length; i++) 146 | { 147 | try 148 | { 149 | SaveAsset(array[i], legacyLoading); 150 | } 151 | catch (Exception ex) 152 | { 153 | Plugin.Log.LogError("Failed to load an assetbundle! Path: " + array[i]); 154 | } 155 | } 156 | } 157 | 158 | } 159 | 160 | /// 161 | /// Saves all assets from a bundle at the given path. It is not recommended to call this method, as it can introduce instability. 162 | /// 163 | public static void SaveAsset(string path, bool legacyLoad) 164 | { 165 | AssetBundle bundle = AssetBundle.LoadFromFile(path); 166 | try 167 | { 168 | string[] assetPaths = bundle.GetAllAssetNames(); 169 | //Plugin.Log.LogMessage("Assets: [" + string.Join(", ", array) + "]"); 170 | foreach (string assetPath in assetPaths) 171 | { 172 | Plugin.Log.LogMessage("Got asset for load: " + assetPath); 173 | UnityEngine.Object loadedAsset = bundle.LoadAsset(assetPath); 174 | if (loadedAsset == null) 175 | { 176 | Plugin.Log.LogWarning($"Skipped/failed loading an asset (from bundle '{bundle.name}') - Asset path: {loadedAsset}"); 177 | continue; 178 | } 179 | 180 | string text2 = legacyLoad ? assetPath.ToUpper() : assetPath.ToLower(); 181 | 182 | if (assets.ContainsKey(text2)) 183 | { 184 | Plugin.Log.LogError("BundleAPI got duplicate asset!"); 185 | return; 186 | } 187 | assets.TryAdd(text2, loadedAsset); 188 | Plugin.Log.LogMessage("Loaded asset: " + loadedAsset.name); 189 | } 190 | } 191 | finally 192 | { 193 | bundle?.Unload(false); 194 | } 195 | } 196 | 197 | /// 198 | /// Get an asset from all the loaded assets. Use this to access your custom assets. Returned value may be null. 199 | /// 200 | public static TAsset GetLoadedAsset(string itemPath) where TAsset : UnityEngine.Object 201 | { 202 | UnityEngine.Object? asset = null; 203 | if (LegacyLoadingEnabled) 204 | assets.TryGetValue(itemPath.ToUpper(), out asset); 205 | 206 | if (asset == null) 207 | assets.TryGetValue(itemPath.ToLower(), out asset); 208 | 209 | return (TAsset)asset; 210 | } 211 | 212 | /// 213 | /// Loads an entire asset bundle. It is recommended to use this to load asset bundles. 214 | /// 215 | /// The file system path of the asset bundle. 216 | /// Whether or not to cache the loaded bundle. Set to if you cache it yourself. 217 | /// A containing all assets from the bundle mapped to their path in lowercase. 218 | public static LoadedAssetBundle LoadAssetBundle(string filePath, bool cache = true) 219 | { 220 | if (loadedAssetBundles.TryGetValue(filePath, out LoadedAssetBundle _assets)) 221 | return _assets; 222 | 223 | Dictionary assetPairs = new Dictionary(); 224 | 225 | AssetBundle bundle = AssetBundle.LoadFromFile(filePath); 226 | try 227 | { 228 | string[] assetPaths = bundle.GetAllAssetNames(); 229 | //Plugin.Log.LogMessage("Assets: [" + string.Join(", ", array) + "]"); 230 | foreach (string assetPath in assetPaths) 231 | { 232 | UnityEngine.Object loadedAsset = bundle.LoadAsset(assetPath); 233 | if (loadedAsset == null) 234 | { 235 | Plugin.Log.LogWarning($"Skipped/failed loading an asset (from bundle '{bundle.name}') - Asset path: {loadedAsset}"); 236 | continue; 237 | } 238 | 239 | string path = assetPath.ToLower(); 240 | 241 | if (assets.ContainsKey(path)) 242 | { 243 | Plugin.Log.LogError("BundleAPI got duplicate asset!"); 244 | return null; 245 | } 246 | 247 | assets.TryAdd(path, loadedAsset); 248 | 249 | assetPairs.Add(path, loadedAsset); 250 | } 251 | } 252 | finally 253 | { 254 | bundle?.Unload(false); 255 | } 256 | 257 | if (cache) loadedAssetBundles.Add(filePath, new LoadedAssetBundle(assetPairs)); 258 | 259 | return new LoadedAssetBundle(assetPairs); 260 | } 261 | 262 | private static void LoadAssetsCompleted() 263 | { 264 | Plugin.Log.LogMessage("BundleAPI finished loading all assets."); 265 | } 266 | } 267 | } 268 | #pragma warning restore CS0618 // Type or member is obsolete - we're the ones obsoleting things. 269 | -------------------------------------------------------------------------------- /LC-API/BundleAPI/LoadedAssetBundle.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace LC_API.BundleAPI 4 | { 5 | /// 6 | /// A loaded asset bundle. 7 | /// 8 | public class LoadedAssetBundle 9 | { 10 | private Dictionary _loadedAssets; 11 | 12 | internal LoadedAssetBundle(Dictionary loadedAssets) 13 | { 14 | _loadedAssets = loadedAssets; 15 | } 16 | 17 | /// 18 | /// Gets an asset from the bundle. 19 | /// 20 | /// The type of the asset. Usually . 21 | /// The path to the asset in the bundle. 22 | /// The asset. 23 | public TAsset GetAsset(string path) where TAsset : UnityEngine.Object 24 | { 25 | string lowerPath = path.ToLower(); 26 | 27 | if (_loadedAssets.TryGetValue(lowerPath, out UnityEngine.Object obj)) 28 | { 29 | return obj as TAsset; 30 | } 31 | 32 | return null; 33 | } 34 | 35 | /// 36 | /// Tries to get an asset from the bundle. 37 | /// 38 | /// The type of the asset. Usually . 39 | /// The path to the asset in the bundle. 40 | /// Outputs the asset. 41 | /// if the asset is found. if the asset isn't found, or couldn't be casted to TAsset 42 | public bool TryGetAsset(string path, out TAsset asset) where TAsset : UnityEngine.Object 43 | { 44 | string lowerPath = path.ToLower(); 45 | 46 | asset = null; 47 | 48 | if (_loadedAssets.TryGetValue(lowerPath, out UnityEngine.Object obj)) 49 | { 50 | if (obj is TAsset tasset) asset = tasset; 51 | } 52 | 53 | return asset != null; 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /LC-API/CheatDatabase.cs: -------------------------------------------------------------------------------- 1 | using BepInEx; 2 | using BepInEx.Bootstrap; 3 | using LC_API.GameInterfaceAPI.Features; 4 | using LC_API.Networking; 5 | using System.Collections.Generic; 6 | 7 | namespace LC_API 8 | { 9 | internal static class CheatDatabase 10 | { 11 | const string SIG_REQ_GUID = "LC_API_ReqGUID"; 12 | const string SIG_SEND_MODS = "LC_APISendMods"; 13 | 14 | private static Dictionary PluginsLoaded = new Dictionary(); 15 | 16 | public static void RunLocalCheatDetector() 17 | { 18 | PluginsLoaded = Chainloader.PluginInfos; 19 | 20 | foreach (PluginInfo info in PluginsLoaded.Values) 21 | { 22 | switch (info.Metadata.GUID) 23 | { 24 | case "mikes.lethalcompany.mikestweaks": 25 | case "mom.llama.enhancer": 26 | case "Posiedon.GameMaster": 27 | case "LethalCompanyScalingMaster": 28 | case "verity.amberalert": 29 | ServerAPI.ModdedServer.SetServerModdedOnly(); 30 | break; 31 | } 32 | } 33 | 34 | } 35 | 36 | public static void OtherPlayerCheatDetector() 37 | { 38 | Plugin.Log.LogWarning("Asking all other players for their mod list.."); 39 | Player.LocalPlayer.QueueTip("Mod List:", "Asking all other players for installed mods.."); 40 | Player.LocalPlayer.QueueTip("Mod List:", "Check the logs for more detailed results.\n(Note that if someone doesnt show up on the list, they may not have LC_API installed)"); 41 | Network.Broadcast(SIG_REQ_GUID); 42 | } 43 | 44 | [NetworkMessage(SIG_SEND_MODS)] 45 | internal static void ReceivedModListHandler(ulong senderId, List mods) 46 | { 47 | Player player = Player.Get(senderId); 48 | string data = $"{player.Username} responded with these mods:\n{string.Join("\n", mods)}"; 49 | Player.LocalPlayer.QueueTip("Mod List:", data); 50 | Plugin.Log.LogWarning(data); 51 | } 52 | 53 | [NetworkMessage(SIG_REQ_GUID)] 54 | internal static void ReceivedModListHandler(ulong senderId) 55 | { 56 | List mods = new List(); 57 | foreach (PluginInfo info in PluginsLoaded.Values) 58 | { 59 | mods.Add(info.Metadata.GUID); 60 | } 61 | 62 | Network.Broadcast(SIG_SEND_MODS, mods); 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /LC-API/ClientAPI/CommandHandler.cs: -------------------------------------------------------------------------------- 1 | using BepInEx; 2 | using BepInEx.Configuration; 3 | using GameNetcodeStuff; 4 | using HarmonyLib; 5 | using System; 6 | using System.Collections.Generic; 7 | using System.Linq; 8 | using System.Reflection; 9 | using System.Reflection.Emit; 10 | using UnityEngine.EventSystems; 11 | 12 | namespace LC_API.ClientAPI 13 | { 14 | /// 15 | /// Provides an easy way for developers to add chat-based commands. 16 | /// 17 | public static class CommandHandler 18 | { 19 | internal static ConfigEntry commandPrefix; 20 | 21 | internal static Dictionary> CommandHandlers = new Dictionary>(); 22 | 23 | internal static Dictionary> CommandAliases = new Dictionary>(); 24 | 25 | /// 26 | /// Registers a command with no aliases. 27 | /// 28 | /// The command string. No spaces. 29 | /// The handler itself. Passes a string[] of arguments. 30 | /// Whether or not the command handler was added. 31 | public static bool RegisterCommand(string command, Action handler) 32 | { 33 | // The handler is not capable of handling commands with spaces. 34 | if (command.Contains(" ") || CommandHandlers.ContainsKey(command)) return false; 35 | 36 | CommandHandlers.Add(command, handler); 37 | 38 | return true; 39 | } 40 | 41 | /// 42 | /// Registers a command with aliases. 43 | /// 44 | /// The command string. No spaces. 45 | /// A list of aliases. None of them can have spaces, and a handler cannot exist with that string. 46 | /// The handler itself. Passes a string[] of arguments. 47 | /// 48 | public static bool RegisterCommand(string command, List aliases, Action handler) 49 | { 50 | // The handler is not capable of handling commands with spaces. 51 | if (command.Contains(" ") || GetCommandHandler(command) != null) return false; 52 | 53 | foreach (string alias in aliases) 54 | { 55 | if (alias.Contains(" ") || GetCommandHandler(alias) != null) return false; 56 | } 57 | 58 | CommandHandlers.Add(command, handler); 59 | 60 | CommandAliases.Add(command, aliases); 61 | 62 | return true; 63 | } 64 | 65 | /// 66 | /// Unregisters a command. 67 | /// 68 | /// The command string to unregister. 69 | /// true if the command existed and was unregistered, false otherwise. 70 | public static bool UnregisterCommand(string command) 71 | { 72 | CommandAliases.Remove(command); 73 | return CommandHandlers.Remove(command); 74 | } 75 | 76 | internal static Action GetCommandHandler(string command) 77 | { 78 | if (CommandHandlers.TryGetValue(command, out var handler)) return handler; 79 | 80 | foreach (var alias in CommandAliases) 81 | { 82 | if (alias.Value.Contains(command)) return CommandHandlers[alias.Key]; 83 | } 84 | 85 | return null; 86 | } 87 | 88 | internal static bool TryGetCommandHandler(string command, out Action handler) 89 | { 90 | handler = GetCommandHandler(command); 91 | return handler != null; 92 | } 93 | 94 | internal static class SubmitChatPatch 95 | { 96 | private static bool HandleMessage(HUDManager manager) 97 | { 98 | string message = manager.chatTextField.text; 99 | 100 | if (!message.IsNullOrWhiteSpace() && message.StartsWith(commandPrefix.Value)) 101 | { 102 | string[] split = message.Split(' '); 103 | 104 | string command = split[0].Substring(commandPrefix.Value.Length); 105 | 106 | if (TryGetCommandHandler(command, out var handler)) 107 | { 108 | string[] arguments = split.Skip(1).ToArray(); 109 | try 110 | { 111 | handler.Invoke(arguments); 112 | } 113 | catch (Exception ex) 114 | { 115 | Plugin.Log.LogError($"Error handling command: {command}"); 116 | Plugin.Log.LogError(ex); 117 | } 118 | 119 | manager.localPlayer.isTypingChat = false; 120 | manager.chatTextField.text = ""; 121 | EventSystem.current.SetSelectedGameObject(null); 122 | manager.typingIndicator.enabled = false; 123 | 124 | return true; 125 | } 126 | } 127 | 128 | return false; 129 | } 130 | 131 | internal static IEnumerable Transpiler(IEnumerable instructions, ILGenerator generator) 132 | { 133 | List newInstructions = new List(instructions); 134 | 135 | Label returnLabel = generator.DefineLabel(); 136 | 137 | newInstructions[newInstructions.Count - 1].labels.Add(returnLabel); 138 | 139 | int index = newInstructions.FindIndex(i => i.opcode == OpCodes.Ldfld && 140 | (FieldInfo)i.operand == AccessTools.Field(typeof(PlayerControllerB), nameof(PlayerControllerB.isPlayerDead))) - 2; 141 | 142 | newInstructions.InsertRange(index, new CodeInstruction[] 143 | { 144 | // if (SubmitChatPatch.HandleMessage(this)) return; 145 | new CodeInstruction(OpCodes.Ldarg_0).MoveLabelsFrom(newInstructions[index]), 146 | new CodeInstruction(OpCodes.Call, AccessTools.Method(typeof(SubmitChatPatch), nameof(SubmitChatPatch.HandleMessage))), 147 | new CodeInstruction(OpCodes.Brtrue, returnLabel) 148 | }); 149 | 150 | for (int z = 0; z < newInstructions.Count; z++) yield return newInstructions[z]; 151 | } 152 | } 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /LC-API/Comp/LC_APIManager.cs: -------------------------------------------------------------------------------- 1 | using LC_API.GameInterfaceAPI; 2 | using LC_API.GameInterfaceAPI.Features; 3 | using LC_API.ServerAPI; 4 | using System.Collections; 5 | using UnityEngine; 6 | using UnityEngine.SceneManagement; 7 | 8 | namespace LC_API.Comp 9 | { 10 | internal class LC_APIManager : MonoBehaviour 11 | { 12 | public static MenuManager MenuManager; 13 | private static int playerCount; 14 | private static bool wanttoCheckMods; 15 | private static float lobbychecktimer; 16 | 17 | public void Awake() 18 | { 19 | SceneManager.sceneLoaded += OnSceneLoaded; 20 | } 21 | 22 | public void Update() 23 | { 24 | GameState.GSUpdate(); 25 | GameTips.UpdateInternal(); 26 | if (!ModdedServer.setModdedOnly) 27 | { 28 | ModdedServer.OnSceneLoaded(); 29 | } 30 | else if (ModdedServer.ModdedOnly) 31 | { 32 | if (MenuManager != null) 33 | { 34 | if (MenuManager.versionNumberText) 35 | { 36 | MenuManager.versionNumberText.text = $"v{GameNetworkManager.Instance.gameVersionNum - 16440}\nMOD"; 37 | } 38 | } 39 | } 40 | 41 | if (GameNetworkManager.Instance != null) 42 | { 43 | if (playerCount < GameNetworkManager.Instance.connectedPlayers) 44 | { 45 | lobbychecktimer = -4.5f; 46 | wanttoCheckMods = true; 47 | } 48 | playerCount = GameNetworkManager.Instance.connectedPlayers; 49 | } 50 | if (lobbychecktimer < 0) 51 | { 52 | lobbychecktimer += Time.deltaTime; 53 | } 54 | else if (wanttoCheckMods && HUDManager.Instance != null && Player.HostPlayer != null && Player.LocalPlayer != null) 55 | { 56 | wanttoCheckMods = false; 57 | CD(); 58 | } 59 | } 60 | 61 | // For pre-placed items 62 | internal void OnSceneLoaded(Scene scene, LoadSceneMode mode) 63 | { 64 | foreach (GrabbableObject grabbable in FindObjectsOfType()) 65 | { 66 | if (!grabbable.TryGetComponent(out GameInterfaceAPI.Features.Item _)) 67 | { 68 | grabbable.gameObject.AddComponent(); 69 | } 70 | } 71 | } 72 | 73 | private void CD() 74 | { 75 | CheatDatabase.OtherPlayerCheatDetector(); 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /LC-API/Data/NetworkBroadcastDataType.cs: -------------------------------------------------------------------------------- 1 | namespace LC_API.Data 2 | { 3 | internal enum NetworkBroadcastDataType 4 | { 5 | Unknown, 6 | BDint, 7 | BDfloat, 8 | BDvector3, 9 | BDstring, 10 | BDlistString 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /LC-API/Data/ShipState.cs: -------------------------------------------------------------------------------- 1 | namespace LC_API.Data 2 | { 3 | /// 4 | /// Describes the state the company ship is currently in. 5 | /// 6 | public enum ShipState 7 | { 8 | /// 9 | /// The ship is currently in orbit. 10 | /// Players are likely either goofing off, AFK, or picking the next moon to visit. 11 | /// 12 | InOrbit, 13 | /// 14 | /// The ship is currently on a moon. 15 | /// Players are likely getting torn limb from limb, getting , or witnessing horrors target their fellow man. 16 | /// 17 | OnMoon, 18 | /// 19 | /// The ship has been told to leave and it is currently doing so. 20 | /// This could either be caused by ghosts of players' pasts (dead players) telling the autopilot to do so, or a living player pulling the lever. Either way, the ship is taking off. 21 | /// 22 | LeavingMoon, 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /LC-API/Exceptions/NoAuthorityException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace LC_API.Exceptions 4 | { 5 | internal class NoAuthorityException : Exception 6 | { 7 | internal NoAuthorityException(string message) : base(message) { } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /LC-API/Extensions/DelegateExtensions.cs: -------------------------------------------------------------------------------- 1 | using BepInEx; 2 | using BepInEx.Logging; 3 | using HarmonyLib; 4 | using System; 5 | using System.Linq; 6 | using System.Reflection; 7 | 8 | namespace LC_API.Extensions 9 | { 10 | /// 11 | /// Extension methods for "Delegates". Basically makes working with 'callable' (invokable) things easier. 12 | /// 13 | public static class DelegateExtensions 14 | { 15 | private static readonly PropertyInfo PluginGetLogger = AccessTools.Property(typeof(BaseUnityPlugin), "Logger"); 16 | 17 | /// 18 | /// Call an action (usually a list of hook callbacks) in a way that logs errors instead of throwing them. This avoids only executing some callbacks if one errors out. 19 | /// 20 | public static void InvokeActionSafe(this Action action) 21 | { 22 | if (action == null) return; 23 | 24 | foreach (Delegate invoker in action.GetInvocationList()) 25 | { 26 | try 27 | { 28 | Action call = (Action)invoker; 29 | call(); 30 | } 31 | catch (Exception ex) 32 | { 33 | Plugin.Log.LogError("Exception while invoking hook callback!"); 34 | 35 | string asmName = invoker.GetMethodInfo().DeclaringType.Assembly.FullName; 36 | PluginInfo plugin = BepInEx.Bootstrap.Chainloader.PluginInfos.Values.FirstOrDefault(pi => pi.Instance.GetType().Assembly.FullName == asmName); 37 | if (plugin == null) 38 | { 39 | Plugin.Log.LogError(ex.ToString()); 40 | return; 41 | } 42 | 43 | var pluginsLogger = (ManualLogSource)PluginGetLogger.GetValue(plugin.Instance); 44 | pluginsLogger.LogError(ex.ToString()); 45 | } 46 | } 47 | } 48 | 49 | 50 | /// 51 | /// Call an action (usually a list of hook callbacks) in a way that logs errors instead of throwing them. This avoids only executing some callbacks if one errors out. 52 | /// 53 | public static void InvokeActionSafe(this Action action, T param) 54 | { 55 | if (action == null) return; 56 | 57 | foreach (Delegate invoker in action.GetInvocationList()) 58 | { 59 | try 60 | { 61 | Action call = (Action)invoker; 62 | call(param); 63 | } 64 | catch (Exception ex) 65 | { 66 | Plugin.Log.LogError("Exception while invoking hook callback!"); 67 | 68 | string asmName = invoker.GetMethodInfo().DeclaringType.Assembly.FullName; 69 | PluginInfo plugin = BepInEx.Bootstrap.Chainloader.PluginInfos.Values.FirstOrDefault(pi => pi.Instance.GetType().Assembly.FullName == asmName); 70 | if (plugin == null) 71 | { 72 | Plugin.Log.LogError(ex.ToString()); 73 | return; 74 | } 75 | 76 | var pluginsLogger = (ManualLogSource)PluginGetLogger.GetValue(plugin.Instance); 77 | pluginsLogger.LogError(ex.ToString()); 78 | } 79 | } 80 | } 81 | 82 | /// 83 | /// Call an action (usually a list of hook callbacks) in a way that logs errors instead of throwing them. This avoids only executing some callbacks if one errors out. 84 | /// 85 | public static void InvokeActionSafe(this Action action, T1 param1, T2 param2) 86 | { 87 | if (action == null) return; 88 | 89 | foreach (Delegate invoker in action.GetInvocationList()) 90 | { 91 | try 92 | { 93 | Action call = (Action)invoker; 94 | call(param1, param2); 95 | } 96 | catch (Exception ex) 97 | { 98 | Plugin.Log.LogError("Exception while invoking hook callback!"); 99 | 100 | string asmName = invoker.GetMethodInfo().DeclaringType.Assembly.FullName; 101 | PluginInfo plugin = BepInEx.Bootstrap.Chainloader.PluginInfos.Values.FirstOrDefault(pi => pi.Instance.GetType().Assembly.FullName == asmName); 102 | if (plugin == null) 103 | { 104 | Plugin.Log.LogError(ex.ToString()); 105 | return; 106 | } 107 | 108 | var pluginsLogger = (ManualLogSource)PluginGetLogger.GetValue(plugin.Instance); 109 | pluginsLogger.LogError(ex.ToString()); 110 | } 111 | } 112 | } 113 | 114 | 115 | // internal because it uses the slow DynamicInvoke, and almost any event delegate can be an Action(//etc), at least in modding. 116 | internal static void InvokeParameterlessDelegate(this T paramlessDelegate) where T : Delegate 117 | { 118 | if (paramlessDelegate == null) return; 119 | 120 | foreach (Delegate invoker in paramlessDelegate.GetInvocationList()) 121 | { 122 | try 123 | { 124 | T call = (T)invoker; 125 | call.DynamicInvoke(); 126 | } 127 | catch (Exception ex) 128 | { 129 | Plugin.Log.LogError("Exception while invoking hook callback!"); 130 | 131 | string asmName = invoker.GetMethodInfo().DeclaringType.Assembly.FullName; 132 | PluginInfo plugin = BepInEx.Bootstrap.Chainloader.PluginInfos.Values.FirstOrDefault(pi => pi.Instance.GetType().Assembly.FullName == asmName); 133 | if (plugin == null) 134 | { 135 | Plugin.Log.LogError(ex.ToString()); 136 | return; 137 | } 138 | 139 | var pluginsLogger = (ManualLogSource)PluginGetLogger.GetValue(plugin.Instance); 140 | pluginsLogger.LogError(ex.ToString()); 141 | } 142 | } 143 | } 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /LC-API/GameInterfaceAPI/Events/Cache/Player.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace LC_API.GameInterfaceAPI.Events.Cache 4 | { 5 | internal static class Player 6 | { 7 | internal static List ConnectedPlayers { get; private set; } = new List(); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /LC-API/GameInterfaceAPI/Events/EventArgs/Player/DiedEventArgs.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | 3 | namespace LC_API.GameInterfaceAPI.Events.EventArgs.Player 4 | { 5 | /// 6 | /// Contains all the information after a dies. 7 | /// 8 | public class DiedEventArgs : System.EventArgs 9 | { 10 | /// 11 | /// Gets the player that died. 12 | /// 13 | public Features.Player Player { get; } 14 | 15 | /// 16 | /// Gets the force that was added to the ragdoll. 17 | /// 18 | public Vector3 Force { get; } 19 | 20 | /// 21 | /// Gets whether or not a ragdoll was spawned. 22 | /// 23 | public bool SpawnBody { get; } 24 | 25 | /// 26 | /// Gets the cause of death. 27 | /// 28 | public CauseOfDeath CauseOfDeath { get; } 29 | 30 | /// 31 | /// Gets the death animation index. 32 | /// 33 | public int DeathAnimation { get; } 34 | 35 | /// 36 | /// Initializes a new instance of the class. 37 | /// 38 | /// 39 | /// 40 | /// 41 | /// 42 | /// 43 | public DiedEventArgs(Features.Player player, Vector3 force, bool spawnBody, CauseOfDeath causeOfDeath, int deathAnimation) 44 | { 45 | Player = player; 46 | Force = force; 47 | SpawnBody = spawnBody; 48 | CauseOfDeath = causeOfDeath; 49 | DeathAnimation = deathAnimation; 50 | } 51 | } 52 | } -------------------------------------------------------------------------------- /LC-API/GameInterfaceAPI/Events/EventArgs/Player/DroppingItemEventArgs.cs: -------------------------------------------------------------------------------- 1 | using Unity.Netcode; 2 | using UnityEngine; 3 | 4 | namespace LC_API.GameInterfaceAPI.Events.EventArgs.Player 5 | { 6 | public class DroppingItemEventArgs : System.EventArgs 7 | { 8 | public Features.Player Player { get; } 9 | 10 | public Features.Item Item { get; } 11 | 12 | public bool Placing { get; } 13 | 14 | public Vector3 TargetPosition { get; set; } 15 | 16 | public int FloorYRotation { get; set; } 17 | 18 | public NetworkObject ParentObjectTo { get; set; } 19 | 20 | public bool MatchRotationOfParent { get; set; } 21 | 22 | public bool DroppedInShip { get; set; } 23 | 24 | public bool IsAllowed { get; set; } = true; 25 | 26 | public DroppingItemEventArgs(Features.Player player, Features.Item item, bool placeObject, Vector3 targetPosition, 27 | int floorYRotation, NetworkObject parentObjectTo, bool matchRotationOfParent, bool droppedInShip) 28 | { 29 | Player = player; 30 | Item = item; 31 | Placing = placeObject; 32 | TargetPosition = targetPosition; 33 | FloorYRotation = floorYRotation; 34 | ParentObjectTo = parentObjectTo; 35 | MatchRotationOfParent = matchRotationOfParent; 36 | DroppedInShip = droppedInShip; 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /LC-API/GameInterfaceAPI/Events/EventArgs/Player/DyingEventArgs.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | 3 | namespace LC_API.GameInterfaceAPI.Events.EventArgs.Player 4 | { 5 | /// 6 | /// Contains all the information before a is dies. 7 | /// 8 | public class DyingEventArgs : System.EventArgs 9 | { 10 | /// 11 | /// Gets the player that is dying. 12 | /// 13 | public Features.Player Player { get; } 14 | 15 | /// 16 | /// Gets or sets the force to add to the ragdoll. 17 | /// 18 | public Vector3 Force { get; set; } 19 | 20 | /// 21 | /// Gets or sets whether or not to spawn a ragdoll. 22 | /// 23 | public bool SpawnBody { get; set; } 24 | 25 | /// 26 | /// Gets or sets the cause of death. 27 | /// 28 | public CauseOfDeath CauseOfDeath { get; set; } 29 | 30 | /// 31 | /// Gets or sets the death animation index. 32 | /// 33 | public int DeathAnimation { get; set; } 34 | 35 | /// 36 | /// Gets or sets whether this death is allowed to occur. 37 | /// 38 | public bool IsAllowed { get; set; } = true; 39 | 40 | /// 41 | /// Initializes a new instance of the class. 42 | /// 43 | /// 44 | /// 45 | /// 46 | /// 47 | /// 48 | public DyingEventArgs(Features.Player player, Vector3 force, bool spawnBody, CauseOfDeath causeOfDeath, int deathAnimation) 49 | { 50 | Player = player; 51 | Force = force; 52 | SpawnBody = spawnBody; 53 | CauseOfDeath = causeOfDeath; 54 | DeathAnimation = deathAnimation; 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /LC-API/GameInterfaceAPI/Events/EventArgs/Player/GrabbedItemEventArgs.cs: -------------------------------------------------------------------------------- 1 | namespace LC_API.GameInterfaceAPI.Events.EventArgs.Player 2 | { 3 | public class GrabbedItemEventArgs : System.EventArgs 4 | { 5 | public Features.Player Player { get; } 6 | 7 | public Features.Item Item { get; } 8 | 9 | public GrabbedItemEventArgs(Features.Player player, Features.Item item) 10 | { 11 | Player = player; 12 | Item = item; 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /LC-API/GameInterfaceAPI/Events/EventArgs/Player/GrabbingItemEventArgs.cs: -------------------------------------------------------------------------------- 1 | namespace LC_API.GameInterfaceAPI.Events.EventArgs.Player 2 | { 3 | /// 4 | /// Contains all the information before a picks up an . 5 | /// 6 | public class GrabbingItemEventArgs : System.EventArgs 7 | { 8 | /// 9 | /// The picking up the . 10 | /// 11 | public Features.Player Player { get; } 12 | 13 | /// 14 | /// The being picked up. 15 | /// 16 | public Features.Item Item { get; } 17 | 18 | /// 19 | /// Gets or sets whether this is allowed to occur. 20 | /// 21 | public bool IsAllowed { get; set; } = true; 22 | 23 | /// 24 | /// Initializes a new instance of the class. 25 | /// 26 | /// 27 | /// 28 | public GrabbingItemEventArgs(Features.Player player, Features.Item item) 29 | { 30 | Player = player; 31 | Item = item; 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /LC-API/GameInterfaceAPI/Events/EventArgs/Player/HurtEventArgs.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | 3 | namespace LC_API.GameInterfaceAPI.Events.EventArgs.Player 4 | { 5 | /// 6 | /// Contains all the information after a is hurt. 7 | /// 8 | public class HurtEventArgs : System.EventArgs 9 | { 10 | /// 11 | /// Gets the player that is taking damage. 12 | /// 13 | public Features.Player Player { get; } 14 | 15 | /// 16 | /// Gets the amount of damage the took. 17 | /// 18 | public int Damage { get; } 19 | 20 | /// 21 | /// Gets or sets whether or not this damage will play sfx, if it has any. 22 | /// 23 | public bool HasSFX { get; set; } 24 | 25 | /// 26 | /// Gets or sets the cause of death. 27 | /// 28 | public CauseOfDeath CauseOfDeath { get; set; } 29 | 30 | /// 31 | /// Gets or sets the death animation index. 32 | /// 33 | public int DeathAnimation { get; set; } 34 | 35 | /// 36 | /// Gets or sets whether or not this damage is considered fall damage. 37 | /// 38 | public bool FallDamage { get; set; } 39 | 40 | /// 41 | /// Gets or sets the force to add to the ragdoll if the player is killed. 42 | /// 43 | public Vector3 Force { get; set; } 44 | 45 | /// 46 | /// Gets whether or not this damage will kill the player. 47 | /// 48 | public bool Killing => Player.Health <= 0; 49 | 50 | /// 51 | /// Initializes a new instance of the class. 52 | /// 53 | /// 54 | /// 55 | /// 56 | /// 57 | /// 58 | /// 59 | /// 60 | public HurtEventArgs(Features.Player player, int damage, bool hasSFX, CauseOfDeath causeOfDeath, int deathAnimation, 61 | bool fallDamage, Vector3 force) 62 | { 63 | Player = player; 64 | Damage = damage; 65 | HasSFX = hasSFX; 66 | CauseOfDeath = causeOfDeath; 67 | DeathAnimation = deathAnimation; 68 | FallDamage = fallDamage; 69 | Force = force; 70 | } 71 | } 72 | } -------------------------------------------------------------------------------- /LC-API/GameInterfaceAPI/Events/EventArgs/Player/HurtingEventArgs.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | 3 | namespace LC_API.GameInterfaceAPI.Events.EventArgs.Player 4 | { 5 | /// 6 | /// Contains all the information before a is hurt. 7 | /// 8 | public class HurtingEventArgs : System.EventArgs 9 | { 10 | /// 11 | /// Gets the player that is taking damage. 12 | /// 13 | public Features.Player Player { get; } 14 | 15 | /// 16 | /// Gets or sets the amount of damage the will take. 17 | /// 18 | public int Damage { get; set; } 19 | 20 | /// 21 | /// Gets or sets whether or not this damage will play sfx, if it has any. 22 | /// 23 | public bool HasSFX { get; set; } 24 | 25 | /// 26 | /// Gets or sets the cause of death. 27 | /// 28 | public CauseOfDeath CauseOfDeath { get; set; } 29 | 30 | /// 31 | /// Gets or sets the death animation index. 32 | /// 33 | public int DeathAnimation { get; set; } 34 | 35 | /// 36 | /// Gets or sets whether or not this damage is considered fall damage. 37 | /// 38 | public bool FallDamage { get; set; } 39 | 40 | /// 41 | /// Gets or sets the force to add to the ragdoll if the player is killed. 42 | /// 43 | public Vector3 Force { get; set; } 44 | 45 | /// 46 | /// Gets or sets whether this damage is allowed to occur. 47 | /// 48 | public bool IsAllowed { get; set; } = true; 49 | 50 | /// 51 | /// Initializes a new instance of the class. 52 | /// 53 | /// 54 | /// 55 | /// 56 | /// 57 | /// 58 | /// 59 | /// 60 | public HurtingEventArgs(Features.Player player, int damage, bool hasSFX, CauseOfDeath causeOfDeath, int deathAnimation, 61 | bool fallDamage, Vector3 force) 62 | { 63 | Player = player; 64 | Damage = damage; 65 | HasSFX = hasSFX; 66 | CauseOfDeath = causeOfDeath; 67 | DeathAnimation = deathAnimation; 68 | FallDamage = fallDamage; 69 | Force = force; 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /LC-API/GameInterfaceAPI/Events/EventArgs/Player/JoinedEventArgs.cs: -------------------------------------------------------------------------------- 1 | namespace LC_API.GameInterfaceAPI.Events.EventArgs.Player 2 | { 3 | /// 4 | /// Contains all the information after a joined the server. Including the host. 5 | /// 6 | public class JoinedEventArgs : System.EventArgs 7 | { 8 | /// 9 | /// Gets the joined player. 10 | /// 11 | public Features.Player Player { get; } 12 | 13 | /// 14 | /// Initializes a new instance of the class. 15 | /// 16 | /// 17 | public JoinedEventArgs(Features.Player player) 18 | { 19 | Player = player; 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /LC-API/GameInterfaceAPI/Events/EventArgs/Player/LeftEventArgs.cs: -------------------------------------------------------------------------------- 1 | namespace LC_API.GameInterfaceAPI.Events.EventArgs.Player 2 | { 3 | /// 4 | /// Contains all the information right before a leaves the server. Including the local client. 5 | /// 6 | public class LeftEventArgs : System.EventArgs 7 | { 8 | /// 9 | /// Gets the player that is leaving. 10 | /// 11 | public Features.Player Player { get; } 12 | 13 | /// 14 | /// Initializes a new instance of the class. 15 | /// 16 | /// 17 | public LeftEventArgs(Features.Player player) 18 | { 19 | Player = player; 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /LC-API/GameInterfaceAPI/Events/EventArgs/Player/StartGrabbingItemEventArgs.cs: -------------------------------------------------------------------------------- 1 | namespace LC_API.GameInterfaceAPI.Events.EventArgs.Player 2 | { 3 | public class StartGrabbingItemEventArgs : System.EventArgs 4 | { 5 | public Features.Player Player { get; } 6 | 7 | public Features.Item Item { get; } 8 | 9 | public bool IsAllowed { get; set; } = true; 10 | 11 | public StartGrabbingItemEventArgs(Features.Player player, Features.Item item) 12 | { 13 | Player = player; 14 | Item = item; 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /LC-API/GameInterfaceAPI/Events/EventExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace LC_API.GameInterfaceAPI.Events 4 | { 5 | #pragma warning disable CS1591 // Missing XML comment for publicly visible type or member 6 | public static class EventExtensions 7 | { 8 | public static void InvokeSafely(this Events.CustomEventHandler ev, T arg) 9 | where T : System.EventArgs 10 | { 11 | if (ev == null) 12 | return; 13 | 14 | foreach (Events.CustomEventHandler handler in ev.GetInvocationList()) 15 | { 16 | try 17 | { 18 | handler(arg); 19 | } 20 | catch (Exception ex) 21 | { 22 | Plugin.Log.LogError(ex); 23 | } 24 | } 25 | } 26 | 27 | public static void InvokeSafely(this Events.CustomEventHandler ev) 28 | { 29 | if (ev == null) 30 | return; 31 | 32 | foreach (Events.CustomEventHandler handler in ev.GetInvocationList()) 33 | { 34 | try 35 | { 36 | handler(); 37 | } 38 | catch (Exception ex) 39 | { 40 | Plugin.Log.LogError(ex); 41 | } 42 | } 43 | } 44 | } 45 | } 46 | #pragma warning restore CS1591 // Missing XML comment for publicly visible type or member -------------------------------------------------------------------------------- /LC-API/GameInterfaceAPI/Events/Events.cs: -------------------------------------------------------------------------------- 1 | using HarmonyLib; 2 | using System; 3 | using System.Diagnostics; 4 | 5 | namespace LC_API.GameInterfaceAPI.Events 6 | { 7 | #pragma warning disable CS1591 // Missing XML comment for publicly visible type or member 8 | public static class Events 9 | { 10 | public delegate void CustomEventHandler(TEventArgs ev) 11 | where TEventArgs : System.EventArgs; 12 | 13 | public delegate void CustomEventHandler(); 14 | 15 | internal static void Patch(Harmony harmony) 16 | { 17 | var method = new StackTrace().GetFrame(1).GetMethod(); 18 | var assembly = method.ReflectedType.Assembly; 19 | foreach (Type t in AccessTools.GetTypesFromAssembly(assembly)) 20 | { 21 | if (t.Namespace.StartsWith("LC_API.GameInterfaceAPI.Events.Patches")) 22 | { 23 | harmony.CreateClassProcessor(t).Patch(); 24 | } 25 | } 26 | } 27 | } 28 | } 29 | #pragma warning restore CS1591 // Missing XML comment for publicly visible type or member 30 | -------------------------------------------------------------------------------- /LC-API/GameInterfaceAPI/Events/Handlers/Player.cs: -------------------------------------------------------------------------------- 1 | using LC_API.GameInterfaceAPI.Events.EventArgs.Player; 2 | using static LC_API.GameInterfaceAPI.Events.Events; 3 | 4 | namespace LC_API.GameInterfaceAPI.Events.Handlers 5 | { 6 | /// 7 | /// Provides event handlers relating to Players. 8 | /// 9 | public static class Player 10 | { 11 | /// 12 | /// Invoked after a joined the server, including the host. 13 | /// 14 | public static event CustomEventHandler Joined; 15 | 16 | /// 17 | /// Invoked right before a leaves the server, including the local client. 18 | /// 19 | public static event CustomEventHandler Left; 20 | 21 | /// 22 | /// Invoked before a is hurt. 23 | /// 24 | public static event CustomEventHandler Hurting; 25 | 26 | /// 27 | /// Invoked after a is hurt. 28 | /// 29 | public static event CustomEventHandler Hurt; 30 | 31 | /// 32 | /// Invoked before a dies. 33 | /// 34 | public static event CustomEventHandler Dying; 35 | 36 | /// 37 | /// Invoked after a dies. 38 | /// 39 | public static event CustomEventHandler Died; 40 | 41 | /// 42 | /// Invoked before a starts grabbing an . 43 | /// 44 | public static event CustomEventHandler StartGrabbingItem; 45 | 46 | /// 47 | /// Invoked before a completes a grab on an . 48 | /// 49 | public static event CustomEventHandler GrabbingItem; 50 | 51 | /// 52 | /// Invoked after a completes a grab on an . 53 | /// 54 | public static event CustomEventHandler GrabbedItem; 55 | 56 | /// 57 | /// Invoked before a drops an . 58 | /// 59 | public static event CustomEventHandler DroppingItem; 60 | 61 | /// 62 | /// Called after a joined the server, including the host. 63 | /// 64 | /// The event arguments. 65 | public static void OnJoined(JoinedEventArgs ev) => Joined.InvokeSafely(ev); 66 | 67 | /// 68 | /// Called right before a leaves the server, including the local client. 69 | /// 70 | /// The event arguments. 71 | public static void OnLeft(LeftEventArgs ev) => Left.InvokeSafely(ev); 72 | 73 | /// 74 | /// Called before a is hurt. 75 | /// 76 | /// The event arguments. 77 | public static void OnHurting(HurtingEventArgs ev) => Hurting.InvokeSafely(ev); 78 | 79 | /// 80 | /// Called after a is hurt. 81 | /// 82 | /// The event arguments. 83 | public static void OnHurt(HurtEventArgs ev) => Hurt.InvokeSafely(ev); 84 | 85 | /// 86 | /// Called before a dies. 87 | /// 88 | /// The event arguments. 89 | public static void OnDying(DyingEventArgs ev) => Dying.InvokeSafely(ev); 90 | 91 | /// 92 | /// Called after a dies. 93 | /// 94 | /// The event arguments. 95 | public static void OnDied(DiedEventArgs ev) => Died.InvokeSafely(ev); 96 | 97 | /// 98 | /// Called before a starts grabbing an . 99 | /// 100 | /// The event arguments. 101 | public static void OnStartGrabbingItem(StartGrabbingItemEventArgs ev) => StartGrabbingItem.InvokeSafely(ev); 102 | 103 | /// 104 | /// Called before a completes a grab on an . 105 | /// 106 | /// The event arguments. 107 | public static void OnGrabbingItem(GrabbingItemEventArgs ev) => GrabbingItem.InvokeSafely(ev); 108 | 109 | /// 110 | /// Called after a completes a grab on an . 111 | /// 112 | /// The event arguments. 113 | public static void OnGrabbedItem(GrabbedItemEventArgs ev) => GrabbedItem.InvokeSafely(ev); 114 | 115 | /// 116 | /// Called before a drops an . 117 | /// 118 | /// The event arguments. 119 | public static void OnDroppingItem(DroppingItemEventArgs ev) => DroppingItem.InvokeSafely(ev); 120 | } 121 | } -------------------------------------------------------------------------------- /LC-API/GameInterfaceAPI/Events/Patches/Internal/DisplayTipPatch.cs: -------------------------------------------------------------------------------- 1 | using HarmonyLib; 2 | using LC_API.GameInterfaceAPI.Features; 3 | 4 | namespace LC_API.GameInterfaceAPI.Events.Patches.Internal 5 | { 6 | [HarmonyPatch(typeof(HUDManager), nameof(HUDManager.DisplayTip))] 7 | class DisplayTipPatch 8 | { 9 | // Normally we wouldn't prefix return false, however, we need all uses of `DisplayTip` to go through 10 | // our system in order to not cause improper timing. All uses of base game's `DisplayTip` will bypass queue 11 | // and immediately be shown, but preserving the currently showing tip, if there is one, and continuing it after 12 | // the tip is complete. 13 | private static bool Prefix(string headerText, string bodyText, bool isWarning, bool useSave, string prefsKey) 14 | { 15 | Features.Player player = Features.Player.LocalPlayer; 16 | 17 | if (player == null) return true; 18 | 19 | player.ShowTip(headerText, bodyText, 5, isWarning, useSave, prefsKey); 20 | 21 | return false; 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /LC-API/GameInterfaceAPI/Events/Patches/Internal/GameNetworkManagerStartPatch.cs: -------------------------------------------------------------------------------- 1 | using HarmonyLib; 2 | using LC_API.BundleAPI; 3 | using System; 4 | using System.IO; 5 | using Unity.Netcode; 6 | using UnityEngine; 7 | 8 | namespace LC_API.GameInterfaceAPI.Events.Patches.Internal 9 | { 10 | [HarmonyPatch(typeof(GameNetworkManager), nameof(GameNetworkManager.Start))] 11 | class GameNetworkManagerStartPatch 12 | { 13 | private static readonly string BUNDLE_PATH = Path.Combine(Plugin.Instance.Info.Location.Substring(0, Plugin.Instance.Info.Location.LastIndexOf(Path.DirectorySeparatorChar)), "Bundles", "networking"); 14 | 15 | private const string PLAYER_NETWORKING_ASSET_LOCATION = "assets/lc_api/playernetworkingprefab.prefab"; 16 | 17 | private static void Postfix(GameNetworkManager __instance) 18 | { 19 | if (Plugin.configVanillaSupport.Value) return; 20 | 21 | if (!File.Exists(BUNDLE_PATH)) 22 | { 23 | throw new Exception("Networking bundle not found at expected path."); 24 | } 25 | 26 | NetworkManager networkManager = __instance.GetComponent(); 27 | 28 | LoadedAssetBundle assets = BundleLoader.LoadAssetBundle(BUNDLE_PATH, false); 29 | 30 | GameObject playerObj = assets.GetAsset(PLAYER_NETWORKING_ASSET_LOCATION); 31 | playerObj.AddComponent(); 32 | playerObj.AddComponent(); 33 | networkManager.AddNetworkPrefab(playerObj); 34 | Features.Player.PlayerNetworkPrefab = playerObj; 35 | 36 | foreach (NetworkPrefab prefab in networkManager.NetworkConfig.Prefabs.Prefabs) 37 | { 38 | if (prefab.Prefab.GetComponent() != null) 39 | { 40 | prefab.Prefab.AddComponent(); 41 | } 42 | } 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /LC-API/GameInterfaceAPI/Events/Patches/Internal/PlayerControllerBStartPatch.cs: -------------------------------------------------------------------------------- 1 | using GameNetcodeStuff; 2 | using HarmonyLib; 3 | using Unity.Netcode; 4 | using UnityEngine; 5 | 6 | namespace LC_API.GameInterfaceAPI.Events.Patches.Internal 7 | { 8 | [HarmonyPatch(typeof(PlayerControllerB), nameof(PlayerControllerB.Start))] 9 | class PlayerControllerBStartPatch 10 | { 11 | private static void Postfix(PlayerControllerB __instance) 12 | { 13 | if (Plugin.configVanillaSupport.Value) return; 14 | 15 | if (__instance.IsServer && !Features.Player.TryGet(__instance, out Features.Player _)) 16 | { 17 | GameObject go = UnityEngine.Object.Instantiate(Features.Player.PlayerNetworkPrefab); 18 | go.SetActive(true); 19 | go.GetComponent().PlayerController = __instance; 20 | go.GetComponent().Spawn(false); 21 | } 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /LC-API/GameInterfaceAPI/Events/Patches/Player/Die.cs: -------------------------------------------------------------------------------- 1 | using GameNetcodeStuff; 2 | using HarmonyLib; 3 | using LC_API.GameInterfaceAPI.Events.EventArgs.Player; 4 | using System.Collections.Generic; 5 | using System.Reflection.Emit; 6 | using UnityEngine; 7 | 8 | namespace LC_API.GameInterfaceAPI.Events.Patches.Player 9 | { 10 | [HarmonyPatch(typeof(PlayerControllerB), nameof(PlayerControllerB.KillPlayer))] 11 | internal class Dying 12 | { 13 | private static DyingEventArgs CallEvent(PlayerControllerB playerController, Vector3 force, bool spawnBody, 14 | CauseOfDeath causeOfDeath, int deathAnimation) 15 | { 16 | if (Plugin.configVanillaSupport.Value) return null; 17 | 18 | DyingEventArgs ev = new DyingEventArgs(Features.Player.GetOrAdd(playerController), force, spawnBody, 19 | causeOfDeath, deathAnimation); 20 | 21 | Handlers.Player.OnDying(ev); 22 | 23 | return ev; 24 | } 25 | 26 | private static IEnumerable Transpiler(IEnumerable instructions, ILGenerator generator) 27 | { 28 | List newInstructions = new List(instructions); 29 | const int offset = 3; 30 | 31 | int index = newInstructions.FindLastIndex(i => i.OperandIs(AccessTools.Method(typeof(PlayerControllerB), 32 | nameof(PlayerControllerB.AllowPlayerDeath)))) + offset; 33 | 34 | Label nullLabel = generator.DefineLabel(); 35 | Label notAllowedLabel = generator.DefineLabel(); 36 | Label skipLabel = generator.DefineLabel(); 37 | 38 | CodeInstruction[] inst = new CodeInstruction[] 39 | { 40 | // DyingEventArgs ev = Dying.CallEvent(PlayerControllerB, Vector3, bool, CauseOfDeath, int) 41 | new CodeInstruction(OpCodes.Ldarg_0).MoveLabelsFrom(newInstructions[index]), 42 | new CodeInstruction(OpCodes.Ldarg_1), 43 | new CodeInstruction(OpCodes.Ldarg_2), 44 | new CodeInstruction(OpCodes.Ldarg_3), 45 | new CodeInstruction(OpCodes.Ldarg, 4), 46 | new CodeInstruction(OpCodes.Call, AccessTools.Method(typeof(Dying), nameof(Dying.CallEvent))), 47 | new CodeInstruction(OpCodes.Dup), 48 | 49 | // if (ev is null) -> base game code 50 | new CodeInstruction(OpCodes.Dup), 51 | new CodeInstruction(OpCodes.Brfalse_S, nullLabel), 52 | 53 | // if (!ev.IsAllowed) return 54 | new CodeInstruction(OpCodes.Call, AccessTools.PropertyGetter(typeof(DyingEventArgs), nameof(DyingEventArgs.IsAllowed))), 55 | new CodeInstruction(OpCodes.Brfalse_S, notAllowedLabel), 56 | 57 | // Duplicating the stack is more memory efficient than making a local 58 | new CodeInstruction(OpCodes.Dup), 59 | new CodeInstruction(OpCodes.Dup), 60 | new CodeInstruction(OpCodes.Dup), 61 | 62 | // bodyVelocity = ev.Force 63 | new CodeInstruction(OpCodes.Call, AccessTools.PropertyGetter(typeof(DyingEventArgs), nameof(DyingEventArgs.Force))), 64 | new CodeInstruction(OpCodes.Starg_S, 1), 65 | 66 | // spawnBody = ev.SpawnBody 67 | new CodeInstruction(OpCodes.Call, AccessTools.PropertyGetter(typeof(DyingEventArgs), nameof(DyingEventArgs.SpawnBody))), 68 | new CodeInstruction(OpCodes.Starg_S, 2), 69 | 70 | // causeOfDeath = ev.CauseOfDeath 71 | new CodeInstruction(OpCodes.Call, AccessTools.PropertyGetter(typeof(DyingEventArgs), nameof(DyingEventArgs.CauseOfDeath))), 72 | new CodeInstruction(OpCodes.Starg_S, 3), 73 | 74 | // deathAnimation = ev.DeathAnimation 75 | new CodeInstruction(OpCodes.Call, AccessTools.PropertyGetter(typeof(DyingEventArgs), nameof(DyingEventArgs.DeathAnimation))), 76 | new CodeInstruction(OpCodes.Starg_S, 4), 77 | 78 | new CodeInstruction(OpCodes.Br, skipLabel), 79 | new CodeInstruction(OpCodes.Pop).WithLabels(nullLabel), 80 | new CodeInstruction(OpCodes.Pop), 81 | new CodeInstruction(OpCodes.Br, skipLabel), 82 | new CodeInstruction(OpCodes.Pop).WithLabels(notAllowedLabel), 83 | new CodeInstruction(OpCodes.Ret) 84 | }; 85 | 86 | newInstructions.InsertRange(index, inst); 87 | 88 | newInstructions[index + inst.Length].labels.Add(skipLabel); 89 | 90 | for (int i = 0; i < newInstructions.Count; i++) yield return newInstructions[i]; 91 | } 92 | } 93 | 94 | [HarmonyPatch(typeof(PlayerControllerB), nameof(PlayerControllerB.KillPlayerClientRpc))] 95 | internal class Died 96 | { 97 | private static void Prefix(PlayerControllerB __instance, bool spawnBody, Vector3 bodyVelocity, 98 | int causeOfDeath, int deathAnimation) 99 | { 100 | if (Plugin.configVanillaSupport.Value) return; 101 | 102 | Features.Player player = Features.Player.GetOrAdd(__instance); 103 | 104 | // The local player Dying event has already been fired 105 | if (player.IsLocalPlayer) return; 106 | 107 | Handlers.Player.OnDying(new DyingEventArgs(player, bodyVelocity, 108 | spawnBody, (CauseOfDeath)causeOfDeath, deathAnimation)); 109 | } 110 | 111 | private static void Postfix(PlayerControllerB __instance, bool spawnBody, Vector3 bodyVelocity, 112 | int causeOfDeath, int deathAnimation) 113 | { 114 | if (Plugin.configVanillaSupport.Value) return; 115 | 116 | Handlers.Player.OnDied(new DiedEventArgs(Features.Player.GetOrAdd(__instance), bodyVelocity, 117 | spawnBody, (CauseOfDeath)causeOfDeath, deathAnimation)); 118 | } 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /LC-API/GameInterfaceAPI/Events/Patches/Player/DropItem.cs: -------------------------------------------------------------------------------- 1 | using GameNetcodeStuff; 2 | using HarmonyLib; 3 | using LC_API.GameInterfaceAPI.Events.EventArgs.Player; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Reflection.Emit; 7 | using System.Text; 8 | using Unity.Netcode; 9 | using UnityEngine; 10 | 11 | namespace LC_API.GameInterfaceAPI.Events.Patches.Player 12 | { 13 | [HarmonyPatch(typeof(PlayerControllerB), nameof(PlayerControllerB.DiscardHeldObject))] 14 | internal class DroppingItem 15 | { 16 | internal static DroppingItemEventArgs CallEvent(PlayerControllerB playerController, bool placeObject, Vector3 targetPosition, 17 | int floorYRotation, NetworkObject parentObjectTo, bool matchRotationOfParent, bool droppedInShip) 18 | { 19 | if (Plugin.configVanillaSupport.Value) return null; 20 | 21 | Features.Player player = Features.Player.GetOrAdd(playerController); 22 | 23 | Features.Item item = Features.Item.GetOrAdd(playerController.currentlyHeldObjectServer); 24 | 25 | DroppingItemEventArgs ev = new DroppingItemEventArgs(player, item, placeObject, targetPosition, floorYRotation, parentObjectTo, matchRotationOfParent, droppedInShip); 26 | 27 | Handlers.Player.OnDroppingItem(ev); 28 | 29 | player.CallDroppingItemOnOtherClients(item, placeObject, targetPosition, floorYRotation, parentObjectTo, matchRotationOfParent, droppedInShip); 30 | 31 | return ev; 32 | } 33 | 34 | private static IEnumerable Transpiler(IEnumerable instructions, ILGenerator generator) 35 | { 36 | List newInstructions = new List(instructions); 37 | 38 | int animIndex = newInstructions.FindIndex(i => i.opcode == OpCodes.Ldarg_1); 39 | 40 | CodeInstruction[] animatorStuff = newInstructions.GetRange(0, animIndex).ToArray(); 41 | 42 | newInstructions.RemoveRange(0, animIndex); 43 | 44 | LocalBuilder isInShipLocal = generator.DeclareLocal(typeof(bool)); 45 | 46 | { 47 | const int offset = 1; 48 | 49 | int index = newInstructions.FindIndex(i => i.opcode == OpCodes.Stloc_0) + offset; 50 | 51 | Label nullLabel = generator.DefineLabel(); 52 | Label notAllowedLabel = generator.DefineLabel(); 53 | Label skipLabel = generator.DefineLabel(); 54 | 55 | CodeInstruction[] inst = new CodeInstruction[] 56 | { 57 | // DroppingItemEventArgs ev = DroppingItem.CallEvent(PlayerControllerB, bool, Vector3, 58 | // int, NetworkObject, bool, bool) 59 | new CodeInstruction(OpCodes.Ldarg_0).MoveLabelsFrom(newInstructions[index]), 60 | new CodeInstruction(OpCodes.Ldarg_1), 61 | new CodeInstruction(OpCodes.Ldarg_3), 62 | new CodeInstruction(OpCodes.Ldloc, 4), 63 | new CodeInstruction(OpCodes.Ldarg_2), 64 | new CodeInstruction(OpCodes.Ldarg, 4), 65 | new CodeInstruction(OpCodes.Ldarg_0), 66 | new CodeInstruction(OpCodes.Ldfld, AccessTools.Field(typeof(PlayerControllerB), nameof(PlayerControllerB.isInHangarShipRoom))), 67 | new CodeInstruction(OpCodes.Call, AccessTools.Method(typeof(DroppingItem), nameof(DroppingItem.CallEvent))), 68 | 69 | // if (ev is null) -> base game code 70 | new CodeInstruction(OpCodes.Dup), 71 | new CodeInstruction(OpCodes.Brfalse_S, nullLabel), 72 | 73 | new CodeInstruction(OpCodes.Dup), 74 | 75 | // if (!ev.IsAllowed) return 76 | new CodeInstruction(OpCodes.Call, AccessTools.PropertyGetter(typeof(DroppingItemEventArgs), nameof(DroppingItemEventArgs.IsAllowed))), 77 | new CodeInstruction(OpCodes.Brfalse_S, notAllowedLabel), 78 | 79 | new CodeInstruction(OpCodes.Dup), 80 | new CodeInstruction(OpCodes.Dup), 81 | new CodeInstruction(OpCodes.Dup), 82 | new CodeInstruction(OpCodes.Dup), 83 | 84 | // placePosition = ev.TargetPosition 85 | new CodeInstruction(OpCodes.Call, AccessTools.PropertyGetter(typeof(DroppingItemEventArgs), nameof(DroppingItemEventArgs.TargetPosition))), 86 | new CodeInstruction(OpCodes.Starg, 3), 87 | 88 | // floorYRot2 = ev.FloorYRotation 89 | new CodeInstruction(OpCodes.Call, AccessTools.PropertyGetter(typeof(DroppingItemEventArgs), nameof(DroppingItemEventArgs.FloorYRotation))), 90 | new CodeInstruction(OpCodes.Stloc, 4), 91 | 92 | // parentObjectTo = ev.ParentObjectTo 93 | new CodeInstruction(OpCodes.Call, AccessTools.PropertyGetter(typeof(DroppingItemEventArgs), nameof(DroppingItemEventArgs.ParentObjectTo))), 94 | new CodeInstruction(OpCodes.Starg, 2), 95 | 96 | // matchRotationOfParent = ev.MatchRotationOfParent 97 | new CodeInstruction(OpCodes.Call, AccessTools.PropertyGetter(typeof(DroppingItemEventArgs), nameof(DroppingItemEventArgs.MatchRotationOfParent))), 98 | new CodeInstruction(OpCodes.Starg, 4), 99 | 100 | // droppedInShip = ev.DroppedInShip 101 | new CodeInstruction(OpCodes.Call, AccessTools.PropertyGetter(typeof(DroppingItemEventArgs), nameof(DroppingItemEventArgs.DroppedInShip))), 102 | new CodeInstruction(OpCodes.Stloc, isInShipLocal.LocalIndex), 103 | }; 104 | 105 | inst = inst.AddRangeToArray(animatorStuff); 106 | 107 | inst = inst.AddRangeToArray(new CodeInstruction[] 108 | { 109 | new CodeInstruction(OpCodes.Br, skipLabel), 110 | new CodeInstruction(OpCodes.Pop).WithLabels(nullLabel), 111 | new CodeInstruction(OpCodes.Ldarg_0), 112 | new CodeInstruction(OpCodes.Ldfld, AccessTools.Field(typeof(PlayerControllerB), nameof(PlayerControllerB.isInHangarShipRoom))), 113 | new CodeInstruction(OpCodes.Stloc, isInShipLocal.LocalIndex), 114 | }); 115 | 116 | inst = inst.AddRangeToArray(animatorStuff); 117 | 118 | inst = inst.AddRangeToArray(new CodeInstruction[] 119 | { 120 | new CodeInstruction(OpCodes.Br, skipLabel), 121 | new CodeInstruction(OpCodes.Pop).WithLabels(notAllowedLabel), 122 | new CodeInstruction(OpCodes.Ldarg_0), 123 | new CodeInstruction(OpCodes.Ldc_I4_0), 124 | new CodeInstruction(OpCodes.Stfld, AccessTools.Field(typeof(PlayerControllerB), nameof(PlayerControllerB.throwingObject))), 125 | new CodeInstruction(OpCodes.Ret) 126 | }); 127 | 128 | newInstructions.InsertRange(index, inst); 129 | 130 | newInstructions[index + inst.Length].labels.Add(skipLabel); 131 | 132 | newInstructions.RemoveRange(index + inst.Length + 1, 4); 133 | 134 | newInstructions.InsertRange(index + inst.Length + 1, new CodeInstruction[] 135 | { 136 | new CodeInstruction(OpCodes.Ldloc, isInShipLocal.LocalIndex), 137 | new CodeInstruction(OpCodes.Ldloc, isInShipLocal.LocalIndex) 138 | }); 139 | } 140 | 141 | { 142 | const int offset = 1; 143 | 144 | int index = newInstructions.FindLastIndex(i => i.opcode == OpCodes.Stloc_S) + offset; 145 | 146 | Label nullLabel = generator.DefineLabel(); 147 | Label notAllowedLabel = generator.DefineLabel(); 148 | Label skipLabel = generator.DefineLabel(); 149 | 150 | CodeInstruction[] inst = new CodeInstruction[] 151 | { 152 | // DroppingItemEventArgs ev = DroppingItem.CallEvent(PlayerControllerB, bool, Vector3, 153 | // int, NetworkObject, bool, bool) 154 | new CodeInstruction(OpCodes.Ldarg_0).MoveLabelsFrom(newInstructions[index]), 155 | new CodeInstruction(OpCodes.Ldarg_1), 156 | new CodeInstruction(OpCodes.Ldloc_1), 157 | new CodeInstruction(OpCodes.Ldloc_0), 158 | new CodeInstruction(OpCodes.Ldarg_2), 159 | new CodeInstruction(OpCodes.Ldarg, 4), 160 | new CodeInstruction(OpCodes.Ldloc_2), 161 | new CodeInstruction(OpCodes.Call, AccessTools.Method(typeof(DroppingItem), nameof(DroppingItem.CallEvent))), 162 | 163 | // if (ev is null) -> base game code 164 | new CodeInstruction(OpCodes.Dup), 165 | new CodeInstruction(OpCodes.Brfalse_S, nullLabel), 166 | 167 | new CodeInstruction(OpCodes.Dup), 168 | 169 | // if (!ev.IsAllowed) return 170 | new CodeInstruction(OpCodes.Call, AccessTools.PropertyGetter(typeof(DroppingItemEventArgs), nameof(DroppingItemEventArgs.IsAllowed))), 171 | new CodeInstruction(OpCodes.Brfalse_S, notAllowedLabel), 172 | 173 | new CodeInstruction(OpCodes.Dup), 174 | new CodeInstruction(OpCodes.Dup), 175 | new CodeInstruction(OpCodes.Dup), 176 | new CodeInstruction(OpCodes.Dup), 177 | 178 | // targetFloorPosition = ev.TargetPosition 179 | new CodeInstruction(OpCodes.Call, AccessTools.PropertyGetter(typeof(DroppingItemEventArgs), nameof(DroppingItemEventArgs.TargetPosition))), 180 | new CodeInstruction(OpCodes.Stloc_1), 181 | 182 | // floorYRot = ev.FloorYRotation 183 | new CodeInstruction(OpCodes.Call, AccessTools.PropertyGetter(typeof(DroppingItemEventArgs), nameof(DroppingItemEventArgs.FloorYRotation))), 184 | new CodeInstruction(OpCodes.Stloc_0), 185 | 186 | // parentObjectTo = ev.ParentObjectTo 187 | new CodeInstruction(OpCodes.Call, AccessTools.PropertyGetter(typeof(DroppingItemEventArgs), nameof(DroppingItemEventArgs.ParentObjectTo))), 188 | new CodeInstruction(OpCodes.Starg, 2), 189 | 190 | // matchRotationOfParent = ev.MatchRotationOfParent 191 | new CodeInstruction(OpCodes.Call, AccessTools.PropertyGetter(typeof(DroppingItemEventArgs), nameof(DroppingItemEventArgs.MatchRotationOfParent))), 192 | new CodeInstruction(OpCodes.Starg, 4), 193 | 194 | // droppedInShip = ev.DroppedInShip 195 | new CodeInstruction(OpCodes.Call, AccessTools.PropertyGetter(typeof(DroppingItemEventArgs), nameof(DroppingItemEventArgs.DroppedInShip))), 196 | new CodeInstruction(OpCodes.Stloc, isInShipLocal.LocalIndex), 197 | }; 198 | 199 | inst = inst.AddRangeToArray(animatorStuff); 200 | 201 | inst = inst.AddRangeToArray(new CodeInstruction[] 202 | { 203 | new CodeInstruction(OpCodes.Br, skipLabel), 204 | new CodeInstruction(OpCodes.Pop).WithLabels(nullLabel), 205 | new CodeInstruction(OpCodes.Ldloc_2), 206 | new CodeInstruction(OpCodes.Stloc, isInShipLocal.LocalIndex), 207 | }); 208 | 209 | inst = inst.AddRangeToArray(animatorStuff); 210 | 211 | inst = inst.AddRangeToArray(new CodeInstruction[] 212 | { 213 | new CodeInstruction(OpCodes.Br, skipLabel), 214 | new CodeInstruction(OpCodes.Pop).WithLabels(notAllowedLabel), 215 | new CodeInstruction(OpCodes.Ldarg_0), 216 | new CodeInstruction(OpCodes.Ldc_I4_0), 217 | new CodeInstruction(OpCodes.Stfld, AccessTools.Field(typeof(PlayerControllerB), nameof(PlayerControllerB.throwingObject))), 218 | new CodeInstruction(OpCodes.Ret) 219 | }); 220 | 221 | newInstructions.InsertRange(index, inst); 222 | 223 | newInstructions[index + inst.Length].labels.Add(skipLabel); 224 | 225 | newInstructions.RemoveRange(index + inst.Length + 1, 3); 226 | 227 | newInstructions.InsertRange(index + inst.Length + 1, new CodeInstruction[] 228 | { 229 | new CodeInstruction(OpCodes.Ldloc, isInShipLocal.LocalIndex), 230 | new CodeInstruction(OpCodes.Ldloc, isInShipLocal.LocalIndex) 231 | }); 232 | } 233 | 234 | for (int i = 0; i < newInstructions.Count; i++) yield return newInstructions[i]; 235 | } 236 | } 237 | } 238 | -------------------------------------------------------------------------------- /LC-API/GameInterfaceAPI/Events/Patches/Player/GrabItem.cs: -------------------------------------------------------------------------------- 1 | using GameNetcodeStuff; 2 | using HarmonyLib; 3 | using LC_API.GameInterfaceAPI.Events.EventArgs.Player; 4 | using System.Collections.Generic; 5 | using System.Reflection.Emit; 6 | using Unity.Netcode; 7 | using UnityEngine; 8 | 9 | namespace LC_API.GameInterfaceAPI.Events.Patches.Player 10 | { 11 | [HarmonyPatch(typeof(PlayerControllerB), nameof(PlayerControllerB.BeginGrabObject))] 12 | internal class GrabbingItem 13 | { 14 | internal static StartGrabbingItemEventArgs CallStartGrabbingItem(PlayerControllerB playerController, GrabbableObject grabbableObject) 15 | { 16 | if (Plugin.configVanillaSupport.Value) return null; 17 | 18 | StartGrabbingItemEventArgs ev = new StartGrabbingItemEventArgs(Features.Player.GetOrAdd(playerController), 19 | Features.Item.GetOrAdd(grabbableObject)); 20 | 21 | Handlers.Player.OnStartGrabbingItem(ev); 22 | 23 | return ev; 24 | } 25 | 26 | internal static GrabbingItemEventArgs CallGrabbingItem(PlayerControllerB playerController, GrabbableObject grabbableObject) 27 | { 28 | if (Plugin.configVanillaSupport.Value) return null; 29 | 30 | GrabbingItemEventArgs ev = new GrabbingItemEventArgs(Features.Player.GetOrAdd(playerController), 31 | Features.Item.GetOrAdd(grabbableObject)); 32 | 33 | Handlers.Player.OnGrabbingItem(ev); 34 | 35 | return ev; 36 | } 37 | 38 | private static IEnumerable Transpiler(IEnumerable instructions, ILGenerator generator) 39 | { 40 | List newInstructions = new List(instructions); 41 | 42 | { 43 | const int offset = 15; 44 | 45 | int index = newInstructions.FindLastIndex(i => i.opcode == OpCodes.Stfld && 46 | i.OperandIs(AccessTools.Field(typeof(PlayerControllerB), nameof(PlayerControllerB.currentlyGrabbingObject)))) + offset; 47 | 48 | Label nullLabel = generator.DefineLabel(); 49 | Label notAllowedLabel = generator.DefineLabel(); 50 | Label skipLabel = generator.DefineLabel(); 51 | 52 | CodeInstruction[] inst = new CodeInstruction[] 53 | { 54 | // StartGrabbingItemEventArgs ev = GrabbingItem.CallStartGrabbingItem(PlayerControllerB, GrabbableObject) 55 | new CodeInstruction(OpCodes.Ldarg_0).MoveLabelsFrom(newInstructions[index]), 56 | new CodeInstruction(OpCodes.Ldarg_0), 57 | new CodeInstruction(OpCodes.Ldfld, AccessTools.Field(typeof(PlayerControllerB), nameof(PlayerControllerB.currentlyGrabbingObject))), 58 | new CodeInstruction(OpCodes.Call, AccessTools.Method(typeof(GrabbingItem), nameof(GrabbingItem.CallStartGrabbingItem))), 59 | 60 | // if (ev is null) -> base game code 61 | new CodeInstruction(OpCodes.Dup), 62 | new CodeInstruction(OpCodes.Brfalse_S, nullLabel), 63 | 64 | // if (!ev.IsAllowed) return 65 | new CodeInstruction(OpCodes.Call, AccessTools.PropertyGetter(typeof(StartGrabbingItemEventArgs), nameof(StartGrabbingItemEventArgs.IsAllowed))), 66 | new CodeInstruction(OpCodes.Brfalse_S, notAllowedLabel), 67 | 68 | new CodeInstruction(OpCodes.Br, skipLabel), 69 | new CodeInstruction(OpCodes.Pop).WithLabels(nullLabel), 70 | new CodeInstruction(OpCodes.Br, skipLabel), 71 | new CodeInstruction(OpCodes.Ret).WithLabels(notAllowedLabel) 72 | }; 73 | 74 | newInstructions.InsertRange(index, inst); 75 | 76 | newInstructions[index + inst.Length].labels.Add(skipLabel); 77 | } 78 | 79 | { 80 | const int offset = -2; 81 | 82 | int index = newInstructions.FindLastIndex(i => i.opcode == OpCodes.Ldstr && 83 | i.OperandIs("GrabInvalidated")) + offset; 84 | 85 | Label nullLabel = generator.DefineLabel(); 86 | Label notAllowedLabel = generator.DefineLabel(); 87 | Label skipLabel = generator.DefineLabel(); 88 | 89 | CodeInstruction[] inst = new CodeInstruction[] 90 | { 91 | // GrabbingItemEventArgs ev = GrabbingItem.CallGrabbingItem(PlayerControllerB, GrabbableObject) 92 | new CodeInstruction(OpCodes.Ldarg_0).MoveLabelsFrom(newInstructions[index]), 93 | new CodeInstruction(OpCodes.Ldarg_0), 94 | new CodeInstruction(OpCodes.Ldfld, AccessTools.Field(typeof(PlayerControllerB), nameof(PlayerControllerB.currentlyGrabbingObject))), 95 | new CodeInstruction(OpCodes.Call, AccessTools.Method(typeof(GrabbingItem), nameof(GrabbingItem.CallGrabbingItem))), 96 | 97 | // if (ev is null) -> base game code 98 | new CodeInstruction(OpCodes.Dup), 99 | new CodeInstruction(OpCodes.Brfalse_S, nullLabel), 100 | 101 | // if (!ev.IsAllowed) return 102 | new CodeInstruction(OpCodes.Call, AccessTools.PropertyGetter(typeof(StartGrabbingItemEventArgs), nameof(StartGrabbingItemEventArgs.IsAllowed))), 103 | new CodeInstruction(OpCodes.Brfalse_S, notAllowedLabel), 104 | 105 | new CodeInstruction(OpCodes.Br, skipLabel), 106 | new CodeInstruction(OpCodes.Pop).WithLabels(nullLabel), 107 | new CodeInstruction(OpCodes.Br, skipLabel), 108 | new CodeInstruction(OpCodes.Ret).WithLabels(notAllowedLabel) 109 | }; 110 | 111 | newInstructions.InsertRange(index, inst); 112 | 113 | newInstructions[index + inst.Length].labels.Add(skipLabel); 114 | } 115 | 116 | for (int i = 0; i < newInstructions.Count; i++) yield return newInstructions[i]; 117 | } 118 | } 119 | 120 | [HarmonyPatch(typeof(PlayerControllerB), nameof(PlayerControllerB.GrabObjectClientRpc))] 121 | internal class GrabbedItem 122 | { 123 | internal static void CallEvent(PlayerControllerB player) 124 | { 125 | if (Plugin.configVanillaSupport.Value) return; 126 | 127 | Handlers.Player.OnGrabbedItem(new GrabbedItemEventArgs(Features.Player.GetOrAdd(player), 128 | Features.Item.GetOrAdd(player.currentlyHeldObjectServer))); 129 | } 130 | 131 | private static IEnumerable Transpiler(IEnumerable instructions, ILGenerator generator) 132 | { 133 | List newInstructions = new List(instructions); 134 | 135 | { 136 | const int offset = 1; 137 | 138 | int index = newInstructions.FindLastIndex(i => i.opcode == OpCodes.Callvirt && 139 | i.OperandIs(AccessTools.Method(typeof(AudioSource), 140 | nameof(AudioSource.PlayOneShot), new[] { typeof(AudioClip), typeof(float) }))) + offset; 141 | 142 | CodeInstruction[] inst = new CodeInstruction[] 143 | { 144 | // GrabbedItem.CallEvent(PlayerControllerB) 145 | new CodeInstruction(OpCodes.Ldarg_0).MoveLabelsFrom(newInstructions[index]), 146 | new CodeInstruction(OpCodes.Call, AccessTools.Method(typeof(GrabbedItem), nameof(GrabbedItem.CallEvent))), 147 | }; 148 | 149 | newInstructions.InsertRange(index, inst); 150 | } 151 | 152 | { 153 | const int offset = 1; 154 | 155 | int index = newInstructions.FindIndex(i => i.opcode == OpCodes.Call && 156 | i.OperandIs(AccessTools.PropertyGetter(typeof(NetworkBehaviour), nameof(NetworkBehaviour.IsOwner)))) + offset; 157 | 158 | Label skipToLabel = generator.DefineLabel(); 159 | 160 | newInstructions[index].operand = skipToLabel; 161 | 162 | CodeInstruction[] inst = new CodeInstruction[] 163 | { 164 | new CodeInstruction(OpCodes.Br, newInstructions[newInstructions.Count - 1].labels[0]), 165 | 166 | // GrabbedItem.CallEvent(PlayerControllerB) 167 | new CodeInstruction(OpCodes.Ldarg_0).WithLabels(skipToLabel), 168 | new CodeInstruction(OpCodes.Call, AccessTools.Method(typeof(GrabbedItem), nameof(GrabbedItem.CallEvent))), 169 | }; 170 | 171 | newInstructions.InsertRange(newInstructions.Count - 1, inst); 172 | } 173 | 174 | for (int i = 0; i < newInstructions.Count; i++) yield return newInstructions[i]; 175 | } 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /LC-API/GameInterfaceAPI/Events/Patches/Player/Hurt.cs: -------------------------------------------------------------------------------- 1 | using GameNetcodeStuff; 2 | using HarmonyLib; 3 | using LC_API.GameInterfaceAPI.Events.EventArgs.Player; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Reflection.Emit; 7 | using UnityEngine; 8 | 9 | namespace LC_API.GameInterfaceAPI.Events.Patches.Player 10 | { 11 | [HarmonyPatch(typeof(PlayerControllerB), nameof(PlayerControllerB.DamagePlayer))] 12 | internal class Hurt 13 | { 14 | private static HurtingEventArgs CallHurtingEvent(PlayerControllerB playerController, int damage, bool hasSFX, CauseOfDeath causeOfDeath, 15 | int deathAnimation, bool fallDamage, Vector3 force) 16 | { 17 | if (Plugin.configVanillaSupport.Value) return null; 18 | 19 | Features.Player player = Features.Player.GetOrAdd(playerController); 20 | 21 | HurtingEventArgs ev = new HurtingEventArgs(player, damage, hasSFX, 22 | causeOfDeath, deathAnimation, fallDamage, force); 23 | 24 | Handlers.Player.OnHurting(ev); 25 | 26 | player.CallHurtingOnOtherClients(damage, hasSFX, causeOfDeath, deathAnimation, fallDamage, force); 27 | 28 | return ev; 29 | } 30 | 31 | private static HurtEventArgs CallHurtEvent(PlayerControllerB playerController, int damage, bool hasSFX, CauseOfDeath causeOfDeath, 32 | int deathAnimation, bool fallDamage, Vector3 force) 33 | { 34 | if (Plugin.configVanillaSupport.Value) return null; 35 | 36 | Features.Player player = Features.Player.GetOrAdd(playerController); 37 | 38 | HurtEventArgs ev = new HurtEventArgs(player, damage, hasSFX, 39 | causeOfDeath, deathAnimation, fallDamage, force); 40 | 41 | Handlers.Player.OnHurt(ev); 42 | 43 | player.CallHurtOnOtherClients(damage, hasSFX, causeOfDeath, deathAnimation, fallDamage, force); 44 | 45 | return ev; 46 | } 47 | 48 | private static IEnumerable Transpiler(IEnumerable instructions, ILGenerator generator) 49 | { 50 | List newInstructions = new List(instructions); 51 | 52 | { 53 | const int offset = 3; 54 | 55 | int index = newInstructions.FindLastIndex(i => i.OperandIs(AccessTools.Method(typeof(PlayerControllerB), 56 | nameof(PlayerControllerB.AllowPlayerDeath)))) + offset; 57 | 58 | Label nullLabel = generator.DefineLabel(); 59 | Label notAllowedLabel = generator.DefineLabel(); 60 | Label skipLabel = generator.DefineLabel(); 61 | 62 | CodeInstruction[] inst = new CodeInstruction[] 63 | { 64 | // HurtingEventArgs ev = Hurt.CallHurtingEvent(PlayerControllerB, int, bool, CauseOfDeath, int, bool, Vector3) 65 | new CodeInstruction(OpCodes.Ldarg_0).MoveLabelsFrom(newInstructions[index]), 66 | new CodeInstruction(OpCodes.Ldarg_1), 67 | new CodeInstruction(OpCodes.Ldarg_2), 68 | new CodeInstruction(OpCodes.Ldarg, 4), 69 | new CodeInstruction(OpCodes.Ldarg, 5), 70 | new CodeInstruction(OpCodes.Ldarg, 6), 71 | new CodeInstruction(OpCodes.Ldarg, 7), 72 | new CodeInstruction(OpCodes.Call, AccessTools.Method(typeof(Hurt), nameof(Hurt.CallHurtingEvent))), 73 | new CodeInstruction(OpCodes.Dup), 74 | 75 | // if (ev is null) -> base game code 76 | new CodeInstruction(OpCodes.Dup), 77 | new CodeInstruction(OpCodes.Brfalse_S, nullLabel), 78 | 79 | // if (!ev.IsAllowed) return 80 | new CodeInstruction(OpCodes.Call, AccessTools.PropertyGetter(typeof(HurtingEventArgs), nameof(HurtingEventArgs.IsAllowed))), 81 | new CodeInstruction(OpCodes.Brfalse_S, notAllowedLabel), 82 | 83 | // Duplicating the stack is more memory efficient than making a local 84 | new CodeInstruction(OpCodes.Dup), 85 | new CodeInstruction(OpCodes.Dup), 86 | new CodeInstruction(OpCodes.Dup), 87 | new CodeInstruction(OpCodes.Dup), 88 | new CodeInstruction(OpCodes.Dup), 89 | 90 | // damage = ev.Damage 91 | new CodeInstruction(OpCodes.Call, AccessTools.PropertyGetter(typeof(HurtingEventArgs), nameof(HurtingEventArgs.Damage))), 92 | new CodeInstruction(OpCodes.Starg_S, 1), 93 | 94 | // hasDamageSFX = ev.HasSFX 95 | new CodeInstruction(OpCodes.Call, AccessTools.PropertyGetter(typeof(HurtingEventArgs), nameof(HurtingEventArgs.HasSFX))), 96 | new CodeInstruction(OpCodes.Starg_S, 2), 97 | 98 | // causeOfDeath = ev.CauseOfDeath 99 | new CodeInstruction(OpCodes.Call, AccessTools.PropertyGetter(typeof(HurtingEventArgs), nameof(HurtingEventArgs.CauseOfDeath))), 100 | new CodeInstruction(OpCodes.Starg_S, 4), 101 | 102 | // deathAnimation = ev.DeathAnimation 103 | new CodeInstruction(OpCodes.Call, AccessTools.PropertyGetter(typeof(HurtingEventArgs), nameof(HurtingEventArgs.DeathAnimation))), 104 | new CodeInstruction(OpCodes.Starg_S, 5), 105 | 106 | // fallDamage = ev.FallDamage 107 | new CodeInstruction(OpCodes.Call, AccessTools.PropertyGetter(typeof(HurtingEventArgs), nameof(HurtingEventArgs.FallDamage))), 108 | new CodeInstruction(OpCodes.Starg_S, 6), 109 | 110 | // force = ev.Force 111 | new CodeInstruction(OpCodes.Call, AccessTools.PropertyGetter(typeof(HurtingEventArgs), nameof(HurtingEventArgs.Force))), 112 | new CodeInstruction(OpCodes.Starg_S, 7), 113 | new CodeInstruction(OpCodes.Br, skipLabel), 114 | 115 | new CodeInstruction(OpCodes.Pop).WithLabels(nullLabel), 116 | new CodeInstruction(OpCodes.Pop), 117 | new CodeInstruction(OpCodes.Br, skipLabel), 118 | new CodeInstruction(OpCodes.Pop).WithLabels(notAllowedLabel), 119 | new CodeInstruction(OpCodes.Ret) 120 | }; 121 | 122 | newInstructions.InsertRange(index, inst); 123 | 124 | newInstructions[index + inst.Length].labels.Add(skipLabel); 125 | } 126 | 127 | { 128 | const int offset = 1; 129 | 130 | int index = newInstructions.FindLastIndex(i => i.opcode == OpCodes.Stfld 131 | && i.OperandIs(AccessTools.Field(typeof(PlayerControllerB), nameof(PlayerControllerB.health)))) + offset; 132 | 133 | Label nullLabel = generator.DefineLabel(); 134 | Label skipLabel = generator.DefineLabel(); 135 | 136 | CodeInstruction[] inst = new CodeInstruction[] 137 | { 138 | // HurtEventArgs ev = Hurt.CallHurtEvent(PlayerControllerB, int, bool, CauseOfDeath, int, bool, Vector3) 139 | new CodeInstruction(OpCodes.Ldarg_0).MoveLabelsFrom(newInstructions[index]), 140 | new CodeInstruction(OpCodes.Ldarg_1), 141 | new CodeInstruction(OpCodes.Ldarg_2), 142 | new CodeInstruction(OpCodes.Ldarg, 4), 143 | new CodeInstruction(OpCodes.Ldarg, 5), 144 | new CodeInstruction(OpCodes.Ldarg, 6), 145 | new CodeInstruction(OpCodes.Ldarg, 7), 146 | new CodeInstruction(OpCodes.Call, AccessTools.Method(typeof(Hurt), nameof(Hurt.CallHurtEvent))), 147 | 148 | // if (ev is null) -> base game code 149 | new CodeInstruction(OpCodes.Dup), 150 | new CodeInstruction(OpCodes.Brfalse_S, nullLabel), 151 | 152 | // Duplicating the stack is more memory efficient than making a local 153 | new CodeInstruction(OpCodes.Dup), 154 | new CodeInstruction(OpCodes.Dup), 155 | new CodeInstruction(OpCodes.Dup), 156 | new CodeInstruction(OpCodes.Dup), 157 | 158 | // hasDamageSFX = ev.HasSFX 159 | new CodeInstruction(OpCodes.Call, AccessTools.PropertyGetter(typeof(HurtingEventArgs), nameof(HurtingEventArgs.HasSFX))), 160 | new CodeInstruction(OpCodes.Starg_S, 2), 161 | 162 | // causeOfDeath = ev.CauseOfDeath 163 | new CodeInstruction(OpCodes.Call, AccessTools.PropertyGetter(typeof(HurtingEventArgs), nameof(HurtingEventArgs.CauseOfDeath))), 164 | new CodeInstruction(OpCodes.Starg_S, 4), 165 | 166 | // deathAnimation = ev.DeathAnimation 167 | new CodeInstruction(OpCodes.Call, AccessTools.PropertyGetter(typeof(HurtingEventArgs), nameof(HurtingEventArgs.DeathAnimation))), 168 | new CodeInstruction(OpCodes.Starg_S, 5), 169 | 170 | // fallDamage = ev.FallDamage 171 | new CodeInstruction(OpCodes.Call, AccessTools.PropertyGetter(typeof(HurtingEventArgs), nameof(HurtingEventArgs.FallDamage))), 172 | new CodeInstruction(OpCodes.Starg_S, 6), 173 | 174 | // force = ev.Force 175 | new CodeInstruction(OpCodes.Call, AccessTools.PropertyGetter(typeof(HurtingEventArgs), nameof(HurtingEventArgs.Force))), 176 | new CodeInstruction(OpCodes.Starg_S, 7), 177 | 178 | new CodeInstruction(OpCodes.Br, skipLabel), 179 | new CodeInstruction(OpCodes.Pop).WithLabels(nullLabel), 180 | }; 181 | 182 | newInstructions.InsertRange(index, inst); 183 | 184 | newInstructions[index + inst.Length].labels.Add(skipLabel); 185 | } 186 | 187 | for (int i = 0; i < newInstructions.Count; i++) yield return newInstructions[i]; 188 | } 189 | } 190 | } -------------------------------------------------------------------------------- /LC-API/GameInterfaceAPI/Events/Patches/Player/Joined.cs: -------------------------------------------------------------------------------- 1 | using GameNetcodeStuff; 2 | using HarmonyLib; 3 | using LC_API.GameInterfaceAPI.Events.EventArgs.Player; 4 | using System.Collections; 5 | using Unity.Netcode; 6 | using UnityEngine; 7 | 8 | namespace LC_API.GameInterfaceAPI.Events.Patches.Player 9 | { 10 | [HarmonyPatch(typeof(StartOfRound), nameof(StartOfRound.OnPlayerConnectedClientRpc))] 11 | internal static class Joined 12 | { 13 | private static void Prefix(ulong clientId, int assignedPlayerObjectId) 14 | { 15 | if (Plugin.configVanillaSupport.Value) return; 16 | 17 | if (NetworkManager.Singleton.IsServer || NetworkManager.Singleton.IsHost) 18 | { 19 | PlayerControllerB playerController = StartOfRound.Instance.allPlayerScripts[assignedPlayerObjectId]; 20 | Features.Player.GetOrAdd(playerController).NetworkClientId.Value = clientId; 21 | } 22 | } 23 | 24 | private static void Postfix(ulong clientId, int assignedPlayerObjectId) 25 | { 26 | if (Plugin.configVanillaSupport.Value) return; 27 | 28 | PlayerControllerB playerController = StartOfRound.Instance.allPlayerScripts[assignedPlayerObjectId]; 29 | if (!Cache.Player.ConnectedPlayers.Contains(clientId)) 30 | { 31 | Cache.Player.ConnectedPlayers.Add(clientId); 32 | 33 | playerController.StartCoroutine(JoinedCoroutine(playerController)); 34 | } 35 | } 36 | 37 | // Since we have to wait for players' client id to sync to the player instance, we have to constantly check 38 | // if the player and its controller were linked yet. Very annoying. 39 | internal static IEnumerator JoinedCoroutine(PlayerControllerB controller) 40 | { 41 | yield return new WaitUntil(() => StartOfRound.Instance.localPlayerController != null); 42 | 43 | Features.Player player = Features.Player.GetOrAdd(controller); 44 | 45 | while (player == null) 46 | { 47 | yield return new WaitForSeconds(0.1f); 48 | 49 | player = Features.Player.GetOrAdd(controller); 50 | } 51 | 52 | if (player.IsLocalPlayer) 53 | { 54 | Features.Player.LocalPlayer = player; 55 | } 56 | 57 | Handlers.Player.OnJoined(new JoinedEventArgs(player)); 58 | } 59 | } 60 | 61 | [HarmonyPatch(typeof(PlayerControllerB), nameof(PlayerControllerB.ConnectClientToPlayerObject))] 62 | internal static class Joined2 63 | { 64 | private static void Postfix(PlayerControllerB __instance) 65 | { 66 | if (Plugin.configVanillaSupport.Value) return; 67 | 68 | if (NetworkManager.Singleton.IsServer || NetworkManager.Singleton.IsHost) 69 | { 70 | Features.Player.GetOrAdd(__instance).NetworkClientId.Value = __instance.actualClientId; 71 | } 72 | 73 | if (!Cache.Player.ConnectedPlayers.Contains(__instance.actualClientId)) 74 | { 75 | Cache.Player.ConnectedPlayers.Add(__instance.actualClientId); 76 | 77 | __instance.StartCoroutine(Joined.JoinedCoroutine(__instance)); 78 | } 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /LC-API/GameInterfaceAPI/Events/Patches/Player/Left.cs: -------------------------------------------------------------------------------- 1 | using GameNetcodeStuff; 2 | using HarmonyLib; 3 | using LC_API.GameInterfaceAPI.Events.EventArgs.Player; 4 | 5 | namespace LC_API.GameInterfaceAPI.Events.Patches.Player 6 | { 7 | [HarmonyPatch(typeof(StartOfRound), nameof(StartOfRound.OnPlayerDC))] 8 | internal static class Left 9 | { 10 | private static void Prefix(StartOfRound __instance, int playerObjectNumber, ulong clientId) 11 | { 12 | if (Plugin.configVanillaSupport.Value) return; 13 | 14 | PlayerControllerB player = __instance.allPlayerScripts[playerObjectNumber]; 15 | if (__instance.ClientPlayerList.ContainsKey(clientId) && 16 | Cache.Player.ConnectedPlayers.Contains(player.actualClientId)) 17 | { 18 | Cache.Player.ConnectedPlayers.Remove(player.actualClientId); 19 | Handlers.Player.OnLeft(new LeftEventArgs(Features.Player.GetOrAdd(player))); 20 | } 21 | } 22 | } 23 | 24 | [HarmonyPatch(typeof(StartOfRound), nameof(StartOfRound.OnLocalDisconnect))] 25 | internal static class LocalLeft 26 | { 27 | private static void Prefix(StartOfRound __instance) 28 | { 29 | if (Plugin.configVanillaSupport.Value) return; 30 | 31 | PlayerControllerB player = __instance.localPlayerController; 32 | Handlers.Player.OnLeft(new LeftEventArgs(Features.Player.GetOrAdd(player))); 33 | Cache.Player.ConnectedPlayers.Clear(); 34 | 35 | foreach (PlayerControllerB playerController in StartOfRound.Instance.allPlayerScripts) 36 | { 37 | playerController.isPlayerDead = false; 38 | playerController.isPlayerControlled = false; 39 | } 40 | 41 | Features.Player.Dictionary.Clear(); 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /LC-API/GameInterfaceAPI/Features/Item.cs: -------------------------------------------------------------------------------- 1 | using LC_API.Exceptions; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using Unity.Netcode; 5 | using UnityEngine; 6 | 7 | namespace LC_API.GameInterfaceAPI.Features 8 | { 9 | /// 10 | /// Encapsulates a for easier interacting. 11 | /// 12 | public class Item : NetworkBehaviour 13 | { 14 | internal static GameObject ItemNetworkPrefab { get; set; } 15 | 16 | /// 17 | /// Gets a dictionary containing all s that are currently spawned in the world or in s' inventories. 18 | /// 19 | public static Dictionary Dictionary { get; } = new Dictionary(); 20 | 21 | /// 22 | /// Gets a list containing all s. 23 | /// 24 | public static IReadOnlyCollection List => Dictionary.Values; 25 | 26 | /// 27 | /// Gets the encapsulated 28 | /// 29 | public GrabbableObject GrabbableObject { get; private set; } 30 | 31 | /// 32 | /// Gets the 's item properties. 33 | /// These do not network, it is recommended to use the getters/setters on the itself. 34 | /// 35 | public global::Item ItemProperties => GrabbableObject.itemProperties; 36 | 37 | /// 38 | /// Gets the 's . 39 | /// These do not network, it is recommended to use the getters/setters on the itself. 40 | /// 41 | public ScanNodeProperties ScanNodeProperties { get; set; } 42 | 43 | /// 44 | /// Gets whether or not this is currently being held. 45 | /// 46 | public bool IsHeld => GrabbableObject.isHeld; 47 | 48 | /// 49 | /// Gets whether or not this is two handed. 50 | /// 51 | public bool IsTwoHanded => ItemProperties.twoHanded; 52 | 53 | /// 54 | /// Gets the that is currently holding this . if not held. 55 | /// 56 | public Player Holder => IsHeld ? Player.Dictionary.TryGetValue(GrabbableObject.playerHeldBy, out Player p) ? p : null : null; 57 | 58 | /// 59 | /// Gets or sets the 's name. 60 | /// 61 | /// Thrown when attempting to set item name from the client. 62 | public string Name 63 | { 64 | get 65 | { 66 | return ItemProperties.itemName; 67 | } 68 | set 69 | { 70 | if (!(NetworkManager.Singleton.IsServer || NetworkManager.Singleton.IsHost)) 71 | { 72 | throw new NoAuthorityException("Tried to set item name on client."); 73 | } 74 | 75 | string current = ItemProperties.itemName.ToLower(); 76 | 77 | CloneProperties(); 78 | 79 | ItemProperties.itemName = value; 80 | OverrideTooltips(current, value.ToLower()); 81 | 82 | ScanNodeProperties.headerText = value; 83 | 84 | SetGrabbableNameClientRpc(value); 85 | } 86 | } 87 | 88 | [ClientRpc] 89 | private void SetGrabbableNameClientRpc(string name) 90 | { 91 | string current = ItemProperties.itemName.ToLower(); 92 | 93 | CloneProperties(); 94 | 95 | ItemProperties.itemName = name; 96 | OverrideTooltips(current, name.ToLower()); 97 | 98 | ScanNodeProperties.headerText = name; 99 | } 100 | 101 | private void OverrideTooltips(string oldName, string newName) 102 | { 103 | for (int i = 0; i < ItemProperties.toolTips.Length; i++) 104 | { 105 | ItemProperties.toolTips[i] = ItemProperties.toolTips[i].ReplaceWithCase(oldName, newName); 106 | } 107 | 108 | if (IsHeld && Holder == Player.LocalPlayer) GrabbableObject.SetControlTipsForItem(); 109 | } 110 | 111 | /// 112 | /// Gets or sets the position of this . 113 | /// 114 | /// Thrown when attempting to set the item's position from the client. 115 | public Vector3 Position 116 | { 117 | get 118 | { 119 | return GrabbableObject.transform.position; 120 | } 121 | set 122 | { 123 | if (!(NetworkManager.Singleton.IsServer || NetworkManager.Singleton.IsHost)) 124 | { 125 | throw new NoAuthorityException("Tried to set item position on client."); 126 | } 127 | 128 | GrabbableObject.startFallingPosition = value; 129 | GrabbableObject.targetFloorPosition = value; 130 | GrabbableObject.transform.position = value; 131 | 132 | SetItemPositionClientRpc(value); 133 | } 134 | } 135 | 136 | [ClientRpc] 137 | private void SetItemPositionClientRpc(Vector3 pos) 138 | { 139 | GrabbableObject.startFallingPosition = pos; 140 | GrabbableObject.targetFloorPosition = pos; 141 | GrabbableObject.transform.position = pos; 142 | } 143 | 144 | /// 145 | /// Gets or sets the rotation of this . Does not sync 146 | /// 147 | public Quaternion Rotation 148 | { 149 | get 150 | { 151 | return GrabbableObject.transform.rotation; 152 | } 153 | set 154 | { 155 | GrabbableObject.transform.rotation = value; 156 | } 157 | } 158 | 159 | /// 160 | /// Sets the 's rotation and syncs it across all clients. 161 | /// 162 | /// The desired rotation. 163 | /// Thrown when attempting to sync rotation to other clients while not being the host. 164 | public void SetAndSyncRotation(Quaternion rotation) 165 | { 166 | if (!(NetworkManager.Singleton.IsServer || NetworkManager.Singleton.IsHost)) 167 | { 168 | throw new NoAuthorityException("Tried to sync item rotation from client."); 169 | } 170 | 171 | SetItemRotationClientRpc(rotation); 172 | } 173 | 174 | [ClientRpc] 175 | private void SetItemRotationClientRpc(Quaternion rotation) 176 | { 177 | Rotation = rotation; 178 | } 179 | 180 | /// 181 | /// Sets the scale of the . Does not sync. 182 | /// 183 | public Vector3 Scale 184 | { 185 | get 186 | { 187 | return GrabbableObject.transform.localScale; 188 | } 189 | set 190 | { 191 | GrabbableObject.transform.localScale = value; 192 | } 193 | } 194 | 195 | /// 196 | /// Sets the 's scale and syncs it across all clients. 197 | /// 198 | /// The desired scale. 199 | /// Thrown when attempting to sync scale to other clients while not being the host. 200 | public void SetAndSyncScale(Vector3 scale) 201 | { 202 | if (!(NetworkManager.Singleton.IsServer || NetworkManager.Singleton.IsHost)) 203 | { 204 | throw new NoAuthorityException("Tried to sync item scale from client."); 205 | } 206 | 207 | SetItemScaleClientRpc(scale); 208 | } 209 | 210 | [ClientRpc] 211 | private void SetItemScaleClientRpc(Vector3 scale) 212 | { 213 | Scale = scale; 214 | } 215 | 216 | /// 217 | /// Gets or sets whether this should be considered scrap. 218 | /// 219 | /// Thrown when attempting to set isScrap from the client. 220 | public bool IsScrap 221 | { 222 | get 223 | { 224 | return ItemProperties.isScrap; 225 | } 226 | set 227 | { 228 | if (!(NetworkManager.Singleton.IsServer || NetworkManager.Singleton.IsHost)) 229 | { 230 | throw new NoAuthorityException("Tried to set item name on client."); 231 | } 232 | 233 | CloneProperties(); 234 | 235 | ItemProperties.isScrap = value; 236 | 237 | SetIsScrapClientRpc(value); 238 | } 239 | } 240 | 241 | [ClientRpc] 242 | private void SetIsScrapClientRpc(bool isScrap) 243 | { 244 | CloneProperties(); 245 | 246 | ItemProperties.isScrap = isScrap; 247 | } 248 | 249 | /// 250 | /// Gets or sets this 's scrap value. 251 | /// 252 | /// Thrown when attempting to set scrap value from the client. 253 | public int ScrapValue 254 | { 255 | get 256 | { 257 | return GrabbableObject.scrapValue; 258 | } 259 | set 260 | { 261 | if (!(NetworkManager.Singleton.IsServer || NetworkManager.Singleton.IsHost)) 262 | { 263 | throw new NoAuthorityException("Tried to set scrap value on client."); 264 | } 265 | 266 | GrabbableObject.SetScrapValue(value); 267 | 268 | SetScrapValueClientRpc(value); 269 | } 270 | } 271 | 272 | [ClientRpc] 273 | private void SetScrapValueClientRpc(int scrapValue) 274 | { 275 | GrabbableObject.SetScrapValue(scrapValue); 276 | } 277 | 278 | /// 279 | /// Removes the from its current holder. 280 | /// 281 | /// The position to place the object after removing. 282 | /// The rotation the object should have after removing. 283 | public void RemoveFromHolder(Vector3 position = default, Quaternion rotation = default) 284 | { 285 | if (!(NetworkManager.Singleton.IsServer || NetworkManager.Singleton.IsHost)) 286 | { 287 | throw new NoAuthorityException("Tried to remove item from player on client."); 288 | } 289 | 290 | if (!IsHeld) return; 291 | 292 | NetworkObject.RemoveOwnership(); 293 | 294 | Holder.Inventory.RemoveItem(this); 295 | 296 | RemoveFromHolderClientRpc(); 297 | 298 | Position = position; 299 | Rotation = rotation; 300 | } 301 | 302 | [ClientRpc] 303 | private void RemoveFromHolderClientRpc() 304 | { 305 | if (!IsHeld) return; 306 | 307 | Holder.Inventory.RemoveItem(this); 308 | } 309 | 310 | /// 311 | /// Enables/disables the 's physics. 312 | /// 313 | /// to enable physics, otherwise. 314 | public void EnablePhysics(bool enable) => GrabbableObject.EnablePhysics(enable); 315 | 316 | /// 317 | /// Enables/disables the 's meshes. 318 | /// 319 | /// to enable meshes, otherwise. 320 | public void EnableMeshes(bool enable) => GrabbableObject.EnableItemMeshes(enable); 321 | 322 | /// 323 | /// Start the falling to the ground. 324 | /// 325 | /// Whether or not to add some randomness to the position. 326 | public void FallToGround(bool randomizePosition = false) 327 | { 328 | GrabbableObject.FallToGround(randomizePosition); 329 | } 330 | 331 | /// 332 | /// Pockets the by disabling its meshes. Plays pocket sound effects. 333 | /// 334 | /// if the was able to be pocketed, otherwise. 335 | public bool PocketItem() 336 | { 337 | // We can only pocket items that are currently being held in a player's hand. Two handed 338 | // objects cannot be pocketed. 339 | if (!IsHeld || Holder.HeldItem != this || IsTwoHanded) return false; 340 | 341 | GrabbableObject.PocketItem(); 342 | 343 | return true; 344 | } 345 | 346 | /// 347 | /// Gives this to the specific player. Deleting it from another 's inventory, if necessary. 348 | /// 349 | /// The player to give the item to. 350 | /// Whether or not to switch to the item. Forced for 2 handed items. 351 | /// if the player had an open slot to add the item to, otherwise. 352 | public bool GiveTo(Player player, bool switchTo = true) 353 | { 354 | if (!(NetworkManager.Singleton.IsServer || NetworkManager.Singleton.IsHost)) 355 | { 356 | throw new NoAuthorityException("Tried to give item to player on client."); 357 | } 358 | 359 | return player.Inventory.TryAddItem(this, switchTo); 360 | } 361 | 362 | /// 363 | /// Initializes the with base game scrap values. 364 | /// 365 | public void InitializeScrap() 366 | { 367 | if (RoundManager.Instance.AnomalyRandom != null) InitializeScrap((int)(RoundManager.Instance.AnomalyRandom.Next(ItemProperties.minValue, ItemProperties.maxValue) * RoundManager.Instance.scrapValueMultiplier)); 368 | else InitializeScrap((int)(UnityEngine.Random.Range(ItemProperties.minValue, ItemProperties.maxValue) * RoundManager.Instance.scrapValueMultiplier)); 369 | } 370 | 371 | /// 372 | /// Initializes the with a specific scrap value. 373 | /// 374 | /// The desired scrap value. 375 | /// Thrown when trying to initialize scrap from the client. 376 | public void InitializeScrap(int scrapValue) 377 | { 378 | if (!(NetworkManager.Singleton.IsServer || NetworkManager.Singleton.IsHost)) 379 | { 380 | throw new NoAuthorityException("Tried to initialize scrap on client."); 381 | } 382 | 383 | ScrapValue = scrapValue; 384 | 385 | InitializeScrapClientRpc(); 386 | } 387 | 388 | [ClientRpc] 389 | private void InitializeScrapClientRpc() 390 | { 391 | if (GrabbableObject.gameObject.TryGetComponent(out MeshFilter filter) 392 | && ItemProperties.meshVariants != null && ItemProperties.meshVariants.Length != 0) 393 | { 394 | if (RoundManager.Instance.ScrapValuesRandom != null) 395 | filter.mesh = ItemProperties.meshVariants[RoundManager.Instance.ScrapValuesRandom.Next(ItemProperties.meshVariants.Length)]; 396 | else 397 | filter.mesh = ItemProperties.meshVariants[0]; 398 | } 399 | 400 | if (GrabbableObject.gameObject.TryGetComponent(out MeshRenderer renderer) 401 | && ItemProperties.materialVariants != null && ItemProperties.materialVariants.Length != 0) 402 | { 403 | if (RoundManager.Instance.ScrapValuesRandom != null) 404 | renderer.sharedMaterial = ItemProperties.materialVariants[RoundManager.Instance.ScrapValuesRandom.Next(ItemProperties.materialVariants.Length)]; 405 | else 406 | renderer.sharedMaterial = ItemProperties.materialVariants[0]; 407 | } 408 | } 409 | 410 | /// 411 | /// Creates and spawns an in the world. 412 | /// 413 | /// The item's name. Uses a simple Contains check to see if the provided item name is contained in the actual item's name. Case insensitive. 414 | /// Whether or not to initialize the item after spawning. 415 | /// The position to spawn at. 416 | /// The rotation to spawn at. 417 | /// A new , or if the provided item name is not found. 418 | /// Thrown when trying to spawn an on the client. 419 | public static Item CreateAndSpawnItem(string itemName, bool andInitialize = true, Vector3 position = default, Quaternion rotation = default) 420 | { 421 | if (!(NetworkManager.Singleton.IsServer || NetworkManager.Singleton.IsHost)) 422 | { 423 | throw new NoAuthorityException("Tried to create and spawn item on client."); 424 | } 425 | 426 | string name = itemName.ToLower(); 427 | 428 | GameObject go = StartOfRound.Instance.allItemsList.itemsList.FirstOrDefault(i => i.itemName.ToLower().Contains(name))?.spawnPrefab; 429 | if (go != null) 430 | { 431 | GameObject instantiated = Instantiate(go, position, rotation); 432 | 433 | instantiated.GetComponent().Spawn(); 434 | 435 | Item item = instantiated.GetComponent(); 436 | 437 | if (item.IsScrap && andInitialize) item.InitializeScrap(); 438 | 439 | return item; 440 | } 441 | 442 | return null; 443 | } 444 | 445 | /// 446 | /// Creates an and gives it to a specific . 447 | /// 448 | /// The item's name. Uses a simple Contains check to see if the provided item name is contained in the actual item's name. Case insensitive. 449 | /// The to give the to. 450 | /// Whether or not to initialize this item after spawning. 451 | /// Whether or not to switch to the item. Forced for 2 handed items. 452 | /// A new , or if the provided item name is not found. 453 | /// Thrown when trying to spawn an on the client. 454 | public static Item CreateAndGiveItem(string itemName, Player player, bool andInitialize = true, bool switchTo = true) 455 | { 456 | if (!(NetworkManager.Singleton.IsServer || NetworkManager.Singleton.IsHost)) 457 | { 458 | throw new NoAuthorityException("Tried to create and give item on client."); 459 | } 460 | 461 | string name = itemName.ToLower(); 462 | 463 | GameObject go = StartOfRound.Instance.allItemsList.itemsList.FirstOrDefault(i => i.itemName.ToLower().Contains(name))?.spawnPrefab; 464 | if (go != null) 465 | { 466 | GameObject instantiated = Instantiate(go, Vector3.zero, default); 467 | 468 | instantiated.GetComponent().Spawn(); 469 | 470 | Item item = instantiated.GetComponent(); 471 | 472 | if (item.IsScrap && andInitialize) item.InitializeScrap(); 473 | 474 | item.GiveTo(player, switchTo); 475 | 476 | return item; 477 | } 478 | 479 | return null; 480 | } 481 | 482 | #region Unity related things 483 | private void Awake() 484 | { 485 | GrabbableObject = GetComponent(); 486 | ScanNodeProperties = GrabbableObject.gameObject.GetComponentInChildren(); 487 | 488 | Dictionary.Add(GrabbableObject, this); 489 | } 490 | 491 | // All items have the same properties, so if we change it, we need to clone it. 492 | private bool hasNewProps = false; 493 | private void CloneProperties() 494 | { 495 | global::Item newProps = Instantiate(ItemProperties); 496 | 497 | // Don't want to destroy any that aren't custom 498 | if (hasNewProps) Destroy(ItemProperties); 499 | 500 | GrabbableObject.itemProperties = newProps; 501 | 502 | hasNewProps = true; 503 | } 504 | 505 | /// 506 | /// For internal use. Do not use. 507 | /// 508 | public override void OnDestroy() 509 | { 510 | Dictionary.Remove(GrabbableObject); 511 | 512 | base.OnDestroy(); 513 | } 514 | #endregion 515 | 516 | #region Item getters 517 | public static Item GetOrAdd(GrabbableObject grabbableObject) 518 | { 519 | if (Dictionary.TryGetValue(grabbableObject, out Item item)) 520 | return item; 521 | 522 | return grabbableObject.gameObject.AddComponent(); ; 523 | } 524 | 525 | public static Item Get(GrabbableObject grabbableObject) 526 | { 527 | if (Dictionary.TryGetValue(grabbableObject, out Item item)) 528 | return item; 529 | 530 | return null; 531 | } 532 | 533 | public static bool TryGet(GrabbableObject grabbableObject, out Item item) 534 | { 535 | return Dictionary.TryGetValue(grabbableObject, out item); 536 | } 537 | 538 | public static Item Get(ulong netId) 539 | { 540 | return List.FirstOrDefault(i => i.NetworkObjectId == netId); 541 | } 542 | 543 | public static bool TryGet(ulong netId, out Item item) 544 | { 545 | item = Get(netId); 546 | 547 | return item != null; 548 | } 549 | #endregion 550 | } 551 | } 552 | -------------------------------------------------------------------------------- /LC-API/GameInterfaceAPI/Features/Tip.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace LC_API.GameInterfaceAPI.Features 4 | { 5 | public class Tip : IComparable 6 | { 7 | public string Header { get; } 8 | 9 | public string Message { get; } 10 | 11 | public float Duration { get; } 12 | 13 | public float TimeLeft { get; internal set; } 14 | 15 | public int Priority { get; } = 0; 16 | 17 | public bool IsWarning { get; set; } = false; 18 | 19 | public bool UseSave { get; set; } = false; 20 | 21 | public string PreferenceKey { get; set; } = "LC_Tip1"; 22 | 23 | internal int TipId { get; set; } 24 | 25 | public Tip(string header, string message, float duration, int priority, bool isWarning, bool useSave, string prefsKey, int tipId) 26 | { 27 | Header = header; 28 | Message = message; 29 | Duration = duration; 30 | TimeLeft = duration; 31 | Priority = priority; 32 | 33 | IsWarning = isWarning; 34 | UseSave = useSave; 35 | PreferenceKey = prefsKey; 36 | 37 | TipId = tipId; 38 | } 39 | 40 | public int CompareTo(Tip other) 41 | { 42 | int diff = other.Priority - Priority; 43 | 44 | if (diff < 0) return -1; 45 | 46 | if (diff > 0) return 1; 47 | 48 | return TipId - other.TipId; 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /LC-API/GameInterfaceAPI/GameState.cs: -------------------------------------------------------------------------------- 1 | using LC_API.Data; 2 | using LC_API.Extensions; 3 | using System; 4 | 5 | namespace LC_API.GameInterfaceAPI 6 | { 7 | /// 8 | /// Contains callbacks and information about the game's current state. 9 | /// 10 | public static class GameState 11 | { 12 | // stops the compiler whining about nullability 13 | private readonly static Action NothingAction = () => { }; 14 | 15 | /// 16 | /// Provides the count of living players, as gotten through 17 | /// 18 | public static int AlivePlayerCount { get; private set; } 19 | /// 20 | /// The state the ship is currently in. See . 21 | /// 22 | public static ShipState ShipState { get; private set; } 23 | 24 | /// 25 | /// Executes the frame after a player dies (determined by a living player count comparison) 26 | /// 27 | public static event Action PlayerDied = NothingAction; 28 | /// 29 | /// Executes the frame after the ship has landed. 30 | /// 31 | public static event Action LandOnMoon = NothingAction; 32 | /// 33 | /// Executes when the ship reaches orbit. 34 | /// 35 | public static event Action WentIntoOrbit = NothingAction; 36 | /// 37 | /// Executes when the ship starts taking off. 38 | /// 39 | public static event Action ShipStartedLeaving = NothingAction; 40 | 41 | internal static void GSUpdate() 42 | { 43 | if (StartOfRound.Instance == null) 44 | return; 45 | 46 | if (StartOfRound.Instance.shipHasLanded && ShipState != ShipState.OnMoon) 47 | { 48 | ShipState = ShipState.OnMoon; 49 | LandOnMoon.InvokeActionSafe(); 50 | } 51 | 52 | if (StartOfRound.Instance.inShipPhase && ShipState != ShipState.InOrbit) 53 | { 54 | ShipState = ShipState.InOrbit; 55 | WentIntoOrbit.InvokeActionSafe(); 56 | } 57 | 58 | if (StartOfRound.Instance.shipIsLeaving && ShipState != ShipState.LeavingMoon) 59 | { 60 | ShipState = ShipState.LeavingMoon; 61 | ShipStartedLeaving.InvokeActionSafe(); 62 | } 63 | 64 | int aliveP = AlivePlayerCount; 65 | if (aliveP < StartOfRound.Instance.livingPlayers) 66 | { 67 | PlayerDied.InvokeActionSafe(); 68 | } 69 | AlivePlayerCount = StartOfRound.Instance.livingPlayers; 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /LC-API/GameInterfaceAPI/GameTips.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using UnityEngine; 4 | 5 | namespace LC_API.GameInterfaceAPI 6 | { 7 | /// 8 | /// Allows for displaying information through the tip box on the players screen, without any tips overlapping. 9 | /// 10 | [Obsolete("Use Player::QueueTip instead.")] 11 | public class GameTips 12 | { 13 | private static List tipHeaders = new List(); 14 | private static List tipBodys = new List(); 15 | private static float lastMessageTime; 16 | 17 | /// 18 | /// Add a tip to the tip que. 19 | /// 20 | public static void ShowTip(string header, string body) 21 | { 22 | tipHeaders.Add(header); 23 | tipBodys.Add(body); 24 | } 25 | 26 | public static void UpdateInternal() 27 | { 28 | lastMessageTime -= Time.deltaTime; 29 | if (tipHeaders.Count > 0 & lastMessageTime < 0) 30 | { 31 | lastMessageTime = 5f; 32 | if (HUDManager.Instance != null) 33 | { 34 | HUDManager.Instance.DisplayTip(tipHeaders[0], tipBodys[0]); 35 | } 36 | tipHeaders.RemoveAt(0); 37 | tipBodys.RemoveAt(0); 38 | } 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /LC-API/LC-API.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netstandard2.1;net472 5 | LC_API 6 | Lethal Company API 7 | Utilities for plugin devs 8 | true 9 | latest 10 | 11 | https://api.nuget.org/v3/index.json; 12 | https://nuget.bepinex.dev/v3/index.json; 13 | https://nuget.samboy.dev/v3/index.json 14 | 15 | LC_API 16 | True 17 | v 18 | 19 | 20 | 21 | 22 | LethalCompany.LC-API 23 | 2018,steven4547466 24 | 2018 25 | LICENSE 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | bin 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | runtime; build; native; contentfiles; analyzers; buildtransitive 51 | all 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | $(LETHAL_COMPANY_DIR)\Lethal Company_Data\Managed\Assembly-CSharp.dll 64 | 65 | 66 | $(LETHAL_COMPANY_DIR)\Lethal Company_Data\Managed\Assembly-CSharp-firstpass.dll 67 | 68 | 69 | $(LETHAL_COMPANY_DIR)\Lethal Company_Data\Managed\Facepunch.Steamworks.Win64.dll 70 | 71 | 72 | $(LETHAL_COMPANY_DIR)\Lethal Company_Data\Managed\Newtonsoft.Json.dll 73 | 74 | 75 | $(LETHAL_COMPANY_DIR)\Lethal Company_Data\Managed\Unity.InputSystem.dll 76 | 77 | 78 | $(LETHAL_COMPANY_DIR)\Lethal Company_Data\Managed\Unity.Netcode.Runtime.dll 79 | 80 | 81 | $(LETHAL_COMPANY_DIR)\Lethal Company_Data\Managed\Unity.TextMeshPro.dll 82 | 83 | 84 | $(LETHAL_COMPANY_DIR)\Lethal Company_Data\Managed\UnityEngine.UI.dll 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | true 94 | 95 | 96 | 97 | 98 | 99 | $(MinVerMajor).$(MinVerMinor).$(MinVerPatch) 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | $(MinVerMajor).$(MinVerMinor).$(MinVerPatch) 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | -------------------------------------------------------------------------------- /LC-API/ManualPatches/ServerPatch.cs: -------------------------------------------------------------------------------- 1 | using LC_API.Comp; 2 | using LC_API.ServerAPI; 3 | using Steamworks; 4 | using Steamworks.Data; 5 | using UnityEngine; 6 | using UnityEngine.InputSystem; 7 | 8 | namespace LC_API.ManualPatches 9 | { 10 | internal static class ServerPatch 11 | { 12 | internal static bool OnLobbyCreate(GameNetworkManager __instance, Result result, Lobby lobby) 13 | { 14 | if (result != Result.OK) 15 | { 16 | Debug.LogError(string.Format("Lobby could not be created! {0}", result), __instance); 17 | } 18 | __instance.lobbyHostSettings.lobbyName = "[MODDED]" + __instance.lobbyHostSettings.lobbyName.ToString(); 19 | Plugin.Log.LogMessage("server pre-setup success"); 20 | return (true); 21 | } 22 | 23 | internal static bool CacheMenuManager(MenuManager __instance) 24 | { 25 | LC_APIManager.MenuManager = __instance; 26 | return true; 27 | } 28 | 29 | internal static bool ChatCommands(HUDManager __instance, InputAction.CallbackContext context) 30 | { 31 | if (__instance.chatTextField.text.ToLower().Contains("/modcheck")) 32 | { 33 | CheatDatabase.OtherPlayerCheatDetector(); 34 | return false; 35 | } 36 | return true; 37 | } 38 | 39 | internal static void GameNetworkManagerAwake(GameNetworkManager __instance) 40 | { 41 | if (GameNetworkManager.Instance == null) ModdedServer.GameVersion = __instance.gameVersionNum; 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /LC-API/Networking/Serializers.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using UnityEngine; 3 | 4 | namespace LC_API.Networking.Serializers 5 | { 6 | #pragma warning disable 7 | /// 8 | /// Serializes a . 9 | /// 10 | public struct Vector2S 11 | { 12 | public float x { get; set; } 13 | public float y { get; set; } 14 | 15 | public Vector2S(float x, float y) 16 | { 17 | this.x = x; 18 | this.y = y; 19 | } 20 | 21 | public static implicit operator Vector2(Vector2S vector2S) => vector2S.vector2; 22 | 23 | public static implicit operator Vector2S(Vector2 vector2) => new Vector2S(vector2.x, vector2.y); 24 | 25 | 26 | private Vector2? v2 = null; 27 | 28 | [JsonIgnore] 29 | public Vector2 vector2 30 | { 31 | get 32 | { 33 | if (!v2.HasValue) v2 = new Vector2(x, y); 34 | return v2.Value; 35 | } 36 | } 37 | } 38 | 39 | /// 40 | /// Serializes a . 41 | /// 42 | public struct Vector2IntS 43 | { 44 | public int x { get; set; } 45 | public int y { get; set; } 46 | 47 | public Vector2IntS(int x, int y) 48 | { 49 | this.x = x; 50 | this.y = y; 51 | } 52 | 53 | public static implicit operator Vector2Int(Vector2IntS vector2S) => vector2S.vector2; 54 | 55 | public static implicit operator Vector2IntS(Vector2Int vector2) => new Vector2IntS(vector2.x, vector2.y); 56 | 57 | 58 | private Vector2Int? v2 = null; 59 | 60 | [JsonIgnore] 61 | public Vector2Int vector2 62 | { 63 | get 64 | { 65 | if (!v2.HasValue) v2 = new Vector2Int(x, y); 66 | return v2.Value; 67 | } 68 | } 69 | } 70 | 71 | /// 72 | /// Serializes a . 73 | /// 74 | public struct Vector3S 75 | { 76 | public float x { get; set; } 77 | public float y { get; set; } 78 | public float z { get; set; } 79 | 80 | public Vector3S(float x, float y, float z) 81 | { 82 | this.x = x; 83 | this.y = y; 84 | this.z = z; 85 | } 86 | 87 | public static implicit operator Vector3(Vector3S vector3S) => vector3S.vector3; 88 | 89 | public static implicit operator Vector3S(Vector3 vector3) => new Vector3S(vector3.x, vector3.y, vector3.z); 90 | 91 | 92 | private Vector3? v3 = null; 93 | 94 | [JsonIgnore] 95 | public Vector3 vector3 96 | { 97 | get 98 | { 99 | if (!v3.HasValue) v3 = new Vector3(x, y, z); 100 | return v3.Value; 101 | } 102 | } 103 | } 104 | 105 | /// 106 | /// Serializes a . 107 | /// 108 | public struct Vector3IntS 109 | { 110 | public int x { get; set; } 111 | public int y { get; set; } 112 | public int z { get; set; } 113 | 114 | public Vector3IntS(int x, int y, int z) 115 | { 116 | this.x = x; 117 | this.y = y; 118 | this.z = z; 119 | } 120 | 121 | public static implicit operator Vector3Int(Vector3IntS vector3S) => vector3S.vector3; 122 | 123 | public static implicit operator Vector3IntS(Vector3Int vector3) => new Vector3IntS(vector3.x, vector3.y, vector3.z); 124 | 125 | 126 | private Vector3Int? v3 = null; 127 | 128 | [JsonIgnore] 129 | public Vector3Int vector3 130 | { 131 | get 132 | { 133 | if (!v3.HasValue) v3 = new Vector3Int(x, y, z); 134 | return v3.Value; 135 | } 136 | } 137 | } 138 | 139 | /// 140 | /// Serializes a . 141 | /// 142 | public struct Vector4S 143 | { 144 | public float x { get; set; } 145 | public float y { get; set; } 146 | public float z { get; set; } 147 | public float w { get; set; } 148 | 149 | public Vector4S(float x, float y, float z, float w) 150 | { 151 | this.x = x; 152 | this.y = y; 153 | this.z = z; 154 | this.w = w; 155 | } 156 | 157 | public static implicit operator Vector4(Vector4S vector4S) => vector4S.Vector4; 158 | 159 | public static implicit operator Vector4S(Vector4 vector4) => new Vector4S(vector4.x, vector4.y, vector4.z, vector4.w); 160 | 161 | 162 | private Vector4? v4 = null; 163 | 164 | [JsonIgnore] 165 | public Vector4 Vector4 166 | { 167 | get 168 | { 169 | if (!v4.HasValue) v4 = new Vector4(x, y, z, w); 170 | return v4.Value; 171 | } 172 | } 173 | } 174 | 175 | /// 176 | /// Serializes a . 177 | /// 178 | public struct QuaternionS 179 | { 180 | public float x { get; set; } 181 | public float y { get; set; } 182 | public float z { get; set; } 183 | public float w { get; set; } 184 | 185 | public QuaternionS(float x, float y, float z, float w) 186 | { 187 | this.x = x; 188 | this.y = y; 189 | this.z = z; 190 | this.w = w; 191 | } 192 | 193 | public static implicit operator Quaternion(QuaternionS quaternionS) => quaternionS.Quaternion; 194 | 195 | public static implicit operator QuaternionS(Quaternion quaternion) => new QuaternionS(quaternion.x, quaternion.y, quaternion.z, quaternion.w); 196 | 197 | 198 | private Quaternion? q = null; 199 | 200 | [JsonIgnore] 201 | public Quaternion Quaternion 202 | { 203 | get 204 | { 205 | if (!q.HasValue) q = new Quaternion(x, y, z, w); 206 | return q.Value; 207 | } 208 | } 209 | } 210 | 211 | /// 212 | /// Serializes a . 213 | /// 214 | public struct ColorS 215 | { 216 | public float r { get; set; } 217 | public float g { get; set; } 218 | public float b { get; set; } 219 | public float a { get; set; } 220 | 221 | public ColorS(float r, float g, float b, float a) 222 | { 223 | this.r = r; 224 | this.g = g; 225 | this.b = b; 226 | this.a = a; 227 | } 228 | 229 | public static implicit operator Color(ColorS colorS) => colorS.Color; 230 | 231 | public static implicit operator ColorS(Color color) => new ColorS(color.r, color.g, color.b, color.a); 232 | 233 | 234 | private Color? c = null; 235 | 236 | [JsonIgnore] 237 | public Color Color 238 | { 239 | get 240 | { 241 | if (!c.HasValue) c = new Color(r, g, b, a); 242 | return c.Value; 243 | } 244 | } 245 | } 246 | 247 | /// 248 | /// Serializes a . 249 | /// 250 | public struct Color32S 251 | { 252 | public byte r { get; set; } 253 | public byte g { get; set; } 254 | public byte b { get; set; } 255 | public byte a { get; set; } 256 | 257 | public Color32S(byte r, byte g, byte b, byte a) 258 | { 259 | this.r = r; 260 | this.g = g; 261 | this.b = b; 262 | this.a = a; 263 | } 264 | 265 | public static implicit operator Color32(Color32S colorS) => colorS.Color; 266 | 267 | public static implicit operator Color32S(Color32 color) => new Color32S(color.r, color.g, color.b, color.a); 268 | 269 | 270 | private Color32? c = null; 271 | 272 | [JsonIgnore] 273 | public Color32 Color 274 | { 275 | get 276 | { 277 | if (!c.HasValue) c = new Color32(r, g, b, a); 278 | return c.Value; 279 | } 280 | } 281 | } 282 | 283 | /// 284 | /// Serializes a . 285 | /// 286 | public struct RayS 287 | { 288 | public Vector3S origin { get; set; } 289 | public Vector3S direction { get; set; } 290 | 291 | public RayS(Vector3 origin, Vector3 direction) 292 | { 293 | this.origin = origin; 294 | this.direction = direction; 295 | } 296 | 297 | public static implicit operator Ray(RayS rayS) => rayS.Ray; 298 | 299 | public static implicit operator RayS(Ray ray) => new RayS(ray.origin, ray.direction); 300 | 301 | 302 | private Ray? r = null; 303 | 304 | [JsonIgnore] 305 | public Ray Ray 306 | { 307 | get 308 | { 309 | if (!r.HasValue) r = new Ray(origin, direction); 310 | return r.Value; 311 | } 312 | } 313 | } 314 | 315 | /// 316 | /// Serializes a . 317 | /// 318 | public struct Ray2DS 319 | { 320 | public Vector2S origin { get; set; } 321 | public Vector2S direction { get; set; } 322 | 323 | public Ray2DS(Vector2 origin, Vector2 direction) 324 | { 325 | this.origin = origin; 326 | this.direction = direction; 327 | } 328 | 329 | public static implicit operator Ray2D(Ray2DS ray2DS) => ray2DS.Ray; 330 | 331 | public static implicit operator Ray2DS(Ray2D ray2D) => new Ray2DS(ray2D.origin, ray2D.direction); 332 | 333 | 334 | private Ray2D? r = null; 335 | 336 | [JsonIgnore] 337 | public Ray2D Ray 338 | { 339 | get 340 | { 341 | if (!r.HasValue) r = new Ray2D(origin, direction); 342 | return r.Value; 343 | } 344 | } 345 | } 346 | #pragma warning restore 347 | } 348 | -------------------------------------------------------------------------------- /LC-API/Plugin.cs: -------------------------------------------------------------------------------- 1 | using BepInEx; 2 | using BepInEx.Configuration; 3 | using BepInEx.Logging; 4 | using HarmonyLib; 5 | using LC_API.ClientAPI; 6 | using LC_API.Comp; 7 | using LC_API.GameInterfaceAPI.Events; 8 | using LC_API.ManualPatches; 9 | using LC_API.Networking; 10 | using LC_API.ServerAPI; 11 | using System.Reflection; 12 | using Unity.Netcode; 13 | using UnityEngine; 14 | 15 | namespace LC_API 16 | { 17 | // .____ _________ _____ __________ .___ 18 | // | | \_ ___ \ / _ \ \______ \| | 19 | // | | / \ \/ / /_\ \ | ___/| | 20 | // | |___\ \____ / | \| | | | 21 | // |_______ \\______ /______\____|__ /|____| |___| 22 | // \/ \//_____/ \/ 23 | /// 24 | /// The Lethal Company modding API plugin! 25 | /// 26 | [BepInPlugin(MyPluginInfo.PLUGIN_GUID, MyPluginInfo.PLUGIN_NAME, MyPluginInfo.PLUGIN_VERSION)] 27 | public sealed class Plugin : BaseUnityPlugin 28 | { 29 | internal static Plugin Instance { get; private set; } 30 | /// 31 | /// Runs after the LC API plugin's "Awake" method is finished. 32 | /// 33 | public static bool Initialized { get; private set; } 34 | 35 | #pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. 36 | internal static ManualLogSource Log; 37 | 38 | private ConfigEntry configOverrideModServer; 39 | private ConfigEntry configLegacyAssetLoading; 40 | private ConfigEntry configDisableBundleLoader; 41 | internal static ConfigEntry configVanillaSupport; 42 | #pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. 43 | 44 | 45 | internal static Harmony Harmony; 46 | 47 | private void Awake() 48 | { 49 | Instance = this; 50 | configOverrideModServer = Config.Bind("General", "Force modded server browser", false, "Should the API force you into the modded server browser?"); 51 | configLegacyAssetLoading = Config.Bind("General", "Legacy asset bundle loading", false, "Should the BundleLoader use legacy asset loading? Turning this on may help with loading assets from older plugins."); 52 | configDisableBundleLoader = Config.Bind("General", "Disable BundleLoader", false, "Should the BundleLoader be turned off? Enable this if you are having problems with mods that load assets using a different method from LC_API's BundleLoader."); 53 | configVanillaSupport = Config.Bind("Compatibility", "Vanilla Compatibility", false, "Allows you to join vanilla servers, but disables many networking-related things and could cause mods to not work properly."); 54 | CommandHandler.commandPrefix = Config.Bind("General", "Prefix", "/", "Command prefix"); 55 | 56 | Log = Logger; 57 | // Plugin startup logic 58 | Logger.LogWarning("\n.____ _________ _____ __________ .___ \r\n| | \\_ ___ \\ / _ \\ \\______ \\| | \r\n| | / \\ \\/ / /_\\ \\ | ___/| | \r\n| |___\\ \\____ / | \\| | | | \r\n|_______ \\\\______ /______\\____|__ /|____| |___| \r\n \\/ \\//_____/ \\/ \r\n "); 59 | Logger.LogInfo($"LC_API Starting up.."); 60 | if (configOverrideModServer.Value) 61 | { 62 | ModdedServer.SetServerModdedOnly(); 63 | } 64 | 65 | if (configVanillaSupport.Value) Logger.LogInfo("LC_API is starting with VANILLA SUPPORT ENABLED."); 66 | 67 | Harmony = new Harmony("ModAPI"); 68 | MethodInfo originalLobbyCreated = AccessTools.Method(typeof(GameNetworkManager), "SteamMatchmaking_OnLobbyCreated"); 69 | MethodInfo originalLobbyJoinable = AccessTools.Method(typeof(GameNetworkManager), "LobbyDataIsJoinable"); 70 | 71 | MethodInfo patchLobbyCreate = AccessTools.Method(typeof(ServerPatch), nameof(ServerPatch.OnLobbyCreate)); 72 | 73 | MethodInfo originalMenuAwake = AccessTools.Method(typeof(MenuManager), "Awake"); 74 | 75 | MethodInfo patchCacheMenuMgr = AccessTools.Method(typeof(ServerPatch), nameof(ServerPatch.CacheMenuManager)); 76 | 77 | MethodInfo originalAddChatMsg = AccessTools.Method(typeof(HUDManager), "AddChatMessage"); 78 | 79 | MethodInfo originalSubmitChat = AccessTools.Method(typeof(HUDManager), "SubmitChat_performed"); 80 | 81 | MethodInfo patchSubmitChat = AccessTools.Method(typeof(CommandHandler.SubmitChatPatch), nameof(CommandHandler.SubmitChatPatch.Transpiler)); 82 | 83 | MethodInfo originalGameManagerAwake = AccessTools.Method(typeof(GameNetworkManager), nameof(GameNetworkManager.Awake)); 84 | 85 | MethodInfo patchGameManagerAwake = AccessTools.Method(typeof(ServerPatch), nameof(ServerPatch.GameNetworkManagerAwake)); 86 | 87 | MethodInfo originalStartClient = AccessTools.Method(typeof(NetworkManager), nameof(NetworkManager.StartClient)); 88 | MethodInfo originalStartHost = AccessTools.Method(typeof(NetworkManager), nameof(NetworkManager.StartHost)); 89 | MethodInfo originalShutdown = AccessTools.Method(typeof(NetworkManager), nameof(NetworkManager.Shutdown)); 90 | 91 | MethodInfo registerPatch = AccessTools.Method(typeof(RegisterPatch), nameof(RegisterPatch.Postfix)); 92 | MethodInfo unregisterPatch = AccessTools.Method(typeof(UnregisterPatch), nameof(UnregisterPatch.Postfix)); 93 | 94 | Harmony.Patch(originalMenuAwake, new HarmonyMethod(patchCacheMenuMgr)); 95 | Harmony.Patch(originalLobbyCreated, new HarmonyMethod(patchLobbyCreate)); 96 | Harmony.Patch(originalSubmitChat, null, null, new HarmonyMethod(patchSubmitChat)); 97 | Harmony.Patch(originalGameManagerAwake, new HarmonyMethod(patchGameManagerAwake)); 98 | 99 | Harmony.Patch(originalStartClient, null, new HarmonyMethod(registerPatch)); 100 | Harmony.Patch(originalStartHost, null, new HarmonyMethod(registerPatch)); 101 | 102 | Harmony.Patch(originalShutdown, null, new HarmonyMethod(unregisterPatch)); 103 | 104 | Network.Init(); 105 | Events.Patch(Harmony); 106 | } 107 | 108 | internal void Start() 109 | { 110 | Initialize(); 111 | } 112 | 113 | internal void OnDestroy() 114 | { 115 | Initialize(); 116 | } 117 | 118 | internal void Initialize() 119 | { 120 | if (!Initialized) 121 | { 122 | Initialized = true; 123 | if (!configDisableBundleLoader.Value) 124 | { 125 | BundleAPI.BundleLoader.Load(configLegacyAssetLoading.Value); 126 | } 127 | GameObject gameObject = new GameObject("API"); 128 | DontDestroyOnLoad(gameObject); 129 | gameObject.AddComponent(); 130 | Logger.LogInfo($"LC_API Started!"); 131 | CheatDatabase.RunLocalCheatDetector(); 132 | } 133 | } 134 | 135 | internal static void PatchMethodManual(MethodInfo method, MethodInfo patch, Harmony harmony) 136 | { 137 | harmony.Patch(method, new HarmonyMethod(patch)); 138 | } 139 | } 140 | } -------------------------------------------------------------------------------- /LC-API/ServerAPI/ModdedServer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | #pragma warning disable CS0618 // Member is obsolete 4 | namespace LC_API.ServerAPI 5 | { 6 | /// 7 | /// You're probably here for 8 | /// 9 | public static class ModdedServer 10 | { 11 | private static bool moddedOnly; 12 | [Obsolete("Use SetServerModdedOnly() instead. This will be removed/private in a future update.")] 13 | public static bool setModdedOnly; // obsolete for the purposes of getting peoples' IDE's to bitch at them. 14 | 15 | public static int GameVersion { get; internal set; } 16 | 17 | /// 18 | /// Has the user been placed in modded only servers? 19 | /// 20 | public static bool ModdedOnly 21 | { 22 | get { return moddedOnly; } 23 | } 24 | 25 | /// 26 | /// Call this method to make your plugin place the user in modded only servers. 27 | /// 28 | public static void SetServerModdedOnly() 29 | { 30 | moddedOnly = true; 31 | Plugin.Log.LogMessage("A plugin has set your game to only allow you to play with other people who have mods!"); 32 | } 33 | 34 | /// 35 | /// For internal use. Do not call this method. 36 | /// 37 | public static void OnSceneLoaded() 38 | { 39 | if (GameNetworkManager.Instance && ModdedOnly) 40 | { 41 | GameNetworkManager.Instance.gameVersionNum += 16440; 42 | setModdedOnly = true; 43 | } 44 | } 45 | } 46 | } 47 | #pragma warning restore CS0618 // Member is obsolete 48 | -------------------------------------------------------------------------------- /LC-API/ServerAPI/Networking.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using UnityEngine; 5 | 6 | namespace LC_API.ServerAPI 7 | { 8 | /// 9 | /// Networking solution to broadcast and receive data over the server. Use the delegates GetString, GetInt, GetFloat, and GetVector3 for receiving data. Note that the local player will not receive data that they broadcast. 10 | /// The second parameter for each of the events is the signature string. 11 | /// 12 | [Obsolete("ServerAPI.Networking is obsolete and will be removed in future versions. Use LC_API.Networking.Network.")] 13 | public static class Networking 14 | { 15 | private const string StringMessageRegistrationName = "LCAPI_NET_LEGACY_STRING"; 16 | private const string ListStringMessageRegistrationName = "LCAPI_NET_LEGACY_LISTSTRING"; 17 | private const string IntMessageRegistrationName = "LCAPI_NET_LEGACY_INT"; 18 | private const string FloatMessageRegistrationName = "LCAPI_NET_LEGACY_FLOAT"; 19 | private const string Vector3MessageRegistrationName = "LCAPI_NET_LEGACY_VECTOR3"; 20 | 21 | private const string SyncVarMessageRegistrationName = "LCAPI_NET_LEGACY_SYNCVAR_SET"; 22 | 23 | /// 24 | /// Delegate for receiving a string value. Second parameter is the signature. 25 | /// (that signature would have been the signature given to , for example) 26 | /// 27 | public static Action GetString = (_, _) => { }; 28 | /// 29 | /// Delegate for receiving a list of string values. Second parameter is the signature. 30 | /// (that signature would have been the signature given to , for example) 31 | /// 32 | public static Action, string> GetListString = (_, _) => { }; 33 | /// 34 | /// Delegate for receiving a int value. Second parameter is the signature. 35 | /// (that signature would have been the signature given to , for example) 36 | /// 37 | public static Action GetInt = (_, _) => { }; 38 | /// 39 | /// Delegate for receiving a float value. Second parameter is the signature. 40 | /// (that signature would have been the signature given to , for example) 41 | /// 42 | public static Action GetFloat = (_, _) => { }; 43 | /// 44 | /// Delegate for receiving a Vector3 value. Second parameter is the signature. 45 | /// (that signature would have been the signature given to , for example) 46 | /// 47 | public static Action GetVector3 = (_, _) => { }; 48 | 49 | private static Dictionary syncStringVars = new Dictionary(); 50 | 51 | /// 52 | /// Send data across the network. The signature is an identifier for use when receiving data. 53 | /// 54 | public static void Broadcast(string data, string signature) 55 | { 56 | if (data.Contains("/")) 57 | { 58 | Plugin.Log.LogError("Invalid character in broadcasted string event! ( / )"); 59 | return; 60 | } 61 | LC_API.Networking.Network.Broadcast(StringMessageRegistrationName, new Data(signature: signature, value: data)); 62 | } 63 | 64 | /// 65 | /// Send data across the network. The signature is an identifier for use when receiving data. 66 | /// 67 | public static void Broadcast(List data, string signature) 68 | { 69 | string dataFormatted = ""; 70 | foreach (var item in data) 71 | { 72 | if (item.Contains("/")) 73 | { 74 | Plugin.Log.LogError("Invalid character in broadcasted string event! ( / )"); 75 | return; 76 | } 77 | if (item.Contains("\n")) 78 | { 79 | Plugin.Log.LogError("Invalid character in broadcasted string event! ( NewLine )"); 80 | return; 81 | } 82 | dataFormatted += item + "\n"; 83 | } 84 | LC_API.Networking.Network.Broadcast(ListStringMessageRegistrationName, new Data(signature: signature, value: dataFormatted)); 85 | } 86 | 87 | /// 88 | /// Send data across the network. The signature is an identifier for use when receiving data. 89 | /// 90 | public static void Broadcast(int data, string signature) 91 | { 92 | LC_API.Networking.Network.Broadcast(IntMessageRegistrationName, new Data(signature: signature, value: data)); 93 | } 94 | 95 | /// 96 | /// Send data across the network. The signature is an identifier for use when receiving data. 97 | /// 98 | public static void Broadcast(float data, string signature) 99 | { 100 | LC_API.Networking.Network.Broadcast(FloatMessageRegistrationName, new Data(signature: signature, value: data)); 101 | } 102 | 103 | /// 104 | /// Send data across the network. The signature is an identifier for use when receiving data. 105 | /// 106 | public static void Broadcast(Vector3 data, string signature) 107 | { 108 | LC_API.Networking.Network.Broadcast(Vector3MessageRegistrationName, new Data(signature: signature, value: data)); 109 | } 110 | 111 | /// 112 | /// Register a Sync Variable. Currently Sync Variables can only store string values. 113 | /// 114 | public static void RegisterSyncVariable(string name) 115 | { 116 | if (!syncStringVars.ContainsKey(name)) 117 | { 118 | syncStringVars.Add(name, ""); 119 | } 120 | else 121 | { 122 | Plugin.Log.LogError("Cannot register Sync Variable! A Sync Variable has already been registered with name " + name); 123 | } 124 | } 125 | 126 | /// 127 | /// Set the value of a Sync Variable. 128 | /// 129 | public static void SetSyncVariable(string name, string value) 130 | { 131 | if (syncStringVars.ContainsKey(name)) 132 | { 133 | syncStringVars[name] = value; 134 | List syncString = new List(); 135 | syncString.Add(name); 136 | syncString.Add(value); 137 | Broadcast(syncString, SyncVarMessageRegistrationName); 138 | } 139 | else 140 | { 141 | Plugin.Log.LogError("Cannot set the value of Sync Variable " + name + " as it is not registered!"); 142 | } 143 | } 144 | 145 | private static void SetSyncVariableB(string name, string value) 146 | { 147 | if (syncStringVars.ContainsKey(name)) 148 | { 149 | syncStringVars[name] = value; 150 | } 151 | else 152 | { 153 | Plugin.Log.LogError("Cannot set the value of Sync Variable " + name + " as it is not registered!"); 154 | } 155 | } 156 | 157 | internal static void LCAPI_NET_SYNCVAR_SET(List list, string arg2) 158 | { 159 | if (arg2 == SyncVarMessageRegistrationName) 160 | { 161 | SetSyncVariableB(list[0], list[1]); 162 | } 163 | } 164 | 165 | /// 166 | /// Get the value of a Sync Variable. 167 | /// 168 | public static string GetSyncVariable(string name) 169 | { 170 | if (syncStringVars.ContainsKey(name)) 171 | { 172 | return syncStringVars[name]; 173 | } 174 | else 175 | { 176 | Plugin.Log.LogError("Cannot get the value of Sync Variable " + name + " as it is not registered!"); 177 | return ""; 178 | } 179 | } 180 | 181 | private sealed class Data 182 | { 183 | public readonly string Signature; 184 | public readonly T Value; 185 | 186 | public Data(string signature, T value) 187 | { 188 | Signature = signature; 189 | Value = value; 190 | } 191 | } 192 | 193 | internal static void InitializeLegacyNetworking() 194 | { 195 | GetListString += LCAPI_NET_SYNCVAR_SET; 196 | 197 | LC_API.Networking.Network.RegisterMessage( 198 | StringMessageRegistrationName, 199 | false, 200 | (ulong senderId, Data data) => GetString(data.Value, data.Signature)); 201 | LC_API.Networking.Network.RegisterMessage( 202 | ListStringMessageRegistrationName, 203 | false, 204 | (ulong senderId, Data data) => GetListString(data.Value.Split('\n').ToList(), data.Signature)); 205 | LC_API.Networking.Network.RegisterMessage( 206 | IntMessageRegistrationName, 207 | false, 208 | (ulong senderId, Data data) => GetInt(data.Value, data.Signature)); 209 | LC_API.Networking.Network.RegisterMessage( 210 | FloatMessageRegistrationName, 211 | false, 212 | (ulong senderId, Data data) => GetFloat(data.Value, data.Signature)); 213 | LC_API.Networking.Network.RegisterMessage( 214 | Vector3MessageRegistrationName, 215 | false, 216 | (ulong senderId, Data data) => GetVector3(data.Value, data.Signature)); 217 | } 218 | } 219 | } -------------------------------------------------------------------------------- /LC-API/Utils.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using System.Text.RegularExpressions; 4 | 5 | namespace LC_API 6 | { 7 | /// 8 | /// Provides a few useful utilities for the API. 9 | /// 10 | public static class Utils 11 | { 12 | // Thanks Elias https://stackoverflow.com/a/11105164 13 | /// 14 | /// Replaces a string and attempts to keep its casing. 15 | /// 16 | /// The input . 17 | /// The to replace. 18 | /// The replacement . 19 | /// The with all instances of toReplace replaced with replacement. 20 | public static string ReplaceWithCase(this string input, string toReplace, string replacement) 21 | { 22 | Dictionary map = new Dictionary 23 | { 24 | { toReplace, replacement } 25 | }; 26 | 27 | return input.ReplaceWithCase(map); 28 | } 29 | 30 | // Thanks Elias https://stackoverflow.com/a/11105164 31 | /// 32 | /// Replaces a string and attempts to keep its casing. 33 | /// 34 | /// The input . 35 | /// A of strings to replace and their replacement. 36 | /// The completed . 37 | public static string ReplaceWithCase(this string input, Dictionary map) 38 | { 39 | string temp = input; 40 | foreach (var entry in map) 41 | { 42 | string key = entry.Key; 43 | string value = entry.Value; 44 | temp = Regex.Replace(temp, key, match => 45 | { 46 | bool isFirstUpper, isEachUpper, isAllUpper; 47 | 48 | string sentence = match.Value; 49 | char[] sentenceArray = sentence.ToCharArray(); 50 | 51 | string[] words = sentence.Split(' '); 52 | 53 | isFirstUpper = char.IsUpper(sentenceArray[0]); 54 | 55 | isEachUpper = words.All(w => char.IsUpper(w[0]) || !char.IsLetter(w[0])); 56 | 57 | isAllUpper = sentenceArray.All(c => char.IsUpper(c) || !char.IsLetter(c)); 58 | 59 | if (isAllUpper) 60 | return value.ToUpper(); 61 | 62 | if (isEachUpper) 63 | { 64 | string capitalized = Regex.Replace(value, @"\b\w", charMatch => charMatch.Value.ToUpper()); 65 | return capitalized; 66 | } 67 | 68 | 69 | char[] result = value.ToCharArray(); 70 | result[0] = isFirstUpper 71 | ? char.ToUpper(result[0]) 72 | : char.ToLower(result[0]); 73 | return new string(result); 74 | }, RegexOptions.IgnoreCase); 75 | } 76 | 77 | return temp; 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # LC-API 2 | 3 | [![Build](https://github.com/steven4547466/LC-API/actions/workflows/build.yml/badge.svg)](https://github.com/steven4547466/LC-API/actions/workflows/build.yml) 4 | [![Latest Version](https://img.shields.io/thunderstore/v/2018/LC_API?logo=thunderstore&logoColor=white)](https://thunderstore.io/c/lethal-company/p/2018/LC_API) 5 | [![Total Downloads](https://img.shields.io/thunderstore/dt/2018/LC_API?logo=thunderstore&logoColor=white)](https://thunderstore.io/c/lethal-company/p/2018/LC_API) 6 | 7 | The definitive Lethal Company modding API. Includes some very useful features to make modding life easier. 8 | 9 | # For Developers 10 | If you want to use the API in your plugin, add the LC_API.dll as a project reference! 11 | 12 | ## Contributing 13 | If you wish to contribute to this project, you will need [unity netcode weaver](https://github.com/EvaisaDev/UnityNetcodeWeaver/releases) 14 | to implement custom networking properly. Follow their instructions to get NetcodeWeaver set-up for patching Lethal Company mods 15 | and keep note of the filepath where you chose to install it. 16 | 17 | Once you have forked and cloned the repository, you will need to create a file in the solution folder called `LC-API.csproj.user` 18 | to set paths to build dependencies. Here's a template for that file's contents: 19 | ```xml 20 | 21 | 22 | 23 | F:/SteamLibrary/steamapps/common/Lethal Company 24 | $(APPDATA)/r2modmanPlus-local/LethalCompany/profiles/Test LC API 25 | $(SolutionDir)NetcodeWeaver 26 | 27 | 28 | 30 | 31 | 35 | 36 | 37 | 38 | ``` 39 | 40 | It is vital that you change the `NETCODE_PATCHER_DIR` property to the location of your local NetcodeWeaver installation. 41 | 42 | Ensure your Assembly CSharp is set `Publicize="true"` in the .csproj file to ensure it gets publicized. 43 | 44 | Once you have completed these steps, you will be able to properly build the solution. 45 | 46 | ## Making a PR 47 | Your [pull request](https://github.com/steven4547466/LC-API/pulls) should target the [dev branch](https://github.com/steven4547466/LC-API/tree/dev). This is because the `main` branch is reserved for tested features that are ready for release. Basically if someone were to clone the repo, they should be able to build `main` and use it without any fear of broken things. 48 | 49 | The `dev` branch, however, may contain untested features and is used to build release candidates. Before releases, the `dev` branch will be frozen and tested for issues, when it passes its testing then it will be merged into `main` and a release will be made. Pre-releases may come from the `dev` branch for release candidates and testing. These will be generally stable, but may still contain broken features before testing is done. 50 | 51 | Pull requests targeting the `main` branch will not be merged in most circumstances. Most merges to `main` will be directly from the frozen `dev` branch after testing. 52 | 53 | # Features 54 | AssetBundle loading - Put asset bundles in BepInEx > Bundles and load them using BundleAPI.BundleLoader.GetLoadedAsset 55 | 56 | ServerAPI - Utilities relating to the network and server. This includes: 57 | 58 | ModdedServer - Automatically alerts other users when you host a server that your server is modded. 59 | It also lets mod authors make their mods put users in special matchmaking where they can only play with other modded users 60 | 61 | Networking - Easily send data across the network to sync data between clients 62 | 63 | # Installation 64 | 65 | ## R2ModMan or Thunderstore Manager (highly recommended) 66 | 67 | ### R2ModMan 68 | 1. Go to the [thunderstore page](https://thunderstore.io/c/lethal-company/p/2018/LC_API) 69 | 2. Click `Install with Mod Manager` 70 | 71 | ### Thunderstore Manager 72 | (if the above doesn't work for you, open up the Thunderstore App to do the following) 73 | 1. Click `Get mods`/`Online` (whatever it happens to be called) 74 | 2. Search for LC API 75 | 3. Download it 76 | 77 | ## Manual 78 | 1. Go to the [thunderstore page](https://thunderstore.io/c/lethal-company/p/2018/LC_API) 79 | 2. Click `Manual Download` 80 | 3. Unzip files 81 | 4. Navigate to `2018-LC_API-VERSION/BepinEx/plugins` and copy the contents 82 | 5. Find your BepinEx installation's plugin folder, by default it would be in steamapps: `steamapps\common\Lethal Company\BepInEx\plugins` 83 | 6. Create a folder titled `2018-LC_API` 84 | 7. Paste the contents into that folder 85 | 86 | **DO NOT PLACE THE BUNDLES FOLDER IN THE PREMADE `BepinEx/Bundles` FOLDER!** It must be in the `2018-LC_API` folder. 87 | 88 | If you did all of this correctly, it should load properly. 89 | 90 | The resulting file structure should look like this: 91 | ``` 92 | BepinEx 93 | ├───Bundles 94 | ├───cache 95 | ├───config 96 | ├───core 97 | ├───patchers 98 | └───plugins 99 | └───2018-LC_API 100 | ├───Bundles 101 | │ └───networking 102 | └───LC_API.dll 103 | ``` 104 | 105 | # TODO 106 | - Picking up and dropping item events 107 | - Including `Player.StartGrabbingItem`, `Player.GrabbingItem`, `Player.GrabbedItem`, `Player.DroppingItem`, and `Player.DroppedItem` 108 | - The present-tense versions are cancellable, `StartGrabbingItem`, `GrabbingItem` and `DroppingItem`. 109 | - `StartGrabbingItem` exists for items that take time to pickup. 110 | - Using item events 111 | - For shovels, that's hitting 112 | - For boomboxes, that's activating music 113 | - etc. though this would all fall under one event probably just `Item.Using` or `Item.Activating` 114 | - Player begin and end moving events 115 | - Player crouch and jump events 116 | - Changing item event 117 | - Selling items event 118 | - Probably one for selling many items on the counter which will also call an event for each individual item being sold 119 | - Item spawning event 120 | - Enemy stuff is a bit further out, but here's some events that will be useful for them (as well as their past tense versions): 121 | - Spawning 122 | - Hurting 123 | - Dying 124 | - Attacking (when the enemy attacks a player) 125 | - There may be ones specific to some kind of enemy as well 126 | - `Player.EnteringFacility` and `Player.LeavingFacility` 127 | - With a bool for using fire escape -------------------------------------------------------------------------------- /assets/bundles/networking: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/u-2018/LC-API/cfbc25acc43f0dd8eb5fd18546b404786b32267e/assets/bundles/networking -------------------------------------------------------------------------------- /assets/icons/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/u-2018/LC-API/cfbc25acc43f0dd8eb5fd18546b404786b32267e/assets/icons/icon.png -------------------------------------------------------------------------------- /assets/thunderstore.toml: -------------------------------------------------------------------------------- 1 | [config] 2 | schemaVersion = "0.0.1" 3 | 4 | [general] 5 | repository = "https://thunderstore.io" 6 | 7 | [package] 8 | namespace = "2018" 9 | name = "LC_API" 10 | description = "Multipurpose modding API for Lethal Company" 11 | websiteUrl = "https://github.com/steven4547466/LC-API" 12 | containsNsfwContent = false 13 | 14 | [package.dependencies] 15 | BepInEx-BepInExPack = "5.4.2100" 16 | 17 | [build] 18 | icon = "icons/icon.png" 19 | readme = "../README.md" 20 | outdir = "../dist" 21 | 22 | [[build.copy]] 23 | source = "../LC-API/bin/netstandard2.1/LC_API.dll" 24 | target = "BepInEx/plugins/LC_API.dll" 25 | 26 | [[build.copy]] 27 | source = "./bundles" 28 | target = "BepInEx/plugins/Bundles/" 29 | 30 | [[build.copy]] 31 | source = "../CHANGELOG.md" 32 | target = "CHANGELOG.md" 33 | 34 | [[build.copy]] 35 | source = "../LICENSE" 36 | target = "LICENSE" 37 | 38 | [publish] 39 | communities = ["lethal-company"] 40 | 41 | [publish.categories] 42 | lethal-company = ["bepinex", "libraries", "tools"] --------------------------------------------------------------------------------