baseline: OpenMU upstream b5a0961 (fresh source)

This commit is contained in:
Acentech Dev
2026-07-14 19:00:35 +03:00
parent 450402e47d
commit 36fc125d5c
11968 changed files with 748705 additions and 0 deletions

View File

@@ -0,0 +1,57 @@
// <copyright file="CaptionHelper.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.SourceGenerators;
using System.Text.RegularExpressions;
using Microsoft.CodeAnalysis.CSharp.Syntax;
/// <summary>
/// Helper class to generate captions for types.
/// </summary>
public static class CaptionHelper
{
private static readonly Regex WordSeparatorRegex = new("([a-z])([A-Z])", RegexOptions.Compiled);
/// <summary>
/// Separates the words by a space. Words are detected by upper case letters.
/// </summary>
/// <param name="input">The input.</param>
/// <returns>The separated words.</returns>
public static string SeparateWords(string input)
{
return WordSeparatorRegex.Replace(input, "$1 $2")
.Replace(" Definitions", "s")
.Replace(" Definition", "s");
}
/// <summary>
/// Gets a nice caption for types.
/// </summary>
/// <param name="type">The type.</param>
/// <returns>A nice caption for types.</returns>
public static string GetTypeCaption(ClassDeclarationSyntax type)
{
return SeparateWords(type.Identifier.Text);
}
/// <summary>
/// Gets a pluralized caption for a type.
/// </summary>
/// <param name="type">The type.</param>
/// <returns>A nice caption for types.</returns>
public static string GetPluralizedTypeCaption(ClassDeclarationSyntax type)
{
var result = GetTypeCaption(type);
result = result
.Replace(" Definitions", "s")
.Replace(" Definition", "s");
if (!result.EndsWith("s"))
{
result += "s";
}
return result;
}
}

View File

