Commit 238ed70b authored by SherryChaos's avatar SherryChaos

Use zero-width codec for online appearance sync

avoid disturbing other clients
parent 9aa76c97
using MDPro3.Duel.YGOSharp;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using MDPro3.Duel.YGOSharp; using System.Linq;
using System.Text;
namespace MDPro3 namespace MDPro3
{ {
...@@ -18,9 +20,6 @@ namespace MDPro3 ...@@ -18,9 +20,6 @@ namespace MDPro3
public static class OnlineAppearanceSync public static class OnlineAppearanceSync
{ {
public const string Prefix = "mdp3acc:v2:";
public const string LegacyPrefix = "/mdp3acc:v1:";
private const int DefaultCase = 1080001; private const int DefaultCase = 1080001;
private const int DefaultProtector = 1070001; private const int DefaultProtector = 1070001;
private const int DefaultField = 1090001; private const int DefaultField = 1090001;
...@@ -38,7 +37,9 @@ namespace MDPro3 ...@@ -38,7 +37,9 @@ namespace MDPro3
public static string BuildMessage(Deck deck) public static string BuildMessage(Deck deck)
{ {
var data = BuildValidatedData(deck); var data = BuildValidatedData(deck);
return $"{Prefix}{data.Case},{data.Protector},{data.Field},{data.Grave},{data.Stand},{data.Mate},{data.Face},{data.Frame}"; var ints = new int[]
{ data.Case, data.Protector, data.Field, data.Grave, data.Stand, data.Mate, data.Face, data.Frame };
return ZeroWidthIntsCodec.Encode(ints);
} }
public static string BuildMessageForLocalPlayer(Deck deck) public static string BuildMessageForLocalPlayer(Deck deck)
...@@ -53,68 +54,25 @@ namespace MDPro3 ...@@ -53,68 +54,25 @@ namespace MDPro3
if (!TryExtractPayload(content, out var payload)) if (!TryExtractPayload(content, out var payload))
return false; return false;
var parts = payload.Split(',');
if (parts.Length != 6 && parts.Length != 8)
return false;
if (!int.TryParse(parts[0], out var deckCase))
return false;
if (!int.TryParse(parts[1], out var protector))
return false;
if (!int.TryParse(parts[2], out var field))
return false;
if (!int.TryParse(parts[3], out var grave))
return false;
if (!int.TryParse(parts[4], out var stand))
return false;
if (!int.TryParse(parts[5], out var mate))
return false;
var defaultFace = GetDefaultCode(Items.ItemType.Face, DefaultFace);
var defaultFrame = GetDefaultCode(Items.ItemType.Frame, DefaultFrame);
var face = defaultFace;
var frame = defaultFrame;
if (parts.Length >= 8)
{
if (!int.TryParse(parts[6], out face))
return false;
if (!int.TryParse(parts[7], out frame))
return false;
}
data = new OnlineAppearanceData data = new OnlineAppearanceData
{ {
Case = EnsureValidCode(deckCase, Items.ItemType.Case, DefaultCase), Case = EnsureValidCode(payload[0], Items.ItemType.Case, DefaultCase),
Protector = EnsureValidCode(protector, Items.ItemType.Protector, DefaultProtector), Protector = EnsureValidCode(payload[1], Items.ItemType.Protector, DefaultProtector),
Field = EnsureValidCode(field, Items.ItemType.Mat, DefaultField), Field = EnsureValidCode(payload[2], Items.ItemType.Mat, DefaultField),
Grave = EnsureValidCode(grave, Items.ItemType.Grave, DefaultGrave), Grave = EnsureValidCode(payload[3], Items.ItemType.Grave, DefaultGrave),
Stand = EnsureValidCode(stand, Items.ItemType.Stand, DefaultStand), Stand = EnsureValidCode(payload[4], Items.ItemType.Stand, DefaultStand),
Mate = EnsureValidCode(mate, Items.ItemType.Mate, DefaultMate), Mate = EnsureValidCode(payload[5], Items.ItemType.Mate, DefaultMate),
Face = EnsureValidCode(face, Items.ItemType.Face, defaultFace), Face = EnsureValidCode(payload[6], Items.ItemType.Face, DefaultFace),
Frame = EnsureValidCode(frame, Items.ItemType.Frame, defaultFrame), Frame = EnsureValidCode(payload[7], Items.ItemType.Frame, DefaultFrame),
}; };
return true; return true;
} }
private static bool TryExtractPayload(string content, out string payload) private static bool TryExtractPayload(string content, out int[] payload)
{ {
payload = null; payload = ZeroWidthIntsCodec.Decode(content);
if (string.IsNullOrEmpty(content)) if (payload == null) return false;
return false; return true;
if (content.StartsWith(Prefix, StringComparison.Ordinal))
{
payload = content.Substring(Prefix.Length).Trim().TrimEnd('\0');
return true;
}
if (content.StartsWith(LegacyPrefix, StringComparison.Ordinal))
{
payload = content.Substring(LegacyPrefix.Length).Trim().TrimEnd('\0');
return true;
}
return false;
} }
public static bool IsValid(OnlineAppearanceData data) public static bool IsValid(OnlineAppearanceData data)
...@@ -255,11 +213,116 @@ namespace MDPro3 ...@@ -255,11 +213,116 @@ namespace MDPro3
if (list == null) if (list == null)
return false; return false;
for (var i = 0; i < list.Count; i++) return list.Any(item => item.id == id);
if (list[i].id == id) }
return true; }
public static class ZeroWidthIntsCodec
{
// 定义4个零宽字符表示2比特
private const char ZW_00 = '\u200B'; // 零宽空格 -> 00
private const char ZW_01 = '\u200C'; // 零宽非连接符 -> 01
private const char ZW_10 = '\u200D'; // 零宽连接符 -> 10
private const char ZW_11 = '\u200E'; // 从左到右标记 -> 11
// 起始标记:两个连续ZW_10 (U+200D U+200D)
private const string START_MARKER = "\u200D\u200D";
// 映射表:2比特值 -> 字符
private static readonly char[] Bit2Char = new char[4] { ZW_00, ZW_01, ZW_10, ZW_11 };
// 反向映射:字符 -> 2比特值
private static readonly Dictionary<char, byte> Char2Bit = new()
{{ ZW_00, 0 }, { ZW_01, 1 }, { ZW_10, 2 }, { ZW_11, 3 }};
public static string Encode(int[] ints)
{
if (ints == null || ints.Length != 8)
throw new ArgumentException("需要长度为8的int数组");
// 1. 将8个int转换为字节数组(小端序)
byte[] bytes = new byte[32];
for (int i = 0; i < 8; i++)
{
byte[] intBytes = BitConverter.GetBytes(ints[i]);
if (!BitConverter.IsLittleEndian)
Array.Reverse(intBytes);
Array.Copy(intBytes, 0, bytes, i * 4, 4);
}
// 2. 将字节数组转换为比特流(使用BitArray,索引0为最低位)
System.Collections.BitArray bits = new(bytes);
return false; // 3. 每次取2比特,映射为字符
StringBuilder sb = new();
sb.Append(START_MARKER);
for (int i = 0; i < bits.Length; i += 2)
{
// 构建2比特值 (低位在前,即bit i为低位,bit i+1为高位)
int value = (bits[i] ? 1 : 0) | ((bits[i + 1] ? 1 : 0) << 1);
sb.Append(Bit2Char[value]);
}
return sb.ToString();
}
public static int[] Decode(string hiddenMessage)
{
if (string.IsNullOrEmpty(hiddenMessage))
return null;
// 查找起始标记
int markerIndex = hiddenMessage.IndexOf(START_MARKER, StringComparison.Ordinal);
if (markerIndex == -1)
return null;
string dataPart = hiddenMessage[(markerIndex + START_MARKER.Length)..];
// 收集比特
List<bool> bits = new();
foreach (char c in dataPart)
{
if (!Char2Bit.TryGetValue(c, out byte twoBits))
{
// 由于我们预期只有映射字符,所以如果出现未知字符,可视为无效消息。
return null;
}
// 将2比特拆分为两个布尔值,注意低位先存
bits.Add((twoBits & 1) != 0); // 低位
bits.Add((twoBits & 2) != 0); // 高位
}
// 校验比特数:应为256
if (bits.Count != 256)
{
// 可能数据损坏
return null;
}
// 将比特数组转回字节数组
byte[] bytes = new byte[32];
for (int i = 0; i < bits.Count; i++)
{
if (bits[i])
{
int byteIndex = i / 8;
int bitIndex = i % 8;
bytes[byteIndex] |= (byte)(1 << bitIndex);
}
}
// 解析8个int
int[] result = new int[8];
for (int i = 0; i < 8; i++)
{
byte[] intBytes = new byte[4];
Array.Copy(bytes, i * 4, intBytes, 0, 4);
if (!BitConverter.IsLittleEndian)
Array.Reverse(intBytes);
result[i] = BitConverter.ToInt32(intBytes, 0);
}
return result;
} }
} }
} }
...@@ -471,7 +471,6 @@ namespace MDPro3 ...@@ -471,7 +471,6 @@ namespace MDPro3
return; return;
var syncMessage = OnlineAppearanceSync.BuildMessageForLocalPlayer(deckFor); var syncMessage = OnlineAppearanceSync.BuildMessageForLocalPlayer(deckFor);
CtosMessage_Chat(syncMessage); CtosMessage_Chat(syncMessage);
Debug.Log($"[OnlineAppearance] Sent sync payload: {syncMessage}");
} }
public static void CtosMessage_UpdateAppearanceFromCurrentDeck() public static void CtosMessage_UpdateAppearanceFromCurrentDeck()
......
...@@ -547,7 +547,7 @@ namespace MDPro3.Servant ...@@ -547,7 +547,7 @@ namespace MDPro3.Servant
{ {
int player = r.ReadInt16(); int player = r.ReadInt16();
var length = (int)((r.BaseStream.Length - r.BaseStream.Position) / 2); var length = (int)((r.BaseStream.Length - r.BaseStream.Position) / 2);
var content = r.ReadUnicode((int)length); var content = r.ReadUnicode(length);
if (OnlineAppearanceSync.IsSyncMessage(content)) if (OnlineAppearanceSync.IsSyncMessage(content))
{ {
if (player >= 0 && player < 4 && OnlineAppearanceSync.TryParse(content, out var appearance)) if (player >= 0 && player < 4 && OnlineAppearanceSync.TryParse(content, out var appearance))
...@@ -559,7 +559,7 @@ namespace MDPro3.Servant ...@@ -559,7 +559,7 @@ namespace MDPro3.Servant
} }
else else
{ {
Debug.LogWarning($"[OnlineAppearance] Ignored sync chat. seat={player}, content='{content}'"); Debug.LogWarning($"[OnlineAppearance] Ignored sync chat. seat={player}");
} }
return; return;
} }
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment