Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Custom Configs & Config Syncing #54

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
fd2c459
Update config.mts
Boxofbiscuits97 Dec 16, 2023
b7c3788
Create custom-configs.md
Boxofbiscuits97 Dec 16, 2023
0d10fef
Update config.mts
Boxofbiscuits97 Dec 16, 2023
d3a09ce
Created headings
Boxofbiscuits97 Dec 16, 2023
57e88de
feat: write custom configs page in it's near entirety
Boxofbiscuits97 Dec 16, 2023
ab816fa
fix: change configs page link at the bottom
Boxofbiscuits97 Dec 16, 2023
b2b6d1d
Update config.mts for config syncing page section
Boxofbiscuits97 Dec 16, 2023
db9c199
fix: spelling mistake
Boxofbiscuits97 Dec 16, 2023
ad2df95
fix: actually fixed link spelling mistakes
Boxofbiscuits97 Dec 16, 2023
0d960a3
fix: change custom configs to class example
Boxofbiscuits97 Dec 17, 2023
fd6b66a
feat: custom config syncing docs
Boxofbiscuits97 Dec 17, 2023
6a9648a
feat: prepared for r2docs rework
Boxofbiscuits97 Dec 17, 2023
669019d
fix: more r2docs preparation
Boxofbiscuits97 Dec 17, 2023
b4fa60f
fix: removed for clarity
Boxofbiscuits97 Dec 18, 2023
8281bc6
fix: removed closed source auto syncing mod section
Boxofbiscuits97 Dec 19, 2023
f7ecfeb
fix: resolved mismatched name conflicts
Boxofbiscuits97 Dec 19, 2023
184bb82
fix: resolved mismatch name conflicts in index file
Boxofbiscuits97 Dec 19, 2023
13d4ae3
fix: moved items to advanced modding category
Boxofbiscuits97 Dec 23, 2023
9d8d5e2
Merge branch 'Boxofbiscuits97-custom-configs' of https://github.com/B…
Boxofbiscuits97 Dec 23, 2023
0e1940a
fix: changed links to not be hardcoded
Boxofbiscuits97 Dec 23, 2023
48e939f
fix: updated links and changed warning continuity
Boxofbiscuits97 Dec 23, 2023
7071866
fix: proper example code and clarity
Boxofbiscuits97 Dec 23, 2023
7172474
fix: clarity and formatting
Boxofbiscuits97 Dec 23, 2023
a757b62
feat: updated examples to match newest version of the gist
Boxofbiscuits97 Dec 23, 2023
5bc6eef
fix: completely added all of the new gist changes
Boxofbiscuits97 Dec 23, 2023
4e61333
fix: changed example to properly reference the class
Boxofbiscuits97 Dec 23, 2023
b7eda5a
fix: example formatting
Boxofbiscuits97 Dec 23, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -131,4 +131,5 @@ docs/.vitepress/cache
.pnp.*

