Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
70 commits
Select commit Hold shift + click to select a range
81b6bce
clear trailing whitespace; add `UseGameLibs` property
Lordfirespeed Mar 27, 2024
166ee1c
add synced entry delta message class
Lordfirespeed Mar 27, 2024
9b01c1d
wireframe behaviour class
Lordfirespeed Mar 27, 2024
ce63a2f
drop net framework
Lordfirespeed Mar 27, 2024
a7a868e
fix the bind overloads and remove the shit one
Lordfirespeed Mar 27, 2024
7ef0044
add config entry extensions
Lordfirespeed Mar 27, 2024
8123482
rename `Extensions`
Lordfirespeed Mar 27, 2024
21f8ae9
don't `Init` (for now)
Lordfirespeed Mar 27, 2024
552b605
fix the synced entry
Lordfirespeed Mar 27, 2024
067abe5
take out all the cruft
Lordfirespeed Mar 27, 2024
3208635
remove redundant qualifier
Lordfirespeed Mar 27, 2024
88971b5
add netcodepatcher cli to tool manifest
Lordfirespeed Mar 28, 2024
723a8e6
add netcode patch target
Lordfirespeed Mar 28, 2024
2b61f0a
ensure netcode patch target completes before thunderstore pack
Lordfirespeed Mar 28, 2024
f896d4f
publicize netcode so I can set `globalObjectIdHash`
Lordfirespeed Mar 28, 2024
f171cef
remove redundant patches
Lordfirespeed Mar 28, 2024
3f791eb
sort ordering of modifiers and fake not-null
Lordfirespeed Mar 28, 2024
b464c5e
make harmony internal + fake notnull
Lordfirespeed Mar 28, 2024
377c2f7
use `RelativePath` instead of `Name`
Lordfirespeed Mar 28, 2024
a31819d
add synced entry container
Lordfirespeed Mar 28, 2024
08b1612
add synced entry base class
Lordfirespeed Mar 28, 2024
29726b2
inherit from the baseclass + do some wacky stuff
Lordfirespeed Mar 28, 2024
8a4650f
make struct public
Lordfirespeed Mar 28, 2024
5ec7cfc
add `ISyncedConfig`
Lordfirespeed Mar 28, 2024
74bb634
add `ISyncedEntryContainer`
Lordfirespeed Mar 28, 2024
fc822cb
make a prefab, do nice stuff with the config file cache
Lordfirespeed Mar 28, 2024
28eaa51
complete implementation of sync behaviour
Lordfirespeed Mar 28, 2024
5316ecb
add / tidy up extension methods
Lordfirespeed Mar 28, 2024
4ce8b54
ensure config files are cached when necessary
Lordfirespeed Mar 28, 2024
acceaca
synced config :)
Lordfirespeed Mar 28, 2024
8b11dd6
add patch for network prefab registration
Lordfirespeed Mar 28, 2024
6d82a43
add patch for network prefab instantiation
Lordfirespeed Mar 28, 2024
9ba2851
bring back the awful inheritance hierarchy
Lordfirespeed Mar 28, 2024
926dab4
update netcode patcher to 4.1.1
Lordfirespeed Mar 28, 2024
96aa7c1
serialize the guid and look it up when needed
Lordfirespeed Mar 28, 2024
e00aadb
use `success` from `TryGetValue`
Lordfirespeed Mar 28, 2024
5f43776
mark byte serializer as obsolete
Lordfirespeed Mar 29, 2024
3298dee
bring back legacy 'extensions' class
Lordfirespeed Mar 29, 2024
06d0f41
make extensions public
Lordfirespeed Mar 29, 2024
fb5c363
move extensions to `CSync.Extensions` namespace
Lordfirespeed Mar 29, 2024
f939d06
switch out `<V>` for `<T>`
Lordfirespeed Mar 29, 2024
3598374
mark all legacy extensions as obsolete
Lordfirespeed Mar 29, 2024
ebdfaa2
standardise trailing whitespace
Lordfirespeed Mar 29, 2024
fa49702
remove unused `using` directives
Lordfirespeed Mar 29, 2024
55cc153
update netcode patcher CLI
Lordfirespeed Mar 30, 2024
07d737f
publicize LC
Lordfirespeed Mar 30, 2024
40dad42
add IdentityEqualityComparer.cs
Lordfirespeed Mar 30, 2024
4c1a2c3
remove `IdentityEqualityComparer.cs`
Lordfirespeed Mar 30, 2024
32454d4
register config instances using a specialized (serializable!) key
Lordfirespeed Mar 31, 2024
0bb318a
add event to populate config entries
Lordfirespeed Mar 31, 2024
fb1bae6
replace `ConfigGuid` with `ConfigInstanceKey`
Lordfirespeed Mar 31, 2024
1213b24
populate entries when a session starts
Lordfirespeed Mar 31, 2024
56849fe
remove subclass ctor patch
Lordfirespeed Mar 31, 2024
cb57e40
add `PublicAPI` attribute
Lordfirespeed Mar 31, 2024
197b24c
make `PopulateEntryContainer` `internal`
Lordfirespeed Mar 31, 2024
0465a5c
add `.Instance` back to `SyncedInstance`
Lordfirespeed Mar 31, 2024
d0e6ca8
add `PublicAPI` annotation
Lordfirespeed Mar 31, 2024
5abc461
make `InstanceKey` public
Lordfirespeed Mar 31, 2024
5b1eae2
ensure `InstanceKey` is serialized correctly
Lordfirespeed Mar 31, 2024
091c4d8
Update ConfigManager.cs
Lordfirespeed Apr 1, 2024
e54861c
Update SyncedInstance.cs
Lordfirespeed Apr 1, 2024
2f923d4
Populate entries on all clients
Lordfirespeed Apr 1, 2024
64286b2
Initialise `_syncEnabled` in `OnNetworkSpawn`
Lordfirespeed Apr 1, 2024
a83cc0a
Actually get relative path, don't use filenme
Lordfirespeed Apr 1, 2024
3e2f139
Add `Identifier` member
Lordfirespeed Apr 1, 2024
df598a7
Use `delta.SyncedEntryIdentifier`
Lordfirespeed Apr 1, 2024
0bf38f3
Fix copy-paste artefact
Lordfirespeed Apr 1, 2024
be8d0ad
Only populate all entries on host
Lordfirespeed Apr 1, 2024
579fc22
Don't populate entries at start of round
Lordfirespeed Apr 1, 2024
107fb04
Populate entries at GameNetworkManager Start
Lordfirespeed Apr 1, 2024
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
6 changes: 6 additions & 0 deletions .config/dotnet-tools.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@
"commands": [
"tcli"
]
},
"evaisa.netcodepatcher.cli": {
"version": "4.2.0",
"commands": [
"netcode-patch"
]
}
}
}
16 changes: 9 additions & 7 deletions CSync/CSync.csproj
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>netstandard2.1;net472;net48</TargetFrameworks>
<TargetFramework>netstandard2.1</TargetFramework>
<AssemblyName>com.sigurd.csync</AssemblyName>
<Description>Configuration file syncing library for BepInEx.</Description>
<RestoreAdditionalProjectSources>
Expand Down Expand Up @@ -43,10 +43,12 @@
<ItemGroup>
<ThunderstoreBuildCopyPath Include="$(TargetPath)" Destination="BepInEx/plugins/$(ProjectName)/"/>
</ItemGroup>

