初始化 monorepo: Go后端(7微服务) + Unity客户端(9模块) + 启动器 HTML5原型: Three.js 3D体素世界, Perlin噪声地形, 原版材质, 22种方块 Minecraft创造模式背包: 双栏布局, 拖拽移动物品, 方向性元件引脚 AI助搭策划文档 + 客户端/服务端骨架 + Docker Compose + CI
This commit is contained in:
@@ -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; }
|
||||
}
|
||||
Reference in New Issue
Block a user