_site/
.jekyll-cache/
.jekyll-cache/
.vscode/launch.json
2 changes: 2 additions & 0 deletions docs/.vitepress/config.mts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@ export default defineConfig({
{
text: 'Advanced Modding Topics',
items: [
{ text: 'Custom Configs', link: '/advanced-modding/custom-configs' },
{ text: 'Custom Config Syncing', link: '/advanced-modding/custom-config-syncing' },
{ text: 'Custom Networking', link: '/advanced-modding/networking' }
]
},
Expand Down
286 changes: 286 additions & 0 deletions docs/advanced-modding/custom-config-syncing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,286 @@
---
prev: true
next: false
description: An intermediate overview of how to sync custom configs for your Lethal Company mods.
---

# Custom Config Syncing

::: warning
**This is an advanced article. While this introduces some C# concepts, it is highly recommended to understand C# and the basics of modding this game <i>before</i> reading this article.**
:::

## Preface
A very common case for many mod developers is wanting to synchronize the host's configuration file with all other players.

There are many different ways you could achieve this, but we will only go through the most straightforward approach that should work for most cases.

## Manually Syncing Instances
For this approach, we will take advantage of [Custom Messages](https://docs-multiplayer.unity3d.com/netcode/current/advanced-topics/message-system/custom-messages/#named-messages), specifically Named Messages.
They are unbound to any GameObject, meaning **NetcodeWeaver** and **NetworkBehaviour** are **NOT necessary**.

::: info
Before you proceed, it is recommended you familiarize yourself with [Custom configs](/advanced-modding/custom-configs).
:::

### Prerequisites
Add an **Assembly Reference** to the following files:<br>
`Unity.Netcode.Runtime.dll`<br>
`Unity.Collections.dll`

These can be found at `.../Lethal Company/Lethal Company_Data/Managed`.

Now create a `SyncedInstance.cs` file which your config will inherit from, this handles the serialization/de-serialization of data.<br>
It also provides some helper methods to prevent repeating ourselves.

```cs
[Serializable]
public class SyncedInstance<T> {
internal static CustomMessagingManager MessageManager => NetworkManager.Singleton.CustomMessagingManager;
internal static bool IsClient => NetworkManager.Singleton.IsClient;
internal static bool IsHost => NetworkManager.Singleton.IsHost;

[NonSerialized]
protected static int IntSize = 4;

public static T Default { get; private set; }
public static T Instance { get; private set; }

public static bool Synced { get; internal set; }

protected void InitInstance(T instance) {
Default = instance;
Instance = instance;

// Makes sure the size of an integer is correct for the current system.
// We use 4 by default as that's the size of an int on 32 and 64 bit systems.
IntSize = sizeof(int);
}

internal static void SyncInstance(byte[] data) {
Instance = DeserializeFromBytes(data);
Synced = true;
}

internal static void RevertSync() {
Instance = Default;
Synced = false;
}

public static byte[] SerializeToBytes(T val) {
BinaryFormatter bf = new();
using MemoryStream stream = new();

try {
bf.Serialize(stream, val);
return stream.ToArray();
}
catch (Exception e) {
Plugin.Logger.LogError($"Error serializing instance: {e}");
return null;
}
}

public static T DeserializeFromBytes(byte[] data) {
BinaryFormatter bf = new();
using MemoryStream stream = new(data);

try {
return (T) bf.Deserialize(stream);
} catch (Exception e) {
Plugin.Logger.LogError($"Error deserializing instance: {e}");
return default;
}
}
}
```

### 1. Inherit SyncedInstance

We will now make use of the config class file made prior by changing this line:

```cs
public class Config
```

Into a synced alternative that can be serialized.

```cs
[Serializable]
public class Config : SyncedInstance<Config>
```

In addition, we need to make sure 'Instance' is a reference to this class by adding another line in the **constructor**.
```cs
public Config(ConfigFile cfg) {
InitInstance(this); // Add this line

// ...
}
```

### 2. Setup request/receiver methods
Now simply paste the three following methods within the class.
While these might look intimidating, they will hopefully start to make more sense in step 3.

```cs
public static void RequestSync() {
if (!IsClient) return;

using FastBufferWriter stream = new(IntSize, Allocator.Temp);
MessageManager.SendNamedMessage("ModName_OnRequestConfigSync", 0uL, stream);
}
```

```cs
public static void OnRequestSync(ulong clientId, FastBufferReader _) {
if (!IsHost) return;

Plugin.Logger.LogInfo($"Config sync request received from client: {clientId}");

byte[] array = SerializeToBytes(Instance);
int value = array.Length;

using FastBufferWriter stream = new(value + IntSize, Allocator.Temp);

try {
stream.WriteValueSafe(in value, default);
stream.WriteBytesSafe(array);

MessageManager.SendNamedMessage("ModName_OnReceiveConfigSync", clientId, stream);
} catch(Exception e) {
Plugin.Logger.LogInfo($"Error occurred syncing config with client: {clientId}\n{e}");
}
}
```

```cs
public static void OnReceiveSync(ulong _, FastBufferReader reader) {
if (!reader.TryBeginRead(IntSize)) {
Plugin.Logger.LogError("Config sync error: Could not begin reading buffer.");
return;
}

reader.ReadValueSafe(out int val, default);
if (!reader.TryBeginRead(val)) {
Plugin.Logger.LogError("Config sync error: Host could not sync.");
return;
}

byte[] data = new byte[val];
reader.ReadBytesSafe(ref data, val);

SyncInstance(data);

Plugin.Logger.LogInfo("Successfully synced config with host.");
}
```

### 3. Apply patch to PlayerControllerB
Add in the following method, replacing "ModName" with the name (or abbreviation) of your mod.

Keep in mind that `ConnectClientToPlayerObject` is run just before the player is spawned.
This means if you are patching `SpawnPlayerAnimation`, you might find it gets called before the config has finished syncing!

```cs
[HarmonyPostfix]
[HarmonyPatch(typeof(PlayerControllerB), "ConnectClientToPlayerObject")]
public static void InitializeLocalPlayer() {
if (IsHost) {
MessageManager.RegisterNamedMessageHandler("ModName_OnRequestConfigSync", OnRequestSync);
Synced = true;

return;
}

Synced = false;
MessageManager.RegisterNamedMessageHandler("ModName_OnReceiveConfigSync", OnReceiveSync);
RequestSync();
}
```

If you are having issues with this patch, you may want to try **GameNetworkManager** instead.
```cs
[HarmonyPatch(typeof(GameNetworkManager), "SteamMatchmaking_OnLobbyMemberJoined")]
```

Finally, we need to make sure the client reverts back to their own config upon leaving.

```cs
[HarmonyPostfix]
[HarmonyPatch(typeof(GameNetworkManager), "StartDisconnect")]
public static void PlayerLeave() {
Config.RevertSync();
}
```

## Synced Config Usage
Every client will now have their config synchronized to the hosts upon joining the game.
All that's left to do is use the synced variables where appropriate.

We can do this by referencing `Config.Instance` from any class.
Here's an example that sets the local player's movement speed.
```cs
public static void ExamplePatch(PlayerControllerB __instance) {
if (__instance == null)
return;

float syncedSpeed = Config.Instance.MOVEMENT_SPEED;
if (__instance.IsOwner && __instance.isPlayerControlled) {
__instance.movementSpeed = syncedSpeed;
Plugin.Logger.LogInfo("Movement speed synced with host config.");
}
}
```

To use client-side variables (not the synced instance), we can access `Config.Default`.
```cs
public static void ExamplePatch(PlayerControllerB __instance) {
if (!__instance) return;

// Sets current stamina, regardless of host config.
__instance.sprintMeter = Config.Default.STAMINA;
}
```

## Troubleshooting
> Syncing doesnt work when I patch manually.

If you're using `PatchAll()` with type parameters, make sure to patch the `Config` class like other files.<br>
Example:
```cs
harmony.PatchAll(typeof(StartMatchLeverPatch));
harmony.PatchAll(typeof(GameNetworkManagerPatch));
harmony.PatchAll(typeof(Config)); // Add this line
```

> I am not seeing any logs from the request/receiver methods?

Harmony may refuse to patch the `InitializeLocalPlayer` method inside `Config.cs` if you have already have a dedicated patch file for `PlayerControllerB`. You can try placing the method there instead.
```cs
[HarmonyPatch(typeof(PlayerControllerB))]
internal class PlayerControllerBPatch
{
[HarmonyPostfix]
[HarmonyPatch("ConnectClientToPlayerObject")]
public static void InitializeLocalPlayer() {
if (Config.IsHost) {
try {
Config.MessageManager.RegisterNamedMessageHandler("ModName_OnRequestConfigSync", Config.OnRequestSync);
Config.Synced = true;
}
catch (Exception e) {
Plugin.Logger.LogError(e);
}

return;
}

Config.Synced = false;
Config.MessageManager.RegisterNamedMessageHandler("ModName_OnReceiveConfigSync", Config.OnReceiveSync);
Config.RequestSync();
}
}
```

<br>If you incounter any other issues with custom configs, please ask on the [community discord](https://discord.gg/nYcQFEpXfU)!
89 changes: 89 additions & 0 deletions docs/advanced-modding/custom-configs.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
---
prev: false
next: true
description: An intermediate overview of how to implement custom configs for your Lethal Company mods.
---

# Custom Configs

::: warning
**This is an advanced article. While this introduces some C# concepts, it is highly recommended to understand C# and the basics of modding this game <i>before</i> reading this article.**
:::

::: info
This tutorial is taken and adapted from the [BepInEx Configuration Documentation](https://docs.bepinex.dev/articles/dev_guide/plugin_tutorial/4_configuration.html). For more resources refer to that.
:::

## Creating Config Entries
Create a config class and add entries for any variables that you want to be configurable.

```cs
public class Config
{
public static ConfigEntry<string> configGreeting;
public static ConfigEntry<bool> configDisplayGreeting;

// ...
}
```

Then we can start binding our config entries to the fields we just created inside of a class constructor.

```cs
public Config(ConfigFile cfg)
{
configGreeting = cfg.Bind(
"General", // Config section
"GreetingText", // Key of this config
"Hello, world!", // Default value
"Greeting text upon game launch" // Description
);

configDisplayGreeting = cfg.Bind(
"General.Toggles", // Config subsection
"DisplayGreeting", // Key of this config
true, // Default value
"To show the greeting text" // Description
);
}
```

We then need to run said constructor to bind said configs to proper values and properties for users.<br><br>
In your main class (usually `Plugin.cs`), implement the constructor with a parameter referencing the file that will be created by BepInEx.

```cs
public class MyExampleMod : BaseUnityPlugin
{
public static new Config MyConfig { get; internal set; }

// ...

private void Awake()
{
MyConfig = new(base.Config);
}
}
```


## Using Config Entries

You can now get the data from the config variables you have made using the `.Value` property.

```cs
private void MyExamplePatch()
{
private void MyExampleMethod()
{
// Instead of just Logger.LogInfo("Hello, world!")
if(Config.configDisplayGreeting.Value)
Logger.LogInfo(Config.configGreeting.Value);
}
}
```

::: danger STOP
Understand that your config file **Will Not Be Created** until your mod is loaded ingame **at least once**. See the [r2modman Configs Page](/installation/configuration) for using your configs.
:::

Now you have config files for your mods! If it's extremely important that your mod has a config value that's the same for every player, you may want to consider reading the page on [custom config syncing](/advanced-modding/custom-config-syncing).
Loading