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,185 @@
using System;
using System.Buffers.Binary;
using System.IO;
using System.Security.Cryptography;
using System.Text;
using PCL.Core.App;
using PCL.Core.Utils.Encryption;
using PCL.Core.Utils.Exts;
using PlainToolkit.CngProtectedData;
using DataProtectionScope = System.Security.Cryptography.DataProtectionScope;
using CngDataProtectionScope = PlainToolkit.CngProtectedData.DataProtectionScope;
namespace PCL.Core.Utils.Secret;
public static class EncryptHelper
{
private static readonly byte[] Key = "PCL CE Encryption Key"u8.ToArray();
public static (IEncryptionProvider Provider, uint Version) DefaultProvider => _DefaultProvider.Value;
private static readonly Lazy<(IEncryptionProvider Provider, uint Version)> _DefaultProvider = new(_SelectBestEncryption);
private static (IEncryptionProvider Provider, uint Version) _SelectBestEncryption()
{
var aesHardwareSupport = System.Runtime.Intrinsics.X86.Aes.IsSupported ||
System.Runtime.Intrinsics.Arm.Aes.IsSupported;
if (aesHardwareSupport && AesGcmProvider.Instance.IsSupported) return (AesGcmProvider.Instance, 2);
if (ChaCha20Poly1305Provider.Instance.IsSupported) return (ChaCha20Poly1305Provider.Instance, 1);
return (ChaCha20SoftwareProvider.Instance, 0);
}
public static string SecretEncrypt(string? data)
{
if (data.IsNullOrEmpty()) return string.Empty;
var rawData = Encoding.UTF8.GetBytes(data);
return Convert.ToBase64String(EncryptionData.ToBytes(new EncryptionData
{ Version = DefaultProvider.Version, Data = DefaultProvider.Provider.Encrypt(rawData, EncryptionKey) }));
}
public static string SecretDecrypt(string? data)
{
if (data.IsNullOrEmpty()) return string.Empty;
var rawData = Convert.FromBase64String(data);
Exception? decryptError;
if (EncryptionData.IsValid(rawData))
{
try
{
var encryptionData = EncryptionData.FromBytes(rawData);
IEncryptionProvider provider = encryptionData.Version switch
{
0 => ChaCha20SoftwareProvider.Instance,
1 => ChaCha20Poly1305Provider.Instance,
2 => AesGcmProvider.Instance,
_ => throw new NotSupportedException("Unsupported encryption version")
};
var decryptedData = provider.Decrypt(encryptionData.Data, EncryptionKey);
return Encoding.UTF8.GetString(decryptedData);
}
catch (Exception ex) { decryptError = ex; }
}
else
{
try
{
#pragma warning disable CS0612,CS0618 // Type or member is obsolete
var decryptedData = AesCbcProvider.Instance.Decrypt(rawData, Encoding.UTF8.GetBytes(IdentifyOld.EncryptKey));
#pragma warning restore CS0612,CS0618 // Type or member is obsolete
return Encoding.UTF8.GetString(decryptedData);
}
catch (Exception ex) { decryptError = ex; }
}
throw new Exception($"Unknown Encryption data, the data may broken", decryptError);
}
#region "加密存储信息数据"
public struct EncryptionData
{
public uint Version;
public byte[] Data;
private const uint MagicNumber = 0x454E4321;
public static EncryptionData FromBase64(string base64)
{
return FromBytes(Convert.FromBase64String(base64));
}
public static EncryptionData FromBytes(ReadOnlySpan<byte> bytes)
{
// 0 - 4 MagicNumber | 4 - 8 version || 8 - 12 bytes rData length | n bytes rData
if (bytes.Length < 12)
throw new ArgumentException("No enough data for EncryptionData", nameof(bytes));
if (BinaryPrimitives.ReadUInt32BigEndian(bytes[..4]) != MagicNumber)
throw new ArgumentException("Unknown data for EncryptionData", nameof(bytes));
var dataLength = BinaryPrimitives.ReadInt32BigEndian(bytes[8..12]);
if (dataLength > bytes.Length - 12)
throw new ArgumentException("No enough data for EncryptionData", nameof(bytes));
if (dataLength < 0)
throw new ArgumentException("Invalid data length for EncryptionData", nameof(bytes));
var rData = bytes[12..(12 + dataLength)];
return new EncryptionData
{
Version = BinaryPrimitives.ReadUInt32BigEndian(bytes[4..8]),
Data = rData.ToArray()
};
}
public static byte[] ToBytes(EncryptionData encryptionData)
{
var length = 12 + encryptionData.Data.Length;
var bytes = new byte[length];
var bytesSpan = bytes.AsSpan();
BinaryPrimitives.WriteUInt32BigEndian(bytesSpan[..4], MagicNumber);
BinaryPrimitives.WriteUInt32BigEndian(bytesSpan[4..8], encryptionData.Version);
BinaryPrimitives.WriteInt32BigEndian(bytesSpan[8..12], encryptionData.Data.Length);
encryptionData.Data.CopyTo(bytesSpan[12..]);
return bytes;
}
public static bool IsValid(ReadOnlySpan<byte> data)
{
try
{
return data.Length >= 12 && BinaryPrimitives.ReadUInt32BigEndian(data[..4]) == MagicNumber;
}
catch
{
return false;
}
}
}
#endregion
#region "密钥存储和获取"
internal static byte[] EncryptionKey { get => _EncryptionKey.Value; }
private static readonly Lazy<byte[]> _EncryptionKey = new(_GetKey);
private static byte[] _GetKey()
{
var keyFile = Path.Combine(Paths.SharedData, "UserKey.bin");
if (File.Exists(keyFile))
{
var buf = File.ReadAllBytes(keyFile);
var data = EncryptionData.FromBytes(buf);
return data.Version switch
{
1 => ProtectedData.Unprotect(data.Data, Key, DataProtectionScope.CurrentUser),
2 => CngProtectedData.Unprotect(data.Data, Key, CngDataProtectionScope.CurrentUser),
_ => throw new NotSupportedException("Unsupported key version")
};
}
var randomKey = new byte[32];
RandomNumberGenerator.Fill(randomKey);
var storeData = EncryptionData.ToBytes(new EncryptionData
{
Version = 2,
Data = CngProtectedData.Protect(randomKey, Key, CngDataProtectionScope.CurrentUser)
});
var tmpFile = $"{keyFile}.tmp{RandomUtils.NextInt(10000, 99999)}";
using (var fs = new FileStream(tmpFile, FileMode.Create, FileAccess.ReadWrite, FileShare.None))
{
fs.Write(storeData);
fs.Flush(true);
}
File.Move(tmpFile, keyFile, true);
return randomKey;
}
#endregion
}
@@ -0,0 +1,81 @@
using System;
using System.Management;
using System.Text;
using PCL.Core.Logging;
using PCL.Core.Utils.Exts;
using PCL.Core.Utils.Hash;
namespace PCL.Core.Utils.Secret;
public class Identify
{
public static byte[] RawId { get => field ??= _GetRawId(); } = null!;
public static string LauncherId { get => field ??= _getLauncherId(); } = null!;
private static byte[] _GetRawId()
{
var code = new StringBuilder();
try
{
code.Append("UUID:").Append(_GetWmiProperty("Win32_ComputerSystemProduct", "UUID"))
.Append("|MB_Prod:").Append(_GetWmiProperty("Win32_BaseBoard", "Product"))
.Append("|MB_SN:").Append(_GetWmiProperty("Win32_BaseBoard", "SerialNumber"))
.Append("|CPU:").Append(_GetWmiProperty("Win32_Processor", "ProcessorId"));
}
catch (Exception ex)
{
LogWrapper.Error(ex, "Identify", "获取设备基础信息失败");
}
return Encoding.UTF8.GetBytes(SHA512Provider.Instance.ComputeHash(code.ToString()).ToHexString());
}
private static string _GetWmiProperty(string className, string propertyName)
{
try
{
using var searcher =
new ManagementObjectSearcher($"SELECT {propertyName} FROM {className}");
using var results = searcher.Get();
foreach (var obj in results)
{
if (obj[propertyName] is not null)
return (obj[propertyName].ToString() ?? string.Empty).Trim();
}
}
catch { /* Ignore */ }
return string.Empty;
}
private static string _getLauncherId()
{
try
{
var prefix = "PCL-CE|"u8.ToArray();
var ctx = RawId;
var suffix = "|LauncherId"u8.ToArray();
var buffer = new byte[prefix.Length + ctx.Length + suffix.Length];
var bufferSpan = buffer.AsSpan();
prefix.CopyTo(bufferSpan[..prefix.Length]);
ctx.CopyTo(bufferSpan.Slice(prefix.Length, ctx.Length));
suffix.CopyTo(bufferSpan.Slice(prefix.Length + ctx.Length, suffix.Length));
Array.Clear(ctx);
var sample = SHA512Provider.Instance.ComputeHash(bufferSpan).ToHexString();
bufferSpan.Clear();
// 16 in length, 8 bytes, 64 bits, enough for us
return sample.Substring(64, 16)
.ToUpper()
.Insert(4, "-")
.Insert(9, "-")
.Insert(14, "-");
}
catch (Exception ex)
{
LogWrapper.Error(ex, "Identify", "无法获取识别码");
return "PCL2-CECE-GOOD-2025";
}
}
}
@@ -0,0 +1,103 @@
using System;
using System.Management;
using PCL.Core.App;
using PCL.Core.Logging;
using PCL.Core.Utils.Hash;
using PCL.Core.Utils.Exts;
namespace PCL.Core.Utils.Secret;
[Obsolete("Use PCL.Core.Utils.Secret.Identify instead")]
public static class IdentifyOld
{
private const string DefaultRawCode = "B09675A9351CBD1FD568056781FE3966DD936CC9B94E51AB5CF67EEB7E74C075";
private static readonly Lazy<string?> _LazyCpuId = new(_GetCpuId);
private static readonly Lazy<string> _LazyRawCode =
new(() => CpuId is null ? DefaultRawCode : SHA256Provider.Instance.ComputeHash(CpuId).ToHexString().ToUpper());
private static readonly Lazy<string> _LaunchId = new(_GetLaunchId);
private static readonly Lazy<string> _LazyEncryptKey =
new(() => SHA512Provider.Instance.ComputeHash(RawCode).ToHexString().Substring(4, 32).ToUpper());
public static string GetGuid() => Guid.NewGuid().ToString();
[Obsolete]
public static string? CpuId => _LazyCpuId.Value;
[Obsolete]
public static string RawCode => _LazyRawCode.Value;
[Obsolete]
public static string LaunchId => _LaunchId.Value;
[Obsolete]
public static string EncryptKey => _LazyEncryptKey.Value;
private static string? _GetCpuId()
{
try
{
using var searcher = new ManagementObjectSearcher("SELECT ProcessorId FROM Win32_Processor");
using var collection = searcher.Get();
foreach (var item in collection)
{
try
{
return item["ProcessorId"]?.ToString();
}
catch (ManagementException ex)
{
LogWrapper.Warn("Identify", $"WMI属性读取失败: {ex.Message}");
}
finally
{
item.Dispose();
}
}
LogWrapper.Warn("Identify", "未找到有效的CPU ID");
return null;
}
catch (ManagementException ex)
{
LogWrapper.Error(ex, "Identify", $"WMI查询失败");
}
catch (System.Runtime.InteropServices.COMException ex)
{
LogWrapper.Error(ex, "Identify", $"COM异常,请确保WMI服务正在运行");
}
catch (UnauthorizedAccessException ex)
{
LogWrapper.Error(ex, "Identify", "访问被拒绝,请以管理员权限运行");
}
catch (Exception ex)
{
LogWrapper.Error(ex, "Identify", $"意外的系统异常");
}
return null;
}
public static string GetMachineId(string randomId)
{
return SHA512Provider.Instance.ComputeHash($"{randomId}|{CpuId}").ToHexString().ToUpper();
}
private static string _GetLaunchId()
{
try
{
if (string.IsNullOrEmpty(States.System.LaunchUuid)) States.System.LaunchUuid = GetGuid();
var hashCode = GetMachineId(States.System.LaunchUuid)
.Substring(6, 16)
.Insert(4, "-")
.Insert(9, "-")
.Insert(14, "-");
return hashCode;
}
catch (Exception ex)
{
LogWrapper.Error(ex, "Identify", "无法获取短识别码");
return "PCL2-CECE-GOOD-2025";
}
}
}