Skip to content

Commit 29e43d0

Browse files
committed
feat: block client-side-only mods in component manager
1 parent 65e6a5d commit 29e43d0

5 files changed

Lines changed: 144 additions & 9 deletions

File tree

MCServerLauncher.WPF/InstanceConsole/Modules/JarMetadataParser.cs

Lines changed: 111 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
namespace MCServerLauncher.WPF.InstanceConsole.Modules;
99

10-
public record JarMetadata(string DisplayName, string Version);
10+
public record JarMetadata(string DisplayName, string Version, bool IsClientSideOnly = false);
1111

1212
public static class JarMetadataParser
1313
{
@@ -63,7 +63,8 @@ public static class JarMetadataParser
6363
?? TryGetString(root, "id")
6464
?? string.Empty;
6565
string version = TryGetString(root, "version") ?? string.Empty;
66-
return new JarMetadata(name, version);
66+
bool isClientSideOnly = IsFabricClientSideOnly(root);
67+
return new JarMetadata(name, version, isClientSideOnly);
6768
}
6869
catch (Exception ex)
6970
{
@@ -96,7 +97,8 @@ public static class JarMetadataParser
9697
name = TryGetString(loader, "id") ?? string.Empty;
9798
}
9899
}
99-
return new JarMetadata(name, version);
100+
bool isClientSideOnly = IsFabricClientSideOnly(root);
101+
return new JarMetadata(name, version, isClientSideOnly);
100102
}
101103
catch (Exception ex)
102104
{
@@ -125,9 +127,10 @@ public static class JarMetadataParser
125127
?? ExtractTomlString(blockBody, "modId")
126128
?? string.Empty;
127129
string version = ExtractTomlString(blockBody, "version") ?? string.Empty;
130+
bool isClientSideOnly = IsForgeFamilyClientSideOnly(content);
128131

129132
// version may contain ${file.jarVersion} placeholder; leave as-is.
130-
return new JarMetadata(name, version);
133+
return new JarMetadata(name, version, isClientSideOnly);
131134
}
132135
catch (Exception ex)
133136
{
@@ -185,4 +188,108 @@ public static class JarMetadataParser
185188
}
186189
return value;
187190
}
191+
192+
public static bool IsClientSideMod(string filePath)
193+
{
194+
try
195+
{
196+
using var archive = ZipFile.OpenRead(filePath);
197+
198+
var fabricEntry = archive.GetEntry("fabric.mod.json") ?? archive.GetEntry("quilt.mod.json");
199+
if (fabricEntry != null)
200+
{
201+
using var stream = fabricEntry.Open();
202+
using var doc = JsonDocument.Parse(stream);
203+
if (IsFabricClientSideOnly(doc.RootElement))
204+
{
205+
return true;
206+
}
207+
}
208+
209+
var tomlEntry = archive.GetEntry("META-INF/mods.toml") ?? archive.GetEntry("META-INF/neoforge.mods.toml");
210+
if (tomlEntry != null)
211+
{
212+
using var stream = tomlEntry.Open();
213+
using var reader = new StreamReader(stream);
214+
string content = reader.ReadToEnd();
215+
if (IsForgeFamilyClientSideOnly(content))
216+
{
217+
return true;
218+
}
219+
}
220+
221+
return false;
222+
}
223+
catch
224+
{
225+
return false;
226+
}
227+
}
228+
229+
private static bool IsFabricClientSideOnly(JsonElement root)
230+
{
231+
if (root.ValueKind != JsonValueKind.Object)
232+
{
233+
return false;
234+
}
235+
236+
if (root.TryGetProperty("environment", out var envElement)
237+
&& envElement.ValueKind == JsonValueKind.String)
238+
{
239+
var env = envElement.GetString();
240+
if (string.Equals(env, "client", StringComparison.OrdinalIgnoreCase))
241+
{
242+
return true;
243+
}
244+
}
245+
246+
if (root.TryGetProperty("quilt_loader", out var loader)
247+
&& loader.ValueKind == JsonValueKind.Object
248+
&& loader.TryGetProperty("metadata", out var meta)
249+
&& meta.ValueKind == JsonValueKind.Object
250+
&& meta.TryGetProperty("environment", out var quiltEnv)
251+
&& quiltEnv.ValueKind == JsonValueKind.String)
252+
{
253+
var env = quiltEnv.GetString();
254+
if (string.Equals(env, "client", StringComparison.OrdinalIgnoreCase))
255+
{
256+
return true;
257+
}
258+
}
259+
260+
return false;
261+
}
262+
263+
private static bool IsForgeFamilyClientSideOnly(string content)
264+
{
265+
var blocks = Regex.Matches(content, @"(?ms)^\[\[.*?\]\](.*?)(?=^\[\[|\z)");
266+
267+
string? minecraftSide = null;
268+
string? firstFoundSide = null;
269+
270+
foreach (Match block in blocks)
271+
{
272+
string blockBody = block.Groups[1].Value;
273+
274+
var modIdMatch = Regex.Match(blockBody, "^\\s*modId\\s*=\\s*[\"'](.*?)[\"']", RegexOptions.Multiline | RegexOptions.IgnoreCase);
275+
var sideMatch = Regex.Match(blockBody, "^\\s*side\\s*=\\s*[\"'](.*?)[\"']", RegexOptions.Multiline | RegexOptions.IgnoreCase);
276+
277+
if (!sideMatch.Success)
278+
{
279+
continue;
280+
}
281+
282+
string currentSide = sideMatch.Groups[1].Value;
283+
firstFoundSide ??= currentSide;
284+
285+
if (modIdMatch.Success && string.Equals(modIdMatch.Groups[1].Value, "minecraft", StringComparison.OrdinalIgnoreCase))
286+
{
287+
minecraftSide = currentSide;
288+
break;
289+
}
290+
}
291+
292+
string? finalSide = minecraftSide ?? firstFoundSide;
293+
return finalSide != null && string.Equals(finalSide, "CLIENT", StringComparison.OrdinalIgnoreCase);
294+
}
188295
}

