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,12 @@
using System.Text.Json.Serialization;
namespace PCL.Core.Minecraft.IdentityModel.Yggdrasil;
/// <summary>
/// Yggdrasil Agent
/// </summary>
public record Agent
{
[JsonPropertyName("name")] public string Name { get; init; } = "minecraft";
[JsonPropertyName("version")] public int Version { get; init; } = 1;
}
@@ -0,0 +1,167 @@
using System;
using System.Net;
using System.Text.Json.Nodes;
using System.Threading;
using System.Threading.Tasks;
using PCL.Core.IO.Net.Http;
using PCL.Core.Utils;
namespace PCL.Core.Minecraft.IdentityModel.Yggdrasil;
/// <summary>
/// 提供 Yggdrasil 传统认证支持
/// </summary>
/// <param name="options"><see cref="YggdrasilLegacyAuthenticateOptions" /> 认证参数</param>
public sealed class YggdrasilLegacyClient(YggdrasilLegacyAuthenticateOptions options)
{
/// <summary>
/// 异步向服务器发送一次登录请求
/// </summary>
/// <param name="token"></param>
/// <returns><see cref="YggdrasilAuthenticateResult" /> 认证结果</returns>
/// <exception cref="ArgumentException">用户名或密码无效</exception>
public async Task<YggdrasilAuthenticateResult?> AuthenticateAsync(CancellationToken token)
{
ArgumentException.ThrowIfNullOrEmpty(options.Username);
ArgumentException.ThrowIfNullOrEmpty(options.Password);
var credential = new YggdrasilCredential
{
User = options.Username,
Password = options.Password
};
var address = $"{options.YggdrasilApiLocation}/authserver/authenticate";
using var response = await HttpRequest
.CreatePost(address)
.WithHeaders(options.Headers ?? [])
.WithJsonContent(credential)
.SendAsync(options.GetClient.Invoke(), cancellationToken: token)
.ConfigureAwait(false);
return await response
.AsJsonAsync<YggdrasilAuthenticateResult>(cancellationToken: token)
.ConfigureAwait(false);
}
/// <summary>
/// 异步向服务器发送一次刷新请求
/// </summary>
/// <param name="token"></param>
/// <param name="seleectedProfile">如果需要选择角色,请填写此参数</param>
public async Task<YggdrasilAuthenticateResult?> RefreshAsync(CancellationToken token, Profile? seleectedProfile)
{
ArgumentException.ThrowIfNullOrEmpty(options.AccessToken);
var refreshData = new YggdrasilRefresh
{
AccessToken = options.AccessToken
};
if (seleectedProfile is not null) refreshData.SelectedProfile = seleectedProfile;
var address = $"{options.YggdrasilApiLocation}/authserver/refresh";
using var response = await HttpRequest
.CreatePost(address)
.WithJsonContent(refreshData)
.WithHeaders(options.Headers ?? [])
.SendAsync(options.GetClient.Invoke(), cancellationToken: token)
.ConfigureAwait(false);
return await response
.AsJsonAsync<YggdrasilAuthenticateResult>(cancellationToken: token)
.ConfigureAwait(false);
}
/// <summary>
/// 异步向服务器发送一次验证请求
/// </summary>
/// <param name="token"></param>
/// <returns></returns>
public async Task<bool> ValidateAsync(CancellationToken token)
{
ArgumentException.ThrowIfNullOrEmpty(options.AccessToken);
var validateData = new YggdrasilRefresh
{
AccessToken = options.AccessToken
};
var address = $"{options.YggdrasilApiLocation}/authserver/invalidate";
using var response = await HttpRequest
.CreatePost(address)
.WithHeaders(options.Headers ?? [])
.WithJsonContent(validateData)
.SendAsync(options.GetClient.Invoke(), cancellationToken: token)
.ConfigureAwait(false);
return response.StatusCode == HttpStatusCode.NoContent;
}
/// <summary>
/// 异步向服务器发送一次注销请求
/// </summary>
/// <param name="token"></param>
public async Task InvalidateAsync(CancellationToken token)
{
ArgumentException.ThrowIfNullOrEmpty(options.AccessToken);
var validateData = new YggdrasilRefresh
{
AccessToken = options.AccessToken
};
var address = $"{options.YggdrasilApiLocation}/authserver/invalidate";
using var _ = await HttpRequest
.CreatePost(address)
.WithHeaders(options.Headers ?? [])
.WithJsonContent(validateData)
.SendAsync(options.GetClient.Invoke(), cancellationToken: token)
.ConfigureAwait(false);
}
/// <summary>
/// 异步向服务器发送登出请求 <br />
/// 这会立刻注销所有会话,无论当前会话是否属于调用方
/// </summary>
/// <param name="token"></param>
/// <returns></returns>
public async Task<(bool IsSuccess, string ErrorDescription)> SignOutAsync(CancellationToken token)
{
// 不想写 Model 了,就这样吧(趴
var signoutData = new JsonObject
{
["username"] = options.Username,
["password"] = options.Password
};
var address = $"{options.YggdrasilApiLocation}/authserver/signout";
using var response = await HttpRequest
.CreatePost(address)
.WithHeaders(options.Headers ?? [])
.WithJsonContent(signoutData)
.SendAsync(options.GetClient.Invoke(), cancellationToken: token)
.ConfigureAwait(false);
if (response.StatusCode == HttpStatusCode.NoContent)
return (true, string.Empty);
var content = await response.AsStringAsync(token).ConfigureAwait(false);
if (string.IsNullOrWhiteSpace(content))
return (false, string.Empty);
JsonNode? data;
try
{
data = JsonCompat.ParseNode(content);
}
catch
{
// 认证服务器偶尔会返回纯文本或 HTML 错误页,保留原始响应便于上层展示和诊断。
return (false, content);
}
var error = data?["errorMessage"]?.ToString() ?? string.Empty;
return (false, error);
}
}
@@ -0,0 +1,30 @@
using System;
using System.Collections.Generic;
using System.Net.Http;
namespace PCL.Core.Minecraft.IdentityModel.Yggdrasil;
public record YggdrasilLegacyAuthenticateOptions
{
/// <summary>
/// API 基地址 (e.g. https://api.example.com/api/yggdrasil)
/// </summary>
public required string YggdrasilApiLocation { get; set; }
/// <summary>
/// 用户名
/// </summary>
public string? Username { get; set; }
/// <summary>
/// 密码
/// </summary>
public string? Password { get; set; }
/// <summary>
/// 访问令牌
/// </summary>
public string? AccessToken { get; set; }
public required Func<HttpClient> GetClient { get; set; }
/// <summary>
/// 请求头
/// </summary>
public Dictionary<string,string>? Headers { get; set; }
}
@@ -0,0 +1,88 @@
using System.Text.Json.Serialization;
namespace PCL.Core.Minecraft.IdentityModel.Yggdrasil;
public record Profile
{
/// <summary>
/// UUID
/// </summary>
[JsonPropertyName("id")] public required string Id { get; init; }
/// <summary>
/// 档案名称
/// </summary>
[JsonPropertyName("name")] public string? Name { get; init; }
/// <summary>
/// 属性信息
/// </summary>
[JsonPropertyName("properties")] public PlayerProperty[]? Properties { get; init; }
}
public record PlayerProperty
{
/// <summary>
/// 属性名称
/// </summary>
[JsonPropertyName("name")] public required string Name { get; init; }
/// <summary>
/// 属性值
/// </summary>
[JsonPropertyName("value")] public required string Value { get; init; }
/// <summary>
/// 数字签名
/// </summary>
[JsonPropertyName("signature")] public string? Signature { get; init; }
}
public record PlayerTextureProperty
{
/// <summary>
/// Unix 时间戳
/// </summary>
[JsonPropertyName("timestamp")] public required long Timestamp { get; init; }
/// <summary>
/// 所有者的 UUID
/// </summary>
[JsonPropertyName("profileId")] public required string ProfileId { get; init; }
/// <summary>
/// 所有者名称
/// </summary>
[JsonPropertyName("profileName")] public required string ProfileName { get; init; }
/// <summary>
/// 材质信息
/// </summary>
[JsonPropertyName("textures")] public required PlayerTextures Textures { get; init; }
}
public record PlayerTextures
{
/// <summary>
/// 皮肤
/// </summary>
[JsonPropertyName("skin")] public required PlayerTexture Skin { get; init; }
/// <summary>
/// 披风
/// </summary>
[JsonPropertyName("cape")] public required PlayerTexture Cape { get; init; }
}
public record PlayerTexture
{
/// <summary>
/// 材质地址
/// </summary>
[JsonPropertyName("Url")] public required string Url { get; init; }
/// <summary>
/// 元数据
/// </summary>
[JsonPropertyName("metadata")] public required PlayerTextureMetadata Metadata { get; init; }
}
public record PlayerTextureMetadata
{
/// <summary>
/// 模型信息 e.g. Steven -> default, Alex -> Slim
/// </summary>
[JsonPropertyName("model")] public required string Model { get; init; }
}
@@ -0,0 +1,49 @@
using System.Text.Json.Serialization;
namespace PCL.Core.Minecraft.IdentityModel.Yggdrasil;
public record YggdrasilCredential
{
[JsonPropertyName("username")] public required string User { get; init; }
[JsonPropertyName("password")] public required string Password { get; init; }
[JsonPropertyName("agent")] public Agent Agent { get; init; } = new();
[JsonPropertyName("requestUser")] public bool RequestUser { get; set; }
}
public record YggdrasilAuthenticateResult
{
/// <summary>
/// 错误类型
/// </summary>
[JsonPropertyName("error")] public string? Error { get; init; }
/// <summary>
/// 错误消息
/// </summary>
[JsonPropertyName("errorMessage")] public string? ErrorMessage { get; init; }
/// <summary>
/// 访问令牌
/// </summary>
[JsonPropertyName("accessToken")] public string? AccessToken { get; init; }
/// <summary>
/// 客户端令牌,基本没用
/// </summary>
[JsonPropertyName("clientToken")] public string? ClientToken { get; init; }
/// <summary>
/// 选择的档案
/// </summary>
[JsonPropertyName("selectedProfile")] public Profile? SelectedProfile { get; init; }
/// <summary>
/// 可用档案
/// </summary>
[JsonPropertyName("availableProfiles")] public Profile[]? AvailableProfiles { get; init; }
/// <summary>
/// 用户信息
/// </summary>
[JsonPropertyName("user")] public Profile? User { get; init; }
}
public record YggdrasilRefresh
{
[JsonPropertyName("accessToken")] public required string AccessToken { get; set; }
[JsonPropertyName("selectedProfile")] public Profile? SelectedProfile { get; set; }
}