├── 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 | 
4 | 
5 | 
6 | 
7 | 
8 | 
9 | 
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