-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathConfigSync.cs
More file actions
207 lines (178 loc) · 7.83 KB
/
ConfigSync.cs
File metadata and controls
207 lines (178 loc) · 7.83 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using BepInEx;
using BepInEx.Bootstrap;
using BepInEx.Configuration;
using BepInEx.Logging;
namespace ModSync
{
/// <summary>
/// Handles reading and in-memory application of BepInEx config files.
/// Nothing is ever written to disk on the client — BoxedValue is set directly
/// on each ConfigEntry so values revert to the player's own settings naturally
/// when they restart or re-bind their config.
/// </summary>
internal static class ConfigSync
{
private static readonly ManualLogSource Log = Plugin.Logger;
// -----------------------------------------------------------------
// Reading (host side)
// -----------------------------------------------------------------
/// <summary>
/// Reads all config files for installed plugins (except ModSync itself)
/// and returns them as a dictionary of { filename -> raw file content }.
/// </summary>
internal static Dictionary<string, string> GetLocalConfigs()
{
var configs = new Dictionary<string, string>();
var configDir = Paths.ConfigPath;
foreach (var kvp in Chainloader.PluginInfos)
{
if (kvp.Key == MyPluginInfo.PLUGIN_GUID)
continue;
var configPath = Path.Combine(configDir, $"{kvp.Key}.cfg");
if (File.Exists(configPath))
{
try
{
configs[Path.GetFileName(configPath)] = File.ReadAllText(configPath);
}
catch (Exception ex)
{
Log.LogWarning($"[ConfigSync] Could not read config for {kvp.Key}: {ex.Message}");
}
}
}
return configs;
}
// -----------------------------------------------------------------
// Serialization
// -----------------------------------------------------------------
/// <summary>
/// Serializes a config dictionary to a byte array for Photon transmission.
/// Format: [int32 count] [ (string key, string value) ... ]
/// </summary>
internal static byte[] Serialize(Dictionary<string, string> configs)
{
using var ms = new MemoryStream();
using var bw = new BinaryWriter(ms, Encoding.UTF8);
bw.Write(configs.Count);
foreach (var kvp in configs)
{
bw.Write(kvp.Key);
bw.Write(kvp.Value);
}
return ms.ToArray();
}
internal static Dictionary<string, string> Deserialize(byte[] data)
{
var configs = new Dictionary<string, string>();
using var ms = new MemoryStream(data);
using var br = new BinaryReader(ms, Encoding.UTF8);
int count = br.ReadInt32();
for (int i = 0; i < count; i++)
{
string key = br.ReadString();
string value = br.ReadString();
configs[key] = value;
}
return configs;
}
// -----------------------------------------------------------------
// Applying (client side — in-memory only, no disk writes)
// -----------------------------------------------------------------
/// <summary>
/// Parses each received config file and sets BoxedValue on matching
/// ConfigEntries in-memory. The client's files on disk are never touched.
/// Returns the number of individual settings applied.
/// </summary>
internal static int ApplyInMemory(Dictionary<string, string> receivedConfigs)
{
int applied = 0;
foreach (var kvp in receivedConfigs)
{
var guid = Path.GetFileNameWithoutExtension(kvp.Key);
if (!Chainloader.PluginInfos.TryGetValue(guid, out var pluginInfo))
{
Log.LogDebug($"[ConfigSync] Skipping config for uninstalled mod: {guid}");
continue;
}
var configFile = pluginInfo.Instance.Config;
var parsed = ParseIni(kvp.Value);
// Disable auto-save so BoxedValue setter doesn't trigger Save() on each change.
// We never call Save() ourselves — no disk writes occur.
var previousSaveOnSet = configFile.SaveOnConfigSet;
configFile.SaveOnConfigSet = false;
try
{
foreach (var definition in configFile.Keys)
{
if (!parsed.TryGetValue(definition.Section, out var section))
continue;
if (!section.TryGetValue(definition.Key, out var rawValue))
continue;
var entry = configFile[definition];
try
{
// TomlTypeConverter handles all BepInEx-supported types
// (bool, int, float, string, enums, KeyCode, Color, etc.)
entry.BoxedValue = TomlTypeConverter.ConvertToValue(rawValue, entry.SettingType);
applied++;
}
catch (Exception ex)
{
Log.LogWarning($"[ConfigSync] Could not apply {guid}/{definition.Section}/{definition.Key}: {ex.Message}");
}
}
}
finally
{
// Always restore the original flag, even if something throws
configFile.SaveOnConfigSet = previousSaveOnSet;
}
Log.LogInfo($"[ConfigSync] Applied in-memory config for: {guid}");
}
return applied;
}
// -----------------------------------------------------------------
// INI parser
// -----------------------------------------------------------------
/// <summary>
/// Parses a BepInEx .cfg file into { Section -> { Key -> Value } }.
/// Skips comment lines (## and #) and blank lines.
/// Values are trimmed; inline comments after the value are not stripped
/// because BepInEx doesn't use them.
/// </summary>
private static Dictionary<string, Dictionary<string, string>> ParseIni(string content)
{
var result = new Dictionary<string, Dictionary<string, string>>(StringComparer.OrdinalIgnoreCase);
Dictionary<string, string>? currentSection = null;
foreach (var rawLine in content.Split('\n'))
{
var line = rawLine.Trim();
if (string.IsNullOrEmpty(line) || line.StartsWith("#"))
continue;
if (line.StartsWith("[") && line.EndsWith("]"))
{
var sectionName = line.Substring(1, line.Length - 2).Trim();
if (!result.TryGetValue(sectionName, out currentSection))
{
currentSection = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
result[sectionName] = currentSection;
}
continue;
}
var eqIdx = line.IndexOf('=');
if (eqIdx > 0 && currentSection != null)
{
var key = line.Substring(0, eqIdx).Trim();
var value = line.Substring(eqIdx + 1).Trim();
currentSection[key] = value;
}
}
return result;
}
}
}