feat: 项目初始化 + 3D方块世界原型 + AI助搭系统
CI / Go Backend (push) Canceled after 0s

初始化 monorepo: Go后端(7微服务) + Unity客户端(9模块) + 启动器

HTML5原型: Three.js 3D体素世界, Perlin噪声地形, 原版材质, 22种方块

Minecraft创造模式背包: 双栏布局, 拖拽移动物品, 方向性元件引脚

AI助搭策划文档 + 客户端/服务端骨架 + Docker Compose + CI
This commit is contained in:
xyou
2026-08-08 14:07:56 +08:00
parent 9500c4c80a
commit f70b061d1a
1972 changed files with 159760 additions and 6 deletions
@@ -0,0 +1,103 @@
using System;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Runtime.Caching;
using System.Threading;
using System.Threading.Tasks;
using Ae.Dns.Client;
using Ae.Dns.Protocol;
using Ae.Dns.Protocol.Enums;
using Ae.Dns.Protocol.Records;
using PCL.Core.IO.Net.Http;
using PCL.Core.Logging;
namespace PCL.Core.IO.Net.Dns;
public class DnsQuery : IDisposable
{
private const string ModuleName = "DoH query";
public static DnsQuery Instance { get; } = new();
private readonly DnsCachingClient _resolver;
private readonly HttpClient[] _httpClients;
private DnsQuery()
{
var proxyHandler = new HttpClientHandler()
{
Proxy = HttpProxyManager.Instance
};
// 使用Ae.Dns创建DoH客户端,支持多个DoH服务器
_httpClients =
[
new HttpClient(proxyHandler)
{
BaseAddress = new Uri("https://doh.pub/")
},
new HttpClient(proxyHandler)
{
BaseAddress = new Uri("https://doh.pysio.online/")
},
new HttpClient(proxyHandler)
{
BaseAddress = new Uri("https://cloudflare-dns.com/")
}
];
_resolver = new DnsCachingClient(
new WeightedDnsRacerClient(2, _httpClients.Select(static x => new DnsHttpClient(x)).ToArray<IDnsClient>()),
new MemoryCache("DoH Query Cache"));
}
public async Task<DnsMessage?> QueryAsync(string host, DnsQueryType qType, CancellationToken cts = default)
{
try
{
return await _resolver.Query(DnsQueryFactory.CreateQuery(host, qType), cts);
}
catch (TaskCanceledException)
{
throw;
}
catch (Exception ex)
{
LogWrapper.Warn(ModuleName, $"Failed to resolve DNS for {host}: {ex.Message}, use system default DNS");
}
return null;
}
public async Task<IPAddress[]?> QueryForIpAsync(string host, CancellationToken cts = default)
{
var queryResponse = await Task.WhenAll(
[
QueryAsync(host, DnsQueryType.A, cts),
QueryAsync(host, DnsQueryType.AAAA, cts)
]
);
if (queryResponse.All(static x => x is null))
{
LogWrapper.Warn(ModuleName, $"Failed to query IP for host {host} using DoH, use system default DNS");
return await System.Net.Dns.GetHostAddressesAsync(host, cts);
}
return queryResponse.Where(static x => x is not null)
.SelectMany(static x => x!.Answers)
.Where(static x => x.Type is DnsQueryType.A or DnsQueryType.AAAA)
.Select(static x => x.Resource as DnsIpAddressResource)
.Where(static x => x is not null)
.Select(static x => x!.IPAddress)
.ToArray();
}
public void Dispose()
{
GC.SuppressFinalize(this);
foreach (var client in _httpClients)
{
client.Dispose();
}
_resolver.Dispose(); // 好像这个包的 Dispose 并没有做什么 lol
}
}
@@ -0,0 +1,67 @@
using System;
using System.Buffers.Binary;
using System.Collections.Generic;
using System.Text;
using Ae.Dns.Protocol.Records;
namespace PCL.Core.IO.Net.Dns;
public class DnsSrvResource : IDnsResource
{
public string Target { get; set; } = "";
public int Weight { get; set; }
public int Priority { get; set; }
public int Port { get; set; }
public void WriteBytes(Memory<byte> bytes, ref int offset)
{
// 6 Bytes for priority, weight, port and 2 bytes for length
var buf = bytes.Span[offset..];
BinaryPrimitives.WriteUInt16BigEndian(buf[offset..(offset + 2)], (ushort)Priority);
offset += 2;
BinaryPrimitives.WriteUInt16BigEndian(buf[offset..(offset + 2)], (ushort)Weight);
offset += 2;
BinaryPrimitives.WriteUInt16BigEndian(buf[offset..(offset + 2)], (ushort)Port);
offset += 2;
// target string
foreach (var seg in Target.Split('.'))
{
var segBuf = Encoding.UTF8.GetBytes(seg);
var segLength = (byte)segBuf.Length;
buf[offset] = segLength;
offset++;
segBuf.CopyTo(buf[offset..]);
offset += segBuf.Length;
}
}
public void ReadBytes(ReadOnlyMemory<byte> bytes, ref int offset, int length)
{
var buf = bytes.Slice(offset, length).Span;
offset += length;
var priorityBuf = buf[..2];
Priority = BinaryPrimitives.ReadUInt16BigEndian(priorityBuf);
var weightBuf = buf[2..4];
Weight = BinaryPrimitives.ReadUInt16BigEndian(weightBuf);
var portBuf = buf[4..6];
Port = BinaryPrimitives.ReadUInt16BigEndian(portBuf);
// left target buf. 1 Byte for length, then the string. Until we got end of string.
var segments = new List<string>();
var i = 6;
while (i < length)
{
var segLength = buf[i];
if (segLength == 0) break;
i++;
if (i + segLength > length)
{
throw new ArgumentException("Invalid DNS SRV resource record: segment length exceeds buffer size");
}
segments.Add(Encoding.UTF8.GetString(buf.Slice(i, segLength)));
i += segLength;
}
Target = string.Join('.', segments);
}
}
@@ -0,0 +1,79 @@
using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Ae.Dns.Protocol;
using PCL.Core.Utils.Exts;
namespace PCL.Core.IO.Net.Dns;
public sealed class WeightedDnsRacerClient : IDnsClient
{
private readonly (IDnsClient Client, byte Weight)[] _clients;
private readonly int _concurrentCount;
private bool _updateWeigh = true;
private readonly object _lock = new();
public WeightedDnsRacerClient(int concurrentCount, params IDnsClient[] clients)
{
if (clients is null || clients.Length < concurrentCount)
throw new ArgumentException("Not enough clients.");
_concurrentCount = concurrentCount;
_clients = clients.Select(static c => (Client: c, Weight: (byte)0)).ToArray();
}
public async Task<DnsMessage> Query(DnsMessage query, CancellationToken ct = default)
{
(IDnsClient Client, byte Weight)[] candidates;
lock (_lock)
{
candidates = _clients
.OrderByDescending(static x => x.Weight)
.Take(_concurrentCount)
.ToArray();
}
using var cts = CancellationTokenSource.CreateLinkedTokenSource(ct);
var tasks = new Task<DnsMessage>[candidates.Length];
for (var i = 0; i < candidates.Length; i++)
{
var task = candidates[i].Client.Query(query, cts.Token);
task.Forget();
tasks[i] = task;
}
var winnerTask = await tasks.WhenAnySuccessAsync().ConfigureAwait(false);
if (winnerTask is null) throw new InvalidOperationException("All queries failed.");
var winner = candidates[Array.IndexOf(tasks, winnerTask)].Client;
if (_updateWeigh)
{
lock (_lock)
{
for (var i = 0; i < _clients.Length; i++)
{
if (_clients[i].Client == winner)
{
if (_clients[i].Weight < 255)
_clients[i] = (_clients[i].Client, (byte)(_clients[i].Weight + 1));
else
_updateWeigh = false;
break;
}
}
}
}
cts.Cancel();
return await winnerTask.ConfigureAwait(false);
}
public void Dispose()
{
foreach (var (client, _) in _clients)
client?.Dispose();
}
}