<ItemGroup>
<PackageReference Include="BepInEx.Analyzers" Version="1.*" PrivateAssets="all"/>
<PackageReference Include="BepInEx.PluginInfoProps" Version="2.*" PrivateAssets="all"/>
<PackageReference Include="BepInEx.AssemblyPublicizer.MSBuild" Version="0.4.2" PrivateAssets="all"/>
<PackageReference Include="PolySharp" Version="1.14.1" PrivateAssets="all"/>
</ItemGroup>

<ItemGroup>
Expand All @@ -55,18 +57,18 @@
</ItemGroup>

<ItemGroup Condition="'$(CI)' != 'true'">
<Reference Include="Assembly-CSharp">
<Reference Include="Assembly-CSharp" Publicize="true">
<HintPath>$(LethalCompanyDir)Lethal Company_Data\Managed\Assembly-CSharp.dll</HintPath>
</Reference>
<Reference Include="Unity.Collections">
<HintPath>$(LethalCompanyDir)Lethal Company_Data\Managed\Unity.Collections.dll</HintPath>
</Reference>
<Reference Include="Unity.Netcode.Runtime">
<Reference Include="Unity.Netcode.Runtime" Publicize="true">
<HintPath>$(LethalCompanyDir)Lethal Company_Data\Managed\Unity.Netcode.Runtime.dll</HintPath>
</Reference>
</ItemGroup>
<ItemGroup Condition="'$(CI)' == 'true'">
<PackageReference Include="LethalCompany.GameLibs.Steam" Version="49.0.0-alpha.1" />

