├── assets ├── repository │ ├── icon.png │ ├── Latency HUD.png │ ├── Custom Username.png │ ├── Manual Server Input.png │ ├── source │ │ ├── LCDirectLAN icon.ora │ │ └── Customizeable Hosting Port.ora │ ├── Customizeable Hosting Port.png │ ├── wiki │ │ └── Installation │ │ │ ├── LaunchGame.png │ │ │ ├── ExtractArchive.png │ │ │ ├── WorkingBepInEx.png │ │ │ └── CopyLCDirectLAN.png │ ├── Modify Connection Timeout value.png │ └── guide │ │ └── Maintainer.md ├── thunderstore │ ├── icon.png │ ├── BepInEx │ │ └── plugins │ │ │ ├── README.md │ │ │ └── libs │ │ │ └── README.md │ ├── manifest.json │ ├── LICENSE │ ├── README.md │ └── CHANGELOG.md └── README.md ├── NuGet.Config ├── CONTRIBUTORS.md ├── LICENSE ├── LCDirectLAN.sln ├── Patches ├── CustomUsername │ ├── StartOfRoundPatch.cs │ ├── PlayerControllerBPatch.cs │ └── UsernameRPC.cs ├── UnityNetworking │ └── UnityTransportPatch.cs ├── PreInitSceneScriptPatch.cs ├── LatencyHUD │ ├── HUDManagerPatch.cs │ └── LatencyRPC.cs └── ConfigurableLAN │ └── MenuManagerPatch.cs ├── Utility ├── allPlayerScripts.cs ├── GameObjectManager.cs └── ResolveDNS.cs ├── CHANGELOG.md ├── Properties └── AssemblyInfo.cs ├── README.md ├── LCDirectLAN.csproj ├── .gitignore └── LCDirectLan.cs /assets/repository/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TIRTAGT/LCDirectLAN/HEAD/assets/repository/icon.png -------------------------------------------------------------------------------- /assets/thunderstore/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TIRTAGT/LCDirectLAN/HEAD/assets/thunderstore/icon.png -------------------------------------------------------------------------------- /assets/repository/Latency HUD.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TIRTAGT/LCDirectLAN/HEAD/assets/repository/Latency HUD.png -------------------------------------------------------------------------------- /assets/repository/Custom Username.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TIRTAGT/LCDirectLAN/HEAD/assets/repository/Custom Username.png -------------------------------------------------------------------------------- /assets/repository/Manual Server Input.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TIRTAGT/LCDirectLAN/HEAD/assets/repository/Manual Server Input.png -------------------------------------------------------------------------------- /assets/repository/source/LCDirectLAN icon.ora: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TIRTAGT/LCDirectLAN/HEAD/assets/repository/source/LCDirectLAN icon.ora -------------------------------------------------------------------------------- /assets/repository/Customizeable Hosting Port.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TIRTAGT/LCDirectLAN/HEAD/assets/repository/Customizeable Hosting Port.png -------------------------------------------------------------------------------- /assets/repository/wiki/Installation/LaunchGame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TIRTAGT/LCDirectLAN/HEAD/assets/repository/wiki/Installation/LaunchGame.png -------------------------------------------------------------------------------- /assets/repository/Modify Connection Timeout value.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TIRTAGT/LCDirectLAN/HEAD/assets/repository/Modify Connection Timeout value.png -------------------------------------------------------------------------------- /assets/repository/wiki/Installation/ExtractArchive.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TIRTAGT/LCDirectLAN/HEAD/assets/repository/wiki/Installation/ExtractArchive.png -------------------------------------------------------------------------------- /assets/repository/wiki/Installation/WorkingBepInEx.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TIRTAGT/LCDirectLAN/HEAD/assets/repository/wiki/Installation/WorkingBepInEx.png -------------------------------------------------------------------------------- /assets/repository/source/Customizeable Hosting Port.ora: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TIRTAGT/LCDirectLAN/HEAD/assets/repository/source/Customizeable Hosting Port.ora -------------------------------------------------------------------------------- /assets/repository/wiki/Installation/CopyLCDirectLAN.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TIRTAGT/LCDirectLAN/HEAD/assets/repository/wiki/Installation/CopyLCDirectLAN.png -------------------------------------------------------------------------------- /NuGet.Config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /assets/thunderstore/BepInEx/plugins/README.md: -------------------------------------------------------------------------------- 1 | # LCDirectLAN - Thunderstore Template 2 | 3 | This is the place to put LCDirectLAN's files without the dependencies. 4 | 5 | Don't forget to delete this README.md template as it is not needed. -------------------------------------------------------------------------------- /assets/thunderstore/BepInEx/plugins/libs/README.md: -------------------------------------------------------------------------------- 1 | # LCDirectLAN - Thunderstore Template 2 | 3 | This is the place to put LCDirectLAN's dependencies and their licenses (separated by folders) 4 | 5 | Don't forget to delete this README.md template as it is not needed. -------------------------------------------------------------------------------- /assets/thunderstore/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "LCDirectLAN", 3 | "description": "Mod that fixes and enhances LAN lobbies without interfering with the Steam-networked lobbies, built around BepInEx.", 4 | "version_number": "1.1.3", 5 | "dependencies": [ 6 | "BepInEx-BepInExPack-5.4.2100" 7 | ], 8 | "website_url": "https://github.com/TIRTAGT/LCDirectLAN" 9 | } -------------------------------------------------------------------------------- /CONTRIBUTORS.md: -------------------------------------------------------------------------------- 1 | # LCDirectLAN - Contributors 2 | 3 | Below is a list of contributors to the LCDirectLAN project, in a chronological order of their contributions. 4 | 5 | For guide on how to contribute to the project, please visit the [Wiki/Contributing](../../wiki/Contributing) page. 6 | 7 | Thank you all for your contributions! 8 | 9 | ---- 10 | 11 | - [Matthew Tirtawidjaja](https://github.com/TIRTAGT) \ 12 | - [CoolLKKPS](https://github.com/CoolLKKPS) 13 | - [Castyblank320](https://github.com/Castyblank320) 14 | -------------------------------------------------------------------------------- /assets/README.md: -------------------------------------------------------------------------------- 1 | # LCDirectLAN - Assets 2 | 3 | This is the assets folder for LCDirectLAN project. 4 | It contains the following files/folders: 5 | - `README.md`: This file. 6 | 7 | - `thunderstore/`: Contains assets ("package") templates for uploading to the Thunderstore page. 8 | 9 | - `repository/`: Contains assets used on the GitHub Repository 10 | 11 | - `repository/source`: Contains source files for the assets used on the GitHub Repository. 12 | 13 | - `repository/wiki`: Contains assets used on the GitHub wiki. 14 | 15 | - `repository/guide`: Contains guides that are not part of the wiki but are used on the GitHub Repository. -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Matthew Tirtawidjaja 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /assets/thunderstore/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Matthew Tirtawidjaja 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /LCDirectLAN.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.9.34607.119 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LCDirectLAN", "LCDirectLAN.csproj", "{8551A54F-0C5C-48D0-ACF2-1C91581CD69D}" 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 | {8551A54F-0C5C-48D0-ACF2-1C91581CD69D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 15 | {8551A54F-0C5C-48D0-ACF2-1C91581CD69D}.Debug|Any CPU.Build.0 = Debug|Any CPU 16 | {8551A54F-0C5C-48D0-ACF2-1C91581CD69D}.Release|Any CPU.ActiveCfg = Release|Any CPU 17 | {8551A54F-0C5C-48D0-ACF2-1C91581CD69D}.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 = {E6BF503D-7247-43AB-95C3-CE3FA53607A9} 24 | EndGlobalSection 25 | EndGlobal 26 | -------------------------------------------------------------------------------- /Patches/CustomUsername/StartOfRoundPatch.cs: -------------------------------------------------------------------------------- 1 | /** 2 | * This source code is part of LCDirectLAN project, 3 | * LCDirectLAN is a mod for Lethal Company that is built around BepInEx to fix and enhances LAN lobbies. 4 | * 5 | * Project Repository: 6 | * https://github.com/TIRTAGT/LCDirectLAN 7 | * 8 | * This project is open source and are released under the MIT License, 9 | * for more information, please read the LICENSE file in the project repository. 10 | * 11 | * Copyright (c) 2024 Matthew Tirtawidjaja 12 | **/ 13 | 14 | namespace LCDirectLAN.Patches.CustomUsername 15 | { 16 | internal class StartOfRoundPatch 17 | { 18 | /// 19 | /// Because the player name on the map screen is already set BEFORE ConnectClientToPlayerObject is called, we need to update it again after the username is set. 20 | /// 21 | public static void LatePatch_PlayerNameOnMapScreen() 22 | { 23 | // Only update the player name on our side when there is a targeted player 24 | if (StartOfRound.Instance.mapScreen.targetedPlayer == null) { return; } 25 | 26 | StartOfRound.Instance.mapScreenPlayerName.text = "MONITORING: " + StartOfRound.Instance.mapScreen.targetedPlayer.playerUsername; 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /assets/thunderstore/README.md: -------------------------------------------------------------------------------- 1 | LCDirectLAN is a mod for Lethal Company that is built around BepInEx to fix and enhances LAN lobbies without interfering with the Steam-networked lobbies. 2 | 3 | This project is open source and are released under the MIT License, To view more detailed information about the mod/plugin/project, please visit the LCDirectLAN [Wiki page](https://github.com/TIRTAGT/LCDirectLAN/wiki) on GitHub. 4 | 5 | ---- 6 | 7 | ### Features (Fixes and Enhancements to LAN Lobbies) 8 | - Modify Lobby Hosting Port, allowing multiple lobbies to be hosted in the same IP Address 9 | 10 | - Manual IP and Port input for joining lobby 11 | 12 | - Supports automatic join configuration via DNS (SRV, TXT, AAAA, A) 13 | 14 | - Custom Username support, not related to the Steam Username 15 | 16 | - In-Game Latency display, (RTT/One-Way Measurement) 17 | 18 | - Modify the game default join connect timeout value to a custom value 19 | 20 | Screenshots, and more detailed information about the features are available on the [Wiki/Features](https://github.com/TIRTAGT/LCDirectLAN/wiki/Features) page. 21 | 22 | ---- 23 | 24 | ### Third-Party DLL Dependencies 25 | - [DnsClient](https://www.nuget.org/packages/DnsClient) 26 | - [System.Buffers](https://www.nuget.org/packages/System.Buffers/) -------------------------------------------------------------------------------- /Patches/UnityNetworking/UnityTransportPatch.cs: -------------------------------------------------------------------------------- 1 | /** 2 | * This source code is part of LCDirectLAN project, 3 | * LCDirectLAN is a mod for Lethal Company that is built around BepInEx to fix and enhances LAN lobbies. 4 | * 5 | * Project Repository: 6 | * https://github.com/TIRTAGT/LCDirectLAN 7 | * 8 | * This project is open source and are released under the MIT License, 9 | * for more information, please read the LICENSE file in the project repository. 10 | * 11 | * Copyright (c) 2024 Matthew Tirtawidjaja 12 | **/ 13 | 14 | using HarmonyLib; 15 | using Unity.Netcode; 16 | using Unity.Netcode.Transports.UTP; 17 | 18 | namespace LCDirectLAN.Patches.UnityNetworking 19 | { 20 | [HarmonyPatch(typeof(NetworkManager))] 21 | internal class UnityTransportPatch 22 | { 23 | [HarmonyPatch("Awake")] 24 | [HarmonyPostfix] 25 | [HarmonyPriority(Priority.VeryLow)] 26 | public static void Postfix_Awake(NetworkManager __instance) 27 | { 28 | UnityTransport UTP = __instance.GetComponent(); 29 | 30 | if (UTP == null) 31 | { 32 | LCDirectLan.Log(BepInEx.Logging.LogLevel.Error, "Cannot modify ConnectTimeout, cannot find NetworkManager !"); 33 | return; 34 | } 35 | 36 | // Check if we should modify default join/connect timeout 37 | short ModifyTimeout = LCDirectLan.GetConfig("Unity Networking", "ConnectTimeout"); 38 | 39 | if (ModifyTimeout >= 1) 40 | { 41 | UTP.MaxConnectAttempts = ModifyTimeout; 42 | LCDirectLan.Log(BepInEx.Logging.LogLevel.Info, $"Modified UnityTransport.MaxConnectAttempts to {ModifyTimeout}"); 43 | } 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Utility/allPlayerScripts.cs: -------------------------------------------------------------------------------- 1 | /** 2 | * This source code is part of LCDirectLAN project, 3 | * LCDirectLAN is a mod for Lethal Company that is built around BepInEx to fix and enhances LAN lobbies. 4 | * 5 | * Project Repository: 6 | * https://github.com/TIRTAGT/LCDirectLAN 7 | * 8 | * This project is open source and are released under the MIT License, 9 | * for more information, please read the LICENSE file in the project repository. 10 | * 11 | * Copyright (c) 2024 Matthew Tirtawidjaja 12 | **/ 13 | 14 | namespace LCDirectLAN.Utility 15 | { 16 | internal class allPlayerScripts 17 | { 18 | /// 19 | /// Get the actual Player ID by their Network ClientID 20 | /// 21 | /// The Network ClientID 22 | /// The actual player index or -1 on failure 23 | public static int GetActualPlayerIndex(ulong ClientID) 24 | { 25 | if (StartOfRound.Instance == null) { return -1; } 26 | 27 | for (int i = 0; i < StartOfRound.Instance.allPlayerScripts.Length; i++) 28 | { 29 | if (StartOfRound.Instance.allPlayerScripts[i].actualClientId == ClientID) 30 | { 31 | return i; 32 | } 33 | } 34 | 35 | return -1; 36 | } 37 | 38 | /// 39 | /// Get player username by their ID 40 | /// 41 | /// The player ID 42 | /// The player username or null on failure 43 | public static string GetPlayerUsername(int PlayerID) 44 | { 45 | if (StartOfRound.Instance == null) { return null; } 46 | 47 | if (PlayerID < 0 || PlayerID >= StartOfRound.Instance.allPlayerScripts.Length) { return null; } 48 | 49 | return StartOfRound.Instance.allPlayerScripts[PlayerID].playerUsername; 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /Patches/PreInitSceneScriptPatch.cs: -------------------------------------------------------------------------------- 1 | /** 2 | * This source code is part of LCDirectLAN project, 3 | * LCDirectLAN is a mod for Lethal Company that is built around BepInEx to fix and enhances LAN lobbies. 4 | * 5 | * Project Repository: 6 | * https://github.com/TIRTAGT/LCDirectLAN 7 | * 8 | * This project is open source and are released under the MIT License, 9 | * for more information, please read the LICENSE file in the project repository. 10 | * 11 | * Copyright (c) 2024 Matthew Tirtawidjaja 12 | **/ 13 | 14 | using System; 15 | using HarmonyLib; 16 | 17 | namespace LCDirectLAN.Patches 18 | { 19 | [HarmonyPatch(typeof(PreInitSceneScript))] 20 | internal class PreInitSceneScriptPatch 21 | { 22 | private static Action LateInjector = null; 23 | private static bool HasAlreadyLaunched = false; 24 | 25 | [HarmonyPatch("ChooseLaunchOption")] 26 | [HarmonyPostfix] 27 | [HarmonyPriority(Priority.VeryLow)] 28 | public static void Postfix_ChooseLaunchOption(PreInitSceneScript __instance, bool online) 29 | { 30 | if (HasAlreadyLaunched) { return; } 31 | 32 | HasAlreadyLaunched = true; 33 | LCDirectLan.IsOnLanMode = !online; 34 | 35 | if (!LCDirectLan.IsOnLanMode) 36 | { 37 | LCDirectLan.Log(BepInEx.Logging.LogLevel.Warning, $"{LCDirectLan.PLUGIN_NAME} is disabled when game is started on Online (steam) mode"); 38 | return; 39 | } 40 | 41 | LCDirectLan.Log(BepInEx.Logging.LogLevel.Info, $"{LCDirectLan.PLUGIN_NAME} is enabled"); 42 | 43 | // Inject the late patch if we are using Late Patching behavior 44 | LateInjector?.BeginInvoke(null, null); 45 | 46 | // Remove the late injector reference 47 | LateInjector = null; 48 | } 49 | 50 | public static void SetLateInjector(Action action) 51 | { 52 | LateInjector = action; 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # LCDirectLAN - Change Logs 2 | 3 | LCDirectLAN is a mod for Lethal Company built around BepInEx that fixes and enhances LAN lobbies without interfering with the Steam-networked lobbies. 4 | 5 | The change logs are organized in reverse chronological order, with the latest release at the top. 6 | Dates are in the format of `YYYY-MM-DD` (Year-Month-Day). 7 | 8 | ---- 9 | ## [1.1.3] - 2024-04-07 10 | - Fix unable to host when HostUsernameInput is false or unavailable 11 | 12 | ## [1.1.2] - 2024-04-07 13 | - Fix prevents hosting and join dialog crashes if CreateHostUsernameInput is disabled or unavailable 14 | - Added maintainers guide, I forgot what to do when trying to release a new version XD 15 | 16 | ## [1.1.1] - 2024-04-06 17 | - Fix invalid port error when the server port input on join window is empty instead of defaulting to config value 18 | - Added username input field for changing host username in-game on the host configuration window 19 | - Fix latency patch throwing errors when PlayerController is being destroyed (user left the game) 20 | 21 | ## [1.1.1] - 2024-04-06 22 | - Fix invalid port error when the server port input on join window is empty instead of defaulting to config value 23 | - Added username input field for changing host username in-game on the host configuration window 24 | - Fix latency patch throwing errors when PlayerController is being destroyed (user left the game) 25 | 26 | ## [1.1.0] - 2024-02-21 27 | - IPv6 hosting and join support 28 | - AAAA DNS Record support for IPv6, SRV support for IPv4 and IPv6 (Adjustable Priority in config) 29 | - Fix invisible join settings window after accidentally clicking host weekly challange 30 | - Fix LatencyRPC timing issues, add alternate latency source 31 | - Fix wrong NetworkManager instance check 32 | - Migrate to .NET Standard 2.1 33 | - Avoid SocketTImeout crash 34 | - Fix HideJoinData doesn't protect config leak 35 | - Add option to disable Latency HUD when hosting 36 | - Use Late Inject to further guard messing up Online mode 37 | 38 | ## [1.0.0] - 2024-02-09 39 | - Initial release -------------------------------------------------------------------------------- /assets/thunderstore/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # LCDirectLAN - Change Logs 2 | 3 | LCDirectLAN is a mod for Lethal Company built around BepInEx that fixes and enhances LAN lobbies without interfering with the Steam-networked lobbies. 4 | 5 | The change logs are organized in reverse chronological order, with the latest release at the top. 6 | Dates are in the format of `YYYY-MM-DD` (Year-Month-Day). 7 | 8 | ---- 9 | ## [1.1.3] - 2024-04-07 10 | - Fix unable to host when HostUsernameInput is false or unavailable 11 | 12 | ## [1.1.2] - 2024-04-07 13 | - Fix prevents hosting and join dialog crashes if CreateHostUsernameInput is disabled or unavailable 14 | - Added maintainers guide, I forgot what to do when trying to release a new version XD 15 | 16 | ## [1.1.1] - 2024-04-06 17 | - Fix invalid port error when the server port input on join window is empty instead of defaulting to config value 18 | - Added username input field for changing host username in-game on the host configuration window 19 | - Fix latency patch throwing errors when PlayerController is being destroyed (user left the game) 20 | 21 | ## [1.1.1] - 2024-04-06 22 | - Fix invalid port error when the server port input on join window is empty instead of defaulting to config value 23 | - Added username input field for changing host username in-game on the host configuration window 24 | - Fix latency patch throwing errors when PlayerController is being destroyed (user left the game) 25 | 26 | ## [1.1.0] - 2024-02-21 27 | - IPv6 hosting and join support 28 | - AAAA DNS Record support for IPv6, SRV support for IPv4 and IPv6 (Adjustable Priority in config) 29 | - Fix invisible join settings window after accidentally clicking host weekly challange 30 | - Fix LatencyRPC timing issues, add alternate latency source 31 | - Fix wrong NetworkManager instance check 32 | - Migrate to .NET Standard 2.1 33 | - Avoid SocketTImeout crash 34 | - Fix HideJoinData doesn't protect config leak 35 | - Add option to disable Latency HUD when hosting 36 | - Use Late Inject to further guard messing up Online mode 37 | 38 | ## [1.0.0] - 2024-02-09 39 | - Initial release -------------------------------------------------------------------------------- /assets/repository/guide/Maintainer.md: -------------------------------------------------------------------------------- 1 | # LCDirectLAN - Maintainer Guide 2 | 3 | ## New release checklist 4 | - [ ] Copy this file somewhere else as a checklist, so checking off items doesn't trigger a changed file in git 5 | - [ ] Bump versions in: 6 | - [ ] `/LCDirectLan.cs` 7 | - [ ] `/assets/thunderstore/manifest.json` 8 | 9 | - [ ] Make a new change log entry in `/CHANGELOG.md` 10 | - [ ] Duplicate the `CHANGELOG.md` entry to `/assets/thunderstore/CHANGELOG.md` 11 | - [ ] Make a new commit to the development branch with the changes above as "Prepare for vX.X.X release" 12 | - [ ] Push that commit to the development branch 13 | - [ ] Make a new pull request to main from the development branch, title should be `vX.X.X`, where `X.X.X` is the version number of the release and the description should be a change log entry (look at the previous pull requests for examples) 14 | - [ ] Merge the pull request with the merge title `vX.X.X (#A)`, where `X.X.X` is the version number and `#A` is the pull request number, and the description should be `Check the pull request for change logs.` 15 | - [ ] Copy the whole `/assets/thunderstore/` folder to a different location to prepare for the release, from now on any mentiong of `$RELEASE_DIR` will be this location instead of the repository folder 16 | - [ ] Rename `$RELEASE_DIR` folder to this format: `TIRTAGT-LCDirectLAN-X.X.X`, where `X.X.X` is the version number 17 | - [ ] Build the source code with `dotnet build -c Release` 18 | - [ ] Move the built `/bin/Release/netstandard2.1/LCDirectLAN.dll` to `$RELEASE_DIR/BepInEx/plugins/LCDirectLAN.dll` 19 | - [ ] Move the `/bin/Release/netstandard2.1/libs/*` contents to `$RELEASE_DIR/BepInEx/plugins/libs/` 20 | - [ ] Delete `$RELEASE_DIR/BepInEx/plugins/libs/README.md` 21 | - [ ] Delete `$RELEASE_DIR/BepInEx/plugins/README.md` 22 | - [ ] Archive the `$RELEASE_DIR` folder contents as a `.zip` file named `TIRTAGT-LCDirectLAN-X.X.X.zip`, where `X.X.X` is the version number 23 | - [ ] Create a new GitHub release and upload the `.zip` file as the binary for that release 24 | - [ ] Upload the `.zip` file to Thunderstore 25 | - [ ] Done! -------------------------------------------------------------------------------- /Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | /** 2 | * This source code is part of LCDirectLAN project, 3 | * LCDirectLAN is a mod for Lethal Company that is built around BepInEx to fix and enhances LAN lobbies. 4 | * 5 | * Project Repository: 6 | * https://github.com/TIRTAGT/LCDirectLAN 7 | * 8 | * This project is open source and are released under the MIT License, 9 | * for more information, please read the LICENSE file in the project repository. 10 | * 11 | * Copyright (c) 2024 Matthew Tirtawidjaja 12 | **/ 13 | 14 | using System.Reflection; 15 | using System.Runtime.InteropServices; 16 | using LCDirectLAN; 17 | 18 | // General Information about an assembly is controlled through the following 19 | // set of attributes. Change these attribute values to modify the information 20 | // associated with an assembly. 21 | [assembly: AssemblyTitle(LCDirectLan.PLUGIN_NAME)] 22 | [assembly: AssemblyDescription(LCDirectLan.PLUGIN_NAME + " is a BepInEx plugin for Lethal Company that fixes and enhances LAN lobbies.")] 23 | [assembly: AssemblyConfiguration(LCDirectLan.PLUGIN_COMPILE_CONFIG)] 24 | [assembly: AssemblyCompany("Matthew Tirtawidjaja")] 25 | [assembly: AssemblyProduct(LCDirectLan.PLUGIN_NAME)] 26 | [assembly: AssemblyCopyright("Copyright © 2024 Matthew Tirtawidjaja")] 27 | 28 | // Setting ComVisible to false makes the types in this assembly not visible 29 | // to COM components. If you need to access a type in this assembly from 30 | // COM, set the ComVisible attribute to true on that type. 31 | [assembly: ComVisible(false)] 32 | 33 | // The following GUID is for the ID of the typelib if this project is exposed to COM 34 | [assembly: Guid("8551a54f-0c5c-48d0-acf2-1c91581cd69d")] 35 | 36 | // Version information for an assembly consists of the following four values: 37 | // 38 | // Major Version 39 | // Minor Version 40 | // Build Number 41 | // Revision 42 | // 43 | // You can specify all the values or you can default the Build and Revision Numbers 44 | // by using the '*' as shown below: 45 | // [assembly: AssemblyVersion("1.0.*")] 46 | [assembly: AssemblyVersion(LCDirectLan.PLUGIN_ASSEMBLY_VERSION)] 47 | [assembly: AssemblyFileVersion(LCDirectLan.PLUGIN_ASSEMBLY_VERSION)] 48 | [assembly: AssemblyInformationalVersion(LCDirectLan.PLUGIN_VERSION)] 49 | [assembly: AssemblyTrademark("Matthew Tirtawidjaja")] 50 | -------------------------------------------------------------------------------- /Patches/CustomUsername/PlayerControllerBPatch.cs: -------------------------------------------------------------------------------- 1 | /** 2 | * This source code is part of LCDirectLAN project, 3 | * LCDirectLAN is a mod for Lethal Company that is built around BepInEx to fix and enhances LAN lobbies. 4 | * 5 | * Project Repository: 6 | * https://github.com/TIRTAGT/LCDirectLAN 7 | * 8 | * This project is open source and are released under the MIT License, 9 | * for more information, please read the LICENSE file in the project repository. 10 | * 11 | * Copyright (c) 2024 Matthew Tirtawidjaja 12 | **/ 13 | 14 | using GameNetcodeStuff; 15 | using HarmonyLib; 16 | 17 | namespace LCDirectLAN.Patches.CustomUsername 18 | { 19 | [HarmonyPatch(typeof(PlayerControllerB))] 20 | internal class PlayerControllerBPatch 21 | { 22 | [HarmonyPatch("ConnectClientToPlayerObject")] 23 | [HarmonyPrefix] 24 | [HarmonyPriority(Priority.VeryLow)] 25 | public static void Prefix_ConnectClientToPlayerObject(PlayerControllerB __instance) 26 | { 27 | LCDirectLan.Log(BepInEx.Logging.LogLevel.Debug, $"Prefix_ConnectClientToPlayerObject({__instance.actualClientId},{__instance.IsServer},{__instance.IsOwner},{__instance.IsClient})"); 28 | 29 | // If this is player is not controlled by us 30 | if (!__instance.IsOwner) 31 | { 32 | LCDirectLan.Log(BepInEx.Logging.LogLevel.Debug, $"Skipped changing username on ClientID {__instance.actualClientId}, because it isn't the local player."); 33 | return; 34 | } 35 | 36 | // If we are server, use the default Host instead of default Join username 37 | if (__instance.IsServer) 38 | { 39 | // If we should use the join username for hosting too 40 | if (LCDirectLan.GetConfig("Custom Username", "MergeDefaultUsername")) 41 | { 42 | __instance.playerUsername = LCDirectLan.GetConfig("Custom Username", "JoinDefaultUsername"); 43 | } 44 | else 45 | { 46 | __instance.playerUsername = LCDirectLan.GetConfig("Custom Username", "HostDefaultUsername"); 47 | } 48 | 49 | LCDirectLan.Log(BepInEx.Logging.LogLevel.Debug, $"ConnectClientToPlayerObject() {__instance.playerUsername} (HOSTING)"); 50 | 51 | UsernameRPC.SendInformationToServerRpc(__instance.playerUsername); 52 | return; 53 | } 54 | 55 | __instance.playerUsername = LCDirectLan.GetConfig("Custom Username", "JoinDefaultUsername"); 56 | LCDirectLan.Log(BepInEx.Logging.LogLevel.Debug, $"ConnectClientToPlayerObject() {__instance.playerUsername} (JOINING)"); 57 | 58 | UsernameRPC.SendInformationToServerRpc(__instance.playerUsername); 59 | LCDirectLan.Log(BepInEx.Logging.LogLevel.Debug, $"Successfully executed SendInformationToServerRpc('{__instance.playerUsername}')"); 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # LCDirectLAN 2 | 3 | ![GitHub Release](https://img.shields.io/github/v/release/TIRTAGT/LCDirectLAN?sort=semver&display_name=release&label=Latest%20Release&cacheSeconds=10800&color=92c00a) 4 | ![GitHub Repo stars](https://img.shields.io/github/stars/TIRTAGT/LCDirectLAN?label=GitHub%20Stars&style=flat&color=92c00a) 5 | ![GitHub Downloads (all assets, all releases)](https://img.shields.io/github/downloads/TIRTAGT/LCDirectLAN/total?label=GitHub%20Downloads&color=92c00a) 6 | ![GitHub License](https://img.shields.io/github/license/TIRTAGT/LCDirectLAN?cacheSeconds=86400&color=92c00a) 7 | ![Thunderstore Version](https://img.shields.io/thunderstore/v/TIRTAGT/LCDirectLAN?label=Thunderstore%20Version) 8 | ![Thunderstore Likes](https://img.shields.io/thunderstore/likes/TIRTAGT/LCDirectLAN?style=flat&label=Thunderstore%20Likes&color=0b7dbe) 9 | ![Thunderstore Downloads](https://img.shields.io/thunderstore/dt/TIRTAGT/LCDirectLAN?label=Thunderstore%20Downloads&color=0b7dbe) 10 | 11 | 12 | 13 | 14 | 15 | LCDirectLAN is a mod for Lethal Company that is built around BepInEx to fix and enhances LAN lobbies without interfering with the Steam-networked lobbies. 16 | 17 | Lethal Company's LAN mode is not exactly "**LAN** only" mode, but more of a "**Direct Connect** mode", as it allows players to join lobbies directly anywhere it is hosted, including **over the internet**. Unfortunately as far as v49, the game's built-in LAN join didn't seem to work... atleast for my community, this mod aims to fix that and even provide more Quality of Life features for LAN lobbies. 18 | 19 |
20 | 21 | This project is open source and are released under the MIT License, for more information, please read the [LICENSE](./LICENSE) file in the project repository. 22 | 23 | Checkout the project [Wiki page](../../wiki) for more detailed information such as Installation, Configuration, Compatibility, and more. 24 | 25 |
26 | 27 | ---- 28 | 29 | ### Features (Fixes and Enhancements to LAN Lobbies) 30 | - Modify Lobby Hosting Port, allowing multiple lobbies to be hosted in the same IP Address 31 | 32 | - Manual IP and Port input for joining lobby 33 | 34 | - Supports automatic join configuration via DNS (SRV, TXT, AAAA, A) 35 | 36 | - Custom Username support, not related to the Steam Username 37 | 38 | - In-Game Latency display, (RTT/One-Way Measurement) 39 | 40 | - Slow Server Detection 41 | 42 | - Modify the game default join connect timeout value to a custom value 43 | 44 | Screenshots, and more detailed information about the features are available on the [Wiki/Features](../../wiki/Features) page. 45 | 46 | ---- 47 | 48 | ### Contributing 49 | 50 | Contributions to LCDirectLAN are welcome, whether it's bug report, interesting feature request, or pull requests. 51 | 52 | For guide on contributing source code or directly modifying the project's code, please set up your development environment by following the [Wiki/Contributing](../../wiki/Contributing) page. 53 | 54 | ---- 55 | 56 | ### Third-Party DLL Dependencies 57 | - [DnsClient](https://www.nuget.org/packages/DnsClient) 58 | - [System.Buffers](https://www.nuget.org/packages/System.Buffers/) -------------------------------------------------------------------------------- /Utility/GameObjectManager.cs: -------------------------------------------------------------------------------- 1 | /** 2 | * This source code is part of LCDirectLAN project, 3 | * LCDirectLAN is a mod for Lethal Company that is built around BepInEx to fix and enhances LAN lobbies. 4 | * 5 | * Project Repository: 6 | * https://github.com/TIRTAGT/LCDirectLAN 7 | * 8 | * This project is open source and are released under the MIT License, 9 | * for more information, please read the LICENSE file in the project repository. 10 | * 11 | * Copyright (c) 2024 Matthew Tirtawidjaja 12 | **/ 13 | using UnityEngine; 14 | 15 | namespace LCDirectLAN.Utility 16 | { 17 | internal class GameObjectManager 18 | { 19 | /// 20 | /// Utility function to ensure a GameObject exists in the scene, also outputs the GameObject reference 21 | /// 22 | /// The path to the GameObject 23 | /// The GameObject reference for use 24 | /// True if the GameObject exists, False otherwise 25 | public static bool EnsureGameObjectExist(string path, out GameObject obj) 26 | { 27 | bool exist = IsExist(path, out obj); 28 | 29 | if (!exist) 30 | { 31 | LCDirectLan.Log(BepInEx.Logging.LogLevel.Error, $"Cannot find {path} !"); 32 | return false; 33 | } 34 | 35 | return true; 36 | } 37 | 38 | /// 39 | /// Utility function to ensure a GameObject exists in the scene 40 | /// 41 | /// The path to the GameObject 42 | /// The GameObject reference for use 43 | /// True if the GameObject exists, False otherwise 44 | public static bool IsExist(string name, out GameObject obj) 45 | { 46 | obj = GameObject.Find(name); 47 | return obj != null; 48 | } 49 | 50 | /// 51 | /// Utility function to ensure a GameObject exists in the scene 52 | /// 53 | /// The path to the GameObject 54 | /// True if the GameObject exists, False otherwise 55 | public static bool IsExist(string name) 56 | { 57 | return GameObject.Find(name) != null; 58 | } 59 | 60 | /// 61 | /// Utility function to get a GameObject from the scene 62 | /// 63 | /// The path to the GameObject 64 | /// The GameObject reference, null if not found 65 | public static GameObject GetGameObject(string name) 66 | { 67 | return GameObject.Find(name); 68 | } 69 | 70 | /// 71 | /// Utility function to delete a GameObject from the scene 72 | /// 73 | /// The path to the GameObject 74 | /// True if the GameObject is deleted, False otherwise 75 | public static bool DeleteGameObject(string path) 76 | { 77 | if (!IsExist(path, out GameObject obj)) { return false; } 78 | 79 | GameObject.Destroy(obj); 80 | return true; 81 | } 82 | 83 | /// 84 | /// Utility function to delete a GameObject from the scene 85 | /// 86 | /// The GameObject reference 87 | /// True if the GameObject is deleted, False otherwise 88 | public static bool DeleteGameObject(GameObject obj) 89 | { 90 | if (obj == null) { return false; } 91 | 92 | GameObject.Destroy(obj); 93 | return true; 94 | } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /LCDirectLAN.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | netstandard2.1 4 | False 5 | False 6 | false 7 | Library 8 | Properties 9 | LCDirectLAN 10 | LCDirectLAN 11 | 512 12 | true 13 | 14 | 15 | 16 | true 17 | full 18 | false 19 | DEBUG;TRACE 20 | prompt 21 | 4 22 | 23 | 24 | 25 | none 26 | true 27 | TRACE 28 | prompt 29 | 4 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | False 39 | 40 | 41 | False 42 | 43 | 44 | False 45 | 46 | 47 | False 48 | 49 | 50 | False 51 | 52 | 53 | False 54 | 55 | 56 | False 57 | 58 | 59 | False 60 | 61 | 62 | False 63 | 64 | 65 | False 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | -------------------------------------------------------------------------------- /Patches/LatencyHUD/HUDManagerPatch.cs: -------------------------------------------------------------------------------- 1 | /** 2 | * This source code is part of LCDirectLAN project, 3 | * LCDirectLAN is a mod for Lethal Company that is built around BepInEx to fix and enhances LAN lobbies. 4 | * 5 | * Project Repository: 6 | * https://github.com/TIRTAGT/LCDirectLAN 7 | * 8 | * This project is open source and are released under the MIT License, 9 | * for more information, please read the LICENSE file in the project repository. 10 | * 11 | * Copyright (c) 2024 Matthew Tirtawidjaja 12 | **/ 13 | 14 | using HarmonyLib; 15 | using TMPro; 16 | using Unity.Netcode; 17 | using UnityEngine; 18 | 19 | namespace LCDirectLAN.Patches.LatencyHUD 20 | { 21 | [HarmonyPatch(typeof(HUDManager))] 22 | internal class HUDManagerPatch 23 | { 24 | private static GameObject LatencyHUD = null; 25 | private static TextMeshProUGUI LatencyHUD_TMP = null; 26 | private static ulong LatencyValue = 0; 27 | private static bool UpdateLock = false; 28 | 29 | [HarmonyPatch("Awake")] 30 | [HarmonyPostfix] 31 | [HarmonyPriority(Priority.VeryLow)] 32 | public static void Postfix_Awake(HUDManager __instance) 33 | { 34 | LCDirectLan.Log(BepInEx.Logging.LogLevel.Debug, "HUDManager.Postfix_Awake()"); 35 | 36 | GameObject Container = GameObject.Find("Systems/UI/Canvas/IngamePlayerHUD"); 37 | 38 | if (Container == null) { 39 | LCDirectLan.Log(BepInEx.Logging.LogLevel.Error, "Cannot find IngamePlayerHUD GameObject !"); 40 | return; 41 | } 42 | 43 | GameObject a = GameObject.Find("Systems/UI/Canvas/IngamePlayerHUD/LCDirectLAN_LatencyHUD"); 44 | 45 | if (a != null) { 46 | LCDirectLan.Log(BepInEx.Logging.LogLevel.Debug, "LatencyHUD already exists !"); 47 | LatencyHUD = a; 48 | return; 49 | } 50 | 51 | if (NetworkManager.Singleton == null) { 52 | LCDirectLan.Log(BepInEx.Logging.LogLevel.Error, "NetworkManager.Singleton is null !"); 53 | return; 54 | } 55 | 56 | // Check if we shouldn't track latency to ourself 57 | if (LCDirectLan.GetConfig("Latency HUD", "HideHUDWhileHosting") && NetworkManager.Singleton.IsServer) { 58 | LCDirectLan.Log(BepInEx.Logging.LogLevel.Debug, "Latency HUD is disabled while hosting !"); 59 | return; 60 | } 61 | 62 | CreateLatencyHUD(Container); 63 | } 64 | 65 | /// 66 | /// Create the latency HUD GameObject 67 | /// 68 | /// The parent GameObject to attach the HUD to 69 | private static void CreateLatencyHUD(GameObject Container) 70 | { 71 | GameObject a = GameObject.Find("Systems/UI/Canvas/IngamePlayerHUD/TopLeftCorner/WeightUI/Weight"); 72 | 73 | if (a == null) 74 | { 75 | LCDirectLan.Log(BepInEx.Logging.LogLevel.Error, "Cannot find Weight GameObject !"); 76 | return; 77 | } 78 | 79 | LatencyHUD = GameObject.Instantiate(a, Container.transform); 80 | 81 | float OffsetLocation_X = LCDirectLan.GetConfig("Latency HUD", "Offset_X"); 82 | float OffsetLocation_Y = LCDirectLan.GetConfig("Latency HUD", "Offset_Y"); 83 | 84 | LatencyHUD.name = "LCDirectLAN_LatencyHUD"; 85 | LatencyHUD.transform.SetLocalPositionAndRotation(new Vector3(-380 + OffsetLocation_X, 229 + OffsetLocation_Y, 0), LatencyHUD.transform.rotation); 86 | 87 | // Get the TextMeshPro component 88 | LatencyHUD_TMP = LatencyHUD.GetComponent(); 89 | 90 | // Set the text properties 91 | LatencyHUD_TMP.fontSizeMin = 9; 92 | LatencyHUD_TMP.fontSize = LCDirectLan.GetConfig("Latency HUD", "TextSize"); 93 | 94 | // If font size is less than the minimum, set it to the minimum 95 | if (LatencyHUD_TMP.fontSize < LatencyHUD_TMP.fontSizeMin) { 96 | LatencyHUD_TMP.fontSize = LatencyHUD_TMP.fontSizeMin; 97 | } 98 | 99 | LatencyHUD_TMP.text = "Ping : [Calculating] ms"; 100 | LatencyHUD_TMP.maxVisibleCharacters = 23; 101 | } 102 | 103 | [HarmonyPatch("Update")] 104 | [HarmonyPostfix] 105 | [HarmonyPriority(Priority.VeryLow)] 106 | public static void Postfix_Update() 107 | { 108 | // Lock unnecessary update to prevent performance issues 109 | if (UpdateLock) { return; } 110 | UpdateLock = true; 111 | 112 | if (LatencyHUD_TMP == null) { return; } 113 | 114 | // Update the latency HUD 115 | if (LatencyValue > 600) 116 | { 117 | // Change the color to red 118 | LatencyHUD_TMP.color = new Color(1, 0, 0, 1); 119 | } 120 | else if (LatencyValue > 300) 121 | { 122 | // Change the color to orange (similar to the game's color scheme) 123 | LatencyHUD_TMP.color = new Color(0.9528F, 0.3941F, 0, 1); 124 | } 125 | else 126 | { 127 | // Change the color to green 128 | LatencyHUD_TMP.color = new Color(0, 1, 0, 1); 129 | } 130 | 131 | LatencyHUD_TMP.text = $"Ping : {LatencyValue} ms"; 132 | } 133 | 134 | /// 135 | /// Update the latency HUD value, the actual UI update will be done in the Update() method 136 | /// 137 | /// The latency value in ms 138 | public static void UpdateLatencyHUD(ushort latency) 139 | { 140 | if (LatencyHUD_TMP == null) { return; } 141 | 142 | LatencyValue = latency; 143 | UpdateLock = false; 144 | } 145 | 146 | /// 147 | /// Send warning using the in-game warning dialog 148 | /// 149 | /// The message of the warning 150 | public static void DisplayHUDWarning(string message) { 151 | // Make sure we are allowed to send the warning 152 | if (!LCDirectLan.GetConfig("Latency HUD", "DisplayWarningOnFailure")) { return; } 153 | 154 | HUDManager.Instance.DisplayTip("LCDirectLAN - Latency HUD", message, false, false); 155 | } 156 | 157 | /// 158 | /// Destroy the latency HUD GameObject 159 | /// 160 | public static void DestroyLatencyHUD() { 161 | if (LatencyHUD != null) { 162 | LatencyHUD.SetActive(false); 163 | LatencyHUD_TMP = null; 164 | GameObject.Destroy(LatencyHUD); 165 | LatencyHUD = null; 166 | } 167 | } 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /Utility/ResolveDNS.cs: -------------------------------------------------------------------------------- 1 | /** 2 | * This source code is part of LCDirectLAN project, 3 | * LCDirectLAN is a mod for Lethal Company that is built around BepInEx to fix and enhances LAN lobbies. 4 | * 5 | * Project Repository: 6 | * https://github.com/TIRTAGT/LCDirectLAN 7 | * 8 | * This project is open source and are released under the MIT License, 9 | * for more information, please read the LICENSE file in the project repository. 10 | * 11 | * Copyright (c) 2024 Matthew Tirtawidjaja 12 | **/ 13 | 14 | using System; 15 | using System.Net; 16 | using System.Net.Sockets; 17 | using System.Text.RegularExpressions; 18 | using DnsClient; 19 | using DnsClient.Protocol; 20 | 21 | namespace LCDirectLAN.Utility 22 | { 23 | internal class ResolveDNS 24 | { 25 | private static readonly Regex HostnameRuleMatch = new Regex("^(((([a-z0-9])|([a-z0-9](-|_)[a-z]))+\\.)+(([a-z0-9])|([a-z0-9](-|_)[a-z0-9]))+)$"); 26 | 27 | /// 28 | /// Resolve a "A" (IPv4) Record 29 | /// 30 | /// The A record name to resolve 31 | /// The IPv4 address as string, or empty string on failure 32 | public static string ResolveARecord(string record_name) 33 | { 34 | string result = string.Empty; 35 | 36 | LookupClient a = new LookupClient(); 37 | IDnsQueryResponse b = null; 38 | 39 | try { 40 | b = a.Query(record_name, QueryType.A, QueryClass.IN); 41 | } 42 | catch(SocketException e) 43 | { 44 | LCDirectLan.Log(BepInEx.Logging.LogLevel.Error, "Failed to resolve A Record: " + e.Message); 45 | } 46 | 47 | if (b == null) { return result; } 48 | if (b.HasError) { return result; } 49 | 50 | for (int i = 0; i < b.Answers.Count; i++) 51 | { 52 | if (b.Answers[i] == null || !(b.Answers[i] is ARecord)) { continue; } 53 | 54 | ARecord c = (ARecord)b.Answers[i]; 55 | 56 | result = c.Address.ToString(); 57 | break; 58 | } 59 | 60 | return result; 61 | } 62 | 63 | /// 64 | /// Resolve a "AAAA" (IPv6) Record 65 | /// 66 | /// The AAAA record name to resolve 67 | /// The IPv6 address as string, or empty string on failure 68 | public static string ResolveAAAARecord(string record_name) 69 | { 70 | string result = string.Empty; 71 | 72 | LookupClient a = new LookupClient(); 73 | IDnsQueryResponse b = null; 74 | 75 | try 76 | { 77 | b = a.Query(record_name, QueryType.AAAA, QueryClass.IN); 78 | } 79 | catch (SocketException e) 80 | { 81 | LCDirectLan.Log(BepInEx.Logging.LogLevel.Error, "Failed to resolve AAAA Record: " + e.Message); 82 | } 83 | 84 | if (b == null) { return result; } 85 | if (b.HasError) { return result; } 86 | 87 | for (int i = 0; i < b.Answers.Count; i++) 88 | { 89 | if (b.Answers[i] == null || !(b.Answers[i] is AaaaRecord)) { continue; } 90 | 91 | AaaaRecord c = (AaaaRecord)b.Answers[i]; 92 | 93 | result = c.Address.ToString(); 94 | break; 95 | } 96 | 97 | return result; 98 | } 99 | 100 | /// 101 | /// Resolve a TXT Record 102 | /// 103 | /// The TXT record name to resolve 104 | /// The first TXT data returned or empty when there is no data 105 | public static string ResolveTXTRecord(string record_name) 106 | { 107 | string result = string.Empty; 108 | 109 | LookupClient a = new LookupClient(); 110 | IDnsQueryResponse b = null; 111 | 112 | try 113 | { 114 | b = a.Query(record_name, QueryType.TXT, QueryClass.IN); 115 | } 116 | catch (SocketException e) 117 | { 118 | LCDirectLan.Log(BepInEx.Logging.LogLevel.Error, "Failed to resolve TXT Record: " + e.Message); 119 | } 120 | 121 | if (b == null) { return result; } 122 | if (b.HasError) { return result; } 123 | 124 | for (int i = 0; i < b.Answers.Count; i++) 125 | { 126 | if (b.Answers[i] == null || !(b.Answers[i] is TxtRecord)) { continue; } 127 | 128 | TxtRecord c = (TxtRecord) b.Answers[i]; 129 | 130 | foreach (string d in c.EscapedText) 131 | { 132 | result = d; 133 | break; 134 | } 135 | break; 136 | } 137 | 138 | return result; 139 | } 140 | 141 | /// 142 | /// Resolve a SRV Record 143 | /// 144 | /// The RSV record name to resolve 145 | /// A tuple containing IPv4 Address as a string, and Port as a UInt16/ushort 146 | public static (string, UInt16) ResolveSRVRecord(string record_name) 147 | { 148 | (string, UInt16) result = (string.Empty, 0); 149 | 150 | LookupClient a = new LookupClient(); 151 | IDnsQueryResponse b = null; 152 | 153 | try 154 | { 155 | b = a.Query(record_name, QueryType.SRV, QueryClass.IN); 156 | } 157 | catch (SocketException e) 158 | { 159 | LCDirectLan.Log(BepInEx.Logging.LogLevel.Error, "Failed to resolve SRV Record: " + e.Message); 160 | return result; 161 | } 162 | 163 | if (b == null) { return result; } 164 | if (b.HasError) { return result; } 165 | 166 | for (int i = 0; i < b.Answers.Count; i++) 167 | { 168 | if (b.Answers[i] == null || !(b.Answers[i] is SrvRecord)) { continue; } 169 | 170 | SrvRecord c = (SrvRecord)b.Answers[i]; 171 | 172 | // Check if we should prioritize IPv6 lookup for the SRV Host 173 | if (LCDirectLan.GetConfig("Join", "SRVHost_PreferIPv6")) { 174 | // Try get host address via AAAA Record first 175 | result.Item1 = ResolveAAAARecord(c.Target.Value); 176 | 177 | if (string.IsNullOrEmpty(result.Item1)) 178 | { 179 | // Try get host address using A Record as fallback 180 | result.Item1 = ResolveARecord(c.Target.Value); 181 | LCDirectLan.Log(BepInEx.Logging.LogLevel.Info, "SRV Host is looked up using A Record"); 182 | } 183 | else { 184 | LCDirectLan.Log(BepInEx.Logging.LogLevel.Info, "SRV Host is looked up using AAAA Record"); 185 | } 186 | 187 | result.Item2 = c.Port; 188 | break; 189 | } 190 | 191 | // Try get host address via A Record first 192 | result.Item1 = ResolveARecord(c.Target.Value); 193 | 194 | if (string.IsNullOrEmpty(result.Item1)) 195 | { 196 | // Try get host address using AAAA Record as fallback 197 | result.Item1 = ResolveAAAARecord(c.Target.Value); 198 | LCDirectLan.Log(BepInEx.Logging.LogLevel.Info, "SRV Host is looked up using AAAA Record"); 199 | } 200 | else { 201 | LCDirectLan.Log(BepInEx.Logging.LogLevel.Info, "SRV Host is looked up using A Record"); 202 | } 203 | 204 | result.Item2 = c.Port; 205 | break; 206 | } 207 | 208 | return result; 209 | } 210 | 211 | /// 212 | /// Check if a string is a valid IPv4 Address 213 | /// 214 | /// The string to be checked 215 | /// Boolean representing whether the string is a valid IPv4 216 | public static bool IsValidIPv4(string ip) 217 | { 218 | if (IPAddress.TryParse(ip, out IPAddress a)) 219 | { 220 | return a.AddressFamily == AddressFamily.InterNetwork; 221 | } 222 | 223 | return false; 224 | } 225 | 226 | /// 227 | /// Check if a string is a valid IPv6 Address 228 | /// 229 | /// The string to be checked 230 | /// Boolean representing whether the string is a valid IPv6 231 | public static bool IsValidIPv6(string ip) 232 | { 233 | if (IPAddress.TryParse(ip, out IPAddress a)) 234 | { 235 | return a.AddressFamily == AddressFamily.InterNetworkV6; 236 | } 237 | 238 | return false; 239 | } 240 | 241 | /// 242 | /// Check if a string is a valid IPv4/IPv6 Address 243 | /// 244 | /// The string to be checked 245 | /// An AddressFamily enum with:

- InterNetwork for IPv4

- InterNetworkV6 for IPv6

- Unknown otherwise.
246 | public static AddressFamily CheckIPType(string ip) 247 | { 248 | if (IsValidIPv4(ip)) { return AddressFamily.InterNetwork; } 249 | 250 | if (IsValidIPv6(ip)) { return AddressFamily.InterNetworkV6; } 251 | 252 | return AddressFamily.Unknown; 253 | } 254 | 255 | public static bool IsOnHostnameFormat(string hostname) 256 | { 257 | // If it's localhost, return true 258 | if (hostname == "localhost") { return true; } 259 | 260 | // If it's an IP, then it's not a hostname 261 | if (CheckIPType(hostname) != AddressFamily.Unknown) { return false; } 262 | 263 | return ResolveDNS.HostnameRuleMatch.IsMatch(hostname); 264 | } 265 | } 266 | } 267 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore the game folder (I copied to the project for easier testing than using Steam's Play button) 2 | Lethal Company 3 | 4 | ## Ignore Visual Studio temporary files, build results, and 5 | ## files generated by popular Visual Studio add-ons. 6 | ## 7 | ## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore 8 | 9 | # User-specific files 10 | *.rsuser 11 | *.suo 12 | *.user 13 | *.userosscache 14 | *.sln.docstates 15 | 16 | # User-specific files (MonoDevelop/Xamarin Studio) 17 | *.userprefs 18 | 19 | # Mono auto generated files 20 | mono_crash.* 21 | 22 | # Build results 23 | [Dd]ebug/ 24 | [Dd]ebugPublic/ 25 | [Rr]elease/ 26 | [Rr]eleases/ 27 | x64/ 28 | x86/ 29 | [Ww][Ii][Nn]32/ 30 | [Aa][Rr][Mm]/ 31 | [Aa][Rr][Mm]64/ 32 | bld/ 33 | [Bb]in/ 34 | [Oo]bj/ 35 | [Ll]og/ 36 | [Ll]ogs/ 37 | 38 | # Visual Studio 2015/2017 cache/options directory 39 | .vs/ 40 | # Uncomment if you have tasks that create the project's static files in wwwroot 41 | #wwwroot/ 42 | 43 | # Visual Studio 2017 auto generated files 44 | Generated\ Files/ 45 | 46 | # MSTest test Results 47 | [Tt]est[Rr]esult*/ 48 | [Bb]uild[Ll]og.* 49 | 50 | # NUnit 51 | *.VisualState.xml 52 | TestResult.xml 53 | nunit-*.xml 54 | 55 | # Build Results of an ATL Project 56 | [Dd]ebugPS/ 57 | [Rr]eleasePS/ 58 | dlldata.c 59 | 60 | # Benchmark Results 61 | BenchmarkDotNet.Artifacts/ 62 | 63 | # .NET Core 64 | project.lock.json 65 | project.fragment.lock.json 66 | artifacts/ 67 | 68 | # ASP.NET Scaffolding 69 | ScaffoldingReadMe.txt 70 | 71 | # StyleCop 72 | StyleCopReport.xml 73 | 74 | # Files built by Visual Studio 75 | *_i.c 76 | *_p.c 77 | *_h.h 78 | *.ilk 79 | *.meta 80 | *.obj 81 | *.iobj 82 | *.pch 83 | *.pdb 84 | *.ipdb 85 | *.pgc 86 | *.pgd 87 | *.rsp 88 | *.sbr 89 | *.tlb 90 | *.tli 91 | *.tlh 92 | *.tmp 93 | *.tmp_proj 94 | *_wpftmp.csproj 95 | *.log 96 | *.tlog 97 | *.vspscc 98 | *.vssscc 99 | .builds 100 | *.pidb 101 | *.svclog 102 | *.scc 103 | 104 | # Chutzpah Test files 105 | _Chutzpah* 106 | 107 | # Visual C++ cache files 108 | ipch/ 109 | *.aps 110 | *.ncb 111 | *.opendb 112 | *.opensdf 113 | *.sdf 114 | *.cachefile 115 | *.VC.db 116 | *.VC.VC.opendb 117 | 118 | # Visual Studio profiler 119 | *.psess 120 | *.vsp 121 | *.vspx 122 | *.sap 123 | 124 | # Visual Studio Trace Files 125 | *.e2e 126 | 127 | # TFS 2012 Local Workspace 128 | $tf/ 129 | 130 | # Guidance Automation Toolkit 131 | *.gpState 132 | 133 | # ReSharper is a .NET coding add-in 134 | _ReSharper*/ 135 | *.[Rr]e[Ss]harper 136 | *.DotSettings.user 137 | 138 | # TeamCity is a build add-in 139 | _TeamCity* 140 | 141 | # DotCover is a Code Coverage Tool 142 | *.dotCover 143 | 144 | # AxoCover is a Code Coverage Tool 145 | .axoCover/* 146 | !.axoCover/settings.json 147 | 148 | # Coverlet is a free, cross platform Code Coverage Tool 149 | coverage*.json 150 | coverage*.xml 151 | coverage*.info 152 | 153 | # Visual Studio code coverage results 154 | *.coverage 155 | *.coveragexml 156 | 157 | # NCrunch 158 | _NCrunch_* 159 | .*crunch*.local.xml 160 | nCrunchTemp_* 161 | 162 | # MightyMoose 163 | *.mm.* 164 | AutoTest.Net/ 165 | 166 | # Web workbench (sass) 167 | .sass-cache/ 168 | 169 | # Installshield output folder 170 | [Ee]xpress/ 171 | 172 | # DocProject is a documentation generator add-in 173 | DocProject/buildhelp/ 174 | DocProject/Help/*.HxT 175 | DocProject/Help/*.HxC 176 | DocProject/Help/*.hhc 177 | DocProject/Help/*.hhk 178 | DocProject/Help/*.hhp 179 | DocProject/Help/Html2 180 | DocProject/Help/html 181 | 182 | # Click-Once directory 183 | publish/ 184 | 185 | # Publish Web Output 186 | *.[Pp]ublish.xml 187 | *.azurePubxml 188 | # Note: Comment the next line if you want to checkin your web deploy settings, 189 | # but database connection strings (with potential passwords) will be unencrypted 190 | *.pubxml 191 | *.publishproj 192 | 193 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 194 | # checkin your Azure Web App publish settings, but sensitive information contained 195 | # in these scripts will be unencrypted 196 | PublishScripts/ 197 | 198 | # NuGet Packages 199 | *.nupkg 200 | # NuGet Symbol Packages 201 | *.snupkg 202 | # The packages folder can be ignored because of Package Restore 203 | **/[Pp]ackages/* 204 | # except build/, which is used as an MSBuild target. 205 | !**/[Pp]ackages/build/ 206 | # Uncomment if necessary however generally it will be regenerated when needed 207 | #!**/[Pp]ackages/repositories.config 208 | # NuGet v3's project.json files produces more ignorable files 209 | *.nuget.props 210 | *.nuget.targets 211 | 212 | # Microsoft Azure Build Output 213 | csx/ 214 | *.build.csdef 215 | 216 | # Microsoft Azure Emulator 217 | ecf/ 218 | rcf/ 219 | 220 | # Windows Store app package directories and files 221 | AppPackages/ 222 | BundleArtifacts/ 223 | Package.StoreAssociation.xml 224 | _pkginfo.txt 225 | *.appx 226 | *.appxbundle 227 | *.appxupload 228 | 229 | # Visual Studio cache files 230 | # files ending in .cache can be ignored 231 | *.[Cc]ache 232 | # but keep track of directories ending in .cache 233 | !?*.[Cc]ache/ 234 | 235 | # Others 236 | ClientBin/ 237 | ~$* 238 | *~ 239 | *.dbmdl 240 | *.dbproj.schemaview 241 | *.jfm 242 | *.pfx 243 | *.publishsettings 244 | orleans.codegen.cs 245 | 246 | # Including strong name files can present a security risk 247 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 248 | #*.snk 249 | 250 | # Since there are multiple workflows, uncomment next line to ignore bower_components 251 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 252 | #bower_components/ 253 | 254 | # RIA/Silverlight projects 255 | Generated_Code/ 256 | 257 | # Backup & report files from converting an old project file 258 | # to a newer Visual Studio version. Backup files are not needed, 259 | # because we have git ;-) 260 | _UpgradeReport_Files/ 261 | Backup*/ 262 | UpgradeLog*.XML 263 | UpgradeLog*.htm 264 | ServiceFabricBackup/ 265 | *.rptproj.bak 266 | 267 | # SQL Server files 268 | *.mdf 269 | *.ldf 270 | *.ndf 271 | 272 | # Business Intelligence projects 273 | *.rdl.data 274 | *.bim.layout 275 | *.bim_*.settings 276 | *.rptproj.rsuser 277 | *- [Bb]ackup.rdl 278 | *- [Bb]ackup ([0-9]).rdl 279 | *- [Bb]ackup ([0-9][0-9]).rdl 280 | 281 | # Microsoft Fakes 282 | FakesAssemblies/ 283 | 284 | # GhostDoc plugin setting file 285 | *.GhostDoc.xml 286 | 287 | # Node.js Tools for Visual Studio 288 | .ntvs_analysis.dat 289 | node_modules/ 290 | 291 | # Visual Studio 6 build log 292 | *.plg 293 | 294 | # Visual Studio 6 workspace options file 295 | *.opt 296 | 297 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 298 | *.vbw 299 | 300 | # Visual Studio 6 auto-generated project file (contains which files were open etc.) 301 | *.vbp 302 | 303 | # Visual Studio 6 workspace and project file (working project files containing files to include in project) 304 | *.dsw 305 | *.dsp 306 | 307 | # Visual Studio 6 technical files 308 | *.ncb 309 | *.aps 310 | 311 | # Visual Studio LightSwitch build output 312 | **/*.HTMLClient/GeneratedArtifacts 313 | **/*.DesktopClient/GeneratedArtifacts 314 | **/*.DesktopClient/ModelManifest.xml 315 | **/*.Server/GeneratedArtifacts 316 | **/*.Server/ModelManifest.xml 317 | _Pvt_Extensions 318 | 319 | # Paket dependency manager 320 | .paket/paket.exe 321 | paket-files/ 322 | 323 | # FAKE - F# Make 324 | .fake/ 325 | 326 | # CodeRush personal settings 327 | .cr/personal 328 | 329 | # Python Tools for Visual Studio (PTVS) 330 | __pycache__/ 331 | *.pyc 332 | 333 | # Cake - Uncomment if you are using it 334 | # tools/** 335 | # !tools/packages.config 336 | 337 | # Tabs Studio 338 | *.tss 339 | 340 | # Telerik's JustMock configuration file 341 | *.jmconfig 342 | 343 | # BizTalk build output 344 | *.btp.cs 345 | *.btm.cs 346 | *.odx.cs 347 | *.xsd.cs 348 | 349 | # OpenCover UI analysis results 350 | OpenCover/ 351 | 352 | # Azure Stream Analytics local run output 353 | ASALocalRun/ 354 | 355 | # MSBuild Binary and Structured Log 356 | *.binlog 357 | 358 | # NVidia Nsight GPU debugger configuration file 359 | *.nvuser 360 | 361 | # MFractors (Xamarin productivity tool) working folder 362 | .mfractor/ 363 | 364 | # Local History for Visual Studio 365 | .localhistory/ 366 | 367 | # Visual Studio History (VSHistory) files 368 | .vshistory/ 369 | 370 | # BeatPulse healthcheck temp database 371 | healthchecksdb 372 | 373 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 374 | MigrationBackup/ 375 | 376 | # Ionide (cross platform F# VS Code tools) working folder 377 | .ionide/ 378 | 379 | # Fody - auto-generated XML schema 380 | FodyWeavers.xsd 381 | 382 | # VS Code files for those working on multiple tools 383 | .vscode/* 384 | !.vscode/settings.json 385 | !.vscode/tasks.json 386 | !.vscode/launch.json 387 | !.vscode/extensions.json 388 | *.code-workspace 389 | 390 | # Local History for Visual Studio Code 391 | .history/ 392 | 393 | # Windows Installer files from build outputs 394 | *.cab 395 | *.msi 396 | *.msix 397 | *.msm 398 | *.msp 399 | 400 | # JetBrains Rider 401 | *.sln.iml -------------------------------------------------------------------------------- /LCDirectLan.cs: -------------------------------------------------------------------------------- 1 | /** 2 | * This source code is part of LCDirectLAN project, 3 | * LCDirectLAN is a mod for Lethal Company that is built around BepInEx to fix and enhances LAN lobbies. 4 | * 5 | * Project Repository: 6 | * https://github.com/TIRTAGT/LCDirectLAN 7 | * 8 | * This project is open source and are released under the MIT License, 9 | * for more information, please read the LICENSE file in the project repository. 10 | * 11 | * Copyright (c) 2024 Matthew Tirtawidjaja 12 | **/ 13 | 14 | using BepInEx; 15 | using BepInEx.Configuration; 16 | using BepInEx.Logging; 17 | using HarmonyLib; 18 | using UnityEngine; 19 | 20 | namespace LCDirectLAN 21 | { 22 | [BepInPlugin(LCDirectLan.PLUGIN_GUID, LCDirectLan.PLUGIN_NAME, LCDirectLan.PLUGIN_VERSION)] 23 | public class LCDirectLan : BaseUnityPlugin 24 | { 25 | public const string PLUGIN_GUID = "TIRTAGT.LCDirectLAN"; 26 | public const string PLUGIN_NAME = "LCDirectLAN"; 27 | /// 28 | /// Version of the plugin that follows semantic versioning format
29 | /// 30 | /// Major - Major version number, incremented when there are significant changes that breaks compatibility
31 | /// Minor - Minor version number, incremented when there are changes that breaks compatibility
32 | /// Build - Build number, incremented when there are changes that doesn't break any compatibility
33 | ///
34 | public const string PLUGIN_VERSION = "1.1.3"; 35 | 36 | /// 37 | /// Version of the plugin assembly that follows "major.minor.build.revision" format
38 | /// 39 | /// Major - Major version number, incremented when there are significant changes that breaks compatibility
40 | /// Minor - Minor version number, incremented when there are changes that breaks compatibility
41 | /// Build - Build number, incremented when there are changes that doesn't break any compatibility
42 | /// Revision - Revision number, 00000 (for Debug/Development) or 10101 (for Release)
43 | ///
44 | #if DEBUG 45 | public const string PLUGIN_ASSEMBLY_VERSION = PLUGIN_VERSION + ".00000"; 46 | public const string PLUGIN_COMPILE_CONFIG = "Debug"; 47 | #else 48 | public const string PLUGIN_ASSEMBLY_VERSION = PLUGIN_VERSION + ".10101"; 49 | public const string PLUGIN_COMPILE_CONFIG = "Release"; 50 | #endif 51 | 52 | private readonly Harmony HarmonyLib = new Harmony(LCDirectLan.PLUGIN_GUID); 53 | private static LCDirectLan Instance; 54 | private static bool IsAlreadyAwake = false; 55 | private static ConfigFile config; 56 | 57 | public static bool IsOnLanMode = false; 58 | 59 | public LCDirectLan() 60 | { 61 | if (LCDirectLan.Instance != null) 62 | { 63 | this.Logger.LogWarning($"{LCDirectLan.PLUGIN_GUID} is already loaded."); 64 | return; 65 | } 66 | 67 | LCDirectLan.Instance = this; 68 | } 69 | 70 | /// 71 | /// Called after the plugin/mod class is initialized on BepInEx (usually before the game starts) 72 | /// 73 | private void Awake() 74 | { 75 | if (IsAlreadyAwake) 76 | { 77 | this.Logger.LogWarning($"{LCDirectLan.PLUGIN_GUID} is already woken up."); 78 | return; 79 | } 80 | 81 | IsAlreadyAwake = true; 82 | 83 | 84 | config = new ConfigFile(Paths.ConfigPath + $"/{LCDirectLan.PLUGIN_GUID}.cfg", true) 85 | { 86 | SaveOnConfigSet = false 87 | }; 88 | 89 | /* Default network configuration when joining LAN lobbies */ 90 | config.Bind("Join", "CreateExtraButton", true, new ConfigDescription("Add \"Direct LAN Join\" button to start menu, this will move other buttons on the UI and may not be compatible with other mods that also changes the start UI.\nDisabling this will override the game's default Join button function instead of creating a new button")); 91 | config.Bind("Join", "DefaultAddress", "127.0.0.1", new ConfigDescription("Default IP/Hostname, change-able in the game")); 92 | config.Bind("Join", "DefaultPort", 7777, new ConfigDescription("Default Port, change-able in the game")); 93 | config.Bind("Join", "RememberLastJoinSettings", false, new ConfigDescription("Overwrite the default join configuration values when changed in the game")); 94 | config.Bind("Join", "HideRawJoinData", 3, new ConfigDescription("Do not display or save the recently joined server data to avoid Server Leak, useful for streamers.\nThis config accepts a bitwise value\nExamples:\n0: Show anything (Disabled/No Hiding)\n1: Hide IP Address\n2. Hide Port Number\n3. Hide IP and Port\n4. Hide Hostname\n7. Hide all of them (IP,Port,Hostname)", new AcceptableValueList(new byte[] { 0, 1, 2, 3, 4, 7 }))); 95 | config.Bind("Join", "SRVHost_PreferIPv6", false, new ConfigDescription("Prefer IPv6 over IPv4 for SRV Record host lookup when using DNS SRV Record to join")); 96 | 97 | /* Default network configuration when hosting LAN lobbies */ 98 | config.Bind("Host", "ListenOnIPv6", false, new ConfigDescription("Should the game listen on IPv6 when hosting instead of IPv4 ?\nDual-Stack Listening is not supported due to the game Unity Transport version.")); 99 | config.Bind("Host", "DefaultPort", 7777, new ConfigDescription("Default Port for hosting, the default vanilla port is 7777")); 100 | 101 | /* Custom Username Feature / Patches */ 102 | config.Bind("Custom Username", "Enabled", false, new ConfigDescription("Enable CustomUsernamePatch ?\nThis patch requires both Server and Client(s) to work correctly, but having this enabled doesn't interfere with vanilla players (or players that didn't have this enabled).")); 103 | config.Bind("Custom Username", "HostDefaultUsername", "Lethal Hoster", new ConfigDescription("Default Username when Hosting a game")); 104 | config.Bind("Custom Username", "CreateHostUsernameInput", true, new ConfigDescription("Create input field for Host Username in the game's host configuration window, this will move other elements on the UI and may not be compatible with other mods that also changes the host UI.\nAdjust the offsets if necessary\n\nRequires the MergeDefaultUsername to be disabled in order to work correctly")); 105 | config.Bind("Custom Username", "HostUsernameInput_Offset_Y", new Vector3(3, -4, -4), new ConfigDescription("Y Offsets for multiple UI elements that were moved to make room for HostUsernameInput if it is enabled\n\nX value is the Header and the Remote/Local selection button\nY value is the confirm button\nZ value is the back button")); 106 | config.Bind("Custom Username", "JoinDefaultUsername", "Lethal Player", new ConfigDescription("Default Username when Joining a game\nChange-able in the game")); 107 | config.Bind("Custom Username", "MergeDefaultUsername", false, new ConfigDescription("Copy/Merge/Use JoinDefaultUsername as HostDefaultUsername too, breaks CreateHostUsernameInput functionality")); 108 | 109 | /* Game Join/Connect Timeout settings */ 110 | config.Bind("Unity Networking", "Enabled", false, new ConfigDescription("Enable UnityNetworkingPatch ?")); 111 | config.Bind("Unity Networking", "ConnectTimeout", -1, new ConfigDescription("Override the game connect/join timeout in seconds (any values lower than 1 will disable this)")); 112 | 113 | /* Latency HUD Patches */ 114 | config.Bind("Latency HUD", "Enabled", true, new ConfigDescription("Enable LatencyHUDPatch ?")); 115 | config.Bind("Latency HUD", "DisableCustomLatencyRPC", false, new ConfigDescription($"Disable {LCDirectLan.PLUGIN_NAME}'s custom RPC and replace use UnityTransport's GetCurrentRtt() for measuring latency, additional warning feature will be disabled too")); 116 | config.Bind("Latency HUD", "HideHUDWhileHosting", true, new ConfigDescription("Hide the Latency HUD when hosting a game, since measuring latency to host (yourself) is not really useful")); 117 | config.Bind("Latency HUD", "RTTMeasurement", true, new ConfigDescription("Measure Round Trip Time (RTT) instead of one-way latency, which is a more accurate latency representation")); 118 | config.Bind("Latency HUD", "DisplayWarningOnFailure", true, new ConfigDescription("Display an in-game warning when there is a problem with LatencyHUDPatch functionality")); 119 | config.Bind("Latency HUD", "Offset_X", 0.0F, new ConfigDescription("Adjust the X position of the Latency HUD\nHigher value moves the HUD to the right, lower value moves the HUD to the left")); 120 | config.Bind("Latency HUD", "Offset_Y", 0.0F, new ConfigDescription("Adjust the Y position of the Latency HUD\nHigher value moves the HUD to the top, lower value moves the HUD to the bottom")); 121 | config.Bind("Latency HUD", "TextSize", 13.0F, new ConfigDescription("Adjust font size of the Latency HUD (Minimum: 9)")); 122 | 123 | config.Save(); 124 | 125 | // Inject script to detect if we are started in LAN or Online mode 126 | Patches.PreInitSceneScriptPatch.SetLateInjector(InjectPatches); 127 | HarmonyLib.PatchAll(typeof(Patches.PreInitSceneScriptPatch)); 128 | 129 | this.Logger.LogInfo($"{LCDirectLan.PLUGIN_NAME} is loaded"); 130 | } 131 | 132 | /// 133 | /// A dedicated inject method to apply patches (allows early or late patching behavior) 134 | /// 135 | private void InjectPatches() { 136 | // Do not inject any further patches if we are not in LAN mode 137 | if (!LCDirectLan.IsOnLanMode) { 138 | this.Logger.LogError($"{LCDirectLan.PLUGIN_NAME} should not be injected when game is started on Online (steam) mode"); 139 | return; 140 | } 141 | 142 | HarmonyLib.PatchAll(typeof(Patches.ConfigurableLAN.MenuManagerPatch)); 143 | 144 | // Only apply username patches if user wants to 145 | if (GetConfig("Custom Username", "Enabled")) 146 | { 147 | HarmonyLib.PatchAll(typeof(Patches.CustomUsername.PlayerControllerBPatch)); 148 | HarmonyLib.PatchAll(typeof(Patches.CustomUsername.UsernameRPC)); 149 | } 150 | 151 | // Only apply Unity Networking when the user wants to 152 | if (GetConfig("Unity Networking", "Enabled")) 153 | { 154 | HarmonyLib.PatchAll(typeof(Patches.UnityNetworking.UnityTransportPatch)); 155 | } 156 | 157 | // Only apply Latency HUD patches when the user wants to 158 | if (GetConfig("Latency HUD", "Enabled")) 159 | { 160 | HarmonyLib.PatchAll(typeof(Patches.LatencyHUD.HUDManagerPatch)); 161 | HarmonyLib.PatchAll(typeof(Patches.LatencyHUD.LatencyRPC)); 162 | } 163 | 164 | this.Logger.LogInfo($"{LCDirectLan.PLUGIN_NAME} patches are injected"); 165 | } 166 | 167 | /// 168 | /// Send a log to the exposed BepInEx Logging system 169 | /// 170 | /// The BepInEx LogLevel 171 | /// The log data 172 | public static void Log(BepInEx.Logging.LogLevel level, object data) 173 | { 174 | LCDirectLan.Instance.Logger.Log(level, data); 175 | } 176 | 177 | /// 178 | /// Get a value from Main section of the current BepInEx Runtime Configuration 179 | /// 180 | /// The config data type 181 | /// The config key 182 | /// The value casted to the data type, otherwise the default value for that data type 183 | public static T GetConfig(string key) 184 | { 185 | return LCDirectLan.GetConfig("Main", key); 186 | } 187 | 188 | /// 189 | /// Get a value from the current BepInEx Runtime Configuration 190 | /// 191 | /// The config data type 192 | /// The config section 193 | /// The config key 194 | /// The value casted to the data type, otherwise the default value for that data type 195 | public static T GetConfig(string section, string key) 196 | { 197 | if (config == null) { return default; } 198 | 199 | 200 | if (config.TryGetEntry(section, key, out ConfigEntry a)) 201 | { 202 | return a.Value; 203 | } 204 | 205 | LCDirectLan.Log(LogLevel.Warning, $"Cannot get config key {key}, no such key on {section}"); 206 | return default; 207 | } 208 | 209 | /// 210 | /// Set a new value on the current BepInEx Runtime Configuration 211 | /// 212 | /// The config data type 213 | /// The config key 214 | /// The new config value 215 | /// True on success, false on failure 216 | public static bool SetConfig(string key, T value) 217 | { 218 | return LCDirectLan.SetConfig(key, value); 219 | } 220 | 221 | /// 222 | /// Set a new value on the current BepInEx Runtime Configuration 223 | /// 224 | /// The config data type 225 | /// The config section 226 | /// The config key 227 | /// The new config value 228 | /// True on success, false on failure 229 | public static bool SetConfig(string section, string key, T value) 230 | { 231 | if (config == null) { return false; } 232 | 233 | 234 | if (!config.TryGetEntry(section, key, out ConfigEntry a)) 235 | { 236 | LCDirectLan.Log(LogLevel.Warning, $"Cannot set config key {key} to {value}, no such key on {section}"); 237 | return false; 238 | } 239 | 240 | a.Value = value; 241 | return true; 242 | } 243 | 244 | /// 245 | /// Writes current BepInEx Runtime Configuration to disk 246 | /// 247 | public static void SaveConfig() 248 | { 249 | if (config == null) { return; } 250 | 251 | config.Save(); 252 | } 253 | } 254 | } 255 | -------------------------------------------------------------------------------- /Patches/LatencyHUD/LatencyRPC.cs: -------------------------------------------------------------------------------- 1 | /** 2 | * This source code is part of LCDirectLAN project, 3 | * LCDirectLAN is a mod for Lethal Company that is built around BepInEx to fix and enhances LAN lobbies. 4 | * 5 | * Project Repository: 6 | * https://github.com/TIRTAGT/LCDirectLAN 7 | * 8 | * This project is open source and are released under the MIT License, 9 | * for more information, please read the LICENSE file in the project repository. 10 | * 11 | * Copyright (c) 2024 Matthew Tirtawidjaja 12 | **/ 13 | 14 | using GameNetcodeStuff; 15 | using HarmonyLib; 16 | using System; 17 | using System.Collections; 18 | using Unity.Collections; 19 | using Unity.Netcode; 20 | using Unity.Netcode.Transports.UTP; 21 | using UnityEngine; 22 | 23 | namespace LCDirectLAN.Patches.LatencyHUD 24 | { 25 | internal class LatencyRPC : NetworkBehaviour 26 | { 27 | private static bool IsWaitingPingCallback = false; 28 | private static Coroutine LatencyTrackerCoroutine; 29 | private static bool UseCustomLatencyRPC = true; 30 | private static UnityTransport UnityTransportObject; 31 | 32 | [HarmonyPatch(typeof(PlayerControllerB), "ConnectClientToPlayerObject")] 33 | [HarmonyPostfix] 34 | [HarmonyPriority(Priority.VeryLow)] 35 | public static void Postfix_ConnectClientToPlayerObject(PlayerControllerB __instance) 36 | { 37 | LCDirectLan.Log(BepInEx.Logging.LogLevel.Debug, "LatencyRPC.Postfix_ConnectClientToPlayerObject()"); 38 | 39 | if (__instance.NetworkManager == null || !__instance.NetworkManager.IsListening) { return; } 40 | 41 | // Only manage player object that is controlled by us 42 | if (!__instance.IsOwner) { return; } 43 | 44 | // Check if we should use Custom Latency RPC or UnityTransport's RTT 45 | UseCustomLatencyRPC = !LCDirectLan.GetConfig("Latency HUD", "DisableCustomLatencyRPC"); 46 | 47 | if (UseCustomLatencyRPC) { 48 | LCDirectLan.Log(BepInEx.Logging.LogLevel.Debug, "Using Custom Latency RPC"); 49 | } 50 | else { 51 | LCDirectLan.Log(BepInEx.Logging.LogLevel.Debug, "Using UnityTransport's RTT"); 52 | 53 | // Get the UnityTransport object 54 | UnityTransportObject = GameObject.Find("NetworkManager").GetComponent(); 55 | 56 | // Start tracking latency here, no need to listen using Custom Latency RPC 57 | StartTrackingLatency(__instance); 58 | return; 59 | } 60 | 61 | // Listen callback from server for our (client) ping request 62 | NetworkManager.Singleton.CustomMessagingManager.RegisterNamedMessageHandler(LCDirectLan.PLUGIN_NAME + "_ClientLatencyRequestCallback_ToClientRpc", new CustomMessagingManager.HandleNamedMessageDelegate(ClientLatencyRequestCallback)); 63 | LCDirectLan.Log(BepInEx.Logging.LogLevel.Debug, $"Listening _ClientLatencyRequestCallback_ToClientRpc"); 64 | 65 | // Do not allow clients to handle server events 66 | if (!__instance.NetworkManager.IsServer) { 67 | // As a client, start tracking latency here 68 | StartTrackingLatency(__instance); 69 | return; 70 | } 71 | 72 | // Listen ping request from client 73 | NetworkManager.Singleton.CustomMessagingManager.RegisterNamedMessageHandler(LCDirectLan.PLUGIN_NAME + "_ClientLatencyRequest_ToServerRpc", new CustomMessagingManager.HandleNamedMessageDelegate(ClientLatencyRequest)); 74 | LCDirectLan.Log(BepInEx.Logging.LogLevel.Debug, $"Listening _ClientLatencyRequest_ToServerRpc()"); 75 | 76 | // Listen callback from client to give back our (server) ping request 77 | NetworkManager.Singleton.CustomMessagingManager.RegisterNamedMessageHandler(LCDirectLan.PLUGIN_NAME + "_ServerLatencyRequestCallback_ToServerRpc", new CustomMessagingManager.HandleNamedMessageDelegate(ServerLatencyRequestCallback)); 78 | LCDirectLan.Log(BepInEx.Logging.LogLevel.Debug, $"Listening _ServerLatencyRequestCallback_ToServerRpc()"); 79 | 80 | // Check if we shouldn't track latency to ourself 81 | if (LCDirectLan.GetConfig("Latency HUD", "HideHUDWhileHosting") && NetworkManager.Singleton.IsServer) { 82 | LCDirectLan.Log(BepInEx.Logging.LogLevel.Debug, "Not tracking latency as a server !"); 83 | return; 84 | } 85 | 86 | // As a server, start tracking latency here 87 | StartTrackingLatency(__instance); 88 | } 89 | 90 | [HarmonyPatch(typeof(PlayerControllerB), "OnDestroy")] 91 | [HarmonyPrefix] 92 | [HarmonyPriority(Priority.VeryLow)] 93 | public static void Prefix_OnDestroy(PlayerControllerB __instance) 94 | { 95 | // Only manage player object that is controlled by us 96 | if (!__instance.IsOwner) { return; } 97 | 98 | LCDirectLan.Log(BepInEx.Logging.LogLevel.Debug, "LatencyRPC.Prefix_OnDestroy()"); 99 | 100 | // Stop tracking latency 101 | StopTrackingLatency(__instance); 102 | UnityTransportObject = null; 103 | 104 | if (__instance.NetworkManager == null || !__instance.NetworkManager.IsListening) { return; } 105 | if (NetworkManager.Singleton == null) { return; } 106 | 107 | // Unregister message handlers 108 | NetworkManager.Singleton.CustomMessagingManager.UnregisterNamedMessageHandler(LCDirectLan.PLUGIN_NAME + "_ClientLatencyRequestCallback_ToClientRpc"); 109 | 110 | // Do not allow clients to handle server events 111 | if (__instance.NetworkManager.IsServer) { 112 | NetworkManager.Singleton.CustomMessagingManager.UnregisterNamedMessageHandler(LCDirectLan.PLUGIN_NAME + "_ClientLatencyRequest_ToServerRpc"); 113 | NetworkManager.Singleton.CustomMessagingManager.UnregisterNamedMessageHandler(LCDirectLan.PLUGIN_NAME + "_ServerLatencyRequestCallback_ToServerRpc"); 114 | } 115 | } 116 | 117 | /// 118 | /// Start tracking latency 119 | /// 120 | /// A MonoBehaviour instance in order to be able to start coroutines 121 | public static void StartTrackingLatency(MonoBehaviour __instance) 122 | { 123 | LCDirectLan.Log(BepInEx.Logging.LogLevel.Debug, "LatencyRPC.StartTrackingLatency()"); 124 | 125 | // Avoid starting multiple coroutines 126 | if (LatencyTrackerCoroutine != null) { 127 | LCDirectLan.Log(BepInEx.Logging.LogLevel.Warning, "LatencyTrackerCoroutine is already running !"); 128 | return; 129 | } 130 | 131 | // Start the latency tracking coroutine 132 | LatencyTrackerCoroutine = __instance.StartCoroutine(LatencyTracker_Coroutine()); 133 | } 134 | 135 | /// 136 | /// The latency tracking coroutine 137 | /// 138 | /// An IEnumerator for the coroutine 139 | private static IEnumerator LatencyTracker_Coroutine() 140 | { 141 | if (!LCDirectLan.IsOnLanMode) { yield break; } 142 | 143 | float SecondsSinceIsWaitingPing = 0; 144 | float TargetPollingInterval = 3; // in seconds 145 | float ServerWarnTimeout = 4.5F; // in seconds 146 | bool RetryPing = false; 147 | bool HasSentHUDWarning = false; 148 | bool KeepRunning = true; 149 | bool IsServerSupportLatencyRPC = false; 150 | 151 | while (KeepRunning) 152 | { 153 | if (!IsWaitingPingCallback || RetryPing) { 154 | if (!UseCustomLatencyRPC) { 155 | IsWaitingPingCallback = false; 156 | 157 | if (!FetchUnityLatency(ref SecondsSinceIsWaitingPing)) { 158 | HUDManagerPatch.DestroyLatencyHUD(); 159 | yield break; 160 | } 161 | } 162 | else { 163 | if (NetworkManager.Singleton == null) { 164 | LCDirectLan.Log(BepInEx.Logging.LogLevel.Warning, "NetworkManager.Singleton is null, cannot send LatencyRPC ping request !"); 165 | 166 | HUDManagerPatch.DisplayHUDWarning("Unable to find NetworkManager for LatencyRPC ping request"); 167 | 168 | // Get the UnityTransport object 169 | UnityTransportObject = GameObject.Find("NetworkManager").GetComponent(); 170 | 171 | // Try again without Custom Latency RPC 172 | UseCustomLatencyRPC = false; 173 | RetryPing = true; 174 | continue; 175 | } 176 | 177 | SendCustomLatencyRequest(ref SecondsSinceIsWaitingPing, ref IsWaitingPingCallback); 178 | } 179 | } 180 | 181 | yield return new WaitForSeconds(0.2F); 182 | SecondsSinceIsWaitingPing += 0.2F; 183 | 184 | // If we aren't waiting for a callback 185 | if (!IsWaitingPingCallback) { 186 | if (UseCustomLatencyRPC) { IsServerSupportLatencyRPC = true; } 187 | RetryPing = false; 188 | HasSentHUDWarning = false; 189 | 190 | // Check if we should sleep until the next polling interval 191 | if (SecondsSinceIsWaitingPing < TargetPollingInterval) { 192 | // Sleep until the next polling interval 193 | yield return new WaitForSeconds(TargetPollingInterval - SecondsSinceIsWaitingPing); 194 | } 195 | 196 | // Continue to the next polling interval 197 | continue; 198 | } 199 | 200 | // Check if we have waited for too long 201 | if (SecondsSinceIsWaitingPing >= ServerWarnTimeout) 202 | { 203 | // If Server haven't responded to any of our ping request, they may not support LatencyRPC 204 | if (UseCustomLatencyRPC && !IsServerSupportLatencyRPC) { 205 | LCDirectLan.Log(BepInEx.Logging.LogLevel.Warning, "Server did not respond to first ping request, server may not support LatencyRPC !"); 206 | HUDManagerPatch.DisplayHUDWarning("Server did not support LatencyRPC, switching to UnityTransport..."); 207 | 208 | // Get the UnityTransport object 209 | UnityTransportObject = GameObject.Find("NetworkManager").GetComponent(); 210 | 211 | // Switch to use UnityTransport's RTT 212 | UseCustomLatencyRPC = false; 213 | 214 | // Retry the ping request 215 | RetryPing = true; 216 | 217 | // Continue to the next polling interval 218 | continue; 219 | } 220 | 221 | LCDirectLan.Log(BepInEx.Logging.LogLevel.Warning, "Server have not responded to our last ping request !"); 222 | HUDManagerPatch.UpdateLatencyHUD((ushort)(SecondsSinceIsWaitingPing * 1000)); 223 | 224 | if (!HasSentHUDWarning) { 225 | // Send a warning to the HUD 226 | HUDManagerPatch.DisplayHUDWarning("Server stopped responding to ping, slow server or connection lost ?"); 227 | HasSentHUDWarning = true; 228 | } 229 | 230 | // Throttle retrying the ping request 231 | yield return new WaitForSeconds(1.5F); 232 | SecondsSinceIsWaitingPing += 1.5F; 233 | 234 | // Retry the ping request 235 | RetryPing = true; 236 | } 237 | } 238 | } 239 | 240 | /// 241 | /// Fetch the latency using UnityTransport's RTT 242 | /// 243 | /// Seconds since the last time we are waiting for a ping callback 244 | /// Boolean representing whether the latency is successfully fetched 245 | private static bool FetchUnityLatency(ref float SecondsSinceIsWaitingPing) { 246 | if (UnityTransportObject == null) { 247 | LCDirectLan.Log(BepInEx.Logging.LogLevel.Error, "UnityTransportObject is null, Latency Patch will be disabled !"); 248 | HUDManagerPatch.DisplayHUDWarning("Unable to find UnityTransport, Latency Patch will be disabled."); 249 | 250 | return false; 251 | } 252 | 253 | SecondsSinceIsWaitingPing = 0; 254 | 255 | ulong latency = UnityTransportObject.GetCurrentRtt(0); 256 | 257 | // Check if we should measure RTT instead of one-way latency 258 | if (LCDirectLan.GetConfig("Latency HUD", "RTTMeasurement")) { 259 | LCDirectLan.Log(BepInEx.Logging.LogLevel.Debug, $"Our RTT Latency (Unity): {latency}ms"); 260 | } 261 | else { 262 | latency /= 2; 263 | LCDirectLan.Log(BepInEx.Logging.LogLevel.Debug, $"Our One-Way Latency (Unity): {latency}ms"); 264 | } 265 | 266 | // Update the HUD 267 | HUDManagerPatch.UpdateLatencyHUD((ushort)latency); 268 | 269 | return true; 270 | } 271 | 272 | /// 273 | /// Send a ping request to the server (using LCDirectLAN's Latency RPC, requires server support) 274 | /// 275 | /// Seconds since the last time we are waiting for a ping callback 276 | /// Whether we are waiting for a ping callback 277 | private static void SendCustomLatencyRequest(ref float SecondsSinceIsWaitingPing, ref bool IsWaitingPingCallback) { 278 | // Send a ping request to the server 279 | FastBufferWriter writer = new FastBufferWriter(8, Allocator.Temp); 280 | writer.WriteValue(GetCurrentEpochMilis()); 281 | 282 | if (!IsWaitingPingCallback) { 283 | IsWaitingPingCallback = true; 284 | SecondsSinceIsWaitingPing = 0; 285 | } 286 | 287 | NetworkManager.Singleton.CustomMessagingManager.SendNamedMessage(LCDirectLan.PLUGIN_NAME + "_ClientLatencyRequest_ToServerRpc", 0, writer, NetworkDelivery.Unreliable); 288 | } 289 | 290 | /// 291 | /// Stop tracking latency 292 | /// 293 | /// A MonoBehaviour instance in order to be able to stop coroutines 294 | public static void StopTrackingLatency(MonoBehaviour __instance) 295 | { 296 | LCDirectLan.Log(BepInEx.Logging.LogLevel.Debug, "LatencyRPC.StopTrackingLatency()"); 297 | 298 | // Stop the latency tracking coroutine 299 | if (LatencyTrackerCoroutine != null) { 300 | if (__instance == null) { 301 | LCDirectLan.Log(BepInEx.Logging.LogLevel.Warning, "Cannot stop LatencyRPC.LatencyTrackerCoroutine, MonoBehaviour instance is null !"); 302 | LatencyTrackerCoroutine = null; 303 | return; 304 | } 305 | 306 | __instance.StopCoroutine(LatencyTrackerCoroutine); 307 | LatencyTrackerCoroutine = null; 308 | } 309 | } 310 | 311 | [ServerRpc] 312 | public static void ClientLatencyRequest(ulong ClientID, FastBufferReader reader) 313 | { 314 | /** ClientLatencyRequest to Server Event Payload: 315 | * byte 1-8: Client-side timestamp when the request is sent 316 | **/ 317 | reader.ReadValue(out long ClientRequestTimestamp); 318 | 319 | FastBufferWriter writer = new FastBufferWriter(16, Allocator.Temp); 320 | 321 | // Write back the client's timestamp 322 | writer.WriteValue(ClientRequestTimestamp); 323 | 324 | // Include our timestamp back to the client so we can calculate the latency using our timing 325 | writer.WriteValue(GetCurrentEpochMilis()); 326 | 327 | // Send callback to the client 328 | NetworkManager.Singleton.CustomMessagingManager.SendNamedMessage(LCDirectLan.PLUGIN_NAME + "_ClientLatencyRequestCallback_ToClientRpc", ClientID, writer, NetworkDelivery.Unreliable); 329 | 330 | // No need to expose the data for the host (Player #0) since the server is also a client 331 | if (ClientID == 0) { return; } 332 | 333 | LCDirectLan.Log(BepInEx.Logging.LogLevel.Debug, $"Replied to ClientID {ClientID}'s ping request !"); 334 | } 335 | 336 | /// 337 | /// Event handler for Server side ping callback 338 | /// 339 | /// The request source (always the server, which is 0) 340 | /// The buffer data 341 | [ClientRpc] 342 | public static void ClientLatencyRequestCallback(ulong ClientID, FastBufferReader reader) 343 | { 344 | /** ClientLatencyRequestCallback to Client Event Payload: 345 | * byte 1-8: Client-side timestamp when the request is sent 346 | * byte 9-16: Server-side timestamp when the request is received 347 | **/ 348 | long ClientReceivedTimestamp = GetCurrentEpochMilis(); 349 | 350 | reader.ReadValue(out long ClientRequestTimestamp); 351 | reader.ReadValue(out long ServerRequestTimestamp); 352 | 353 | FastBufferWriter writer = new FastBufferWriter(8, Allocator.Temp); 354 | 355 | // Send the server's timestamp back to the server so they can calculate the latency using their own timing 356 | writer.WriteValue(ServerRequestTimestamp); 357 | 358 | // Send callback to the server 359 | NetworkManager.Singleton.CustomMessagingManager.SendNamedMessage(LCDirectLan.PLUGIN_NAME + "_ServerLatencyRequestCallback_ToServerRpc", 0, writer, NetworkDelivery.Unreliable); 360 | 361 | ushort latency = (ushort)(ClientReceivedTimestamp - ClientRequestTimestamp); 362 | 363 | // Check if we should measure RTT instead of one-way latency 364 | if (LCDirectLan.GetConfig("Latency HUD", "RTTMeasurement")) { 365 | LCDirectLan.Log(BepInEx.Logging.LogLevel.Debug, $"Our RTT Latency: {latency}ms"); 366 | } 367 | else { 368 | latency /= 2; 369 | LCDirectLan.Log(BepInEx.Logging.LogLevel.Debug, $"Our One-Way Latency: {latency}ms"); 370 | } 371 | 372 | // Update the HUD 373 | HUDManagerPatch.UpdateLatencyHUD(latency); 374 | 375 | IsWaitingPingCallback = false; 376 | } 377 | 378 | /// 379 | /// Event handler for Server ping request callback 380 | /// 381 | /// The callback source (the client) 382 | /// The buffer data 383 | [ServerRpc] 384 | public static void ServerLatencyRequestCallback(ulong ClientID, FastBufferReader reader) 385 | { 386 | /** ClientLatencyRequestCallback to Client Event Payload: 387 | * byte 1-8: Server-side timestamp when the request is received 388 | **/ 389 | long ServerReceivedTimestamp = GetCurrentEpochMilis(); 390 | 391 | reader.ReadValue(out long ServerRequestTimestamp); 392 | 393 | // Get actual Player ID 394 | int PlayerID = Utility.allPlayerScripts.GetActualPlayerIndex(ClientID); 395 | 396 | // No need to show latency data for our self (server/host/Player #0) 397 | if (PlayerID == 0) { return; } 398 | 399 | short latency = (short)(ServerReceivedTimestamp - ServerRequestTimestamp); 400 | 401 | string PlayerUsername = Utility.allPlayerScripts.GetPlayerUsername(PlayerID); 402 | 403 | // If player does not have custom username, log as Player #ID 404 | if (PlayerUsername == $"Player #{PlayerID}") { 405 | LCDirectLan.Log(BepInEx.Logging.LogLevel.Debug, $"Player #{PlayerID}'s RTT Latency: {latency}ms, Estimated One-Way Latency: {latency / 2}ms"); 406 | return; 407 | } 408 | 409 | // Player has custom username, log as their username 410 | LCDirectLan.Log(BepInEx.Logging.LogLevel.Debug, $"{PlayerUsername}'s RTT Latency: {latency}ms, Estimated One-Way Latency: {latency / 2}ms"); 411 | } 412 | 413 | /// 414 | /// Get the current epoch time in milliseconds 415 | /// 416 | /// The current epoch time in milliseconds 417 | private static long GetCurrentEpochMilis() 418 | { 419 | return DateTimeOffset.Now.ToUnixTimeMilliseconds(); 420 | } 421 | } 422 | } 423 | -------------------------------------------------------------------------------- /Patches/CustomUsername/UsernameRPC.cs: -------------------------------------------------------------------------------- 1 | /** 2 | * This source code is part of LCDirectLAN project, 3 | * LCDirectLAN is a mod for Lethal Company that is built around BepInEx to fix and enhances LAN lobbies. 4 | * 5 | * Project Repository: 6 | * https://github.com/TIRTAGT/LCDirectLAN 7 | * 8 | * This project is open source and are released under the MIT License, 9 | * for more information, please read the LICENSE file in the project repository. 10 | * 11 | * Copyright (c) 2024 Matthew Tirtawidjaja 12 | **/ 13 | 14 | using GameNetcodeStuff; 15 | using HarmonyLib; 16 | using System.Collections.Generic; 17 | using System.Text; 18 | using Unity.Collections; 19 | using Unity.Netcode; 20 | using UnityEngine; 21 | 22 | namespace LCDirectLAN.Patches.CustomUsername 23 | { 24 | internal class UsernameRPC : NetworkBehaviour 25 | { 26 | /// 27 | /// A array of boolean flags to keep track of which player have successfully synced their username to the server 28 | /// 29 | private static bool[] PlayerSyncStatus = new bool[0]; 30 | 31 | private static byte PlayerSyncBroadcastID = 0; 32 | 33 | /// 34 | /// Specify the maximum time to wait for all clients to sync their usernames to the server, in seconds 35 | ///

36 | /// Must be a value between 1 and 255 37 | ///



38 | /// 39 | /// When this timeout is reached, the server will display a warning message to the host that some clients may not have synced their usernames 40 | ///
41 | private static readonly byte PlayerSyncWaitTimeout = 30; 42 | 43 | private static bool IsBroadcastingPlayerUsernames = false; 44 | /// 45 | /// Inject the UsernameRPC on PlayerControllerB's ConnectClientToPlayerObject() 46 | /// 47 | /// 48 | [HarmonyPatch(typeof(PlayerControllerB), "ConnectClientToPlayerObject")] 49 | [HarmonyPrefix] 50 | [HarmonyPriority(Priority.High)] 51 | public static void Prefix_ConnectClientToPlayerObject(PlayerControllerB __instance) 52 | { 53 | LCDirectLan.Log(BepInEx.Logging.LogLevel.Debug, $"UsernameRPC.Prefix_ConnectClientToPlayerObject({__instance.NetworkManager.LocalClientId})"); 54 | 55 | if (__instance.NetworkManager == null || !__instance.NetworkManager.IsListening) 56 | { 57 | return; 58 | } 59 | 60 | // If this is player is not controlled by us 61 | if (!__instance.IsOwner) { return; } 62 | 63 | // Listen for server side username refresh events (for syncing) 64 | NetworkManager.Singleton.CustomMessagingManager.RegisterNamedMessageHandler(LCDirectLan.PLUGIN_NAME + "_GlobalUsernameRefresh_ToClientRpc", new CustomMessagingManager.HandleNamedMessageDelegate(GlobalUsernameRefresh_ToClientRpc)); 65 | LCDirectLan.Log(BepInEx.Logging.LogLevel.Debug, $"Listening _GlobalUsernameRefresh_ToClientRpc()"); 66 | 67 | // Do not allow clients to handle server events 68 | if (!__instance.NetworkManager.IsServer) { return; } 69 | 70 | // Initialize the PlayerSyncStatus array 71 | PlayerSyncStatus = new bool[StartOfRound.Instance.allPlayerScripts.Length]; 72 | for (int i = 0; i < PlayerSyncStatus.Length; i++) { PlayerSyncStatus[i] = false; } 73 | 74 | // Listen for username change events 75 | NetworkManager.Singleton.CustomMessagingManager.RegisterNamedMessageHandler(LCDirectLan.PLUGIN_NAME + "_ClientUsernameChanged_ToServerRpc", new CustomMessagingManager.HandleNamedMessageDelegate(ClientUsernameChanged_ToServerRpc)); 76 | LCDirectLan.Log(BepInEx.Logging.LogLevel.Debug, $"Listening _ClientUsernameChanged_ToServerRpc()"); 77 | 78 | // Listen for username refresh callback events 79 | NetworkManager.Singleton.CustomMessagingManager.RegisterNamedMessageHandler(LCDirectLan.PLUGIN_NAME + "_ClientReceivedUsernameRefresh_ToServerRpc", new CustomMessagingManager.HandleNamedMessageDelegate(ClientReceivedUsernameRefresh_ToServerRpc)); 80 | LCDirectLan.Log(BepInEx.Logging.LogLevel.Debug, $"Listening _ClientReceivedUsernameRefresh_ToServerRpc()"); 81 | } 82 | 83 | /// 84 | /// Event handler for the client request to update their username on the server and broadcast to other clients 85 | /// 86 | /// The client who sent username change event to the server 87 | /// 88 | [ServerRpc] 89 | public static void ClientUsernameChanged_ToServerRpc(ulong ClientID, FastBufferReader reader) 90 | { 91 | /** This event is called from client to server when the client wants to update it's username (just joined) 92 | * 93 | * ClientUsernameChanged to Server Event Payload: 94 | * byte 1: UsernameLength 95 | * byte 2 - UsernameLength + 1: Usernamed encoded in ASCII bytes 96 | **/ 97 | if (!NetworkManager.Singleton.IsServer) { return; } 98 | 99 | int PlayerID = Utility.allPlayerScripts.GetActualPlayerIndex(ClientID); 100 | 101 | LCDirectLan.Log(BepInEx.Logging.LogLevel.Debug, $"SERVER: Received username changed event from ClientID {ClientID}, PlayerID {PlayerID} !"); 102 | 103 | // Try reading the username length 104 | if (reader.TryBeginRead(1)) 105 | { 106 | reader.ReadValue(out byte UsernameLength); 107 | 108 | if (UsernameLength == 0) 109 | { 110 | LCDirectLan.Log(BepInEx.Logging.LogLevel.Warning, $"SERVER: Client sent a empty username change request, IGNORED !"); 111 | return; 112 | } 113 | 114 | if (!reader.TryBeginRead(UsernameLength)) 115 | { 116 | LCDirectLan.Log(BepInEx.Logging.LogLevel.Warning, $"SERVER: Unable to read client username, cannot read {UsernameLength} bytes from reader !"); 117 | return; 118 | } 119 | 120 | byte[] Username = new byte[UsernameLength]; 121 | reader.ReadBytes(ref Username, UsernameLength); 122 | string a = Encoding.ASCII.GetString(Username); 123 | 124 | LCDirectLan.Log(BepInEx.Logging.LogLevel.Debug, $"SERVER: Changing Player #{PlayerID} username to: {a}"); 125 | 126 | StartOfRound.Instance.allPlayerScripts[PlayerID].playerUsername = a; 127 | LCDirectLan.Log(BepInEx.Logging.LogLevel.Info, $"SERVER: Sucessfully changed Player #{PlayerID}'s username to: {a}"); 128 | 129 | SendGlobalUsernameRefreshToClient(); 130 | return; 131 | } 132 | 133 | LCDirectLan.Log(BepInEx.Logging.LogLevel.Warning, $"SERVER: Client sent a broken payload, no UsernameLength !"); 134 | } 135 | 136 | /// 137 | /// Event handler for the client's request to refresh their cached players username from the server 138 | /// 139 | /// The client who have received the username refresh broadcast from server 140 | /// 141 | [ServerRpc] 142 | public static void ClientReceivedUsernameRefresh_ToServerRpc(ulong ClientID, FastBufferReader reader) { 143 | /** This event is called from client to server when the client received a username refresh from the server 144 | * 145 | * ClientReceivedUsernameRefresh to Server Event Payload: 146 | * byte 1: BroadcastID 147 | **/ 148 | if (!NetworkManager.Singleton.IsServer) { return; } 149 | 150 | int PlayerID = Utility.allPlayerScripts.GetActualPlayerIndex(ClientID); 151 | 152 | LCDirectLan.Log(BepInEx.Logging.LogLevel.Debug, $"SERVER: Received UsernameBroadcastCallback from Client #{ClientID}, Player #{PlayerID} for BroadcastID #{PlayerSyncBroadcastID} !"); 153 | 154 | // If PlayerID is out of range, ignore 155 | if (PlayerID == -1) 156 | { 157 | LCDirectLan.Log(BepInEx.Logging.LogLevel.Warning, $"SERVER: Player #{PlayerID} is not controlled by anyone, ignoring UsernameBroadcastCallback !"); 158 | return; 159 | } 160 | 161 | // Try reading the player ID 162 | if (reader.TryBeginRead(1)) 163 | { 164 | reader.ReadValue(out byte BroadcastID); 165 | 166 | // If the broadcast ID is not the same as the current one, ignore as it's outdated or broken 167 | if (BroadcastID != PlayerSyncBroadcastID) 168 | { 169 | LCDirectLan.Log(BepInEx.Logging.LogLevel.Warning, $"SERVER: Player #{PlayerID} is lagging behind broadcast {BroadcastID} !"); 170 | return; 171 | } 172 | 173 | if (PlayerID >= PlayerSyncStatus.Length) 174 | { 175 | LCDirectLan.Log(BepInEx.Logging.LogLevel.Warning, $"SERVER: Player #{PlayerID} is out of range in PlayerSyncStatus !"); 176 | return; 177 | } 178 | 179 | PlayerSyncStatus[PlayerID] = true; 180 | LCDirectLan.Log(BepInEx.Logging.LogLevel.Info, $"SERVER: Player #{PlayerID} has successfully synced usernames !"); 181 | return; 182 | } 183 | 184 | LCDirectLan.Log(BepInEx.Logging.LogLevel.Warning, $"SERVER: Player #{PlayerID} sent a broken payload, no BroadcastID !"); 185 | } 186 | 187 | /// 188 | /// Send a request to all clients to refresh their cached players username from the server 189 | /// 190 | private static void SendGlobalUsernameRefreshToClient() 191 | { 192 | LCDirectLan.Log(BepInEx.Logging.LogLevel.Debug, $"SendGlobalUsernameRefreshToClient()"); 193 | 194 | if (!NetworkManager.Singleton.IsServer) { 195 | LCDirectLan.Log(BepInEx.Logging.LogLevel.Warning, $"CLIENT: Cannot send global username refresh as a client !!!"); 196 | return; 197 | } 198 | 199 | if (IsBroadcastingPlayerUsernames) { 200 | LCDirectLan.Log(BepInEx.Logging.LogLevel.Warning, $"SERVER: Still trying to broadcast player usernames, ignoring request to broadcast again"); 201 | return; 202 | } 203 | 204 | IsBroadcastingPlayerUsernames = true; 205 | 206 | PlayerSyncStatus = new bool[StartOfRound.Instance.allPlayerScripts.Length]; 207 | for (int i = 0; i < PlayerSyncStatus.Length; i++) { PlayerSyncStatus[i] = false; } 208 | 209 | // Generate a unique ID for this broadcast 210 | byte SyncBroadcastID = PlayerSyncBroadcastID; 211 | 212 | // While the broadcast ID is the same as the previous one, generate a new one 213 | while (SyncBroadcastID == PlayerSyncBroadcastID) { 214 | SyncBroadcastID = (byte)Random.Range(0, 255); 215 | } 216 | PlayerSyncBroadcastID = SyncBroadcastID; 217 | 218 | LCDirectLan.Log(BepInEx.Logging.LogLevel.Info, $"SERVER: Sending all usernames available on the server to clients (BroadcastID: {PlayerSyncBroadcastID})..."); 219 | 220 | List DataBuffer = new List(); 221 | int DataBufferSize = 0; 222 | 223 | // Write the broadcast ID 224 | DataBuffer.Add(new byte[1] { SyncBroadcastID }); 225 | DataBufferSize += 1; 226 | 227 | int PlayerCount = StartOfRound.Instance.allPlayerScripts.Length; 228 | 229 | // Write the player count 230 | DataBuffer.Add(new byte[1] { (byte)PlayerCount }); 231 | DataBufferSize += 1; 232 | 233 | for (int i = 0; i < PlayerCount; i++) 234 | { 235 | // If player isn't conntrolled by anyone, send empty username 236 | if (!StartOfRound.Instance.allPlayerScripts[i].isPlayerControlled) { 237 | LCDirectLan.Log(BepInEx.Logging.LogLevel.Debug, $"SERVER: Ignoring Player #{i} as they are not controlled by anyone"); 238 | 239 | // Write the username length 240 | DataBuffer.Add(new byte[1] { 0 }); 241 | DataBufferSize += 1; 242 | continue; 243 | } 244 | 245 | string Username = StartOfRound.Instance.allPlayerScripts[i].playerUsername; 246 | int UsernameLength = Username.Length; 247 | 248 | // Write the username length 249 | DataBuffer.Add(new byte[1] { (byte)UsernameLength }); 250 | DataBufferSize += 1; 251 | 252 | // Encode the username as ASCII and then write it 253 | DataBuffer.Add(Encoding.ASCII.GetBytes(Username)); 254 | DataBufferSize += UsernameLength; 255 | 256 | LCDirectLan.Log(BepInEx.Logging.LogLevel.Debug, $"SERVER: Packed Player #{i} username: {Username}"); 257 | } 258 | 259 | FastBufferWriter writer = new FastBufferWriter(DataBufferSize, Allocator.Temp); 260 | 261 | // Write the buffer 262 | for (int i = 0; i < DataBuffer.Count; i++) 263 | { 264 | writer.WriteBytes(DataBuffer[i], DataBuffer[i].Length, 0); 265 | } 266 | 267 | // Start waiting for all clients to sync their usernames 268 | NetworkManager.Singleton.StartCoroutine(WaitForAllClientsToSyncUsernames()); 269 | 270 | NetworkManager.Singleton.CustomMessagingManager.SendNamedMessageToAll(LCDirectLan.PLUGIN_NAME + "_GlobalUsernameRefresh_ToClientRpc", writer, NetworkDelivery.Reliable); 271 | LCDirectLan.Log(BepInEx.Logging.LogLevel.Info, $"SERVER: Sucessfully sent a global username refresh to all clients"); 272 | IsBroadcastingPlayerUsernames = false; 273 | } 274 | 275 | /// 276 | /// Send a request to a specific client to refresh their cached players username from the server 277 | /// 278 | /// The ClientID to send the request to 279 | private static void SendUsernameRefreshToClient(ulong NetworkClientID) 280 | { 281 | LCDirectLan.Log(BepInEx.Logging.LogLevel.Debug, $"SendUsernameRefreshToClient({NetworkClientID})"); 282 | 283 | if (!NetworkManager.Singleton.IsServer) { 284 | LCDirectLan.Log(BepInEx.Logging.LogLevel.Warning, $"CLIENT: Cannot send username refresh as a client !!!"); 285 | return; 286 | } 287 | 288 | if (IsBroadcastingPlayerUsernames) { 289 | LCDirectLan.Log(BepInEx.Logging.LogLevel.Warning, $"SERVER: Still trying to broadcast player usernames, ignoring request to broadcast again"); 290 | return; 291 | } 292 | 293 | IsBroadcastingPlayerUsernames = true; 294 | 295 | // Reuse the previous broadcast ID if it exist, otherwise generate a new one 296 | byte SyncBroadcastID = PlayerSyncBroadcastID; 297 | 298 | if (SyncBroadcastID == 0) { 299 | SyncBroadcastID = (byte)Random.Range(0, 255); 300 | PlayerSyncBroadcastID = SyncBroadcastID; 301 | } 302 | 303 | LCDirectLan.Log(BepInEx.Logging.LogLevel.Info, $"SERVER: Sending all usernames available on the server to client #{NetworkClientID} (BroadcastID: {PlayerSyncBroadcastID})..."); 304 | 305 | List DataBuffer = new List(); 306 | int DataBufferSize = 0; 307 | 308 | // Write the broadcast ID 309 | DataBuffer.Add(new byte[1] { SyncBroadcastID }); 310 | DataBufferSize += 1; 311 | 312 | int PlayerCount = StartOfRound.Instance.allPlayerScripts.Length; 313 | 314 | // Write the player count 315 | DataBuffer.Add(new byte[1] { (byte)PlayerCount }); 316 | DataBufferSize += 1; 317 | 318 | for (int i = 0; i < PlayerCount; i++) 319 | { 320 | // If player isn't conntrolled by anyone, send empty username 321 | if (!StartOfRound.Instance.allPlayerScripts[i].isPlayerControlled) { 322 | LCDirectLan.Log(BepInEx.Logging.LogLevel.Debug, $"SERVER: Ignoring Player #{i} as they are not controlled by anyone"); 323 | 324 | // Write the username length 325 | DataBuffer.Add(new byte[1] { 0 }); 326 | DataBufferSize += 1; 327 | continue; 328 | } 329 | 330 | string Username = StartOfRound.Instance.allPlayerScripts[i].playerUsername; 331 | int UsernameLength = Username.Length; 332 | 333 | // Write the username length 334 | DataBuffer.Add(new byte[1] { (byte)UsernameLength }); 335 | DataBufferSize += 1; 336 | 337 | // Encode the username as ASCII and then write it 338 | DataBuffer.Add(Encoding.ASCII.GetBytes(Username)); 339 | DataBufferSize += 1 + UsernameLength; 340 | 341 | LCDirectLan.Log(BepInEx.Logging.LogLevel.Debug, $"SERVER: Packed Player #{i} username: {Username}"); 342 | } 343 | 344 | FastBufferWriter writer = new FastBufferWriter(DataBufferSize, Allocator.Temp); 345 | 346 | // Write the buffer 347 | for (int i = 0; i < DataBuffer.Count; i++) 348 | { 349 | writer.WriteBytes(DataBuffer[i], DataBuffer[i].Length, 0); 350 | } 351 | 352 | NetworkManager.Singleton.CustomMessagingManager.SendNamedMessage(LCDirectLan.PLUGIN_NAME + "_GlobalUsernameRefresh_ToClientRpc", NetworkClientID, writer, NetworkDelivery.Reliable); 353 | LCDirectLan.Log(BepInEx.Logging.LogLevel.Info, $"SERVER: Sucessfully sent a username refresh to client #{NetworkClientID}"); 354 | IsBroadcastingPlayerUsernames = false; 355 | } 356 | 357 | /// 358 | /// Wait for all clients to sync their usernames to the server 359 | /// 360 | /// An IEnumerator for the coroutine 361 | private static System.Collections.IEnumerator WaitForAllClientsToSyncUsernames() 362 | { 363 | LCDirectLan.Log(BepInEx.Logging.LogLevel.Debug, $"WaitForAllClientsToSyncUsernames()"); 364 | 365 | if (!NetworkManager.Singleton.IsServer) { 366 | LCDirectLan.Log(BepInEx.Logging.LogLevel.Warning, $"CLIENT: Cannot wait for all clients to sync usernames as a client !!!"); 367 | yield break; 368 | } 369 | 370 | // Wait for a while before checking sync status again (to give time for clients to receive the first broadcast) 371 | yield return new WaitForSeconds(1); 372 | 373 | // Remember which BroadcastID we are waiting for 374 | byte TargetBroadcastID = PlayerSyncBroadcastID; 375 | 376 | // Calculate until when we should wait for all clients to sync their usernames 377 | float TargetWaitTimeout = Time.time + PlayerSyncWaitTimeout; 378 | 379 | // Calculate the interval to refresh the sync status 380 | int RefreshInterval = PlayerSyncWaitTimeout / 10; 381 | 382 | // Wait for all clients to sync their usernames 383 | while (true) 384 | { 385 | // If the timeout is reached, display a warning message to the host that some clients may not have synced their usernames 386 | if (Time.time > TargetWaitTimeout) 387 | { 388 | LCDirectLan.Log(BepInEx.Logging.LogLevel.Warning, $"SERVER: Reached Username Sync Timeout, Some clients may not have synced their usernames !"); 389 | 390 | // List all sync status 391 | for (int i = 0; i < PlayerSyncStatus.Length; i++) 392 | { 393 | // Ignore players that are not controlled by anyone 394 | if (!StartOfRound.Instance.allPlayerScripts[i].isPlayerControlled) { continue; } 395 | 396 | LCDirectLan.Log(BepInEx.Logging.LogLevel.Warning, $"SERVER: Is Client #{i} ({StartOfRound.Instance.allPlayerScripts[i].playerUsername}) has reported they are synced: {PlayerSyncStatus[i]}"); 397 | } 398 | break; 399 | } 400 | 401 | // If the broadcast ID is not the same as the current one, ignore as we may need to wait for a new broadcast 402 | if (TargetBroadcastID != PlayerSyncBroadcastID) 403 | { 404 | LCDirectLan.Log(BepInEx.Logging.LogLevel.Warning, $"SERVER: Broadcast ID mismatch, aborting wait for current sync: {TargetBroadcastID} != {PlayerSyncBroadcastID} !"); 405 | break; 406 | } 407 | 408 | bool IsAllClientsSynced = true; 409 | for (int i = 0; i < PlayerSyncStatus.Length; i++) 410 | { 411 | // Ignore players that are not controlled by anyone 412 | if (!StartOfRound.Instance.allPlayerScripts[i].isPlayerControlled) { continue; } 413 | 414 | if (!PlayerSyncStatus[i]) 415 | { 416 | IsAllClientsSynced = false; 417 | 418 | // Resend the username refresh to clients that haven't synced their usernames 419 | LCDirectLan.Log(BepInEx.Logging.LogLevel.Warning, $"SERVER: Player #{i} ({StartOfRound.Instance.allPlayerScripts[i].playerUsername}) has not reported their username are synced, resending..."); 420 | SendUsernameRefreshToClient(StartOfRound.Instance.allPlayerScripts[i].actualClientId); 421 | break; 422 | } 423 | } 424 | 425 | if (IsAllClientsSynced) { break; } 426 | 427 | // Wait for a while before checking sync status again 428 | yield return new WaitForSeconds(RefreshInterval); 429 | } 430 | 431 | if (Time.time > TargetWaitTimeout) 432 | { 433 | HUDManager.Instance.DisplayTip("LCDirectLAN", "Some clients may not have synced their usernames properly", false, false); 434 | } 435 | else 436 | { 437 | LCDirectLan.Log(BepInEx.Logging.LogLevel.Info, $"SERVER: All clients have successfully synced their usernames !"); 438 | PlayerSyncBroadcastID = 0; 439 | } 440 | } 441 | 442 | /// 443 | /// Send a request to the server to update our (currently owned player object) username 444 | /// 445 | /// The username 446 | public static void SendInformationToServerRpc(string username) 447 | { 448 | if (NetworkManager.Singleton.IsClient) 449 | { 450 | LCDirectLan.Log(BepInEx.Logging.LogLevel.Info, $"CLIENT: Trying to send our username ({username}) to server"); 451 | 452 | if (username == string.Empty) 453 | { 454 | LCDirectLan.Log(BepInEx.Logging.LogLevel.Error, $"CLIENT: Cannot send empty username !"); 455 | return; 456 | } 457 | 458 | byte[] b = Encoding.ASCII.GetBytes(username); 459 | 460 | if (b.Length > 255) 461 | { 462 | LCDirectLan.Log(BepInEx.Logging.LogLevel.Error, $"CLIENT: Cannot send username, too long ({ b.Length }) !"); 463 | return; 464 | } 465 | 466 | FastBufferWriter writer = new FastBufferWriter(1 + b.Length, Allocator.Temp); 467 | // Write the username length 468 | writer.WriteByte((byte)b.Length); 469 | writer.WriteBytes(b, b.Length, 0); 470 | NetworkManager.Singleton.CustomMessagingManager.SendNamedMessage(LCDirectLan.PLUGIN_NAME + "_ClientUsernameChanged_ToServerRpc", 0, writer, NetworkDelivery.Reliable); 471 | LCDirectLan.Log(BepInEx.Logging.LogLevel.Debug, $"CLIENT: Sucessfully sent our username ({username}) to server"); 472 | } 473 | } 474 | 475 | /// 476 | /// Event handler for the server's request to refresh client cached players from the server 477 | /// 478 | /// The request source (always the server, which is 0) 479 | /// The buffer data 480 | [ClientRpc] 481 | public static void GlobalUsernameRefresh_ToClientRpc(ulong ClientID, FastBufferReader reader) 482 | { 483 | /** This event is called from server to client when there is a update on any of the client username 484 | * 485 | * GlobalUsernameRefresh to Client Event Payload: 486 | * byte 1: BroadcastID 487 | * byte 2: PlayerCount 488 | * byte 3: Player 1's UsernameLength (in game this is Player #0) 489 | * byte 4 - (4 + UsernameLength): Usernamed encoded in ASCII bytes 490 | * 491 | * ==== Multi Player ==== 492 | * byte prev + 1: Player 2's UsernameLength (in game this is Player #1) 493 | * byte prev - (prev + UsernameLength): : Usernamed encoded in ASCII bytes 494 | * 495 | * .... 496 | **/ 497 | 498 | LCDirectLan.Log(BepInEx.Logging.LogLevel.Info, $"CLIENT: Got GlobalUsernameRefresh from Server !"); 499 | 500 | if (!NetworkManager.Singleton.IsClient) { return; } 501 | 502 | if (!reader.TryBeginRead(1)) 503 | { 504 | LCDirectLan.Log(BepInEx.Logging.LogLevel.Warning, $"CLIENT: Server sent a broken GlobalUsernameRefresh payload, no BroadcastID !"); 505 | return; 506 | } 507 | 508 | reader.ReadValue(out byte BroadcastID); 509 | 510 | if (!reader.TryBeginRead(1)) 511 | { 512 | LCDirectLan.Log(BepInEx.Logging.LogLevel.Warning, $"CLIENT: Server sent a broken GlobalUsernameRefresh payload, no PlayerCount !"); 513 | return; 514 | } 515 | 516 | reader.ReadValue(out byte PlayerCount); 517 | 518 | // Make sure we have the same amount of players as the server 519 | if (PlayerCount != StartOfRound.Instance.allPlayerScripts.Length) 520 | { 521 | LCDirectLan.Log(BepInEx.Logging.LogLevel.Warning, $"CLIENT: Server sent a GlobalUsernameRefresh with {PlayerCount} players, but we only have {StartOfRound.Instance.allPlayerScripts.Length} players !"); 522 | return; 523 | } 524 | 525 | QuickMenuManager quickMenuManager = Object.FindObjectOfType(); 526 | 527 | // Iterate for all players 528 | for (int i = 0; i < PlayerCount; i++) 529 | { 530 | if (!reader.TryBeginRead(1)) 531 | { 532 | LCDirectLan.Log(BepInEx.Logging.LogLevel.Error, $"CLIENT: Server sent a broken GlobalUsernameRefresh payload, no UsernameLength !\nThe lobby host may be using a mod that increases the maximum player count in lobby that did not get synced to you."); 533 | return; 534 | } 535 | 536 | reader.ReadValue(out byte UsernameLength); 537 | 538 | if (UsernameLength == 0) 539 | { 540 | // If the player for this username is not controlled by anyone, we know the server will send a empty username 541 | // So we can ignore this and continue 542 | if (!StartOfRound.Instance.allPlayerScripts[i].isPlayerControlled) { 543 | LCDirectLan.Log(BepInEx.Logging.LogLevel.Debug, $"CLIENT: Server ignored username for Player #{i} (Not controlled)"); 544 | continue; 545 | } 546 | 547 | LCDirectLan.Log(BepInEx.Logging.LogLevel.Warning, $"CLIENT: Server sent a empty username for Player #{i}, ignoring !"); 548 | continue; 549 | } 550 | 551 | if (!reader.TryBeginRead(UsernameLength)) 552 | { 553 | LCDirectLan.Log(BepInEx.Logging.LogLevel.Warning, $"CLIENT: Server sent a broken GlobalUsernameRefresh payload, cannot read {UsernameLength} bytes from reader for the Player #{i} !"); 554 | return; 555 | } 556 | 557 | byte[] Username = new byte[UsernameLength]; 558 | reader.ReadBytes(ref Username, UsernameLength); 559 | string a = Encoding.ASCII.GetString(Username); 560 | 561 | LCDirectLan.Log(BepInEx.Logging.LogLevel.Debug, $"CLIENT: Unpacked Player #{i} username: {a}"); 562 | 563 | bool IsSomethingFailed = false; 564 | 565 | if (i < StartOfRound.Instance.allPlayerScripts.Length) 566 | { 567 | // Change the player's PlayerControllerB instance username 568 | StartOfRound.Instance.allPlayerScripts[i].playerUsername = a; 569 | 570 | // Change the overhead username displayed in-game (literally over the player's head) 571 | StartOfRound.Instance.allPlayerScripts[i].usernameBillboardText.text = a; 572 | } 573 | else 574 | { 575 | IsSomethingFailed = true; 576 | LCDirectLan.Log(BepInEx.Logging.LogLevel.Warning, $"CLIENT: Cannot set StartOfRound.allPlayerScripts[{i}], it only has {StartOfRound.Instance.allPlayerScripts.Length} elements"); 577 | } 578 | 579 | if (i < quickMenuManager.playerListSlots.Length) 580 | { 581 | // Change username display on the game pause menu (ESC) 582 | quickMenuManager.playerListSlots[i].usernameHeader.text = a; 583 | } 584 | else 585 | { 586 | IsSomethingFailed = true; 587 | LCDirectLan.Log(BepInEx.Logging.LogLevel.Warning, $"CLIENT: Cannot set quickMenuManager.playerListSlots[{i}], it only has {quickMenuManager.playerListSlots} elements"); 588 | } 589 | 590 | if (i < StartOfRound.Instance.mapScreen.radarTargets.Count) 591 | { 592 | // Change username display on the ship's monitor 593 | StartOfRound.Instance.mapScreen.radarTargets[i].name = a; 594 | } 595 | else 596 | { 597 | IsSomethingFailed = true; 598 | LCDirectLan.Log(BepInEx.Logging.LogLevel.Warning, $"CLIENT: Cannot set StartOfRound.mapScreen.radarTargets[{i}], it only has {StartOfRound.Instance.mapScreen.radarTargets} elements"); 599 | } 600 | 601 | 602 | if (IsSomethingFailed) { 603 | LCDirectLan.Log(BepInEx.Logging.LogLevel.Warning, $"CLIENT: Some problem occured while changing Player #{i}'s username to: {a}]\nThe lobby host may be using a mod that increases the maximum player count in lobby that did not get synced to you."); 604 | return; 605 | } 606 | 607 | LCDirectLan.Log(BepInEx.Logging.LogLevel.Info, $"CLIENT: Sucessfully changed Player #{i}'s username to: {a}"); 608 | } 609 | 610 | // Sync the currently displayed name on the map screen 611 | StartOfRoundPatch.LatePatch_PlayerNameOnMapScreen(); 612 | 613 | // Send a callback to the server that we have successfully synced our usernames 614 | FastBufferWriter writer = new FastBufferWriter(1, Allocator.Temp); 615 | writer.WriteByte(BroadcastID); 616 | NetworkManager.Singleton.CustomMessagingManager.SendNamedMessage(LCDirectLan.PLUGIN_NAME + "_ClientReceivedUsernameRefresh_ToServerRpc", 0, writer, NetworkDelivery.Reliable); 617 | } 618 | } 619 | } 620 | -------------------------------------------------------------------------------- /Patches/ConfigurableLAN/MenuManagerPatch.cs: -------------------------------------------------------------------------------- 1 | /** 2 | * This source code is part of LCDirectLAN project, 3 | * LCDirectLAN is a mod for Lethal Company that is built around BepInEx to fix and enhances LAN lobbies. 4 | * 5 | * Project Repository: 6 | * https://github.com/TIRTAGT/LCDirectLAN 7 | * 8 | * This project is open source and are released under the MIT License, 9 | * for more information, please read the LICENSE file in the project repository. 10 | * 11 | * Copyright (c) 2024 Matthew Tirtawidjaja 12 | **/ 13 | 14 | using HarmonyLib; 15 | using System; 16 | using Unity.Netcode.Transports.UTP; 17 | using UnityEngine; 18 | using UnityEngine.UI; 19 | using TMPro; 20 | using LCDirectLAN.Utility; 21 | using System.Collections; 22 | 23 | namespace LCDirectLAN.Patches.ConfigurableLAN 24 | { 25 | internal class PlayerJoinData { 26 | public string Address; 27 | public string Username; 28 | 29 | public PlayerJoinData() { 30 | Address = ""; 31 | Username = ""; 32 | SetPort("7777"); 33 | } 34 | 35 | public PlayerJoinData(string address = "", string port = "7777", string username = "") { 36 | Address = address; 37 | Username = username; 38 | SetPort(port); 39 | } 40 | 41 | public PlayerJoinData(string address = "", ushort port = 7777, string username = "") { 42 | Address = address; 43 | Username = username; 44 | SetPort(port.ToString()); 45 | } 46 | 47 | public bool SetPort(string RawPort) { 48 | if (!ushort.TryParse(RawPort, out ushort p)) 49 | { 50 | return false; 51 | } 52 | 53 | if (p < 1) { return false; } 54 | 55 | IsPortValid = true; 56 | Port = p; 57 | return true; 58 | } 59 | 60 | public void ClearPort() { 61 | Port = 0; 62 | IsPortValid = false; 63 | } 64 | 65 | private ushort _Port = 0; 66 | public ushort Port { 67 | get { 68 | return _Port; 69 | } 70 | 71 | private set { 72 | _Port = value; 73 | } 74 | } 75 | 76 | private bool _IsPortValid; 77 | public bool IsPortValid { 78 | get { 79 | return _IsPortValid; 80 | } 81 | 82 | private set { 83 | _IsPortValid = value; 84 | } 85 | } 86 | } 87 | 88 | [HarmonyPatch(typeof(MenuManager))] 89 | internal class MenuManagerPatch 90 | { 91 | private static readonly int UsernameLengthLimit = 30; 92 | 93 | private static GameObject DirectJoinObj; 94 | private static GameObject DirectConnectWindow; 95 | 96 | /// 97 | /// A bitwise value for server leak protection 98 | ///
0: Show anything (Disabled/No Hiding)
99 | ///
1: Hide IP Address
100 | ///
2: Hide Port Number
101 | ///
3: Hide both IP Address and Port
102 | ///
4: Hide Hostname
103 | ///
7: Hide all of them (IP,Port,Hostname)
104 | ///
105 | private static byte HideJoinData = 3; 106 | 107 | /// 108 | /// Variable to store the server join data that is fine to be shown in the UI 109 | /// 110 | private static PlayerJoinData PublicServerJoinData = null; 111 | 112 | /// 113 | /// Variable to store the server join data that must not be shown in the UI 114 | /// 115 | private static PlayerJoinData PrivateServerJoinData = null; 116 | 117 | private static bool IsSyncingUIWithData = false; 118 | 119 | private static MenuManager __MenuManager; 120 | 121 | [HarmonyPatch("Start")] 122 | [HarmonyPostfix] 123 | [HarmonyPriority(Priority.VeryLow)] 124 | public static void PrepareDirectLANDialog(MenuManager __instance, ref Boolean ___isInitScene) 125 | { 126 | if (___isInitScene) { return; } 127 | 128 | __MenuManager = __instance; 129 | 130 | PublicServerJoinData = new PlayerJoinData( 131 | LCDirectLan.GetConfig("Join", "DefaultAddress"), 132 | LCDirectLan.GetConfig("Join", "DefaultPort") 133 | ); 134 | HideJoinData = LCDirectLan.GetConfig("Join", "HideRawJoinData"); 135 | 136 | // If CustomUsernamePatch is enabled, we should also load the default username 137 | if (LCDirectLan.GetConfig("Custom Username", "Enabled")) { 138 | PublicServerJoinData.Username = LCDirectLan.GetConfig("Custom Username", "JoinDefaultUsername"); 139 | } 140 | 141 | LCDirectLan.Log(BepInEx.Logging.LogLevel.Info, "Loaded JoinData from config file:"); 142 | LCDirectLan.Log(BepInEx.Logging.LogLevel.Info, $"Address: '{PublicServerJoinData.Address}'"); 143 | LCDirectLan.Log(BepInEx.Logging.LogLevel.Info, $"Port: '{PublicServerJoinData.Port}'"); 144 | LCDirectLan.Log(BepInEx.Logging.LogLevel.Info, $"Username: '{PublicServerJoinData.Username}'"); 145 | 146 | if (HideJoinData > 0) { 147 | LCDirectLan.Log(BepInEx.Logging.LogLevel.Info, $"Applying server leak protection based on HideRawJoinData's bitwise value: {HideJoinData}"); 148 | 149 | // Check if we should hide IP Address 150 | if ((HideJoinData & 1) == 1 && !string.IsNullOrEmpty(PublicServerJoinData.Address) && ResolveDNS.CheckIPType(PublicServerJoinData.Address) != System.Net.Sockets.AddressFamily.Unknown) { 151 | PublicServerJoinData.Address = ""; 152 | LCDirectLan.Log(BepInEx.Logging.LogLevel.Info, "Server IP Address from config is cleared !"); 153 | } 154 | 155 | // Check if we should hide Port Number 156 | if ((HideJoinData & 2) == 2) { 157 | PublicServerJoinData.ClearPort(); 158 | LCDirectLan.Log(BepInEx.Logging.LogLevel.Info, "Server Port value from config is cleared !"); 159 | } 160 | 161 | // Check if we should hide Hostname 162 | if ((HideJoinData & 4) == 4 && ResolveDNS.IsOnHostnameFormat(PublicServerJoinData.Address)) { 163 | PublicServerJoinData.Address = ""; 164 | LCDirectLan.Log(BepInEx.Logging.LogLevel.Info, "Server Hostname value from config is cleared !"); 165 | } 166 | } 167 | 168 | // Check if we should create a new button or reuse the original LAN Join button 169 | if (LCDirectLan.GetConfig("Join", "CreateExtraButton")) 170 | { 171 | CreateDirectJoinButton(); 172 | } 173 | else { 174 | ReuseDirectJoinButton(); 175 | } 176 | 177 | // Check if we should create HostUsernameInputField 178 | if (LCDirectLan.GetConfig("Custom Username", "Enabled") && LCDirectLan.GetConfig("Custom Username", "CreateHostUsernameInput") && !LCDirectLan.GetConfig("Custom Username", "MergeDefaultUsername")) { 179 | CreateHostUsernameInputField(); 180 | } 181 | } 182 | 183 | /// 184 | /// Create a new button for direct LAN join and adjust the position of all other buttons 185 | /// 186 | private static void CreateDirectJoinButton() 187 | { 188 | LCDirectLan.Log(BepInEx.Logging.LogLevel.Debug, "Creating a new LAN Join button !"); 189 | 190 | if (!GameObjectManager.EnsureGameObjectExist("Canvas/MenuContainer/MainButtons", out GameObject _MainButtons)) { return; } 191 | if (!GameObjectManager.EnsureGameObjectExist("Canvas/MenuContainer/MainButtons/HostButton", out GameObject _HostButton)) { return; } 192 | 193 | // Re adjusts position of all menu buttons (using the old position for host button as the place of our new button) 194 | Vector3 HostButtonNewPos = new Vector3(_HostButton.transform.position.x, _HostButton.transform.position.y - 3.5F, _HostButton.transform.position.z); 195 | 196 | ReadjustAllMenuButtons(_MainButtons); 197 | 198 | // Create a duplicate of the HostButton and use the original position 199 | DirectJoinObj = GameObject.Instantiate(_HostButton, _MainButtons.transform); 200 | DirectJoinObj.gameObject.name = "DirectJoinButton"; 201 | DirectJoinObj.transform.SetPositionAndRotation(HostButtonNewPos, DirectJoinObj.transform.rotation); 202 | DirectJoinObj.SetActive(true); // make sure it is not hidden by default 203 | 204 | // Change the text 205 | TextMeshProUGUI b = (TextMeshProUGUI)DirectJoinObj.transform.GetChild(1).GetComponent("TextMeshProUGUI"); 206 | b.text = "> Direct LAN Join"; 207 | 208 | // Listen for onClick event 209 | Button DirectJoinButton = DirectJoinObj.GetComponent