CI / Go Backend (push) Canceled after 0s
初始化 monorepo: Go后端(7微服务) + Unity客户端(9模块) + 启动器 HTML5原型: Three.js 3D体素世界, Perlin噪声地形, 原版材质, 22种方块 Minecraft创造模式背包: 双栏布局, 拖拽移动物品, 方向性元件引脚 AI助搭策划文档 + 客户端/服务端骨架 + Docker Compose + CI
317 lines
11 KiB
C#
317 lines
11 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
|
|
namespace PCL.Core.Minecraft.ResourceProject;
|
|
|
|
public sealed record class ModDependencyReference
|
|
{
|
|
public string ProjectId { get; init; } = string.Empty;
|
|
public string Source { get; init; } = string.Empty;
|
|
public bool IsRequired { get; init; } = true;
|
|
}
|
|
|
|
public sealed record class ModDependencyRequest
|
|
{
|
|
public string TargetMinecraftVersion { get; init; } = string.Empty;
|
|
public List<string> TargetLoaders { get; init; } = [];
|
|
public List<ModDependencyReference> RequiredDependencies { get; init; } = [];
|
|
public List<InstalledModIdentity> InstalledMods { get; init; } = [];
|
|
public Func<string, string, ModDependencyProject?> ProjectResolver { get; init; } = (_, _) => null;
|
|
}
|
|
|
|
public sealed record class ModDependencyProject
|
|
{
|
|
public string ProjectId { get; init; } = string.Empty;
|
|
public string Source { get; init; } = string.Empty;
|
|
public string? ProjectName { get; init; }
|
|
public List<ModDependencyFile> Files { get; init; } = [];
|
|
}
|
|
|
|
public sealed record class ModDependencyFile
|
|
{
|
|
public string Id { get; init; } = string.Empty;
|
|
public string DisplayName { get; init; } = string.Empty;
|
|
public string? Version { get; init; }
|
|
public List<string> GameVersions { get; init; } = [];
|
|
public List<string> Loaders { get; init; } = [];
|
|
public int ReleaseType { get; init; }
|
|
public DateTime ReleaseDate { get; init; }
|
|
public List<ModDependencyReference> RequiredDependencies { get; init; } = [];
|
|
}
|
|
|
|
public sealed record class InstalledModIdentity
|
|
{
|
|
public string? SourceProjectId { get; init; }
|
|
public string? Source { get; init; }
|
|
public string? ModId { get; init; }
|
|
public List<string> GameVersions { get; init; } = [];
|
|
public List<string> Loaders { get; init; } = [];
|
|
}
|
|
|
|
public sealed record class ModDependencyResolutionResult
|
|
{
|
|
public List<ResolvedDependencyInstall> ToInstall { get; } = [];
|
|
public List<UnresolvedDependency> Unresolved { get; } = [];
|
|
public List<IgnoredDependency> Satisfied { get; } = [];
|
|
}
|
|
|
|
public sealed record class ResolvedDependencyInstall
|
|
{
|
|
public string ProjectId { get; init; } = string.Empty;
|
|
public string Source { get; init; } = string.Empty;
|
|
public string? ProjectName { get; init; }
|
|
public ModDependencyFile File { get; init; } = new();
|
|
}
|
|
|
|
public sealed record class UnresolvedDependency
|
|
{
|
|
public string ProjectId { get; init; } = string.Empty;
|
|
public string Source { get; init; } = string.Empty;
|
|
public string Reason { get; init; } = string.Empty;
|
|
}
|
|
|
|
public sealed record class IgnoredDependency
|
|
{
|
|
public string ProjectId { get; init; } = string.Empty;
|
|
public string Source { get; init; } = string.Empty;
|
|
public string Reason { get; init; } = string.Empty;
|
|
}
|
|
|
|
public sealed class ModDependencyResolver
|
|
{
|
|
private const int MaxDepth = 32;
|
|
private static readonly StringComparer Comparer = StringComparer.OrdinalIgnoreCase;
|
|
|
|
public ModDependencyResolutionResult Resolve(ModDependencyRequest request)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(request);
|
|
ArgumentNullException.ThrowIfNull(request.ProjectResolver);
|
|
|
|
var context = new ResolutionContext(request);
|
|
foreach (var dependency in request.RequiredDependencies)
|
|
{
|
|
ResolveDependency(context, dependency, 0);
|
|
}
|
|
|
|
return context.Result;
|
|
}
|
|
|
|
private static void ResolveDependency(ResolutionContext context, ModDependencyReference dependency, int depth)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(dependency.ProjectId) || string.IsNullOrWhiteSpace(dependency.Source))
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (!dependency.IsRequired)
|
|
{
|
|
context.AddSatisfied(dependency.ProjectId, dependency.Source, "Optional dependency ignored.");
|
|
return;
|
|
}
|
|
|
|
if (depth > MaxDepth)
|
|
{
|
|
context.AddUnresolved(dependency.ProjectId, dependency.Source, "Maximum dependency depth exceeded.");
|
|
return;
|
|
}
|
|
|
|
var visitedKey = context.GetVisitedKey(dependency.ProjectId, dependency.Source);
|
|
if (!context.Visited.Add(visitedKey))
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (context.IsInstalledCompatible(dependency.ProjectId, dependency.Source))
|
|
{
|
|
context.AddSatisfied(dependency.ProjectId, dependency.Source, "Already installed and compatible.");
|
|
return;
|
|
}
|
|
|
|
var project = context.Request.ProjectResolver(dependency.Source, dependency.ProjectId);
|
|
if (project is null)
|
|
{
|
|
context.AddUnresolved(dependency.ProjectId, dependency.Source, "Dependency project was not found.");
|
|
return;
|
|
}
|
|
|
|
var selectedFile = SelectBestFile(project.Files, context.TargetMinecraftVersion, context.TargetLoaders);
|
|
if (selectedFile is null)
|
|
{
|
|
context.AddUnresolved(project.ProjectId, project.Source, "No compatible file was found.");
|
|
return;
|
|
}
|
|
|
|
context.AddInstall(project, selectedFile);
|
|
|
|
foreach (var nestedDependency in selectedFile.RequiredDependencies)
|
|
{
|
|
ResolveDependency(context, nestedDependency, depth + 1);
|
|
}
|
|
}
|
|
|
|
private static ModDependencyFile? SelectBestFile(
|
|
IEnumerable<ModDependencyFile> files,
|
|
string targetMinecraftVersion,
|
|
HashSet<string> targetLoaders)
|
|
{
|
|
return files
|
|
.Where(file => IsCompatibleFile(file, targetMinecraftVersion, targetLoaders))
|
|
.OrderByDescending(file => HasExactGameVersionMatch(file, targetMinecraftVersion))
|
|
.ThenByDescending(file => HasLoaderMatch(file, targetLoaders))
|
|
.ThenBy(file => NormalizeReleaseType(file.ReleaseType))
|
|
.ThenByDescending(file => file.ReleaseDate)
|
|
.FirstOrDefault();
|
|
}
|
|
|
|
private static bool IsCompatibleFile(ModDependencyFile file, string targetMinecraftVersion, HashSet<string> targetLoaders)
|
|
{
|
|
if (!HasExactGameVersionMatch(file, targetMinecraftVersion))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (targetLoaders.Count == 0)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
if (file.Loaders.Count == 0)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
return file.Loaders.Any(loader => targetLoaders.Contains(loader));
|
|
}
|
|
|
|
private static bool HasExactGameVersionMatch(ModDependencyFile file, string targetMinecraftVersion)
|
|
{
|
|
return file.GameVersions.Any(version => Comparer.Equals(version, targetMinecraftVersion));
|
|
}
|
|
|
|
private static bool HasLoaderMatch(ModDependencyFile file, HashSet<string> targetLoaders)
|
|
{
|
|
if (targetLoaders.Count == 0)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
return file.Loaders.Any(loader => targetLoaders.Contains(loader));
|
|
}
|
|
|
|
private static int NormalizeReleaseType(int releaseType)
|
|
{
|
|
return releaseType switch
|
|
{
|
|
1 => 1,
|
|
2 => 2,
|
|
3 => 3,
|
|
_ => int.MaxValue,
|
|
};
|
|
}
|
|
|
|
private sealed class ResolutionContext
|
|
{
|
|
private readonly HashSet<string> _installDedupe = new(Comparer);
|
|
private readonly HashSet<string> _unresolvedDedupe = new(Comparer);
|
|
private readonly HashSet<string> _satisfiedDedupe = new(Comparer);
|
|
|
|
public ResolutionContext(ModDependencyRequest request)
|
|
{
|
|
Request = request;
|
|
Result = new ModDependencyResolutionResult();
|
|
Visited = new HashSet<string>(Comparer);
|
|
TargetMinecraftVersion = request.TargetMinecraftVersion ?? string.Empty;
|
|
TargetLoaders = new HashSet<string>(
|
|
request.TargetLoaders.Where(static loader => !string.IsNullOrWhiteSpace(loader)),
|
|
Comparer);
|
|
LoaderSetKey = string.Join(",", TargetLoaders.OrderBy(static loader => loader, Comparer));
|
|
}
|
|
|
|
public ModDependencyRequest Request { get; }
|
|
public ModDependencyResolutionResult Result { get; }
|
|
public HashSet<string> Visited { get; }
|
|
public string TargetMinecraftVersion { get; }
|
|
public HashSet<string> TargetLoaders { get; }
|
|
private string LoaderSetKey { get; }
|
|
|
|
public string GetVisitedKey(string projectId, string source)
|
|
{
|
|
return $"{source}:{projectId}:{TargetMinecraftVersion}:{LoaderSetKey}";
|
|
}
|
|
|
|
public bool IsInstalledCompatible(string projectId, string source)
|
|
{
|
|
return Request.InstalledMods.Any(installed =>
|
|
Comparer.Equals(installed.SourceProjectId, projectId)
|
|
&& Comparer.Equals(installed.Source, source)
|
|
&& installed.GameVersions.Any(version => Comparer.Equals(version, TargetMinecraftVersion))
|
|
&& LoadersCompatible(installed.Loaders));
|
|
}
|
|
|
|
public void AddInstall(ModDependencyProject project, ModDependencyFile file)
|
|
{
|
|
var dedupeKey = GetProjectKey(project.ProjectId, project.Source);
|
|
if (!_installDedupe.Add(dedupeKey))
|
|
{
|
|
return;
|
|
}
|
|
|
|
Result.ToInstall.Add(new ResolvedDependencyInstall
|
|
{
|
|
ProjectId = project.ProjectId,
|
|
Source = project.Source,
|
|
ProjectName = project.ProjectName,
|
|
File = file,
|
|
});
|
|
}
|
|
|
|
public void AddUnresolved(string projectId, string source, string reason)
|
|
{
|
|
var dedupeKey = GetProjectKey(projectId, source);
|
|
if (!_unresolvedDedupe.Add(dedupeKey))
|
|
{
|
|
return;
|
|
}
|
|
|
|
Result.Unresolved.Add(new UnresolvedDependency
|
|
{
|
|
ProjectId = projectId,
|
|
Source = source,
|
|
Reason = reason,
|
|
});
|
|
}
|
|
|
|
public void AddSatisfied(string projectId, string source, string reason)
|
|
{
|
|
var dedupeKey = GetProjectKey(projectId, source);
|
|
if (!_satisfiedDedupe.Add(dedupeKey))
|
|
{
|
|
return;
|
|
}
|
|
|
|
Result.Satisfied.Add(new IgnoredDependency
|
|
{
|
|
ProjectId = projectId,
|
|
Source = source,
|
|
Reason = reason,
|
|
});
|
|
}
|
|
|
|
private bool LoadersCompatible(List<string> installedLoaders)
|
|
{
|
|
if (TargetLoaders.Count == 0 || installedLoaders.Count == 0)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
return installedLoaders.Any(loader => TargetLoaders.Contains(loader));
|
|
}
|
|
|
|
private static string GetProjectKey(string projectId, string source)
|
|
{
|
|
return $"{source}:{projectId}";
|
|
}
|
|
}
|
|
}
|