WIP switch to ServicePoint2 library

This commit is contained in:
Vinzenz Schroeter 2024-05-13 01:23:34 +02:00
parent eb999b0d1a
commit e1cfd714c1
31 changed files with 66 additions and 791 deletions

View file

@ -1,42 +0,0 @@
using System.Diagnostics;
namespace DisplayCommands;
public sealed class ByteGrid(ushort width, ushort height) : IEquatable<ByteGrid>
{
public ushort Height { get; } = height;
public ushort Width { get; } = width;
internal Memory<byte> Data { get; } = new byte[width * height].AsMemory();
public byte this[ushort x, ushort y]
{
get => Data.Span[GetIndex(x, y)];
set => Data.Span[GetIndex(x, y)] = value;
}
public bool Equals(ByteGrid? other)
{
if (ReferenceEquals(null, other)) return false;
if (ReferenceEquals(this, other)) return true;
return Height == other.Height && Width == other.Width && Data.Span.SequenceEqual(other.Data.Span);
}
private int GetIndex(ushort x, ushort y)
{
Debug.Assert(x < Width);
Debug.Assert(y < Height);
return x + y * Width;
}
public void Clear() => Data.Span.Clear();
public override bool Equals(object? obj) => ReferenceEquals(this, obj) || (obj is ByteGrid other && Equals(other));
public override int GetHashCode() => HashCode.Combine(Height, Width, Data);
public static bool operator ==(ByteGrid? left, ByteGrid? right) => Equals(left, right);
public static bool operator !=(ByteGrid? left, ByteGrid? right) => !Equals(left, right);
}

View file

@ -1,71 +0,0 @@
using System.Diagnostics;
using System.Text;
namespace DisplayCommands;
public sealed class Cp437Grid(ushort width, ushort height) : IEquatable<Cp437Grid>
{
private readonly ByteGrid _byteGrid = new(width, height);
private readonly Encoding _encoding = Encoding.GetEncoding(437);
public ushort Height { get; } = height;
public ushort Width { get; } = width;
internal Memory<byte> Data => _byteGrid.Data;
public char this[ushort x, ushort y]
{
get => ByteToChar(_byteGrid[x, y]);
set => _byteGrid[x, y] = CharToByte(value);
}
public string this[ushort row]
{
get
{
var rowStart = row * Width;
return _encoding.GetString(_byteGrid.Data[rowStart..(rowStart + Width)].Span);
}
set
{
ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(row, Height, nameof(row));
ArgumentOutOfRangeException.ThrowIfGreaterThan(value.Length, Width, nameof(value));
ushort x = 0;
for (; x < value.Length; x++)
_byteGrid[x, row] = CharToByte(value[x]);
for (; x < Width; x++)
_byteGrid[x, row] = CharToByte(' ');
}
}
private byte CharToByte(char c)
{
ReadOnlySpan<char> valuesStr = stackalloc char[] { c };
Span<byte> convertedStr = stackalloc byte[1];
var consumed = _encoding.GetBytes(valuesStr, convertedStr);
Debug.Assert(consumed == 1);
return convertedStr[0];
}
private char ByteToChar(byte b)
{
ReadOnlySpan<byte> valueBytes = stackalloc byte[] { b };
Span<char> resultStr = stackalloc char[1];
var consumed = _encoding.GetChars(valueBytes, resultStr);
Debug.Assert(consumed == 1);
return resultStr[0];
}
public bool Equals(Cp437Grid? other)
{
if (ReferenceEquals(null, other)) return false;
if (ReferenceEquals(this, other)) return true;
return Height == other.Height && Width == other.Width && _byteGrid.Equals(other._byteGrid);
}
public override bool Equals(object? obj) => ReferenceEquals(this, obj) || (obj is Cp437Grid other && Equals(other));
public override int GetHashCode() => HashCode.Combine(_byteGrid, Height, Width);
public static bool operator ==(Cp437Grid? left, Cp437Grid? right) => Equals(left, right);
public static bool operator !=(Cp437Grid? left, Cp437Grid? right) => !Equals(left, right);
}

View file

@ -1,23 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="../shared.props" />
<PropertyGroup>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<EnableConfigurationBindingGenerator>true</EnableConfigurationBindingGenerator>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0"/>
<PackageReference Include="Microsoft.Extensions.Options" Version="8.0.2"/>
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="8.0.0"/>
<PackageReference Include="System.Text.Encoding.CodePages" Version="8.0.0"/>
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.1"/>
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="8.0.1"/>
<ProjectReference Include="../EndiannessSourceGenerator/EndiannessSourceGenerator.csproj"
OutputItemType="Analyzer"
ReferenceOutputAssembly="false"/>
</ItemGroup>
</Project>

View file

