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.Collections.Generic;
using MDPro3.Duel.YGOSharp;
using System.Linq;
using System.Text;
namespace MDPro3
{
......@@ -18,9 +20,6 @@ namespace MDPro3
public static class OnlineAppearanceSync
{
public const string Prefix = "mdp3acc:v2:";
public const string LegacyPrefix = "/mdp3acc:v1:";
private const int DefaultCase = 1080001;
private const int DefaultProtector = 1070001;
private const int DefaultField = 1090001;
......@@ -38,7 +37,9 @@ namespace MDPro3
public static string BuildMessage(Deck 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)
......@@ -53,68 +54,25 @@ namespace MDPro3
if (!TryExtractPayload(content, out var payload))
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
{
Case = EnsureValidCode(deckCase, Items.ItemType.Case, DefaultCase),
Protector = EnsureValidCode(protector, Items.ItemType.Protector, DefaultProtector),
Field = EnsureValidCode(field, Items.ItemType.Mat, DefaultField),
Grave = EnsureValidCode(grave, Items.ItemType.Grave, DefaultGrave),
Stand = EnsureValidCode(stand, Items.ItemType.Stand, DefaultStand),
Mate = EnsureValidCode(mate, Items.ItemType.Mate, DefaultMate),
Face = EnsureValidCode(face, Items.ItemType.Face, defaultFace),
Frame = EnsureValidCode(frame, Items.ItemType.Frame, defaultFrame),
Case = EnsureValidCode(payload[0], Items.ItemType.Case, DefaultCase),
Protector = EnsureValidCode(payload[1], Items.ItemType.Protector, DefaultProtector),
Field = EnsureValidCode(payload[2], Items.ItemType.Mat, DefaultField),
Grave = EnsureValidCode(payload[3], Items.ItemType.Grave, DefaultGrave),
Stand = EnsureValidCode(payload[4], Items.ItemType.Stand, DefaultStand),
Mate = EnsureValidCode(payload[5], Items.ItemType.Mate, DefaultMate),
Face = EnsureValidCode(payload[6], Items.ItemType.Face, DefaultFace),
Frame = EnsureValidCode(payload[7], Items.ItemType.Frame, DefaultFrame),
};
return true;
}
private static bool TryExtractPayload(string content, out string payload)
private static bool TryExtractPayload(string content, out int[] payload)
{
payload = null;
if (string.IsNullOrEmpty(content))
return false;
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;
payload = ZeroWidthIntsCodec.Decode(content);
if (payload == null) return false;
return true;
}
public static bool IsValid(OnlineAppearanceData data)
......@@ -255,11 +213,116 @@ namespace MDPro3
if (list == null)
return false;
for (var i = 0; i < list.Count; i++)
if (list[i].id == id)
return true;
return list.Any(item => item.id == id);
}
}
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
return;
var syncMessage = OnlineAppearanceSync.BuildMessageForLocalPlayer(deckFor);
CtosMessage_Chat(syncMessage);
Debug.Log($"[OnlineAppearance] Sent sync payload: {syncMessage}");
}
public static void CtosMessage_UpdateAppearanceFromCurrentDeck()
......
......@@ -547,7 +547,7 @@ namespace MDPro3.Servant
{
int player = r.ReadInt16();
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 (player >= 0 && player < 4 && OnlineAppearanceSync.TryParse(content, out var appearance))
......@@ -559,7 +559,7 @@ namespace MDPro3.Servant
}
else
{
Debug.LogWarning($"[OnlineAppearance] Ignored sync chat. seat={player}, content='{content}'");
Debug.LogWarning($"[OnlineAppearance] Ignored sync chat. seat={player}");
}
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