Implement handshake Hello NetControlMessage, recv and send

This commit is contained in:
AeonLucid
2022-01-12 07:26:25 +01:00
parent 0a27bdd3c0
commit 65a28b32ef
50 changed files with 2768 additions and 156 deletions
@@ -0,0 +1,13 @@
using System.Collections.Generic;
namespace Prospect.Unreal.Generator
{
internal class NetControlMessageEntry
{
public string Name { get; set; }
public int Index { get; set; }
public List<string> Params { get; set; } = new List<string>();
}
}
@@ -0,0 +1,41 @@
using Microsoft.CodeAnalysis.CSharp.Syntax;
using System.Collections.Generic;
using System.Collections.Immutable;
namespace Prospect.Unreal.Generator
{
internal class NetControlMessageParser
{
internal IReadOnlyList<NetControlMessageEntry> Parse(ImmutableArray<AttributeSyntax> attributes)
{
var entries = new List<NetControlMessageEntry>();
foreach (var attribute in attributes)
{
var attributeParams = attribute.ArgumentList.Arguments;
var attributeEntry = new NetControlMessageEntry();
attributeEntry.Name = ((LiteralExpressionSyntax)attributeParams[0].Expression).Token.ValueText; // StringLiteralExpression
attributeEntry.Index = (int)((LiteralExpressionSyntax)attributeParams[1].Expression).Token.Value; // NumericLiteralExpression
for (int i = 2; i < attributeParams.Count; i++)
{
var paramType = ((TypeOfExpressionSyntax)attributeParams[i].Expression);
var paramKeyword = paramType.Type;
if (paramKeyword is IdentifierNameSyntax paramIdentifierName)
{
attributeEntry.Params.Add(paramIdentifierName.Identifier.ValueText);
}
else if (paramKeyword is PredefinedTypeSyntax predefinedTypeSyntax)
{
attributeEntry.Params.Add(predefinedTypeSyntax.Keyword.ValueText);
}
}
entries.Add(attributeEntry);
}
return entries;
}
}
}
@@ -0,0 +1,182 @@
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Prospect.Unreal.Generator.Util;
using Scriban;
using Scriban.Runtime;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Text;
using System.Threading;
namespace Prospect.Unreal.Generator
{
[Generator]
internal class NetControlMessageSourceIncrementalGenerator : IIncrementalGenerator
{
public const string Namespace = "Prospect.Unreal.Net.Packets.Control";
public const string AttributeName = "NetControlMessage";
public void Initialize(IncrementalGeneratorInitializationContext context)
{
var attributes = context.SyntaxProvider
.CreateSyntaxProvider(
predicate: IsSyntaxTargetForGeneration,
transform: GetSemanticTargetForGeneration)
.Where(static m => m is not null);
var compilationAndAttributes = context.CompilationProvider.Combine(attributes.Collect());
context.RegisterSourceOutput(compilationAndAttributes, static (spc, source) => Execute(source.Left, source.Right, spc));
}
private static bool IsSyntaxTargetForGeneration(SyntaxNode node, CancellationToken _) =>
node is AttributeSyntax attribute && attribute.Name is IdentifierNameSyntax;
private static AttributeSyntax GetSemanticTargetForGeneration(GeneratorSyntaxContext context, CancellationToken _)
{
var attribute = (AttributeSyntax)context.Node;
var attributeName = (IdentifierNameSyntax)attribute.Name;
if (attributeName.Identifier.ValueText != AttributeName)
{
return null;
}
return attribute;
}
private static void Execute(Compilation compilation, ImmutableArray<AttributeSyntax> attributes, SourceProductionContext context)
{
var parser = new NetControlMessageParser();
var results = parser.Parse(attributes);
if (results.Count == 0)
{
return;
}
context.AddSource("NMTEnum.g.cs", GenerateEnum(results));
foreach (var result in results)
{
context.AddSource($"NMT_{result.Name}.g.cs", GenerateMessage(result));
}
}
private static string GenerateEnum(IReadOnlyList<NetControlMessageEntry> entries)
{
var templateData = EmbeddedResource.GetContent("Templates/NMTEnum.sbntxt");
var template = Template.Parse(templateData);
return template.Render(new
{
cnamespace = Namespace,
entries = entries
}, member => member.Name).Trim();
}
private static string GenerateMessage(NetControlMessageEntry entry)
{
var scriptObject = new ScriptObject();
scriptObject.Import(typeof(ScribanFunctions));
scriptObject.Add("cnamespace", Namespace);
scriptObject.Add("entry", entry);
var context = new TemplateContext();
context.PushGlobal(scriptObject);
var templateData = EmbeddedResource.GetContent("Templates/NMTMessage.sbntxt");
var template = Template.Parse(templateData);
return template.Render(scriptObject, member => member.Name).Trim();
}
}
internal static class ScribanFunctions
{
[ScriptMemberIgnore]
private static readonly Dictionary<string, ParamDef> TypeDefinitions = new Dictionary<string, ParamDef>
{
{ "byte", new ParamDef("bunch.ReadByte()", "bunch.WriteByte({0})") },
{ "int", new ParamDef("bunch.ReadInt32()", "bunch.WriteInt32({0})") },
{ "uint", new ParamDef("bunch.ReadUInt32()", "bunch.WriteUInt32({0})") },
{ "FString", new ParamDef("bunch.ReadString()", "bunch.WriteString({0})") },
};
public static string SendParams(List<string> args)
{
return MethodParameter(args);
}
public static string ReadOutParams(List<string> args)
{
return MethodParameter(args, "out");
}
public static string ReadOut(string paramType, int index)
{
var paramName = (char)('a' + index);
var paramRead = ReadType(paramType);
if (paramRead.StartsWith("throw")) {
return paramRead;
}
return $"{paramName} = {paramRead}";
}
public static string SendType(string paramType, int index)
{
if (TypeDefinitions.TryGetValue(paramType, out var type))
{
return string.Format(type.Write, (char)('a' + index));
}
return $"throw new NotImplementedException(\"Unsupported type {paramType}\")";
}
public static string ReadType(string param)
{
if (TypeDefinitions.TryGetValue(param, out var type))
{
return type.Read;
}
return $"throw new NotImplementedException(\"Unsupported type {param}\")";
}
[ScriptMemberIgnore]
private static string MethodParameter(List<string> args, string prefix = null)
{
var builder = new StringBuilder();
for (int i = 0; i < args.Count; i++)
{
if (i < args.Count)
{
builder.Append(", ");
}
var paramName = (char)('a' + i);
var paramType = args[i];
if (paramType == "FString")
{
paramType = "string";
}
if (prefix != null)
{
builder.AppendFormat("{0} {1} {2}", prefix, paramType, paramName);
}
else
{
builder.AppendFormat("{0} {1}", paramType, paramName);
}
}
return builder.ToString();
}
}
}
@@ -0,0 +1,53 @@
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using System.Collections.Generic;
namespace Prospect.Unreal.Generator
{
internal class NetControlMessageSyntaxReceiver : ISyntaxContextReceiver
{
public const string AttributeName = "NetControlMessage";
public List<NetControlMessageEntry> Entries { get; } = new List<NetControlMessageEntry>();
public void OnVisitSyntaxNode(GeneratorSyntaxContext context)
{
if (context.Node is not AttributeSyntax attribute)
{
return;
}
if (attribute.Name is not IdentifierNameSyntax identifierName)
{
return;
}
if (identifierName.Identifier.ValueText != AttributeName)
{
return;
}
var attributeParams = attribute.ArgumentList.Arguments;
var attributeEntry = new NetControlMessageEntry();
attributeEntry.Name = ((LiteralExpressionSyntax)attributeParams[0].Expression).Token.ValueText; // StringLiteralExpression
attributeEntry.Index = (int) ((LiteralExpressionSyntax)attributeParams[1].Expression).Token.Value; // NumericLiteralExpression
for (int i = 2; i < attributeParams.Count; i++)
{
var paramType = ((TypeOfExpressionSyntax)attributeParams[i].Expression);
var paramKeyword = paramType.Type;
if (paramKeyword is IdentifierNameSyntax paramIdentifierName)
{
attributeEntry.Params.Add(paramIdentifierName.Identifier.ValueText);
}
else if (paramKeyword is PredefinedTypeSyntax predefinedTypeSyntax)
{
attributeEntry.Params.Add(predefinedTypeSyntax.Keyword.ValueText);
}
}
Entries.Add(attributeEntry);
}
}
}
@@ -0,0 +1,8 @@
{
"profiles": {
"Roslyn": {
"commandName": "DebugRoslynComponent",
"targetProject": "..\\Prospect.Unreal\\Prospect.Unreal.csproj"
}
}
}
@@ -0,0 +1,36 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<LangVersion>9.0</LangVersion>
<IsRoslynComponent>true</IsRoslynComponent>
</PropertyGroup>
<ItemGroup>
<EmbeddedResource Include="Templates\*.sbntxt" />
</ItemGroup>
<ItemGroup>
<None Remove="Templates\NMTMessage.sbntxt" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.0.1" PrivateAssets="all" />
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.3" PrivateAssets="all" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Scriban" Version="2.1.2" GeneratePathProperty="true" PrivateAssets="all" />
</ItemGroup>
<PropertyGroup>
<GetTargetPathDependsOn>$(GetTargetPathDependsOn);GetDependencyTargetPaths</GetTargetPathDependsOn>
</PropertyGroup>
<Target Name="GetDependencyTargetPaths">
<ItemGroup>
<TargetPathWithTargetPlatformMoniker Include="$(PKGScriban)\lib\netstandard2.0\Scriban.dll" IncludeRuntimeDependency="false" />
</ItemGroup>
</Target>
</Project>
@@ -0,0 +1,8 @@
namespace {{ cnamespace }};
public enum NMT
{
{{~ for entry in entries ~}}
{{ entry.Name }} = {{ entry.Index }},
{{~ end ~}}
}
@@ -0,0 +1,36 @@
using Prospect.Unreal.Core;
using Prospect.Unreal.Net;
using Prospect.Unreal.Net.Packets.Bunch;
namespace {{ cnamespace }};
public static class NMT_{{ entry.Name }}
{
public static void Send(UNetConnection conn{{send_params entry.Params}})
{
if (conn.Channels[0] != null && !conn.Channels[0].Closing)
{
using var bunch = new FControlChannelOutBunch(conn.Channels[0], false);
bunch.WriteByte({{ entry.Index }});
{{~ for param in entry.Params ~}}
{{send_type param for.index}};
{{~ end ~}}
conn.Channels[0].SendBunch(bunch, true);
}
}
public static bool Receive(FInBunch bunch{{read_out_params entry.Params}})
{
{{~ for param in entry.Params ~}}
{{read_out param for.index}};
{{~ end ~}}
return !bunch.IsError();
}
public static void Discard(FInBunch bunch)
{
{{~ for param in entry.Params ~}}
{{read_type param}};
{{~ end ~}}
}
}
@@ -0,0 +1,27 @@
using System;
using System.IO;
using System.Reflection;
namespace Prospect.Unreal.Generator.Util
{
internal static class EmbeddedResource
{
public static string GetContent(string relativePath)
{
var baseName = Assembly.GetExecutingAssembly().GetName().Name;
var resourceName = relativePath
.TrimStart('.')
.Replace(Path.DirectorySeparatorChar, '.')
.Replace(Path.AltDirectorySeparatorChar, '.');
using var stream = Assembly.GetExecutingAssembly()
.GetManifestResourceStream(baseName + "." + resourceName);
if (stream == null)
throw new NotSupportedException();
using var reader = new StreamReader(stream);
return reader.ReadToEnd();
}
}
}
@@ -0,0 +1,18 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace Prospect.Unreal.Generator.Util
{
internal class ParamDef
{
public ParamDef(string read, string write)
{
Read = read;
Write = write;
}
public string Read { get; }
public string Write { get; }
}
}