初始化 monorepo: Go后端(7微服务) + Unity客户端(9模块) + 启动器 HTML5原型: Three.js 3D体素世界, Perlin噪声地形, 原版材质, 22种方块 Minecraft创造模式背包: 双栏布局, 拖拽移动物品, 方向性元件引脚 AI助搭策划文档 + 客户端/服务端骨架 + Docker Compose + CI
This commit is contained in:
@@ -0,0 +1,29 @@
|
||||
using System;
|
||||
using fNbt;
|
||||
|
||||
namespace PCL.Core.Minecraft.Saves.Parsing;
|
||||
|
||||
/// <summary>
|
||||
/// 存档解析器接口 —— 负责将 level.dat 中的 NBT 数据转换为 <see cref="SaveInfo"/>。
|
||||
/// 每种格式版本对应一个实现类。
|
||||
/// </summary>
|
||||
public interface ISaveParser
|
||||
{
|
||||
/// <summary>此解析器对应的存档格式版本。</summary>
|
||||
SaveFormatVersion FormatVersion { get; }
|
||||
|
||||
/// <summary>返回此解析器能否处理给定的 NBT 数据。</summary>
|
||||
/// <param name="data">level.dat 中的 Data 复合标签。</param>
|
||||
/// <param name="dataVersion">Data 中的 DataVersion 字段值,如果不存在则为 null。</param>
|
||||
bool CanHandle(NbtCompound data, int? dataVersion);
|
||||
|
||||
/// <summary>
|
||||
/// 解析 NBT 数据并返回 <see cref="SaveInfo"/>。
|
||||
/// 文件系统元数据(创建时间、修改时间)由调用方传入。
|
||||
/// </summary>
|
||||
/// <param name="folderPath">存档文件夹的绝对路径。</param>
|
||||
/// <param name="data">level.dat 中的 Data 复合标签。</param>
|
||||
/// <param name="createdAt">文件夹创建时间(UTC)。</param>
|
||||
/// <param name="modifiedAt">level.dat 最后修改时间(UTC)。</param>
|
||||
SaveInfo Parse(string folderPath, NbtCompound data, DateTime createdAt, DateTime modifiedAt);
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
using System;
|
||||
using fNbt;
|
||||
using System.Numerics;
|
||||
|
||||
namespace PCL.Core.Minecraft.Saves.Parsing.Internal;
|
||||
|
||||
/// <summary>
|
||||
/// NBT 读取工具方法,多个版本解析器共用。
|
||||
/// </summary>
|
||||
internal static class NbtReadHelper
|
||||
{
|
||||
/// <summary>尝试从 NBT 复合标签中读取 long 值。</summary>
|
||||
public static long? TryGetLong(NbtCompound data, string key) =>
|
||||
data.TryGet<NbtLong>(key, out var tag) ? tag!.Value : null;
|
||||
|
||||
/// <summary>读取最后游玩时间并转为 UTC DateTime。</summary>
|
||||
public static DateTime ReadLastPlayed(NbtCompound data) =>
|
||||
EpochMsToUtc(TryGetLong(data, "LastPlayed") ?? 0);
|
||||
|
||||
/// <summary>将 Unix 毫秒时间戳转为 UTC DateTime。</summary>
|
||||
public static DateTime EpochMsToUtc(long ms) =>
|
||||
DateTime.UnixEpoch.AddMilliseconds(ms);
|
||||
|
||||
/// <summary>读取累计游戏时间。Minecraft 以 tick 为单位(20 tick = 1 秒)。</summary>
|
||||
public static TimeSpan ReadPlayTime(NbtCompound data)
|
||||
{
|
||||
var ticks = TryGetLong(data, "Time");
|
||||
return TimeSpan.FromSeconds((ticks ?? 0) / 20.0d);
|
||||
}
|
||||
|
||||
/// <summary>读取游戏模式。hardcore 不是独立的 GameType,而是 Survival + hardcore=1。</summary>
|
||||
public static GameMode ReadGameMode(NbtCompound data, out bool isHardcore)
|
||||
{
|
||||
isHardcore = data.TryGet<NbtByte>("hardcore", out var hc) && hc!.Value == 1;
|
||||
if (isHardcore) return GameMode.Hardcore;
|
||||
var gt = data.TryGet<NbtInt>("GameType", out var gameType) ? gameType!.Value : 0;
|
||||
return gt switch
|
||||
{
|
||||
1 => GameMode.Creative,
|
||||
2 => GameMode.Adventure,
|
||||
3 => GameMode.Spectator,
|
||||
_ => GameMode.Survival,
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>读取出生点坐标 —— 旧版格式(SpawnX/Y/Z 三个独立 int 字段)。</summary>
|
||||
public static Vector3? TryReadSpawnFromFields(NbtCompound data)
|
||||
{
|
||||
if (data.TryGet<NbtInt>("SpawnX", out var sx) &&
|
||||
data.TryGet<NbtInt>("SpawnY", out var sy) &&
|
||||
data.TryGet<NbtInt>("SpawnZ", out var sz))
|
||||
return new Vector3(sx!.Value, sy!.Value, sz!.Value);
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>读取出生点坐标 —— 新版格式(spawn.pos int[] 数组)。</summary>
|
||||
public static Vector3? TryReadSpawnFromPos(NbtCompound data)
|
||||
{
|
||||
if (data.TryGet<NbtCompound>("spawn", out var spawn) &&
|
||||
spawn!.TryGet<NbtIntArray>("pos", out var pos) && pos!.Value.Length == 3)
|
||||
return new Vector3(pos[0], pos[1], pos[2]);
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>读取旧版字节型难度(0=和平, 1=简单, 2=普通, 3=困难)。</summary>
|
||||
public static Difficulty? ReadDifficultyByte(NbtCompound data)
|
||||
{
|
||||
if (data.TryGet<NbtByte>("Difficulty", out var diff))
|
||||
return (Difficulty)diff!.Value;
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>读取 Data.Version 复合标签中的版本信息。</summary>
|
||||
public static (string? name, int? id) ReadVersion(NbtCompound data)
|
||||
{
|
||||
if (data.TryGet<NbtCompound>("Version", out var version))
|
||||
{
|
||||
var name = version!.TryGet<NbtString>("Name", out var n) ? n!.Value : null;
|
||||
var id = version.TryGet<NbtInt>("Id", out var i) ? i!.Value : (int?)null;
|
||||
return (name, id);
|
||||
}
|
||||
return (null, null);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
using System;
|
||||
using fNbt;
|
||||
|
||||
namespace PCL.Core.Minecraft.Saves.Parsing.Internal;
|
||||
|
||||
/// <summary>
|
||||
/// Alpha ~ 1.2.5 的存档格式。
|
||||
/// 特征:没有 DataVersion、没有 allowCommands、没有 Difficulty。
|
||||
/// </summary>
|
||||
internal sealed class Pre113SaveParser : ISaveParser
|
||||
{
|
||||
public SaveFormatVersion FormatVersion => SaveFormatVersion.Pre113;
|
||||
|
||||
public bool CanHandle(NbtCompound data, int? dataVersion)
|
||||
=> dataVersion is null && !data.Contains("allowCommands");
|
||||
|
||||
public SaveInfo Parse(string folderPath, NbtCompound data, DateTime createdAt, DateTime modifiedAt)
|
||||
{
|
||||
return new SaveInfo
|
||||
{
|
||||
LevelName = data.TryGet<NbtString>("LevelName", out var ln) ? ln!.Value : "unknown",
|
||||
VersionName = null,
|
||||
VersionId = null,
|
||||
Seed = NbtReadHelper.TryGetLong(data, "RandomSeed"),
|
||||
LastPlayedUtc = NbtReadHelper.ReadLastPlayed(data),
|
||||
Spawn = NbtReadHelper.TryReadSpawnFromFields(data),
|
||||
GameMode = NbtReadHelper.ReadGameMode(data, out var isHardcore),
|
||||
Difficulty = null,
|
||||
IsDifficultyLocked = false,
|
||||
IsHardcore = isHardcore,
|
||||
AllowCommands = false,
|
||||
PlayTime = NbtReadHelper.ReadPlayTime(data),
|
||||
FolderPath = folderPath,
|
||||
CreatedAt = createdAt,
|
||||
ModifiedAt = modifiedAt,
|
||||
};
|
||||
}
|
||||
}
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
using System;
|
||||
using fNbt;
|
||||
|
||||
namespace PCL.Core.Minecraft.Saves.Parsing.Internal;
|
||||
|
||||
/// <summary>
|
||||
/// 17w47a(1.13) ~ 1.15.2 的存档格式。
|
||||
/// 特征:DataVersion 在 [1443, 2536) 之间,新增 DataPacks 字段。
|
||||
/// </summary>
|
||||
internal sealed class Version113To1152SaveParser : ISaveParser
|
||||
{
|
||||
private readonly ISaveParser _baseParser;
|
||||
|
||||
public Version113To1152SaveParser() : this(new Version19To1122SaveParser()) { }
|
||||
public Version113To1152SaveParser(ISaveParser baseParser) => _baseParser = baseParser;
|
||||
|
||||
public SaveFormatVersion FormatVersion => SaveFormatVersion.Version113To1152;
|
||||
|
||||
public bool CanHandle(NbtCompound data, int? dataVersion)
|
||||
=> dataVersion.HasValue
|
||||
&& dataVersion.Value >= DataVersionBoundaries._17w47a
|
||||
&& dataVersion.Value < DataVersionBoundaries._20w20a;
|
||||
|
||||
public SaveInfo Parse(string folderPath, NbtCompound data, DateTime createdAt, DateTime modifiedAt)
|
||||
=> _baseParser.Parse(folderPath, data, createdAt, modifiedAt);
|
||||
}
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
using System;
|
||||
using fNbt;
|
||||
|
||||
namespace PCL.Core.Minecraft.Saves.Parsing.Internal;
|
||||
|
||||
/// <summary>
|
||||
/// 20w20a(1.16) ~ 1.21.11 的存档格式。
|
||||
/// 特征:DataVersion 在 [2536, 4774) 之间。
|
||||
/// 变更:种子从 Data.RandomSeed 迁移到 Data.WorldGenSettings.seed。
|
||||
/// </summary>
|
||||
internal sealed class Version116To1211SaveParser : ISaveParser
|
||||
{
|
||||
private readonly ISaveParser _baseParser;
|
||||
|
||||
public Version116To1211SaveParser() : this(new Version19To1122SaveParser()) { }
|
||||
public Version116To1211SaveParser(ISaveParser baseParser) => _baseParser = baseParser;
|
||||
|
||||
public SaveFormatVersion FormatVersion => SaveFormatVersion.Version116To1211;
|
||||
|
||||
public bool CanHandle(NbtCompound data, int? dataVersion)
|
||||
=> dataVersion.HasValue
|
||||
&& dataVersion.Value >= DataVersionBoundaries._20w20a
|
||||
&& dataVersion.Value < DataVersionBoundaries._261snapshot6;
|
||||
|
||||
public SaveInfo Parse(string folderPath, NbtCompound data, DateTime createdAt, DateTime modifiedAt)
|
||||
{
|
||||
var baseInfo = _baseParser.Parse(folderPath, data, createdAt, modifiedAt);
|
||||
return baseInfo with
|
||||
{
|
||||
Seed = ReadWorldGenSeed(data),
|
||||
Spawn = NbtReadHelper.TryReadSpawnFromPos(data)
|
||||
?? NbtReadHelper.TryReadSpawnFromFields(data),
|
||||
};
|
||||
}
|
||||
|
||||
internal static long? ReadWorldGenSeed(NbtCompound data)
|
||||
{
|
||||
if (data.TryGet<NbtCompound>("WorldGenSettings", out var wgs) &&
|
||||
wgs!.TryGet<NbtLong>("seed", out var seed))
|
||||
return seed!.Value;
|
||||
return NbtReadHelper.TryGetLong(data, "RandomSeed");
|
||||
}
|
||||
}
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
using System;
|
||||
using fNbt;
|
||||
|
||||
namespace PCL.Core.Minecraft.Saves.Parsing.Internal;
|
||||
|
||||
/// <summary>
|
||||
/// 1.3.1 ~ 1.8.9 的存档格式。
|
||||
/// 特征:没有 DataVersion,有 allowCommands。
|
||||
/// </summary>
|
||||
internal sealed class Version131To189SaveParser : ISaveParser
|
||||
{
|
||||
public SaveFormatVersion FormatVersion => SaveFormatVersion.Version131To189;
|
||||
|
||||
public bool CanHandle(NbtCompound data, int? dataVersion)
|
||||
=> dataVersion is null && data.Contains("allowCommands");
|
||||
|
||||
public SaveInfo Parse(string folderPath, NbtCompound data, DateTime createdAt, DateTime modifiedAt)
|
||||
{
|
||||
return new SaveInfo
|
||||
{
|
||||
LevelName = data.TryGet<NbtString>("LevelName", out var ln) ? ln!.Value : "unknown",
|
||||
VersionName = null,
|
||||
VersionId = null,
|
||||
Seed = NbtReadHelper.TryGetLong(data, "RandomSeed"),
|
||||
LastPlayedUtc = NbtReadHelper.ReadLastPlayed(data),
|
||||
Spawn = NbtReadHelper.TryReadSpawnFromFields(data),
|
||||
GameMode = NbtReadHelper.ReadGameMode(data, out _),
|
||||
Difficulty = NbtReadHelper.ReadDifficultyByte(data),
|
||||
IsDifficultyLocked = data.TryGet<NbtByte>("DifficultyLocked", out var dl) && dl!.Value == 1,
|
||||
IsHardcore = data.TryGet<NbtByte>("hardcore", out var hc) && hc!.Value == 1,
|
||||
AllowCommands = data.TryGet<NbtByte>("allowCommands", out var ac) && ac!.Value == 1,
|
||||
PlayTime = NbtReadHelper.ReadPlayTime(data),
|
||||
FolderPath = folderPath,
|
||||
CreatedAt = createdAt,
|
||||
ModifiedAt = modifiedAt,
|
||||
};
|
||||
}
|
||||
}
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
using System;
|
||||
using fNbt;
|
||||
|
||||
namespace PCL.Core.Minecraft.Saves.Parsing.Internal;
|
||||
|
||||
/// <summary>
|
||||
/// 15w32a(1.9) ~ 1.12.2 的存档格式。
|
||||
/// 特征:DataVersion >= 100 且 < 1443,新增 DataVersion 和 Version 复合标签。
|
||||
/// </summary>
|
||||
internal sealed class Version19To1122SaveParser : ISaveParser
|
||||
{
|
||||
private readonly ISaveParser _baseParser;
|
||||
|
||||
public Version19To1122SaveParser() : this(new Version131To189SaveParser()) { }
|
||||
public Version19To1122SaveParser(ISaveParser baseParser) => _baseParser = baseParser;
|
||||
|
||||
public SaveFormatVersion FormatVersion => SaveFormatVersion.Version19To1122;
|
||||
|
||||
public bool CanHandle(NbtCompound data, int? dataVersion)
|
||||
=> dataVersion.HasValue
|
||||
&& dataVersion.Value >= DataVersionBoundaries._15w32a
|
||||
&& dataVersion.Value < DataVersionBoundaries._17w47a;
|
||||
|
||||
public SaveInfo Parse(string folderPath, NbtCompound data, DateTime createdAt, DateTime modifiedAt)
|
||||
{
|
||||
var baseInfo = _baseParser.Parse(folderPath, data, createdAt, modifiedAt);
|
||||
(var versionName, var versionId) = NbtReadHelper.ReadVersion(data);
|
||||
return baseInfo with { VersionName = versionName, VersionId = versionId };
|
||||
}
|
||||
}
|
||||
+101
@@ -0,0 +1,101 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using fNbt;
|
||||
|
||||
namespace PCL.Core.Minecraft.Saves.Parsing.Internal;
|
||||
|
||||
/// <summary>
|
||||
/// 26.1-snapshot-6 及之后的存档格式(2026 新版本号体系)。
|
||||
/// 特征:DataVersion >= 4774 或存在 difficulty_settings 复合标签。
|
||||
/// 变更:
|
||||
/// - 出生点迁移到 spawn.pos int[3]
|
||||
/// - 难度迁移到 difficulty_settings 复合标签(字符串型)
|
||||
/// - 种子可能在外部文件 data/minecraft/world_gen_settings.dat 中
|
||||
/// </summary>
|
||||
internal sealed class Version261PlusSaveParser : ISaveParser
|
||||
{
|
||||
private readonly ISaveParser _baseParser;
|
||||
|
||||
public Version261PlusSaveParser() : this(new Version19To1122SaveParser()) { }
|
||||
public Version261PlusSaveParser(ISaveParser baseParser) => _baseParser = baseParser;
|
||||
|
||||
public SaveFormatVersion FormatVersion => SaveFormatVersion.Version261Plus;
|
||||
|
||||
public bool CanHandle(NbtCompound data, int? dataVersion)
|
||||
=> dataVersion >= DataVersionBoundaries._261snapshot6
|
||||
|| data.Contains("difficulty_settings");
|
||||
|
||||
public SaveInfo Parse(string folderPath, NbtCompound data, DateTime createdAt, DateTime modifiedAt)
|
||||
{
|
||||
var baseInfo = _baseParser.Parse(folderPath, data, createdAt, modifiedAt);
|
||||
|
||||
var seed = Version116To1211SaveParser.ReadWorldGenSeed(data)
|
||||
?? ReadSeedFromExternalFile(folderPath);
|
||||
|
||||
var spawn = NbtReadHelper.TryReadSpawnFromPos(data)
|
||||
?? NbtReadHelper.TryReadSpawnFromFields(data);
|
||||
|
||||
var difficulty = ReadDifficultySettings(data);
|
||||
var isHardcore = ReadHardcore(data);
|
||||
var isLocked = ReadLocked(data);
|
||||
|
||||
return baseInfo with
|
||||
{
|
||||
Seed = seed,
|
||||
Spawn = spawn,
|
||||
Difficulty = difficulty,
|
||||
IsHardcore = isHardcore,
|
||||
IsDifficultyLocked = isLocked,
|
||||
GameMode = isHardcore ? GameMode.Hardcore : baseInfo.GameMode,
|
||||
};
|
||||
}
|
||||
|
||||
// ── difficulty_settings 复合标签解析 ──
|
||||
|
||||
internal static Difficulty? ReadDifficultySettings(NbtCompound data)
|
||||
{
|
||||
if (data.TryGet<NbtCompound>("difficulty_settings", out var ds) &&
|
||||
ds!.TryGet<NbtString>("difficulty", out var diffStr))
|
||||
{
|
||||
return diffStr!.Value switch
|
||||
{
|
||||
"peaceful" => Difficulty.Peaceful,
|
||||
"easy" => Difficulty.Easy,
|
||||
"normal" => Difficulty.Normal,
|
||||
"hard" => Difficulty.Hard,
|
||||
_ => null,
|
||||
};
|
||||
}
|
||||
return NbtReadHelper.ReadDifficultyByte(data);
|
||||
}
|
||||
|
||||
internal static bool ReadHardcore(NbtCompound data)
|
||||
{
|
||||
if (data.TryGet<NbtCompound>("difficulty_settings", out var ds) &&
|
||||
ds!.TryGet<NbtByte>("hardcore", out var hc))
|
||||
return hc!.Value == 1;
|
||||
return data.TryGet<NbtByte>("hardcore", out var legacyHc) && legacyHc!.Value == 1;
|
||||
}
|
||||
|
||||
internal static bool ReadLocked(NbtCompound data)
|
||||
{
|
||||
if (data.TryGet<NbtCompound>("difficulty_settings", out var ds) &&
|
||||
ds!.TryGet<NbtByte>("locked", out var locked))
|
||||
return locked!.Value == 1;
|
||||
return data.TryGet<NbtByte>("DifficultyLocked", out var dl) && dl!.Value == 1;
|
||||
}
|
||||
|
||||
internal static long? ReadSeedFromExternalFile(string folderPath)
|
||||
{
|
||||
var externalPath = Path.Combine(folderPath, "data", "minecraft", "world_gen_settings.dat");
|
||||
if (!File.Exists(externalPath))
|
||||
return null;
|
||||
try
|
||||
{
|
||||
var nbtFile = new NbtFile(externalPath);
|
||||
var rootData = nbtFile.RootTag.Get<NbtCompound>("data");
|
||||
return rootData?.TryGet<NbtLong>("seed", out var seed) == true ? seed!.Value : null;
|
||||
}
|
||||
catch { return null; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using fNbt;
|
||||
using PCL.Core.Minecraft.Saves.Parsing.Internal;
|
||||
|
||||
namespace PCL.Core.Minecraft.Saves.Parsing;
|
||||
|
||||
/// <summary>
|
||||
/// 解析器工厂 —— 按优先级遍历已注册的解析器,返回第一个能处理给定数据的解析器。
|
||||
/// 默认注册顺序从高版本到低版本,确保最特化的解析器优先匹配。
|
||||
/// 可通过构造函数注入自定义解析器列表。
|
||||
/// </summary>
|
||||
public sealed class SaveParserFactory
|
||||
{
|
||||
private readonly IReadOnlyList<ISaveParser> _parsers;
|
||||
|
||||
/// <summary>使用内置的默认解析器列表初始化(从高版本到低版本)。</summary>
|
||||
public SaveParserFactory()
|
||||
{
|
||||
_parsers =
|
||||
[
|
||||
new Version261PlusSaveParser(), // >= 26.1-snapshot-6
|
||||
new Version116To1211SaveParser(), // 1.16 ~ 1.21.11
|
||||
new Version113To1152SaveParser(), // 1.13 ~ 1.15.2
|
||||
new Version19To1122SaveParser(), // 1.9 ~ 1.12.2
|
||||
new Version131To189SaveParser(), // 1.3.1 ~ 1.8.9
|
||||
new Pre113SaveParser(), // Alpha ~ 1.2.5
|
||||
];
|
||||
}
|
||||
|
||||
/// <summary>使用自定义解析器列表初始化(支持 DI 注入)。解析器按传入顺序求值。</summary>
|
||||
public SaveParserFactory(IEnumerable<ISaveParser> customParsers)
|
||||
{
|
||||
_parsers = customParsers?.ToArray() ?? [];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找第一个能处理给定 NBT 数据的解析器。
|
||||
/// </summary>
|
||||
/// <param name="data">level.dat 中的 Data 复合标签。</param>
|
||||
/// <param name="dataVersion">DataVersion 字段值,如果不存在则为 null。</param>
|
||||
/// <returns>匹配的解析器,未找到时返回 null。</returns>
|
||||
public ISaveParser? Resolve(NbtCompound data, int? dataVersion)
|
||||
{
|
||||
foreach (var parser in _parsers)
|
||||
{
|
||||
if (parser.CanHandle(data, dataVersion))
|
||||
return parser;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user