Skip to content

Commit 6ee7e55

Browse files
Merge pull request #2 from lc-sigurd/sane-rewrite
Sane rewrite
2 parents 4ade64a + 107fb04 commit 6ee7e55

26 files changed

+730
-350
lines changed

.config/dotnet-tools.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,12 @@
77
"commands": [
88
"tcli"
99
]
10+
},
11+
"evaisa.netcodepatcher.cli": {
12+
"version": "4.2.0",
13+
"commands": [
14+
"netcode-patch"
15+
]
1016
}
1117
}
1218
}

CSync/CSync.csproj

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<Project Sdk="Microsoft.NET.Sdk">
22
<PropertyGroup>
3-
<TargetFrameworks>netstandard2.1;net472;net48</TargetFrameworks>
3+
<TargetFramework>netstandard2.1</TargetFramework>
44
<AssemblyName>com.sigurd.csync</AssemblyName>
55
<Description>Configuration file syncing library for BepInEx.</Description>
66
<RestoreAdditionalProjectSources>
@@ -47,6 +47,8 @@
4747
<ItemGroup>
4848
<PackageReference Include="BepInEx.Analyzers" Version="1.*" PrivateAssets="all"/>
4949
<PackageReference Include="BepInEx.PluginInfoProps" Version="2.*" PrivateAssets="all"/>
50+
<PackageReference Include="BepInEx.AssemblyPublicizer.MSBuild" Version="0.4.2" PrivateAssets="all"/>
51+
<PackageReference Include="PolySharp" Version="1.14.1" PrivateAssets="all"/>
5052
</ItemGroup>
5153

5254
<ItemGroup>
@@ -55,18 +57,18 @@
5557
</ItemGroup>
5658

5759
<ItemGroup Condition="'$(CI)' != 'true'">
58-
<Reference Include="Assembly-CSharp">
60+
<Reference Include="Assembly-CSharp" Publicize="true">
5961
<HintPath>$(LethalCompanyDir)Lethal Company_Data\Managed\Assembly-CSharp.dll</HintPath>
6062
</Reference>
6163
<Reference Include="Unity.Collections">
6264
<HintPath>$(LethalCompanyDir)Lethal Company_Data\Managed\Unity.Collections.dll</HintPath>
6365
</Reference>
64-
<Reference Include="Unity.Netcode.Runtime">
66+
<Reference Include="Unity.Netcode.Runtime" Publicize="true">
6567
<HintPath>$(LethalCompanyDir)Lethal Company_Data\Managed\Unity.Netcode.Runtime.dll</HintPath>
6668
</Reference>
6769
</ItemGroup>
6870

