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,231 @@
using Microsoft.Data.Sqlite;
using PCL.Core.Utils.Exts;
using System;
using System.Collections.Concurrent;
using System.IO;
using System.Threading.Tasks;
namespace PCL.Core.Utils.Hash;
public class HashCache
{
private readonly string _dbPath;
public HashCache(string dbPath)
{
_dbPath = dbPath ?? throw new ArgumentNullException(nameof(dbPath));
var dir = Path.GetDirectoryName(Path.GetFullPath(_dbPath));
if (!string.IsNullOrEmpty(dir) && !Directory.Exists(dir))
Directory.CreateDirectory(dir);
_Initialize();
}
private void _Initialize()
{
using var connection = _CreateConnection();
using var cmd = connection.CreateCommand();
cmd.CommandText = """
CREATE TABLE IF NOT EXISTS HashCache (
FilePath TEXT NOT NULL PRIMARY KEY,
FileSize INTEGER NOT NULL,
LastWriteTime TEXT NOT NULL,
MD5 TEXT NULL,
SHA1 TEXT NULL,
SHA256 TEXT NULL,
SHA512 TEXT NULL,
MurmurHash2 TEXT NULL
)
""";
cmd.ExecuteNonQuery();
using var setCmd = connection.CreateCommand();
setCmd.CommandText = "PRAGMA journal_mode=WAL";
setCmd.ExecuteNonQuery();
}
private SqliteConnection _CreateConnection()
{
var connection = new SqliteConnection($"Data Source={_dbPath};Pooling=True");
connection.Open();
return connection;
}
public Task<string> GetMD5Async(string filePath) =>
_GetHashWithPending(filePath, MD5Provider.Instance, "MD5");
public Task<string> GetSHA1Async(string filePath) =>
_GetHashWithPending(filePath, SHA1Provider.Instance, "SHA1");
public Task<string> GetSHA256Async(string filePath) =>
_GetHashWithPending(filePath, SHA256Provider.Instance, "SHA256");
public Task<string> GetSHA512Async(string filePath) =>
_GetHashWithPending(filePath, SHA512Provider.Instance, "SHA512");
public Task<string> GetMurmurHash2Async(string filePath) =>
_GetHashWithPending(filePath, MurmurHash2Provider.Instance, "MurmurHash2");
private static readonly ConcurrentDictionary<string, Task<string>> _FileHashComputePending = new();
public async Task<string> GetHashAsync(string filePath, IHashProvider provider)
{
var algoName = provider switch
{
MD5Provider => "MD5",
SHA1Provider => "SHA1",
SHA256Provider => "SHA256",
SHA512Provider => "SHA512",
MurmurHash2Provider => "MurmurHash2",
_ => throw new ArgumentException($"不支持的哈希算法: {provider.GetType().Name}")
};
var computeKey = $"{filePath}:{algoName}";
return await _GetHashWithPending(filePath, provider, algoName).ConfigureAwait(false);
}
private Task<string> _GetHashWithPending(string filePath, IHashProvider provider, string algoName)
{
var computeKey = $"{filePath}:{algoName}";
return _FileHashComputePending.GetOrAdd(computeKey, key =>
{
var computeTask = _GetHashAsync(filePath, provider, algoName);
_ = computeTask.ContinueWith(t =>
{
_FileHashComputePending.TryRemove(computeKey, out _);
}, TaskContinuationOptions.ExecuteSynchronously);
return computeTask;
});
}
private async Task<string> _GetHashAsync(string filePath, IHashProvider provider, string algoName)
{
if (string.IsNullOrEmpty(filePath))
throw new ArgumentNullException(nameof(filePath));
var fullPath = Path.GetFullPath(filePath);
try
{
var fileInfo = new FileInfo(fullPath);
var fileSize = fileInfo.Length;
var lastWrite = fileInfo.LastWriteTimeUtc.ToString("O");
var cached = await _FindCacheEntryAsync(fullPath).ConfigureAwait(false);
if (cached != null)
{
if (cached.FileSize == fileSize && cached.LastWriteTime == lastWrite)
{
var hash = _GetHashFromEntry(cached, algoName);
if (hash != null)
return hash;
var computedHash = await _ComputeHashAsync(fullPath, provider).ConfigureAwait(false);
await _InsertOrUpdateHashAsync(fullPath, fileSize, lastWrite, algoName, computedHash).ConfigureAwait(false);
return computedHash;
}
else
{
await _DeleteCacheEntryAsync(fullPath).ConfigureAwait(false);
}
}
var computed = await _ComputeHashAsync(fullPath, provider).ConfigureAwait(false);
await _InsertOrUpdateHashAsync(fullPath, fileSize, lastWrite, algoName, computed).ConfigureAwait(false);
return computed;
}
catch (FileNotFoundException)
{
await _DeleteCacheEntryAsync(fullPath).ConfigureAwait(false);
throw;
}
}
private static async Task<string> _ComputeHashAsync(string fullPath, IHashProvider provider)
{
using FileStream fs = new(fullPath, FileMode.Open, FileAccess.Read, FileShare.Read);
return (await provider.ComputeHashAsync(fs).ConfigureAwait(false)).ToHexString();
}
private async Task<CacheEntry?> _FindCacheEntryAsync(string fullPath)
{
using var conn = _CreateConnection();
using var cmd = conn.CreateCommand();
cmd.CommandText = "SELECT FilePath, FileSize, LastWriteTime, MD5, SHA1, SHA256, SHA512, MurmurHash2 FROM HashCache WHERE FilePath = @FilePath";
cmd.Parameters.AddWithValue("@FilePath", fullPath);
using var reader = await cmd.ExecuteReaderAsync().ConfigureAwait(false);
if (!await reader.ReadAsync().ConfigureAwait(false))
return null;
return new CacheEntry
{
FilePath = reader.GetString(0),
FileSize = reader.GetInt64(1),
LastWriteTime = reader.GetString(2),
MD5 = reader.IsDBNull(3) ? null : reader.GetString(3),
SHA1 = reader.IsDBNull(4) ? null : reader.GetString(4),
SHA256 = reader.IsDBNull(5) ? null : reader.GetString(5),
SHA512 = reader.IsDBNull(6) ? null : reader.GetString(6),
MurmurHash2 = reader.IsDBNull(7) ? null : reader.GetString(7)
};
}
private static string? _GetHashFromEntry(CacheEntry entry, string algoName) => algoName switch
{
"MD5" => entry.MD5,
"SHA1" => entry.SHA1,
"SHA256" => entry.SHA256,
"SHA512" => entry.SHA512,
"MurmurHash2" => entry.MurmurHash2,
_ => null
};
private async Task _InsertOrUpdateHashAsync(string fullPath, long fileSize, string lastWrite, string algoName, string hash)
{
if (hash.IsNullOrWhiteSpace()) return;
using var conn = _CreateConnection();
using var cmd = conn.CreateCommand();
cmd.CommandText = """
INSERT INTO HashCache (FilePath, FileSize, LastWriteTime, MD5, SHA1, SHA256, SHA512, MurmurHash2)
VALUES (@FilePath, @FileSize, @LastWriteTime, @MD5, @SHA1, @SHA256, @SHA512, @MurmurHash2)
ON CONFLICT(FilePath) DO UPDATE SET
FileSize = excluded.FileSize,
LastWriteTime = excluded.LastWriteTime,
MD5 = COALESCE(excluded.MD5, HashCache.MD5),
SHA1 = COALESCE(excluded.SHA1, HashCache.SHA1),
SHA256 = COALESCE(excluded.SHA256, HashCache.SHA256),
SHA512 = COALESCE(excluded.SHA512, HashCache.SHA512),
MurmurHash2 = COALESCE(excluded.MurmurHash2, HashCache.MurmurHash2)
""";
cmd.Parameters.AddWithValue("@FilePath", fullPath);
cmd.Parameters.AddWithValue("@FileSize", fileSize);
cmd.Parameters.AddWithValue("@LastWriteTime", lastWrite);
cmd.Parameters.AddWithValue("@MD5", algoName == "MD5" ? hash : DBNull.Value);
cmd.Parameters.AddWithValue("@SHA1", algoName == "SHA1" ? hash : DBNull.Value);
cmd.Parameters.AddWithValue("@SHA256", algoName == "SHA256" ? hash : DBNull.Value);
cmd.Parameters.AddWithValue("@SHA512", algoName == "SHA512" ? hash : DBNull.Value);
cmd.Parameters.AddWithValue("@MurmurHash2", algoName == "MurmurHash2" ? hash : DBNull.Value);
await cmd.ExecuteNonQueryAsync().ConfigureAwait(false);
}
private async Task _DeleteCacheEntryAsync(string fullPath)
{
using var conn = _CreateConnection();
using var cmd = conn.CreateCommand();
cmd.CommandText = "DELETE FROM HashCache WHERE FilePath = @FilePath";
cmd.Parameters.AddWithValue("@FilePath", fullPath);
await cmd.ExecuteNonQueryAsync().ConfigureAwait(false);
}
private sealed class CacheEntry
{
public string FilePath { get; init; } = "";
public long FileSize { get; init; }
public string LastWriteTime { get; init; } = "";
public string? MD5 { get; init; }
public string? SHA1 { get; init; }
public string? SHA256 { get; init; }
public string? SHA512 { get; init; }
public string? MurmurHash2 { get; init; }
}
}
@@ -0,0 +1,16 @@
using System;
using System.IO;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace PCL.Core.Utils.Hash;
public interface IHashProvider
{
ValueTask<byte[]> ComputeHashAsync(Stream input, CancellationToken cancellationToken = default);
byte[] ComputeHash(Stream input);
byte[] ComputeHash(ReadOnlySpan<byte> input);
byte[] ComputeHash(string input, Encoding? en = null);
int Length { get; }
}
@@ -0,0 +1,33 @@
using System;
using System.IO;
using System.Security.Cryptography;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace PCL.Core.Utils.Hash;
public class MD5Provider : IHashProvider {
public static MD5Provider Instance { get; } = new();
public int Length => 32;
public byte[] ComputeHash(ReadOnlySpan<byte> input)
{
return MD5.HashData(input);
}
public byte[] ComputeHash(string input, Encoding? en = null)
{
return ComputeHash(en is null ? Encoding.UTF8.GetBytes(input) : en.GetBytes(input));
}
public byte[] ComputeHash(Stream input)
{
return MD5.HashData(input);
}
public async ValueTask<byte[]> ComputeHashAsync(Stream input, CancellationToken cancellationToken = default)
{
return await MD5.HashDataAsync(input, cancellationToken).ConfigureAwait(false);
}
}
@@ -0,0 +1,88 @@
using System;
using System.Buffers.Binary;
using System.Collections.Generic;
using System.IO;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace PCL.Core.Utils.Hash;
public class MurmurHash2Provider : IHashProvider
{
public static MurmurHash2Provider Instance { get; } = new();
public int Length => 8;
public byte[] ComputeHash(ReadOnlySpan<byte> data)
{
var filtered = new List<byte>(data.Length);
foreach (var b in data)
if (b != 9 && b != 10 && b != 13 && b != 32)
filtered.Add(b);
return _ComputeHash(CollectionsMarshal.AsSpan(filtered));
}
public byte[] ComputeHash(string input, Encoding? encoding = null)
{
encoding ??= Encoding.UTF8;
return ComputeHash(encoding.GetBytes(input));
}
public byte[] ComputeHash(Stream input)
{
using var ms = new MemoryStream();
input.CopyTo(ms);
return ComputeHash(ms.GetBuffer().AsSpan(0, (int)ms.Length));
}
public async ValueTask<byte[]> ComputeHashAsync(Stream input, CancellationToken cancellationToken = default)
{
using var ms = new MemoryStream();
await input.CopyToAsync(ms, cancellationToken).ConfigureAwait(false);
return ComputeHash(ms.GetBuffer().AsSpan(0, (int)ms.Length));
}
private static byte[] _ComputeHash(ReadOnlySpan<byte> data)
{
var h = (uint)(1 ^ data.Length);
var i = 0;
var loopTo = data.Length - 4;
for (; i <= loopTo; i += 4)
{
var k = data[i]
| ((uint)data[i + 1] << 8)
| ((uint)data[i + 2] << 16)
| ((uint)data[i + 3] << 24);
k *= 0x5BD1E995;
k ^= k >> 24;
k *= 0x5BD1E995;
h *= 0x5BD1E995;
h ^= k;
}
switch (data.Length - i)
{
case 3:
h ^= (uint)(data[i] | ((uint)data[i + 1] << 8));
h ^= (uint)data[i + 2] << 16;
h *= 0x5BD1E995;
break;
case 2:
h ^= (uint)(data[i] | ((uint)data[i + 1] << 8));
h *= 0x5BD1E995;
break;
case 1:
h ^= data[i];
h *= 0x5BD1E995;
break;
}
h ^= h >> 13;
h *= 0x5BD1E995;
h ^= h >> 15;
return BitConverter.GetBytes(h);
}
}
@@ -0,0 +1,40 @@
using System;
using System.IO;
using System.Security.Cryptography;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace PCL.Core.Utils.Hash;
public class SHA1Provider : IHashProvider
{
public static SHA1Provider Instance { get; } = new();
public int Length => 40;
public byte[] ComputeHash(byte[] input)
{
return SHA1.HashData(input);
}
public byte[] ComputeHash(ReadOnlySpan<byte> input)
{
return SHA1.HashData(input);
}
public byte[] ComputeHash(string input, Encoding? encoding = null)
{
encoding ??= Encoding.UTF8;
return ComputeHash(encoding.GetBytes(input));
}
public byte[] ComputeHash(Stream input)
{
return SHA1.HashData(input);
}
public async ValueTask<byte[]> ComputeHashAsync(Stream input, CancellationToken cancellationToken = default)
{
return await SHA1.HashDataAsync(input, cancellationToken).ConfigureAwait(false);
}
}
@@ -0,0 +1,40 @@
using System;
using System.IO;
using System.Security.Cryptography;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace PCL.Core.Utils.Hash;
public class SHA256Provider : IHashProvider
{
public static SHA256Provider Instance { get; } = new();
public int Length => 64;
public byte[] ComputeHash(byte[] input)
{
return SHA256.HashData(input);
}
public byte[] ComputeHash(ReadOnlySpan<byte> input)
{
return SHA256.HashData(input);
}
public byte[] ComputeHash(string input, Encoding? encoding = null)
{
encoding ??= Encoding.UTF8;
return ComputeHash(encoding.GetBytes(input));
}
public byte[] ComputeHash(Stream input)
{
return SHA256.HashData(input);
}
public async ValueTask<byte[]> ComputeHashAsync(Stream input, CancellationToken cancellationToken = default)
{
return await SHA256.HashDataAsync(input, cancellationToken).ConfigureAwait(false);
}
}
@@ -0,0 +1,40 @@
using System;
using System.IO;
using System.Security.Cryptography;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace PCL.Core.Utils.Hash;
public class SHA512Provider : IHashProvider
{
public static SHA512Provider Instance { get; } = new();
public int Length => 128;
public byte[] ComputeHash(byte[] input)
{
return SHA512.HashData(input);
}
public byte[] ComputeHash(ReadOnlySpan<byte> input)
{
return SHA512.HashData(input);
}
public byte[] ComputeHash(string input, Encoding? encoding = null)
{
encoding ??= Encoding.UTF8;
return ComputeHash(encoding.GetBytes(input));
}
public byte[] ComputeHash(Stream input)
{
return SHA512.HashData(input);
}
public async ValueTask<byte[]> ComputeHashAsync(Stream input, CancellationToken cancellationToken = default)
{
return await SHA512.HashDataAsync(input, cancellationToken).ConfigureAwait(false);
}
}