@ -1,25 +0,0 @@
using System.Text;
using DisplayCommands.Internals;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
namespace DisplayCommands;
public static class DisplayExtensions
{
static DisplayExtensions()
{
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
}
public static IServiceCollection AddDisplay(
this IServiceCollection services,
IConfigurationSection? configurationSection = null
)
{
services.AddSingleton<IDisplayConnection, DisplayConnection>();
if (configurationSection != null)
services.Configure<DisplayConfiguration>(configurationSection);
return services;
}
}

View file

@ -1,2 +0,0 @@
global using System;
global using System.Threading.Tasks;

View file

@ -1,24 +0,0 @@
namespace DisplayCommands;
public interface IDisplayConnection
{
ValueTask SendClearAsync();
ValueTask SendCp437DataAsync(ushort x, ushort y, Cp437Grid grid);
ValueTask SendBrightnessAsync(byte brightness);
ValueTask SendCharBrightnessAsync(ushort x, ushort y, ByteGrid luma);
ValueTask SendHardResetAsync();
ValueTask SendFadeOutAsync(byte loops);
public ValueTask SendBitmapLinearWindowAsync(ushort x, ushort y, PixelGrid pixels);
/// <summary>
/// Returns the IPv4 address that is associated with the interface with which the display is reachable.
/// </summary>
/// <returns>IPv4 as text</returns>
public string GetLocalIPv4();
}

View file

@ -1,17 +0,0 @@
namespace DisplayCommands.Internals;
internal enum DisplayCommand : ushort
{
Clear = 0x0002,
Cp437Data = 0x0003,
CharBrightness = 0x0005,
Brightness = 0x0007,
HardReset = 0x000b,
FadeOut = 0x000d,
[Obsolete("ignored by display code")] BitmapLegacy = 0x0010,
BitmapLinear = 0x0012,
BitmapLinearWin = 0x0013,
BitmapLinearAnd = 0x0014,
BitmapLinearOr = 0x0015,
BitmapLinearXor = 0x0016
}

View file

@ -1,126 +0,0 @@
using System.Buffers;
using System.Diagnostics;
using System.Net;
using System.Net.Sockets;
using System.Runtime.InteropServices;
using Microsoft.Extensions.Options;
namespace DisplayCommands.Internals;
internal sealed class DisplayConnection(IOptions<DisplayConfiguration> options) : IDisplayConnection, IDisposable
{
private readonly ArrayPool<byte> _arrayPool = ArrayPool<byte>.Shared;
private readonly UdpClient _udpClient = new(options.Value.Hostname, options.Value.Port);
public ValueTask SendClearAsync()
{
var header = new HeaderWindow { Command = (ushort)DisplayCommand.Clear };
return SendAsync(header, Memory<byte>.Empty);
}
public ValueTask SendCp437DataAsync(ushort x, ushort y, Cp437Grid grid)
{
var header = new HeaderWindow
{
Command = (ushort)DisplayCommand.Cp437Data,
Height = grid.Height,
Width = grid.Width,
PosX = x,
PosY = y
};
return SendAsync(header, grid.Data);
}
public ValueTask SendCharBrightnessAsync(ushort x, ushort y, ByteGrid luma)
{
var header = new HeaderWindow
{
Command = (ushort)DisplayCommand.CharBrightness,
PosX = x,
PosY = y,
Height = luma.Height,
Width = luma.Width
};
return SendAsync(header, luma.Data);
}
public async ValueTask SendBrightnessAsync(byte brightness)
{
var header = new HeaderWindow { Command = (ushort)DisplayCommand.Brightness };
var payloadBuffer = _arrayPool.Rent(1);
var payload = payloadBuffer.AsMemory(0, 1);
payload.Span[0] = brightness;
await SendAsync(header, payload);
_arrayPool.Return(payloadBuffer);
}
public ValueTask SendHardResetAsync()
{
var header = new HeaderWindow { Command = (ushort)DisplayCommand.HardReset };
return SendAsync(header, Memory<byte>.Empty);
}
public async ValueTask SendFadeOutAsync(byte loops)
{
var header = new HeaderWindow { Command = (ushort)DisplayCommand.FadeOut };
var payloadBuffer = _arrayPool.Rent(1);
var payload = payloadBuffer.AsMemory(0, 1);
payload.Span[0] = loops;
await SendAsync(header, payload);
_arrayPool.Return(payloadBuffer);
}
public ValueTask SendBitmapLinearWindowAsync(ushort x, ushort y, PixelGrid pixels)
{
var header = new HeaderWindow
{
Command = (ushort)DisplayCommand.BitmapLinearWin,
PosX = x,
PosY = y,
Width = (ushort)(pixels.Width / 8),
Height = pixels.Height
};
return SendAsync(header, pixels.Data);
}
public string GetLocalIPv4()
{
using var socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, 0);
socket.Connect(options.Value.Hostname, options.Value.Port);
var endPoint = socket.LocalEndPoint as IPEndPoint ?? throw new NotSupportedException();
return endPoint.Address.ToString();
}
private async ValueTask SendAsync(HeaderWindow header, Memory<byte> payload)
{
int headerSize;
unsafe
{
// because we specified the struct layout, no platform-specific padding will be added and this is be safe.
headerSize = sizeof(HeaderWindow);
}
Debug.Assert(headerSize == 10);
var messageSize = headerSize + payload.Length;
var buffer = _arrayPool.Rent(messageSize);
var message = buffer.AsMemory(0, messageSize);
MemoryMarshal.Write(message.Span, header);
payload.CopyTo(message[headerSize..]);
await _udpClient.SendAsync(message);
_arrayPool.Return(buffer);
}
public void Dispose() => _udpClient.Dispose();
}