MCServerLauncher.WPF/InstanceConsole/View/Pages/ComponentManagerPage.xaml

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -74,8 +74,15 @@
7474

7575
<!-- Title + Description -->
7676
<StackPanel Grid.Column="1" VerticalAlignment="Center">
77-
<TextBlock Text="{Binding Title}" FontSize="14" FontWeight="SemiBold"
78-
TextTrimming="CharacterEllipsis" />
77+
<StackPanel Orientation="Horizontal">
78+
<TextBlock Text="{Binding Title}" FontSize="14" FontWeight="SemiBold"
79+
TextTrimming="CharacterEllipsis" />
80+
<Border Margin="8,0,0,0" Padding="6,1" CornerRadius="8"
81+
Background="#FFD83B01"
82+
Visibility="{Binding IsClientSideOnly, Converter={StaticResource BoolToVis}}">
83+
<TextBlock Text="{Binding [ComponentManager_ClientSideBadge], Source={x:Static i18n:Lang.Tr}}" FontSize="10" Foreground="White" FontWeight="SemiBold" />
84+
</Border>
85+
</StackPanel>
7986
<TextBlock Text="{Binding Description}" FontSize="11"
8087
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
8188
TextTrimming="CharacterEllipsis" />
@@ -159,8 +166,15 @@
159166
</Ellipse>
160167

161168
<StackPanel Grid.Column="1" VerticalAlignment="Center">
162-
<TextBlock Text="{Binding Title}" FontSize="14" FontWeight="SemiBold"
163-
TextTrimming="CharacterEllipsis" />
169+
<StackPanel Orientation="Horizontal">
170+
<TextBlock Text="{Binding Title}" FontSize="14" FontWeight="SemiBold"
171+
TextTrimming="CharacterEllipsis" />
172+
<Border Margin="8,0,0,0" Padding="6,1" CornerRadius="8"
173+
Background="#FFD83B01"
174+
Visibility="{Binding IsClientSideOnly, Converter={StaticResource BoolToVis}}">
175+
<TextBlock Text="{Binding [ComponentManager_ClientSideBadge], Source={x:Static i18n:Lang.Tr}}" FontSize="10" Foreground="White" FontWeight="SemiBold" />
176+
</Border>
177+
</StackPanel>
164178
<TextBlock Text="{Binding Description}" FontSize="11"
165179
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
166180
TextTrimming="CharacterEllipsis" />

MCServerLauncher.WPF/InstanceConsole/ViewModels/ComponentItemModel.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,14 @@ public partial class ComponentItemModel : ObservableObject
1111
[ObservableProperty] private bool _isEnabled;
1212
[ObservableProperty] private long _fileSize;
1313
[ObservableProperty] private ComponentKind _kind;
14+
[ObservableProperty] private bool _isClientSideOnly;
1415

1516
public string Description => string.IsNullOrEmpty(Version)
1617
? FileName
1718
: $"{FileName} | v{Version}";
1819

1920
public string Title => string.IsNullOrEmpty(DisplayName) ? FileName : DisplayName;
21+
public string BadgeText => IsClientSideOnly ? "Client" : string.Empty;
2022

2123
partial void OnFileNameChanged(string value)
2224
{
@@ -25,6 +27,7 @@ partial void OnFileNameChanged(string value)
2527
}
2628
partial void OnDisplayNameChanged(string value) => OnPropertyChanged(nameof(Title));
2729
partial void OnVersionChanged(string value) => OnPropertyChanged(nameof(Description));
30+
partial void OnIsClientSideOnlyChanged(bool value) => OnPropertyChanged(nameof(BadgeText));
2831
}
2932

3033
public enum ComponentKind

MCServerLauncher.WPF/InstanceConsole/ViewModels/ComponentManagerViewModel.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,7 @@ private async Task<List<ComponentItemModel>> LoadComponentsAsync(string folder,
150150
{
151151
item.DisplayName = metadata.DisplayName;
152152
item.Version = metadata.Version;
153+
item.IsClientSideOnly = metadata.IsClientSideOnly;
153154
}
154155

155156
result.Add(item);
@@ -327,6 +328,16 @@ private async Task UploadFilesAsync(string[] localPaths, string folder, Componen
327328
{
328329
try
329330
{
331+
if (kind == ComponentKind.Mod && JarMetadataParser.IsClientSideMod(local))
332+
{
333+
_notification.Push(
334+
Lang.Tr["Warning"],
335+
string.Format(Lang.Tr["ComponentManager_ClientSideModBlocked"], Path.GetFileName(local)),
336+
true,
337+
InfoBarSeverity.Warning);
338+
continue;
339+
}
340+
330341
var fileName = Path.GetFileName(local);
331342
var target = $"{_instanceRoot}/{folder}/{fileName}";
332343
var ctx = await _daemon.UploadFileAsync(local, target, 1024 * 1024);

0 commit comments

Comments
 (0)