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,237 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Text.Json.Serialization;
namespace PCL.Core.Utils;
/// <summary>
/// System.Text.Json 兼容 Newtonsoft.Json 宽松行为的统一入口。
/// </summary>
public static class JsonCompat
{
public static readonly JsonNodeOptions NodeOptions = new()
{
PropertyNameCaseInsensitive = true
};
public static readonly JsonDocumentOptions DocumentOptions = new()
{
AllowTrailingCommas = true,
CommentHandling = JsonCommentHandling.Skip
};
/// <summary>
/// 统一的宽松 JSON 序列化配置。该实例在静态初始化时已被冻结,调用方不能修改全局行为。
/// 如需追加调用点专用设置,请使用 <c>new JsonSerializerOptions(JsonCompat.SerializerOptions)</c> 克隆后修改。
/// </summary>
public static JsonSerializerOptions SerializerOptions { get; } = _CreateSerializerOptions();
private static JsonSerializerOptions _CreateSerializerOptions()
{
var options = new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true,
NumberHandling = JsonNumberHandling.AllowReadingFromString,
ReadCommentHandling = JsonCommentHandling.Skip,
AllowTrailingCommas = true,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
Converters =
{
new FlexibleDateTimeConverter(),
new FlexibleBoolConverter(),
new FlexibleStringConverter(),
new JsonStringEnumConverter()
}
};
options.MakeReadOnly(true);
return options;
}
public static JsonNode ParseNode(string text)
{
return JsonNode.Parse(text, NodeOptions, DocumentOptions)!;
}
public static T? ToObject<T>(this JsonNode? node)
{
return node is null ? default : node.Deserialize<T>(SerializerOptions);
}
public static JsonArray FromObject<T>(IEnumerable<T> items)
{
var arr = new JsonArray();
foreach (var item in items)
arr.Add(JsonSerializer.SerializeToNode(item, SerializerOptions));
return arr;
}
public static bool TryGetDateTime(JsonNode? node, out DateTime dateTime)
{
dateTime = default;
switch (node)
{
case null:
return false;
case JsonValue value when value.TryGetValue<DateTime>(out var rawDateTime):
dateTime = NormalizeDateTime(rawDateTime);
return true;
case JsonValue value
when value.TryGetValue<string>(out var rawText) && TryParseDateTime(rawText, out dateTime):
return true;
default:
try
{
dateTime = NormalizeDateTime(node.Deserialize<DateTime>(SerializerOptions));
return true;
}
catch (JsonException)
{
return false;
}
catch (InvalidOperationException)
{
return false;
}
catch (NotSupportedException)
{
return false;
}
}
}
public static bool TryParseDateTime(string? value, out DateTime dateTime)
{
dateTime = default;
if (string.IsNullOrWhiteSpace(value)) return false;
// ISO 8601 允许用 24:00:00 表示当日终点(语义等于次日零点),但 .NET 的日期解析器不接受小时为 24,
// 需先把小时 24 归一化为 00,再在解析成功后补一天。例如社区版本清单中的 2009-10-24T24:00:00+00:00。
var addDay = false;
var endOfDay = RegexPatterns.Iso8601EndOfDay.Match(value);
if (endOfDay.Success)
{
value = value.Remove(endOfDay.Index + 1, 2).Insert(endOfDay.Index + 1, "00");
addDay = true;
}
if (DateTimeOffset.TryParse(value, CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind,
out var dateTimeOffset))
{
if (addDay) dateTimeOffset = dateTimeOffset.AddDays(1);
dateTime = dateTimeOffset.LocalDateTime;
return true;
}
if (!DateTime.TryParse(value, CultureInfo.InvariantCulture, DateTimeStyles.AssumeLocal, out var parsed))
return false;
dateTime = NormalizeDateTime(addDay ? parsed.AddDays(1) : parsed);
return true;
}
public static DateTime NormalizeDateTime(DateTime dateTime)
{
return dateTime.Kind == DateTimeKind.Utc ? dateTime.ToLocalTime() : dateTime;
}
public static void Merge(this JsonObject target, JsonNode? source)
{
if (source is not JsonObject sourceObj) return;
foreach (var prop in sourceObj.ToArray())
switch (target[prop.Key])
{
case JsonObject targetChild when
prop.Value is JsonObject sourceChild:
targetChild.Merge(sourceChild);
break;
case JsonArray targetArray when
prop.Value is JsonArray sourceArray:
targetArray.Merge(sourceArray);
break;
default:
target[prop.Key] = prop.Value?.DeepClone();
break;
}
}
public static void Merge(this JsonArray target, JsonNode? source)
{
if (source is not JsonArray sourceArr) return;
foreach (var item in sourceArr)
target.Add(item?.DeepClone());
}
private sealed class FlexibleDateTimeConverter : JsonConverter<DateTime>
{
public override DateTime Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType != JsonTokenType.String)
return NormalizeDateTime(reader.GetDateTime());
var value = reader.GetString();
return TryParseDateTime(value, out var dateTime)
? dateTime
: NormalizeDateTime(reader.GetDateTime());
}
public override void Write(Utf8JsonWriter writer, DateTime value, JsonSerializerOptions options)
{
writer.WriteStringValue(value);
}
}
private sealed class FlexibleBoolConverter : JsonConverter<bool>
{
public override bool Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
return reader.TokenType switch
{
JsonTokenType.True => true,
JsonTokenType.False => false,
JsonTokenType.String when bool.TryParse(reader.GetString(), out var value) => value,
JsonTokenType.String when int.TryParse(reader.GetString(), NumberStyles.Integer,
CultureInfo.InvariantCulture, out var number) => number != 0,
JsonTokenType.Number when reader.TryGetInt64(out var number) => number != 0,
_ => throw new JsonException($"Can not convert JSON token {reader.TokenType} to Boolean.")
};
}
public override void Write(Utf8JsonWriter writer, bool value, JsonSerializerOptions options)
{
writer.WriteBooleanValue(value);
}
}
private sealed class FlexibleStringConverter : JsonConverter<string>
{
public override string? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
return reader.TokenType switch
{
JsonTokenType.Null => null,
JsonTokenType.String => reader.GetString(),
JsonTokenType.Number => _ReadRawJsonValue(ref reader),
JsonTokenType.True => bool.TrueString,
JsonTokenType.False => bool.FalseString,
_ => _ReadRawJsonValue(ref reader)
};
}
private static string _ReadRawJsonValue(ref Utf8JsonReader reader)
{
using var document = JsonDocument.ParseValue(ref reader);
return document.RootElement.GetRawText();
}
public override void Write(Utf8JsonWriter writer, string? value, JsonSerializerOptions options)
{
writer.WriteStringValue(value);
}
}
}