@@ -0,0 +1,186 @@
// <copyright file="CloneableGenerator.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.SourceGenerators;
using System.Collections.Immutable;
using System.Diagnostics;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Text;
/// <summary>
/// A <see cref="IIncrementalGenerator"/> which implements <see cref="ICloneable{}"/>
/// and for convenience also <see cref="IAssignable"/> and <see cref="IAssignable{}"/>.
/// </summary>
[Generator]
public class CloneableGenerator : IIncrementalGenerator
{
private const string CloneableAttributeFullName = "MUnique.OpenMU.Annotations.CloneableAttribute";
private const string CloneableAttributeName = "CloneableAttribute";
private const string IgnoreWhenCloningAttributeName = "IgnoreWhenCloningAttribute";
/// <inheritdoc />
public void Initialize(IncrementalGeneratorInitializationContext context)
{
var classDeclarations = context.SyntaxProvider
.CreateSyntaxProvider(
predicate: static (node, _) => node is ClassDeclarationSyntax { AttributeLists.Count: > 0 },
transform: static (ctx, _) => GetClassWithCloneableAttribute(ctx))
.Where(static m => m is not null);
var compilationAndClasses = context.CompilationProvider.Combine(classDeclarations.Collect());
context.RegisterSourceOutput(compilationAndClasses, (spc, source) => Execute(source.Left, source.Right!, spc));
}
private static ClassDeclarationSyntax? GetClassWithCloneableAttribute(GeneratorSyntaxContext context)
{
var classDeclaration = (ClassDeclarationSyntax)context.Node;
foreach (var attributeList in classDeclaration.AttributeLists)
{
foreach (var attribute in attributeList.Attributes)
{
var symbolInfo = context.SemanticModel.GetSymbolInfo(attribute);
if (symbolInfo.Symbol is IMethodSymbol attributeSymbol)
{
var attributeContainingType = attributeSymbol.ContainingType;
var fullName = attributeContainingType.ToDisplayString();
if (fullName == CloneableAttributeFullName)
{
return classDeclaration;
}
}
}
}
return null;
}
private static void Execute(Compilation compilation, ImmutableArray<ClassDeclarationSyntax?> classes, SourceProductionContext context)
{
if (classes.IsDefaultOrEmpty)
{
return;
}
foreach (var classDeclaration in classes.Distinct())
{
if (classDeclaration is null)
{
continue;
}
var semanticModel = compilation.GetSemanticModel(classDeclaration.SyntaxTree);
var declaredClassSymbol = semanticModel.GetDeclaredSymbol(classDeclaration);
if (declaredClassSymbol is null)
{
continue;
}
var generatedClass = GeneratePartialClass(classDeclaration, declaredClassSymbol);
context.AddSource($"{classDeclaration.Identifier}_Cloneable", SourceText.From(generatedClass.ToString(), Encoding.UTF8));
}
}
private static StringBuilder GeneratePartialClass(ClassDeclarationSyntax annotatedClass, INamedTypeSymbol declaredClassSymbol)
{
var sb = new StringBuilder();
var className = annotatedClass.Identifier.Text;
var ns = declaredClassSymbol.ContainingNamespace?.ToString() ?? string.Empty;
var isInheritedClonable = declaredClassSymbol.BaseType?.GetAttributes().Any(a => a.AttributeClass?.Name == CloneableAttributeName) ?? false;
sb.AppendLine($"""
// <copyright file="{className}_Cloneable.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace {ns};
using System;
using System.Collections.Generic;
using MUnique.OpenMU.DataModel;
using MUnique.OpenMU.DataModel.Configuration;
/// <inheritdoc />
public partial class {className} : IAssignable, IAssignable<{className}>, ICloneable<{className}>
""");
sb.AppendLine("{");
sb.AppendLine($"""
/// <inheritdoc />
public {(isInheritedClonable ? "override" : "virtual")} {className} Clone(GameConfiguration gameConfiguration)
""");
sb.AppendLine(" {");
sb.AppendLine($"""
var clone = new {className}();
clone.AssignValuesOf(this, gameConfiguration);
return clone;
""");
sb.AppendLine(" }");
sb.AppendLine();
sb.AppendLine($$"""
/// <inheritdoc />
public {{(isInheritedClonable ? "override" : "virtual")}} void AssignValuesOf(object other, GameConfiguration gameConfiguration)
{
if (other is {{className}} typedOther)
{
this.AssignValuesOf(typedOther, gameConfiguration);
}
}
/// <inheritdoc />
public virtual void AssignValuesOf({{className}} other, GameConfiguration gameConfiguration)
""");
sb.AppendLine(" {");
if (isInheritedClonable)
{
sb.AppendLine(" base.AssignValuesOf(other, gameConfiguration);");
}
GenerateAssignments(sb, declaredClassSymbol);
sb.AppendLine(" }");
sb.AppendLine("}");
return sb;
}
private static void GenerateAssignments(StringBuilder sb, INamedTypeSymbol declaredClassSymbol)
{
var properties = declaredClassSymbol
.GetMembers()
.OfType<IPropertySymbol>()
.Where(p => p.SetMethod is not null)
.Where(p => !p.GetAttributes().Any(a => object.Equals(a.AttributeClass?.Name, IgnoreWhenCloningAttributeName)));
foreach (var property in properties)
{
if (property.SetMethod?.DeclaredAccessibility == Accessibility.Protected && property.IsVirtual)
{
// collection
var genericElementType = (property.Type as INamedTypeSymbol)?.TypeArguments.FirstOrDefault();
if (genericElementType?.IsValueType ?? false)
{
sb.AppendLine($" this.{property.Name}.AssignCollection(other.{property.Name});");
}
else
{
sb.AppendLine($" this.{property.Name}.AssignCollection(other.{property.Name}, gameConfiguration);");
}
}
else if (property.IsVirtual)
{
sb.AppendLine($" this.{property.Name} = gameConfiguration.GetObjectOfConfig(other.{property.Name});");
}
else
{
// for normal value properties, just assign the value
sb.AppendLine($" this.{property.Name} = other.{property.Name};");
}
}
}
}

View File

@@ -0,0 +1,24 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<LangVersion>latest</LangVersion>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<Nullable>enable</Nullable>
<WarningsAsErrors>nullable;CS4014;VSTHRD110;VSTHRD100</WarningsAsErrors>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp">
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>
</Project>

View File