6971
<ItemGroup Condition="'$(CI)' == 'true' or $(UseGameLibs) == 'true'">
70-
<PackageReference Include="LethalCompany.GameLibs.Steam" Version="49.0.0-alpha.1" />
72+
<PackageReference Include="LethalCompany.GameLibs.Steam" Publicize="true" Version="49.0.0-alpha.1" />
7173
</ItemGroup>
7274
</Project>
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
using BepInEx.Configuration;
2+
using CSync.Lib;
3+
4+
namespace CSync.Extensions;
5+
6+
internal static class ConfigDefinitionExtensions
7+
{
8+
public static SyncedConfigDefinition ToSynced(this ConfigDefinition definition)
9+
{
10+
return new(definition.Section, definition.Key);
11+
}
12+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
using BepInEx.Configuration;
2+
using CSync.Lib;
3+
4+
namespace CSync.Extensions;
5+
6+
internal static class ConfigEntryExtensions
7+
{
8+
public static (string ConfigFileRelativePath, SyncedConfigDefinition Definition) ToSyncedEntryIdentifier(
9+
this ConfigEntryBase entry)
10+
{
11+
return (entry.ConfigFile.GetConfigFileRelativePath(), entry.Definition.ToSynced());
12+
}
13+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
using System.IO;
2+
using BepInEx;
3+
using BepInEx.Configuration;
4+
5+
namespace CSync.Extensions;
6+
7+
internal static class ConfigFileExtensions
8+
{
9+
public static string GetConfigFileRelativePath(this ConfigFile configFile)
10+
{
11+
return Path.GetRelativePath(Paths.BepInExRootPath, configFile.ConfigFilePath);
12+
}
13+
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
using BepInEx.Configuration;
2+
using CSync.Lib;
3+
4+
namespace CSync.Extensions;
5+
6+
/// <summary>
7+
/// Contains helpful extension methods to aid with synchronization and reduce code duplication.
8+
/// </summary>
9+
public static class SyncedBindingExtensions {
10+
/// <summary>
11+
/// Binds an entry to this file and returns the converted synced entry.
12+
/// </summary>
13+
/// <param name="configFile">The currently selected config file.</param>
14+
/// <param name="section">The category that this entry should show under.</param>
15+
/// <param name="key">The name/identifier of this entry.</param>
16+
/// <param name="defaultVal">The value assigned to this entry if not changed.</param>
17+
/// <param name="description">The description indicating what this entry does.</param>
18+
public static SyncedEntry<T> BindSyncedEntry<T>(
19+
this ConfigFile configFile,
20+
string section,
21+
string key,
22+
T defaultVal,
23+
string description
24+
) {
25+
return configFile.BindSyncedEntry(new ConfigDefinition(section, key), defaultVal, new ConfigDescription(description));
26+
}
27+
28+
public static SyncedEntry<T> BindSyncedEntry<T>(
29+
this ConfigFile configFile,
30+
string section,
31+
string key,
32+
T defaultValue,
33+
ConfigDescription? desc = null
34+
) {
35+
return configFile.BindSyncedEntry(new ConfigDefinition(section, key), defaultValue, desc);
36+
}
37+
38+
public static SyncedEntry<T> BindSyncedEntry<T>(
39+
this ConfigFile configFile,
40+
ConfigDefinition definition,
41+
T defaultValue,
42+
string description
43+
) {
44+
return configFile.BindSyncedEntry(definition, defaultValue, new ConfigDescription(description));
45+
}
46+
47+
public static SyncedEntry<T> BindSyncedEntry<T>(
48+
this ConfigFile configFile,
49+
ConfigDefinition definition,
50+
T defaultValue,
51+
ConfigDescription? description = null
52+
) {
53+
ConfigManager.AddToFileCache(configFile);
54+
return configFile.Bind(definition, defaultValue, description).ToSyncedEntry();
55+
}
56+
57+
/// <summary>
58+
/// Converts this entry into a serializable alternative, allowing it to be synced.
59+
/// </summary>
60+
public static SyncedEntry<T> ToSyncedEntry<T>(this ConfigEntry<T> entry) {
61+
return new SyncedEntry<T>(entry);
62+
}
63+
}

CSync/Lib/ConfigManager.cs

Lines changed: 78 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,56 +1,106 @@
1+
using System;
12
using BepInEx.Configuration;
23
using BepInEx;
34
using System.Collections.Generic;
5+
using System.Diagnostics.CodeAnalysis;
46
using System.IO;
5-
using HarmonyLib;
7+
using System.Security.Cryptography;
8+
using System.Text;
9+
using CSync.Extensions;
10+
using JetBrains.Annotations;
11+
using Unity.Netcode;
12+
using UnityEngine;
613

714
namespace CSync.Lib;
815

916
/// <summary>
1017
/// Helper class enabling the user to easily setup CSync.<br></br>
1118
/// Handles config registration, instance syncing and caching of BepInEx files.<br></br>
1219
/// </summary>
20+
[PublicAPI]
1321
public class ConfigManager {
14-
internal static Dictionary<string, ConfigFile> FileCache = [];
15-
internal static Dictionary<string, ISynchronizable> Instances = [];
22+
internal static readonly Dictionary<string, ConfigFile> FileCache = [];
23+
internal static readonly Dictionary<InstanceKey, ISyncedConfig> Instances = [];
1624

17-
internal static ConfigFile GetConfigFile(string fileName) {
18-
bool exists = FileCache.TryGetValue(fileName, out ConfigFile cfg);
19-
if (!exists) {
20-
string absPath = Path.Combine(Paths.ConfigPath, fileName);
25+
private static event Action? OnPopulateEntriesRequested;
26+
internal static void PopulateEntries() => OnPopulateEntriesRequested?.Invoke();
2127

22-
cfg = new(absPath, false);
23-
FileCache.Add(fileName, cfg);
24-
}
28+
private static readonly Lazy<GameObject> LazyPrefab;
29+
internal static GameObject Prefab => LazyPrefab.Value;
30+
31+
static ConfigManager()
32+
{
33+
LazyPrefab = new Lazy<GameObject>(() =>
34+
{
35+
var container = new GameObject("CSyncPrefabContainer")
36+
{
37+
hideFlags = HideFlags.HideAndDontSave
38+
};
39+
container.SetActive(false);
40+
UnityEngine.Object.DontDestroyOnLoad(container);
41+
42+
var prefab = new GameObject("ConfigSyncHolder");
43+
prefab.transform.SetParent(container.transform);
44+
var networkObject = prefab.AddComponent<NetworkObject>();
45+
var hash = MD5.Create().ComputeHash(Encoding.UTF8.GetBytes($"{MyPluginInfo.PLUGIN_GUID}:ConfigSyncHolder"));
46+
networkObject.GlobalObjectIdHash = BitConverter.ToUInt32(hash);
47+
48+
return prefab;
49+
});
50+
}
51+
52+
internal static void AddToFileCache(ConfigFile configFile)
53+
{
54+
FileCache.TryAdd(configFile.GetConfigFileRelativePath(), configFile);
55+
}
56+
57+
internal static ConfigFile GetConfigFile(string relativePath)
58+
{
59+
if (FileCache.TryGetValue(relativePath, out ConfigFile configFile))
60+
return configFile;
2561

26-
return cfg;
62+
string absolutePath = Path.GetFullPath(Path.Combine(Paths.BepInExRootPath, relativePath));
63+
configFile = new(absolutePath, false);
64+
FileCache.Add(relativePath, configFile);
65+
return configFile;
2766
}
2867

2968
/// <summary>
3069
/// Register a config with CSync, making it responsible for synchronization.<br></br>
3170
/// After calling this method, all clients will receive the host's config upon joining.
3271
/// </summary>
33-
public static void Register<T>(T config) where T : SyncedConfig<T>, ISynchronizable {
34-
string guid = config.GUID;
35-
36-
if (config == null) {
37-
Plugin.Logger.LogError($"An error occurred registering config: {guid}\nConfig instance cannot be null!");
72+
public static void Register<T>(T config) where T : SyncedConfig<T> {
73+
if (config is null)
74+
{
75+
throw new ArgumentNullException(nameof(config), "Config instance is null, cannot register.");
3876
}
3977

40-
if (Instances.ContainsKey(guid)) {
41-
Plugin.Logger.LogWarning($"Attempted to register config `{guid}` after it has already been registered!");
42-
return;
78+
var assemblyQualifiedTypeName = typeof(T).AssemblyQualifiedName ?? throw new ArgumentException(nameof(config));
79+
var key = new InstanceKey(config.GUID, assemblyQualifiedTypeName);
80+
81+
try {
82+
Instances.Add(key, config);
4383
}
84+
catch (ArgumentException exc) {
85+
throw new InvalidOperationException($"Attempted to register config instance of type `{typeof(T)}`, but an instance has already been registered.", exc);
86+
}
87+
88+
SyncedInstance<T>.Instance = config;
89+
OnPopulateEntriesRequested += config.PopulateEntryContainer;
4490

45-
config.InitInstance(config);
46-
Instances.Add(guid, config);
91+
var syncBehaviour = Prefab.AddComponent<ConfigSyncBehaviour>();
92+
syncBehaviour.ConfigInstanceKey = key;
4793
}
4894

49-
internal static void SyncInstances() => Instances.Values.Do(i => i.SetupSync());
50-
internal static void RevertSyncedInstances() => Instances.Values.Do(i => i.RevertSync());
51-
}
95+
[UsedImplicitly]
96+
[Serializable]
97+
[SuppressMessage("ReSharper", "Unity.RedundantSerializeFieldAttribute")] // they are *not* redundant!
98+
public readonly record struct InstanceKey(string Guid, string AssemblyQualifiedName)
99+
{
100+
[field: SerializeField]
101+
public string Guid { get; }
52102

53-
public interface ISynchronizable {
54-
void SetupSync();
55-
void RevertSync();
56-
}
103+
[field: SerializeField]
104+
public string AssemblyQualifiedName { get; }
105+
}
106+
}

0 commit comments

Comments
 (0)