View file

@ -1,10 +0,0 @@
namespace DisplayCommands.Internals;
internal enum DisplaySubCommand : ushort
{
BitmapNormal = 0x0,
BitmapCompressZ = 0x677a,
BitmapCompressBz = 0x627a,
BitmapCompressLz = 0x6c7a,
BitmapCompressZs = 0x7a73
}

View file

@ -1,19 +0,0 @@
using System.Runtime.InteropServices;
using EndiannessSourceGenerator;
namespace DisplayCommands.Internals;
[StructEndianness(IsLittleEndian = false)]
[StructLayout(LayoutKind.Sequential, Pack = 16, Size = 10)]
internal partial struct HeaderBitmap
{
private ushort _command;
private ushort _offset;
private ushort _length;
private ushort _subCommand;
private ushort _reserved;
}

View file

@ -1,19 +0,0 @@
using System.Runtime.InteropServices;
using EndiannessSourceGenerator;
namespace DisplayCommands.Internals;
[StructEndianness(IsLittleEndian = false)]
[StructLayout(LayoutKind.Sequential, Pack = 16, Size = 10)]
internal partial struct HeaderWindow
{
private ushort _command;
private ushort _posX;
private ushort _posY;
private ushort _width;
private ushort _height;
}

View file

@ -1,59 +0,0 @@
using System.Diagnostics;
namespace DisplayCommands;
public sealed class PixelGrid(ushort width, ushort height) : IEquatable<PixelGrid>
{
private readonly ByteGrid _byteGrid = new((ushort)(width / 8u), height);
public ushort Width { get; } = width;
public ushort Height { get; } = height;
public Memory<byte> Data => _byteGrid.Data;
public bool this[ushort x, ushort y]
{
get
{
Debug.Assert(y < Height);
var (byteIndex, bitInByteMask) = GetIndexes(x);
var byteVal = _byteGrid[byteIndex, y];
return (byteVal & bitInByteMask) != 0;
}
set
{
Debug.Assert(y < Height);
var (byteIndex, bitInByteMask) = GetIndexes(x);
if (value)
_byteGrid[byteIndex, y] |= bitInByteMask;
else
_byteGrid[byteIndex, y] &= (byte)(ushort.MaxValue ^ bitInByteMask);
}
}
public void Clear() => _byteGrid.Clear();
public bool Equals(PixelGrid? other)
{
if (ReferenceEquals(null, other)) return false;
if (ReferenceEquals(this, other)) return true;
return Width == other.Width && Height == other.Height && _byteGrid.Equals(other._byteGrid);
}
public override bool Equals(object? obj) => ReferenceEquals(this, obj) || (obj is PixelGrid other && Equals(other));
public override int GetHashCode() => HashCode.Combine(_byteGrid, Width, Height);
public static bool operator ==(PixelGrid? left, PixelGrid? right) => Equals(left, right);
public static bool operator !=(PixelGrid? left, PixelGrid? right) => !Equals(left, right);
private (ushort byteIndex, byte bitInByteMask) GetIndexes(int x)
{
Debug.Assert(x < Width);
var byteIndex = (ushort)(x / 8);
Debug.Assert(byteIndex < Width);
var bitInByteIndex = (byte)(7 - x % 8);
Debug.Assert(bitInByteIndex < 8);
var bitInByteMask = (byte)(1 << bitInByteIndex);
return (byteIndex, bitInByteMask);
}
}

View file