@@ -0,0 +1,182 @@
// <copyright file="ResourceGenerator.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.SourceGenerators;
using System.Collections.Immutable;
using System.IO;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
/// <summary>
/// A <see cref="IIncrementalGenerator"/> which creates resource strings for all classes and properties of
/// the data model.
/// </summary>
// [Generator]
public class ResourceGenerator : IIncrementalGenerator
{
/// <inheritdoc />
public void Initialize(IncrementalGeneratorInitializationContext context)
{
var typeDeclarations = context.SyntaxProvider
.CreateSyntaxProvider(
predicate: static (node, _) => node is ClassDeclarationSyntax or EnumDeclarationSyntax,
transform: static (ctx, _) => (BaseTypeDeclarationSyntax)ctx.Node)
.Collect();
var projectDirProvider = context.AnalyzerConfigOptionsProvider
.Select((options, _) =>
{
options.GlobalOptions.TryGetValue("build_property.projectdir", out var projectDir);
return projectDir;
});
var compilationAndTypes = context.CompilationProvider
.Combine(typeDeclarations)
.Combine(projectDirProvider);
context.RegisterSourceOutput(compilationAndTypes, (_, source) =>
{
var ((compilation, types), projectDir) = source;
Execute(compilation, types, projectDir);
});
}
private static void Execute(Compilation compilation, ImmutableArray<BaseTypeDeclarationSyntax> types, string? projectDir)
{
if (types.IsDefaultOrEmpty || string.IsNullOrEmpty(projectDir))
{
return;
}
var declaredTypes = new List<(BaseTypeDeclarationSyntax, INamedTypeSymbol)>();
foreach (var typeDeclaration in types)
{
var semanticModel = compilation.GetSemanticModel(typeDeclaration.SyntaxTree);
var declaredSymbol = semanticModel.GetDeclaredSymbol(typeDeclaration);
if (declaredSymbol is INamedTypeSymbol namedTypeSymbol)
{
declaredTypes.Add((typeDeclaration, namedTypeSymbol));
}
}
var sb = StartResourceFile();
foreach (var (declarationSyntax, namedTypeSymbol) in declaredTypes.OrderBy(tuple => tuple.Item1.Identifier.Text))
{
AppendResourceStrings(sb, declarationSyntax, namedTypeSymbol);
}
sb.AppendLine("</root>");
var targetPath = Path.Combine(projectDir!, "Properties", "ModelResources.resx");
#pragma warning disable RS1035
File.WriteAllText(targetPath, sb.ToString(), Encoding.UTF8);
#pragma warning restore RS1035
}
private static StringBuilder StartResourceFile()
{
var sb = new StringBuilder();
sb.AppendLine($"""
<?xml version="1.0" encoding="utf-8"?>
<root>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
""");
return sb;
}
private static void AppendResourceStrings(StringBuilder sb, BaseTypeDeclarationSyntax annotatedClass, INamedTypeSymbol declaredClassSymbol)
{
switch (annotatedClass)
{
case ClassDeclarationSyntax classDecl:
AppendResourceStrings(sb, classDecl, declaredClassSymbol);
break;
case EnumDeclarationSyntax enumDecl:
AppendResourceStrings(sb, enumDecl);
break;
default:
// do nothing
break;
}
}
private static void AppendResourceStrings(StringBuilder sb, ClassDeclarationSyntax annotatedClass, INamedTypeSymbol declaredClassSymbol)
{
var className = annotatedClass.Identifier.Text;
sb.AppendLine($"""
<data name="{className}_TypeCaption" xml:space="preserve">
<value>{CaptionHelper.GetTypeCaption(annotatedClass)}</value>
</data>
<data name="{className}_TypeCaptionPlural" xml:space="preserve">
<value>{CaptionHelper.GetPluralizedTypeCaption(annotatedClass)}</value>
</data>
<data name="{className}_TypeDescription" xml:space="preserve">
<value></value>
</data>
""");
GenerateProperties(sb, declaredClassSymbol, className);
}
private static void AppendResourceStrings(StringBuilder sb, EnumDeclarationSyntax annotatedEnum)
{
var enumName = annotatedEnum.Identifier.Text;
sb.AppendLine($"""
<data name="{enumName}_TypeCaption" xml:space="preserve">
<value>{CaptionHelper.SeparateWords(enumName)}</value>
</data>
<data name="{enumName}_TypeDescription" xml:space="preserve">
<value></value>
</data>
""");
foreach (var member in annotatedEnum.Members)
{
sb.AppendLine($"""
<data name="{enumName}_{member.Identifier.Text}_Caption" xml:space="preserve">
<value>{CaptionHelper.SeparateWords(member.Identifier.Text)}</value>
</data>
<data name="{enumName}_{member.Identifier.Text}_Description" xml:space="preserve">
<value></value>
</data>
""");
}
}
private static void GenerateProperties(StringBuilder sb, INamedTypeSymbol declaredClassSymbol, string className)
{
var properties = declaredClassSymbol
.GetMembers()
.OfType<IPropertySymbol>()
.Where(ps => ps.DeclaredAccessibility == Accessibility.Public);
foreach (var property in properties)
{
sb.AppendLine($"""
<data name="{className}_{property.Name}_Caption" xml:space="preserve">
<value>{CaptionHelper.SeparateWords(property.Name)}</value>
</data>
<data name="{className}_{property.Name}_Description" xml:space="preserve">
<value></value>
</data>
""");
}
}
}