<ItemGroup Condition="'$(CI)' == 'true' or $(UseGameLibs) == 'true'">
<PackageReference Include="LethalCompany.GameLibs.Steam" Publicize="true" Version="49.0.0-alpha.1" />
</ItemGroup>
</Project>
12 changes: 12 additions & 0 deletions CSync/Extensions/ConfigDefinitionExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
using BepInEx.Configuration;
using CSync.Lib;

namespace CSync.Extensions;

internal static class ConfigDefinitionExtensions
{
public static SyncedConfigDefinition ToSynced(this ConfigDefinition definition)
{
return new(definition.Section, definition.Key);
}
}
13 changes: 13 additions & 0 deletions CSync/Extensions/ConfigEntryExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using BepInEx.Configuration;
using CSync.Lib;

namespace CSync.Extensions;

internal static class ConfigEntryExtensions
{
public static (string ConfigFileRelativePath, SyncedConfigDefinition Definition) ToSyncedEntryIdentifier(
this ConfigEntryBase entry)
{
return (entry.ConfigFile.GetConfigFileRelativePath(), entry.Definition.ToSynced());
}
}
13 changes: 13 additions & 0 deletions CSync/Extensions/ConfigFileExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using System.IO;
using BepInEx;
using BepInEx.Configuration;

namespace CSync.Extensions;

internal static class ConfigFileExtensions
{
public static string GetConfigFileRelativePath(this ConfigFile configFile)
{
return Path.GetRelativePath(Paths.BepInExRootPath, configFile.ConfigFilePath);
}
}
63 changes: 63 additions & 0 deletions CSync/Extensions/SyncedBindingExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
using BepInEx.Configuration;
using CSync.Lib;

namespace CSync.Extensions;

/// <summary>
/// Contains helpful extension methods to aid with synchronization and reduce code duplication.
/// </summary>
public static class SyncedBindingExtensions {
/// <summary>
/// Binds an entry to this file and returns the converted synced entry.
/// </summary>
/// <param name="configFile">The currently selected config file.</param>
/// <param name="section">The category that this entry should show under.</param>
/// <param name="key">The name/identifier of this entry.</param>
/// <param name="defaultVal">The value assigned to this entry if not changed.</param>
/// <param name="description">The description indicating what this entry does.</param>
public static SyncedEntry<T> BindSyncedEntry<T>(
this ConfigFile configFile,
string section,
string key,
T defaultVal,
string description
) {
return configFile.BindSyncedEntry(new ConfigDefinition(section, key), defaultVal, new ConfigDescription(description));
}

public static SyncedEntry<T> BindSyncedEntry<T>(
this ConfigFile configFile,
string section,
string key,
T defaultValue,
ConfigDescription? desc = null
) {
return configFile.BindSyncedEntry(new ConfigDefinition(section, key), defaultValue, desc);
}

public static SyncedEntry<T> BindSyncedEntry<T>(
this ConfigFile configFile,
ConfigDefinition definition,
T defaultValue,
string description
) {
return configFile.BindSyncedEntry(definition, defaultValue, new ConfigDescription(description));
}

public static SyncedEntry<T> BindSyncedEntry<T>(
this ConfigFile configFile,
ConfigDefinition definition,
T defaultValue,
ConfigDescription? description = null
) {
ConfigManager.AddToFileCache(configFile);
return configFile.Bind(definition, defaultValue, description).ToSyncedEntry();
}

/// <summary>
/// Converts this entry into a serializable alternative, allowing it to be synced.
/// </summary>
public static SyncedEntry<T> ToSyncedEntry<T>(this ConfigEntry<T> entry) {
return new SyncedEntry<T>(entry);
}
}
106 changes: 78 additions & 28 deletions CSync/Lib/ConfigManager.cs
Original file line number Diff line number Diff line change
@@ -1,56 +1,106 @@
using System;
using BepInEx.Configuration;
using BepInEx;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using HarmonyLib;
using System.Security.Cryptography;
using System.Text;
using CSync.Extensions;
using JetBrains.Annotations;
using Unity.Netcode;
using UnityEngine;

namespace CSync.Lib;