@ -1,260 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Text;
using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;
namespace EndiannessSourceGenerator;
internal class DebugMeException(string message) : Exception(message);
internal class InvalidUsageException(string message) : Exception(message);
[Generator]
public class StructEndiannessSourceGenerator : ISourceGenerator
{
private const string Namespace = "EndiannessSourceGenerator";
private const string AttributeName = "StructEndiannessAttribute";
private const string IsLittleEndianProperty = "IsLittleEndian";
private const string AttributeSourceCode =
$$"""
// <auto-generated/>
namespace {{Namespace}}
{
[System.AttributeUsage(System.AttributeTargets.Struct)]
public class {{AttributeName}}: System.Attribute
{
public required bool {{IsLittleEndianProperty}} { get; init; }
}
}
""";
public void Initialize(GeneratorInitializationContext context)
{
// Register the attribute source
context.RegisterForPostInitialization(i => i.AddSource($"{AttributeName}.g.cs", AttributeSourceCode));
}
public void Execute(GeneratorExecutionContext context)
{
var treesWithStructsWithAttributes = context.Compilation.SyntaxTrees
.Where(st => st.GetRoot().DescendantNodes()
.OfType<StructDeclarationSyntax>()
.Any(p => p.DescendantNodes()
.OfType<AttributeSyntax>()
.Any()))
.ToList();
foreach (var tree in treesWithStructsWithAttributes)
{
var semanticModel = context.Compilation.GetSemanticModel(tree);
var structsWithAttributes = tree.GetRoot().DescendantNodes()
.OfType<StructDeclarationSyntax>()
.Where(cd => cd.DescendantNodes()
.OfType<AttributeSyntax>()
.Any());
foreach (var structDeclaration in structsWithAttributes)
{
var foundAttribute = GetEndiannessAttribute(structDeclaration, semanticModel);
if (foundAttribute == null)
continue; // not my type
var structIsLittleEndian = GetStructIsLittleEndian(foundAttribute);
HandleStruct(context, structDeclaration, semanticModel, structIsLittleEndian);
}
}
}
private static void HandleStruct(GeneratorExecutionContext context, TypeDeclarationSyntax structDeclaration,
SemanticModel semanticModel, bool structIsLittleEndian)
{
var isPartial = structDeclaration.Modifiers.Any(m => m.IsKind(SyntaxKind.PartialKeyword));
if (!isPartial)
throw new InvalidUsageException("struct is not marked partial");
var accessibilityModifier = structDeclaration.Modifiers.Any(m => m.IsKind(SyntaxKind.InternalKeyword))
? Token(SyntaxKind.InternalKeyword)
: Token(SyntaxKind.PublicKeyword);
var structType = semanticModel.GetDeclaredSymbol(structDeclaration);
if (structType == null)
throw new DebugMeException("struct type info is null");
var structNamespace = structType.ContainingNamespace?.ToDisplayString();
if (structNamespace == null)
throw new InvalidUsageException("struct has to be contained in a namespace");
if (structDeclaration.Members.Any(m => m.IsKind(SyntaxKind.PropertyDeclaration)))
throw new InvalidUsageException("struct cannot have properties");
var fieldDeclarations = structDeclaration.Members
.Where(m => m.IsKind(SyntaxKind.FieldDeclaration)).OfType<FieldDeclarationSyntax>();
var generatedCode = CompilationUnit()
.WithUsings(List<UsingDirectiveSyntax>([
UsingDirective(IdentifierName("System")),
UsingDirective(IdentifierName("System.Buffers.Binary"))
]))
.WithMembers(List<MemberDeclarationSyntax>([
FileScopedNamespaceDeclaration(IdentifierName(structNamespace)),
StructDeclaration(structType.Name)
.WithModifiers(TokenList([accessibilityModifier, Token(SyntaxKind.PartialKeyword)]))
.WithMembers(GenerateStructProperties(fieldDeclarations, semanticModel, structIsLittleEndian))
]))
.NormalizeWhitespace()
.ToFullString();
context.AddSource(
$"{structNamespace}.{structType.Name}.g.cs",
SourceText.From(generatedCode, Encoding.UTF8)
);
}
private static SyntaxList<MemberDeclarationSyntax> GenerateStructProperties(
IEnumerable<FieldDeclarationSyntax> fieldDeclarations, SemanticModel semanticModel, bool structIsLittleEndian)
{
var result = new List<MemberDeclarationSyntax>();
foreach (var field in fieldDeclarations)
{
if (!field.Modifiers.Any(m => m.IsKind(SyntaxKind.PrivateKeyword)))
throw new InvalidUsageException("fields have to be private");
var variableDeclaration = field.DescendantNodes()
.OfType<VariableDeclarationSyntax>()
.FirstOrDefault();
if (variableDeclaration == null)
throw new DebugMeException("variable declaration of field declaration null");
var variableTypeInfo = semanticModel.GetTypeInfo(variableDeclaration.Type).Type;
if (variableTypeInfo == null)
throw new DebugMeException("variable type info of field declaration null");
var typeName = variableTypeInfo.ToDisplayString();
var fieldName = variableDeclaration.Variables.First().Identifier.ToString();
result.Add(GenerateProperty(typeName, structIsLittleEndian, fieldName));
}
return new SyntaxList<MemberDeclarationSyntax>(result);
}
private static PropertyDeclarationSyntax GenerateProperty(string typeName,
bool structIsLittleEndian, string fieldName)
{
var propertyName = GeneratePropertyName(fieldName);
var fieldIdentifier = IdentifierName(fieldName);
ExpressionSyntax condition = MemberAccessExpression(
kind: SyntaxKind.SimpleMemberAccessExpression,
expression: IdentifierName("BitConverter"),
name: IdentifierName("IsLittleEndian")
);
if (!structIsLittleEndian)
condition = PrefixUnaryExpression(SyntaxKind.LogicalNotExpression, condition);
var reverseEndiannessMethod = MemberAccessExpression(
kind: SyntaxKind.SimpleMemberAccessExpression,
expression: IdentifierName("BinaryPrimitives"),
name: IdentifierName("ReverseEndianness")
);
var valueIdentifier = IdentifierName("value");
return PropertyDeclaration(ParseTypeName(typeName), propertyName)
.WithModifiers(TokenList([Token(SyntaxKind.PublicKeyword)]))
.WithAccessorList(AccessorList(List<AccessorDeclarationSyntax>([
AccessorDeclaration(SyntaxKind.GetAccessorDeclaration)
.WithExpressionBody(ArrowExpressionClause(ConditionalExpression(
condition: condition,
whenTrue: fieldIdentifier,
whenFalse: InvocationExpression(
expression: reverseEndiannessMethod,
argumentList: ArgumentList(SingletonSeparatedList(
Argument(fieldIdentifier)
))
)
)))
.WithSemicolonToken(Token(SyntaxKind.SemicolonToken)),
AccessorDeclaration(SyntaxKind.SetAccessorDeclaration)
.WithExpressionBody(ArrowExpressionClause(AssignmentExpression(
kind: SyntaxKind.SimpleAssignmentExpression,
left: fieldIdentifier,
right: ConditionalExpression(
condition: condition,
whenTrue: valueIdentifier,
whenFalse: InvocationExpression(
expression: reverseEndiannessMethod,
argumentList: ArgumentList(SingletonSeparatedList(
Argument(valueIdentifier)
))
)
)
)))
.WithSemicolonToken(Token(SyntaxKind.SemicolonToken))
]))
);
}
private static SyntaxToken GeneratePropertyName(string fieldName)
{
var propertyName = fieldName;
if (propertyName.StartsWith("_"))
propertyName = propertyName.Substring(1);
if (!char.IsLetter(propertyName, 0) || char.IsUpper(propertyName, 0))
throw new InvalidUsageException("field names have to start with a lower case letter");
propertyName = propertyName.Substring(0, 1).ToUpperInvariant()
+ propertyName.Substring(1);
return Identifier(propertyName);
}
private static AttributeSyntax? GetEndiannessAttribute(SyntaxNode structDeclaration, SemanticModel semanticModel)
{
AttributeSyntax? foundAttribute = null;
foreach (var attributeSyntax in structDeclaration.DescendantNodes().OfType<AttributeSyntax>())
{
var attributeTypeInfo = semanticModel.GetTypeInfo(attributeSyntax).Type;
if (attributeTypeInfo == null)
throw new DebugMeException("attribute type info is null");
if (attributeTypeInfo.ContainingNamespace?.Name != Namespace)
continue;
if (attributeTypeInfo.Name != AttributeName)
continue;
foundAttribute = attributeSyntax;
break;
}
return foundAttribute;
}
private static bool GetStructIsLittleEndian(AttributeSyntax foundAttribute)
{
var endiannessArguments = foundAttribute.ArgumentList;
if (endiannessArguments == null)
throw new InvalidUsageException("endianness attribute has no arguments");
var isLittleEndianArgumentSyntax = endiannessArguments.Arguments
.FirstOrDefault(argumentSyntax =>
argumentSyntax.NameEquals?.Name.Identifier.ToString() == IsLittleEndianProperty);
if (isLittleEndianArgumentSyntax == null)
throw new InvalidUsageException("endianness attribute argument not found");
bool? structIsLittleEndian = isLittleEndianArgumentSyntax.Expression.Kind() switch
{
SyntaxKind.FalseLiteralExpression => false,
SyntaxKind.TrueLiteralExpression => true,
SyntaxKind.DefaultLiteralExpression => false,
_ => throw new InvalidUsageException($"{IsLittleEndianProperty} has to be set with a literal")
};
return structIsLittleEndian.Value;
}
}

View file

@ -1,25 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="../shared.props" />
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<IsPackable>false</IsPackable>
<Nullable>enable</Nullable>
<LangVersion>latest</LangVersion>
<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
<IsRoslynComponent>true</IsRoslynComponent>
<PackageId>EndiannessSourceGenerator</PackageId>
<IsAotCompatible>false</IsAotCompatible>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.9.2" PrivateAssets="all" />
</ItemGroup>
</Project>

View file

@ -1,9 +0,0 @@
{
"$schema": "http://json.schemastore.org/launchsettings.json",
"profiles": {
"Generators": {
"commandName": "DebugRoslynComponent",
"targetProject": "../DisplayCommands/DisplayCommands.csproj"
}
}
}

View file

@ -1,5 +0,0 @@
# Endianness Source Generator
When annotating a struct with the `StructEndianness` attribute, this code generator will generate properties for the declared fields.
Each time a property is read or written, the endianness is converted from runtime endianness to struct endianness or vice-versa.

View file

@ -2,10 +2,6 @@
Microsoft Visual Studio Solution File, Format Version 12.00
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TanksServer", "TanksServer\TanksServer.csproj", "{D88BF376-47A4-4C72-ADD1-983F9285C351}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DisplayCommands", "DisplayCommands\DisplayCommands.csproj", "{B4B43561-7A2C-486B-99F7-E58A67BC370A}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EndiannessSourceGenerator", "EndiannessSourceGenerator\EndiannessSourceGenerator.csproj", "{D77FE880-F2B8-43B6-8B33-B6FA089CC337}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "shared", "shared", "{12DB7D48-1BB2-488B-B4D9-4126087D2F8C}"
ProjectSection(SolutionItems) = preProject
global.json = global.json
@ -13,6 +9,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "shared", "shared", "{12DB7D
Dockerfile = Dockerfile
EndProjectSection
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ServicePoint2", "..\..\servicepoint2\servicepoint2-binding-cs\ServicePoint2\ServicePoint2.csproj", "{BC27CED4-82FE-4CD4-B784-B7596586801D}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -23,13 +21,9 @@ Global
{D88BF376-47A4-4C72-ADD1-983F9285C351}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D88BF376-47A4-4C72-ADD1-983F9285C351}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D88BF376-47A4-4C72-ADD1-983F9285C351}.Release|Any CPU.Build.0 = Release|Any CPU
{B4B43561-7A2C-486B-99F7-E58A67BC370A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B4B43561-7A2C-486B-99F7-E58A67BC370A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B4B43561-7A2C-486B-99F7-E58A67BC370A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B4B43561-7A2C-486B-99F7-E58A67BC370A}.Release|Any CPU.Build.0 = Release|Any CPU
{D77FE880-F2B8-43B6-8B33-B6FA089CC337}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{D77FE880-F2B8-43B6-8B33-B6FA089CC337}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D77FE880-F2B8-43B6-8B33-B6FA089CC337}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D77FE880-F2B8-43B6-8B33-B6FA089CC337}.Release|Any CPU.Build.0 = Release|Any CPU
{BC27CED4-82FE-4CD4-B784-B7596586801D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{BC27CED4-82FE-4CD4-B784-B7596586801D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{BC27CED4-82FE-4CD4-B784-B7596586801D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{BC27CED4-82FE-4CD4-B784-B7596586801D}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
EndGlobal

View file

@ -1,13 +1,13 @@
using System.IO;
using System.Text;
using System.Text.Json;
using DisplayCommands;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Diagnostics.HealthChecks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.HttpResults;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Diagnostics.HealthChecks;
using ServicePoint2;
using TanksServer.GameLogic;
using TanksServer.Interactivity;
@ -19,7 +19,7 @@ internal sealed class Endpoints(
ControlsServer controlsServer,
MapService mapService,
ChangeToRequestedMap changeToRequestedMap,
IDisplayConnection displayConnection
Connection displayConnection
)
{
public void Map(WebApplication app)
@ -31,7 +31,7 @@ internal sealed class Endpoints(
app.Map("/controls", ConnectControlsAsync);
app.MapGet("/map", () => mapService.MapNames);
app.MapPost("/map", PostMap);
app.MapPost("/resetDisplay", displayConnection.SendHardResetAsync);
app.MapPost("/resetDisplay", () => displayConnection.Send(Command.HardReset()));
app.MapGet("/map/{name}", GetMapByName);
app.MapHealthChecks("/health", new HealthCheckOptions
@ -117,7 +117,7 @@ internal sealed class Endpoints(
if (!mapService.TryGetPreview(name, out var preview))
return TypedResults.NotFound();
var mapInfo = new MapInfo(prototype.Name, prototype.GetType().Name, preview.Data);
var mapInfo = new MapInfo(prototype.Name, prototype.GetType().Name, preview.Data.ToArray());
return TypedResults.Ok(mapInfo);
}

View file

@ -1,7 +1,7 @@
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using DisplayCommands;
using ServicePoint2;
using TanksServer.Graphics;
namespace TanksServer.GameLogic;
@ -42,7 +42,7 @@ internal sealed class MapService
if (!_mapPrototypes.TryGetValue(name, out var prototype))
return false; // name not found
pixelGrid = new PixelGrid(PixelsPerRow, PixelsPerColumn);
pixelGrid = PixelGrid.New(PixelsPerRow, PixelsPerColumn);
DrawMapStep.Draw(pixelGrid, prototype.CreateInstance());
_mapPreviews.TryAdd(name, pixelGrid); // another thread may have added the map already

View file

@ -1,5 +1,3 @@
using System.Diagnostics;
namespace TanksServer.GameLogic;
internal sealed class SpawnPowerUp(

View file

@ -1,4 +1,4 @@
using DisplayCommands;
using ServicePoint2;
using TanksServer.GameLogic;
namespace TanksServer.Graphics;

View file

@ -1,4 +1,4 @@
using DisplayCommands;
using ServicePoint2;
using TanksServer.GameLogic;
using TanksServer.Interactivity;
@ -10,9 +10,9 @@ internal sealed class GeneratePixelsTickStep(
) : ITickStep
{
private GamePixelGrid _lastGamePixelGrid = new(MapService.PixelsPerRow, MapService.PixelsPerColumn);
private PixelGrid _lastObserverPixelGrid = new(MapService.PixelsPerRow, MapService.PixelsPerColumn);
private PixelGrid _lastObserverPixelGrid = PixelGrid.New(MapService.PixelsPerRow, MapService.PixelsPerColumn);
private GamePixelGrid _gamePixelGrid = new(MapService.PixelsPerRow, MapService.PixelsPerColumn);
private PixelGrid _observerPixelGrid = new(MapService.PixelsPerRow, MapService.PixelsPerColumn);
private PixelGrid _observerPixelGrid = PixelGrid.New(MapService.PixelsPerRow, MapService.PixelsPerColumn);
private readonly List<IDrawStep> _drawSteps = drawSteps.ToList();
private readonly List<IFrameConsumer> _consumers = consumers.ToList();
@ -20,7 +20,7 @@ internal sealed class GeneratePixelsTickStep(
public async ValueTask TickAsync(TimeSpan _)
{
Draw(_gamePixelGrid, _observerPixelGrid);
if (_observerPixelGrid.Data.Span.SequenceEqual(_lastObserverPixelGrid.Data.Span))
if (_observerPixelGrid.Data.SequenceEqual(_lastObserverPixelGrid.Data))
return;
await _consumers.Select(c => c.OnFrameDoneAsync(_gamePixelGrid, _observerPixelGrid))
@ -36,7 +36,7 @@ internal sealed class GeneratePixelsTickStep(
foreach (var step in _drawSteps)
step.Draw(gamePixelGrid);
observerPixelGrid.Clear();
observerPixelGrid.Fill(false);
for (var y = 0; y < MapService.PixelsPerColumn; y++)
for (var x = 0; x < MapService.PixelsPerRow; x++)
{

View file

@ -1,4 +1,4 @@
using DisplayCommands;
using ServicePoint2;
namespace TanksServer.Graphics;

View file

@ -1,5 +1,5 @@
using System.Net.WebSockets;
using DisplayCommands;
using ServicePoint2;
using TanksServer.Graphics;
namespace TanksServer.Interactivity;

View file

@ -1,6 +1,6 @@
using System.Buffers;
using System.Net.WebSockets;
using DisplayCommands;
using ServicePoint2;
using TanksServer.Graphics;
namespace TanksServer.Interactivity;
@ -36,8 +36,9 @@ internal sealed class ClientScreenServerConnection
private Package BuildNextPackage(PixelGrid pixels, GamePixelGrid gamePixelGrid)
{
var nextPixels = _bufferPool.Rent(pixels.Data.Length);
pixels.Data.CopyTo(nextPixels.Memory);
var pixelsData = pixels.Data;
var nextPixels = _bufferPool.Rent(pixelsData.Length);
pixelsData.CopyTo(nextPixels.Memory.Span);
if (_playerDataBuilder == null)
return new Package(nextPixels, null);

View file

@ -13,7 +13,7 @@ internal sealed class PlayerInfoConnection
private readonly MapEntityManager _entityManager;
private readonly BufferPool _bufferPool;
private readonly MemoryStream _tempStream = new();
private IMemoryOwner<byte>? _lastMessage = null;
private IMemoryOwner<byte>? _lastMessage;
public PlayerInfoConnection(
Player player,

View file

@ -1,6 +1,7 @@
using System.Diagnostics;
using System.Net;
using System.Net.Sockets;
using DisplayCommands;
using ServicePoint2;
using TanksServer.GameLogic;
using TanksServer.Graphics;
@ -12,11 +13,11 @@ internal sealed class SendToServicePointDisplay : IFrameConsumer
private const int ScoresHeight = 20;
private const int ScoresPlayerRows = ScoresHeight - 6;
private readonly IDisplayConnection _displayConnection;
private readonly Connection _displayConnection;
private readonly MapService _mapService;
private readonly ILogger<SendToServicePointDisplay> _logger;
private readonly PlayerServer _players;
private readonly Cp437Grid _scoresBuffer;
private readonly ByteGrid _scoresBuffer;
private readonly TimeSpan _minFrameTime;
private readonly IOptionsMonitor<HostConfiguration> _options;
@ -26,11 +27,11 @@ internal sealed class SendToServicePointDisplay : IFrameConsumer
public SendToServicePointDisplay(
PlayerServer players,
ILogger<SendToServicePointDisplay> logger,
IDisplayConnection displayConnection,
Connection displayConnection,
IOptions<HostConfiguration> hostOptions,
MapService mapService,
IOptionsMonitor<HostConfiguration> options
)
IOptionsMonitor<HostConfiguration> options,
IOptions<DisplayConfiguration> displayConfig)
{
_players = players;
_logger = logger;
@ -39,16 +40,15 @@ internal sealed class SendToServicePointDisplay : IFrameConsumer
_minFrameTime = TimeSpan.FromMilliseconds(hostOptions.Value.ServicePointDisplayMinFrameTimeMs);
_options = options;
var localIp = _displayConnection.GetLocalIPv4().Split('.');
var localIp = GetLocalIPv4(displayConfig.Value).Split('.');
Debug.Assert(localIp.Length == 4);
_scoresBuffer = new Cp437Grid(12, 20)
{
[00] = "== TANKS! ==",
[01] = "-- scores --",
[17] = "-- join --",
[18] = string.Join('.', localIp[..2]),
[19] = string.Join('.', localIp[2..])
};
_scoresBuffer = ByteGrid.New(12, 20);
_scoresBuffer[00] = "== TANKS! ==";
_scoresBuffer[01] = "-- scores --";
_scoresBuffer[17] = "-- join --";
_scoresBuffer[18] = string.Join('.', localIp[..2]);
_scoresBuffer[19] = string.Join('.', localIp[2..]);
}
public async Task OnFrameDoneAsync(GamePixelGrid gamePixelGrid, PixelGrid observerPixels)
@ -66,8 +66,8 @@ internal sealed class SendToServicePointDisplay : IFrameConsumer
try
{
await _displayConnection.SendBitmapLinearWindowAsync(0, 0, observerPixels);
await _displayConnection.SendCp437DataAsync(MapService.TilesPerRow, 0, _scoresBuffer);
_displayConnection.Send(Command.BitmapLinearWin(0, 0, observerPixels.Clone()));
_displayConnection.Send(Command.Cp437Data(MapService.TilesPerRow, 0, _scoresBuffer.Clone()));
}
catch (SocketException ex)
{
@ -103,4 +103,12 @@ internal sealed class SendToServicePointDisplay : IFrameConsumer
_scoresBuffer[16] = _mapService.Current.Name[..(Math.Min(ScoresWidth, _mapService.Current.Name.Length) - 1)];
}
private static string GetLocalIPv4(DisplayConfiguration configuration)
{
using var socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, 0);
socket.Connect(configuration.Hostname, configuration.Port);
var endPoint = socket.LocalEndPoint as IPEndPoint ?? throw new NotSupportedException();
return endPoint.Address.ToString();
}
}

View file

@ -1,8 +1,8 @@
namespace DisplayCommands;
namespace TanksServer.Models;
public class DisplayConfiguration
{
public string Hostname { get; set; } = "172.23.42.29";
public int Port { get; set; } = 2342;
}
}

View file

@ -1,8 +1,9 @@
using System.IO;
using DisplayCommands;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.FileProviders;
using ServicePoint2;
using SixLabors.ImageSharp;
using TanksServer.GameLogic;
using TanksServer.Graphics;
using TanksServer.Interactivity;
@ -94,9 +95,14 @@ public static class Program
builder.Services.Configure<GameRules>(builder.Configuration.GetSection("GameRules"));
builder.Services.Configure<HostConfiguration>(builder.Configuration.GetSection("Host"));
builder.Services.Configure<DisplayConfiguration>(builder.Configuration.GetSection("ServicePointDisplay"));
builder.Services.AddSingleton<IFrameConsumer, SendToServicePointDisplay>();
builder.Services.AddDisplay(builder.Configuration.GetSection("ServicePointDisplay"));
builder.Services.AddSingleton<Connection>(sp =>
{
var config = sp.GetRequiredService<IOptions<DisplayConfiguration>>().Value;
return Connection.Open($"{config.Hostname}:{config.Port}");
});
var app = builder.Build();

View file

@ -10,11 +10,14 @@
<PackageReference Include="DotNext.Threading" Version="5.3.0" />
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="8.0.0" />
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.4" />
<ProjectReference Include="../DisplayCommands/DisplayCommands.csproj" />
</ItemGroup>
<ItemGroup>
<None Include="./assets/**" CopyToOutputDirectory="PreserveNewest" CopyToPublishDirectory="Always"/>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\servicepoint2\servicepoint2-binding-cs\ServicePoint2\ServicePoint2.csproj" />
</ItemGroup>
</Project>

View file

@ -16,7 +16,8 @@
}
},
"ServicePointDisplay": {
"Hostname": "172.23.42.29",
//"Hostname": "172.23.42.29",
"Hostname": "127.0.0.1",
"Port": 2342
},
"GameRules": {