/// <summary>
/// Helper class enabling the user to easily setup CSync.<br></br>
/// Handles config registration, instance syncing and caching of BepInEx files.<br></br>
/// </summary>
[PublicAPI]
public class ConfigManager {
internal static Dictionary<string, ConfigFile> FileCache = [];
internal static Dictionary<string, ISynchronizable> Instances = [];
internal static readonly Dictionary<string, ConfigFile> FileCache = [];
internal static readonly Dictionary<InstanceKey, ISyncedConfig> Instances = [];

internal static ConfigFile GetConfigFile(string fileName) {
bool exists = FileCache.TryGetValue(fileName, out ConfigFile cfg);
if (!exists) {
string absPath = Path.Combine(Paths.ConfigPath, fileName);
private static event Action? OnPopulateEntriesRequested;
internal static void PopulateEntries() => OnPopulateEntriesRequested?.Invoke();

cfg = new(absPath, false);
FileCache.Add(fileName, cfg);
}
private static readonly Lazy<GameObject> LazyPrefab;
internal static GameObject Prefab => LazyPrefab.Value;

static ConfigManager()
{
LazyPrefab = new Lazy<GameObject>(() =>
{
var container = new GameObject("CSyncPrefabContainer")
{
hideFlags = HideFlags.HideAndDontSave
};
container.SetActive(false);
UnityEngine.Object.DontDestroyOnLoad(container);

var prefab = new GameObject("ConfigSyncHolder");
prefab.transform.SetParent(container.transform);
var networkObject = prefab.AddComponent<NetworkObject>();
var hash = MD5.Create().ComputeHash(Encoding.UTF8.GetBytes($"{MyPluginInfo.PLUGIN_GUID}:ConfigSyncHolder"));
networkObject.GlobalObjectIdHash = BitConverter.ToUInt32(hash);

return prefab;
});
}

internal static void AddToFileCache(ConfigFile configFile)
{
FileCache.TryAdd(configFile.GetConfigFileRelativePath(), configFile);
}

internal static ConfigFile GetConfigFile(string relativePath)
{
if (FileCache.TryGetValue(relativePath, out ConfigFile configFile))
return configFile;

return cfg;
string absolutePath = Path.GetFullPath(Path.Combine(Paths.BepInExRootPath, relativePath));
configFile = new(absolutePath, false);
FileCache.Add(relativePath, configFile);
return configFile;
}

/// <summary>
/// Register a config with CSync, making it responsible for synchronization.<br></br>
/// After calling this method, all clients will receive the host's config upon joining.
/// </summary>
public static void Register<T>(T config) where T : SyncedConfig<T>, ISynchronizable {
string guid = config.GUID;

if (config == null) {
Plugin.Logger.LogError($"An error occurred registering config: {guid}\nConfig instance cannot be null!");
public static void Register<T>(T config) where T : SyncedConfig<T> {
if (config is null)
{
throw new ArgumentNullException(nameof(config), "Config instance is null, cannot register.");
}

if (Instances.ContainsKey(guid)) {
Plugin.Logger.LogWarning($"Attempted to register config `{guid}` after it has already been registered!");
return;
var assemblyQualifiedTypeName = typeof(T).AssemblyQualifiedName ?? throw new ArgumentException(nameof(config));
var key = new InstanceKey(config.GUID, assemblyQualifiedTypeName);

try {
Instances.Add(key, config);
}
catch (ArgumentException exc) {
throw new InvalidOperationException($"Attempted to register config instance of type `{typeof(T)}`, but an instance has already been registered.", exc);
}

SyncedInstance<T>.Instance = config;
OnPopulateEntriesRequested += config.PopulateEntryContainer;

config.InitInstance(config);
Instances.Add(guid, config);
var syncBehaviour = Prefab.AddComponent<ConfigSyncBehaviour>();
syncBehaviour.ConfigInstanceKey = key;
}

internal static void SyncInstances() => Instances.Values.Do(i => i.SetupSync());
internal static void RevertSyncedInstances() => Instances.Values.Do(i => i.RevertSync());
}
[UsedImplicitly]
[Serializable]
[SuppressMessage("ReSharper", "Unity.RedundantSerializeFieldAttribute")] // they are *not* redundant!
public readonly record struct InstanceKey(string Guid, string AssemblyQualifiedName)
{
[field: SerializeField]
public string Guid { get; }

public interface ISynchronizable {
void SetupSync();
void RevertSync();
}
[field: SerializeField]
public string AssemblyQualifiedName { get; }
}
}
Loading