1222 lines
39 KiB
C#
1222 lines
39 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Text;
|
|
using UnityEngine;
|
|
#if UNITY_EDITOR
|
|
using UnityEditor;
|
|
#endif
|
|
|
|
[Serializable]
|
|
public class TeamShareSlotData
|
|
{
|
|
public int heroId;
|
|
public List<int> skillGroupIds = new List<int>();
|
|
}
|
|
|
|
[Serializable]
|
|
public class TeamShareSnapshot
|
|
{
|
|
public const int CurrentVersion = 3;
|
|
public const int SlotCount = 5;
|
|
public const string DefaultSchemeName = "\u65b9\u68481";
|
|
|
|
public int version = CurrentVersion;
|
|
public int leaderHeroId;
|
|
public string schemeName = DefaultSchemeName;
|
|
public TeamShareSlotData[] slots = CreateDefaultSlots();
|
|
|
|
public static TeamShareSlotData[] CreateDefaultSlots()
|
|
{
|
|
TeamShareSlotData[] result = new TeamShareSlotData[SlotCount];
|
|
for (int i = 0; i < result.Length; i++)
|
|
{
|
|
result[i] = new TeamShareSlotData();
|
|
}
|
|
|
|
return result;
|
|
}
|
|
}
|
|
|
|
[Serializable]
|
|
public class TeamShareApplyReport
|
|
{
|
|
public string shareCode;
|
|
public string schemeName;
|
|
public int appliedHeroCount;
|
|
public int skippedHeroCount;
|
|
public int appliedSkillCount;
|
|
public int skippedSkillCount;
|
|
public List<string> messages = new List<string>();
|
|
}
|
|
|
|
public static class TeamShareCodec
|
|
{
|
|
public const string Prefix = "T3";
|
|
public const string LegacyPrefixV3 = "TS3";
|
|
public const string LegacyPrefixV2 = "BT2";
|
|
public const string LegacyPrefixV1 = "BT1";
|
|
public const string EmptySlotToken = "0";
|
|
|
|
private const char OuterSeparator = '-';
|
|
private const string HeroSlot01Key = "selected_heroSlot01_heroID";
|
|
private const string HeroSlot02Key = "selected_heroSlot02_heroID";
|
|
private const string HeroSlot03Key = "selected_heroSlot03_heroID";
|
|
private const string HeroSlot04Key = "selected_heroSlot04_heroID";
|
|
private const string HeroSlot05Key = "selected_heroSlot05_heroID";
|
|
|
|
private static AllyHero_SO[] cachedHeroes;
|
|
private static Dictionary<int, AllyHero_SO> cachedHeroById;
|
|
|
|
public static bool TryExportCurrentTeam(out string shareCode, out string errorMessage)
|
|
{
|
|
return TryExportCurrentTeam(TeamShareSnapshot.DefaultSchemeName, out shareCode, out errorMessage);
|
|
}
|
|
|
|
public static bool TryExportCurrentTeam(string schemeName, out string shareCode, out string errorMessage)
|
|
{
|
|
shareCode = string.Empty;
|
|
errorMessage = string.Empty;
|
|
|
|
TeamShareSnapshot snapshot;
|
|
if (!TryBuildCurrentSnapshot(schemeName, out snapshot, out errorMessage))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
shareCode = ExportSnapshot(snapshot);
|
|
return true;
|
|
}
|
|
|
|
public static string ExportSnapshot(TeamShareSnapshot snapshot)
|
|
{
|
|
TeamShareSnapshot safeSnapshot = snapshot ?? new TeamShareSnapshot();
|
|
NormalizeSnapshot(safeSnapshot);
|
|
|
|
byte[] payloadBytes = BuildPayloadBytes(safeSnapshot);
|
|
string payloadText = ToBase64Url(payloadBytes);
|
|
return safeSnapshot.schemeName + OuterSeparator + Prefix + payloadText;
|
|
}
|
|
|
|
public static bool TryParse(string shareCode, out TeamShareSnapshot snapshot, out string errorMessage)
|
|
{
|
|
snapshot = null;
|
|
errorMessage = string.Empty;
|
|
|
|
if (string.IsNullOrWhiteSpace(shareCode))
|
|
{
|
|
errorMessage = "Share code is empty.";
|
|
return false;
|
|
}
|
|
|
|
string trimmed = shareCode.Trim();
|
|
string visibleSchemeName;
|
|
string payload;
|
|
SplitVisibleNameAndPayload(trimmed, out visibleSchemeName, out payload);
|
|
if (string.IsNullOrWhiteSpace(payload))
|
|
{
|
|
errorMessage = "Invalid share code format.";
|
|
return false;
|
|
}
|
|
|
|
if (payload.StartsWith(Prefix, StringComparison.Ordinal))
|
|
{
|
|
return TryParseT3Payload(payload, visibleSchemeName, out snapshot, out errorMessage);
|
|
}
|
|
|
|
if (payload.StartsWith(LegacyPrefixV3, StringComparison.Ordinal))
|
|
{
|
|
return TryParseTs3Payload(payload, visibleSchemeName, out snapshot, out errorMessage);
|
|
}
|
|
|
|
if (payload.StartsWith(LegacyPrefixV2, StringComparison.Ordinal) || payload.StartsWith(LegacyPrefixV1, StringComparison.Ordinal))
|
|
{
|
|
return TryParseLegacyPayload(payload, visibleSchemeName, out snapshot, out errorMessage);
|
|
}
|
|
|
|
errorMessage = "Unsupported share code version.";
|
|
return false;
|
|
}
|
|
|
|
public static bool TryValidate(string shareCode, out TeamShareSnapshot snapshot, out string errorMessage)
|
|
{
|
|
snapshot = null;
|
|
errorMessage = string.Empty;
|
|
|
|
TeamShareSnapshot parsed;
|
|
if (!TryParse(shareCode, out parsed, out errorMessage))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (!TryValidateSnapshot(parsed, out errorMessage))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
snapshot = parsed;
|
|
return true;
|
|
}
|
|
|
|
public static bool TryApplyToCurrentTeam(string shareCode, out string errorMessage)
|
|
{
|
|
TeamShareApplyReport report;
|
|
return TryApplyToCurrentTeam(shareCode, out report, out errorMessage);
|
|
}
|
|
|
|
public static bool TryApplyToCurrentTeam(string shareCode, out TeamShareApplyReport report, out string errorMessage)
|
|
{
|
|
errorMessage = string.Empty;
|
|
report = new TeamShareApplyReport
|
|
{
|
|
shareCode = shareCode ?? string.Empty
|
|
};
|
|
|
|
TeamShareSnapshot snapshot;
|
|
if (!TryParse(shareCode, out snapshot, out errorMessage))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (!TryApplySnapshotPermissive(snapshot, report, out errorMessage))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
ApplySnapshotToTeamManager(snapshot);
|
|
ApplySnapshotToSelectionPrefs(snapshot);
|
|
RefreshSelectionViews();
|
|
|
|
report.schemeName = snapshot.schemeName;
|
|
report.shareCode = ExportSnapshot(snapshot);
|
|
return true;
|
|
}
|
|
|
|
public static bool TryBuildCurrentSnapshot(out TeamShareSnapshot snapshot, out string errorMessage)
|
|
{
|
|
return TryBuildCurrentSnapshot(TeamShareSnapshot.DefaultSchemeName, out snapshot, out errorMessage);
|
|
}
|
|
|
|
public static bool TryBuildCurrentSnapshot(string schemeName, out TeamShareSnapshot snapshot, out string errorMessage)
|
|
{
|
|
snapshot = new TeamShareSnapshot();
|
|
snapshot.schemeName = SanitizeSchemeName(schemeName);
|
|
errorMessage = string.Empty;
|
|
|
|
int[] heroIds = ReadCurrentSelectedHeroIds();
|
|
for (int i = 0; i < TeamShareSnapshot.SlotCount; i++)
|
|
{
|
|
TeamShareSlotData slot = new TeamShareSlotData();
|
|
int heroId = i < heroIds.Length ? heroIds[i] : 0;
|
|
slot.heroId = heroId;
|
|
|
|
if (heroId > 0)
|
|
{
|
|
AllyHero_SO hero = FindHeroById(heroId);
|
|
if (hero == null)
|
|
{
|
|
errorMessage = "Unknown hero id in current team: " + heroId;
|
|
return false;
|
|
}
|
|
|
|
hero.LoadEquippedSkillsFromLocal();
|
|
slot.skillGroupIds = SanitizeSkillIds(hero.equippedSkillGroupIDs);
|
|
}
|
|
|
|
snapshot.slots[i] = slot;
|
|
}
|
|
|
|
snapshot.leaderHeroId = ResolveCurrentLeaderHeroId(snapshot);
|
|
NormalizeSnapshot(snapshot);
|
|
return true;
|
|
}
|
|
|
|
public static bool TryValidateSnapshot(TeamShareSnapshot snapshot, out string errorMessage)
|
|
{
|
|
errorMessage = string.Empty;
|
|
if (snapshot == null)
|
|
{
|
|
errorMessage = "Team snapshot is null.";
|
|
return false;
|
|
}
|
|
|
|
NormalizeSnapshot(snapshot);
|
|
EnsureHeroCache();
|
|
|
|
HashSet<int> uniqueHeroIds = new HashSet<int>();
|
|
bool hasAnyHero = false;
|
|
|
|
for (int i = 0; i < TeamShareSnapshot.SlotCount; i++)
|
|
{
|
|
TeamShareSlotData slot = snapshot.slots[i];
|
|
if (slot == null)
|
|
{
|
|
errorMessage = "Slot data is missing.";
|
|
return false;
|
|
}
|
|
|
|
if (slot.heroId <= 0)
|
|
{
|
|
if (slot.skillGroupIds != null && slot.skillGroupIds.Count > 0)
|
|
{
|
|
errorMessage = "Empty slot cannot contain skills.";
|
|
return false;
|
|
}
|
|
|
|
continue;
|
|
}
|
|
|
|
hasAnyHero = true;
|
|
|
|
if (!uniqueHeroIds.Add(slot.heroId))
|
|
{
|
|
errorMessage = "Duplicate hero id detected: " + slot.heroId;
|
|
return false;
|
|
}
|
|
|
|
AllyHero_SO hero = FindHeroById(slot.heroId);
|
|
if (hero == null)
|
|
{
|
|
errorMessage = "Hero not found: " + slot.heroId;
|
|
return false;
|
|
}
|
|
|
|
hero.LoadEquippedEquipmentFromLocal();
|
|
|
|
List<int> sanitizedSkillIds = SanitizeSkillIds(slot.skillGroupIds);
|
|
int maxSkillSlots = Mathf.Max(0, hero.GetEffectiveSkillSlotLimit());
|
|
if (sanitizedSkillIds.Count > maxSkillSlots)
|
|
{
|
|
errorMessage = "Too many skills equipped for hero: " + hero.ally_heroName;
|
|
return false;
|
|
}
|
|
|
|
AllyHero_SO.AllyLevelInfo levelInfo = hero.GetEffectiveLevelForCurrentEXP();
|
|
int currentLevelId = levelInfo != null ? levelInfo.levelID : 0;
|
|
|
|
for (int skillIndex = 0; skillIndex < sanitizedSkillIds.Count; skillIndex++)
|
|
{
|
|
int skillGroupId = sanitizedSkillIds[skillIndex];
|
|
SkillGroup group = hero.GetSkillGroupByID(skillGroupId);
|
|
if (group == null)
|
|
{
|
|
errorMessage = "Skill group id is invalid for hero " + hero.ally_heroName + ": " + skillGroupId;
|
|
return false;
|
|
}
|
|
|
|
int requiredLevel = Mathf.Max(0, group.thisSkill_levelLimit);
|
|
if (currentLevelId < requiredLevel)
|
|
{
|
|
errorMessage = "Hero level is too low for skill: " + (group.groupName ?? skillGroupId.ToString());
|
|
return false;
|
|
}
|
|
}
|
|
|
|
slot.skillGroupIds = sanitizedSkillIds;
|
|
}
|
|
|
|
if (!hasAnyHero)
|
|
{
|
|
errorMessage = "Share code contains no heroes.";
|
|
return false;
|
|
}
|
|
|
|
if (snapshot.leaderHeroId > 0 && !uniqueHeroIds.Contains(snapshot.leaderHeroId))
|
|
{
|
|
errorMessage = "Leader hero is not present in team slots.";
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
private static byte[] BuildPayloadBytes(TeamShareSnapshot snapshot)
|
|
{
|
|
List<byte> bytes = new List<byte>(128);
|
|
WriteVarInt(bytes, snapshot.leaderHeroId);
|
|
|
|
for (int i = 0; i < TeamShareSnapshot.SlotCount; i++)
|
|
{
|
|
TeamShareSlotData slot = snapshot.slots[i] ?? new TeamShareSlotData();
|
|
WriteVarInt(bytes, Mathf.Max(0, slot.heroId));
|
|
|
|
List<int> skillIds = slot.heroId > 0 ? SanitizeSkillIds(slot.skillGroupIds) : new List<int>();
|
|
WriteVarInt(bytes, skillIds.Count);
|
|
for (int j = 0; j < skillIds.Count; j++)
|
|
{
|
|
WriteVarInt(bytes, skillIds[j]);
|
|
}
|
|
}
|
|
|
|
byte[] schemeNameBytes = Encoding.UTF8.GetBytes(SanitizeSchemeName(snapshot.schemeName));
|
|
bytes.AddRange(schemeNameBytes);
|
|
|
|
ushort checksum = ComputeShortHashUShort(bytes);
|
|
bytes.Add((byte)(checksum & 0xFF));
|
|
bytes.Add((byte)((checksum >> 8) & 0xFF));
|
|
|
|
return bytes.ToArray();
|
|
}
|
|
|
|
private static bool TryParseT3Payload(string payload, string visibleSchemeName, out TeamShareSnapshot snapshot, out string errorMessage)
|
|
{
|
|
snapshot = null;
|
|
errorMessage = string.Empty;
|
|
|
|
string encodedPayload = payload.Substring(Prefix.Length);
|
|
byte[] rawBytes;
|
|
if (!TryFromBase64Url(encodedPayload, out rawBytes))
|
|
{
|
|
errorMessage = "Invalid share code payload.";
|
|
return false;
|
|
}
|
|
|
|
if (rawBytes == null || rawBytes.Length < 2)
|
|
{
|
|
errorMessage = "Invalid share code payload.";
|
|
return false;
|
|
}
|
|
|
|
int checksumIndex = rawBytes.Length - 2;
|
|
ushort expectedChecksum = (ushort)(
|
|
rawBytes[checksumIndex]
|
|
| (rawBytes[checksumIndex + 1] << 8));
|
|
|
|
List<byte> payloadBytes = new List<byte>(checksumIndex);
|
|
for (int i = 0; i < checksumIndex; i++)
|
|
{
|
|
payloadBytes.Add(rawBytes[i]);
|
|
}
|
|
|
|
ushort actualChecksum = ComputeShortHashUShort(payloadBytes);
|
|
if (expectedChecksum != actualChecksum)
|
|
{
|
|
errorMessage = "Share code checksum failed.";
|
|
return false;
|
|
}
|
|
|
|
TeamShareSnapshot parsed = new TeamShareSnapshot();
|
|
int readIndex = 0;
|
|
|
|
if (!TryReadVarInt(payloadBytes, ref readIndex, out parsed.leaderHeroId))
|
|
{
|
|
errorMessage = "Leader hero id is invalid.";
|
|
return false;
|
|
}
|
|
|
|
for (int slotIndex = 0; slotIndex < TeamShareSnapshot.SlotCount; slotIndex++)
|
|
{
|
|
TeamShareSlotData slot = new TeamShareSlotData();
|
|
int heroId;
|
|
if (!TryReadVarInt(payloadBytes, ref readIndex, out heroId))
|
|
{
|
|
errorMessage = "Invalid slot hero id.";
|
|
return false;
|
|
}
|
|
|
|
slot.heroId = heroId;
|
|
|
|
int skillCount;
|
|
if (!TryReadVarInt(payloadBytes, ref readIndex, out skillCount) || skillCount < 0)
|
|
{
|
|
errorMessage = "Invalid skill count.";
|
|
return false;
|
|
}
|
|
|
|
for (int skillIndex = 0; skillIndex < skillCount; skillIndex++)
|
|
{
|
|
int skillId;
|
|
if (!TryReadVarInt(payloadBytes, ref readIndex, out skillId))
|
|
{
|
|
errorMessage = "Invalid skill id.";
|
|
return false;
|
|
}
|
|
|
|
slot.skillGroupIds.Add(skillId);
|
|
}
|
|
|
|
parsed.slots[slotIndex] = slot;
|
|
}
|
|
|
|
int nameByteCount = payloadBytes.Count - readIndex;
|
|
if (nameByteCount < 0)
|
|
{
|
|
errorMessage = "Invalid share code payload.";
|
|
return false;
|
|
}
|
|
|
|
string embeddedSchemeName = nameByteCount > 0
|
|
? Encoding.UTF8.GetString(payloadBytes.GetRange(readIndex, nameByteCount).ToArray())
|
|
: string.Empty;
|
|
parsed.schemeName = SanitizeSchemeName(embeddedSchemeName);
|
|
if (string.IsNullOrWhiteSpace(parsed.schemeName))
|
|
{
|
|
parsed.schemeName = SanitizeSchemeName(visibleSchemeName);
|
|
}
|
|
|
|
NormalizeSnapshot(parsed);
|
|
snapshot = parsed;
|
|
return true;
|
|
}
|
|
|
|
private static bool TryParseTs3Payload(string payload, string visibleSchemeName, out TeamShareSnapshot snapshot, out string errorMessage)
|
|
{
|
|
snapshot = null;
|
|
errorMessage = string.Empty;
|
|
|
|
string encodedPayload = payload.Substring(Prefix.Length);
|
|
byte[] rawBytes;
|
|
if (!TryFromBase64Url(encodedPayload, out rawBytes))
|
|
{
|
|
errorMessage = "Invalid share code payload.";
|
|
return false;
|
|
}
|
|
|
|
if (rawBytes == null || rawBytes.Length < 4)
|
|
{
|
|
errorMessage = "Invalid share code payload.";
|
|
return false;
|
|
}
|
|
|
|
int checksumIndex = rawBytes.Length - 4;
|
|
uint expectedChecksum = (uint)(
|
|
rawBytes[checksumIndex]
|
|
| (rawBytes[checksumIndex + 1] << 8)
|
|
| (rawBytes[checksumIndex + 2] << 16)
|
|
| (rawBytes[checksumIndex + 3] << 24));
|
|
|
|
List<byte> payloadBytes = new List<byte>(checksumIndex);
|
|
for (int i = 0; i < checksumIndex; i++)
|
|
{
|
|
payloadBytes.Add(rawBytes[i]);
|
|
}
|
|
|
|
uint actualChecksum = ComputeShortHashUInt(payloadBytes);
|
|
if (expectedChecksum != actualChecksum)
|
|
{
|
|
errorMessage = "Share code checksum failed.";
|
|
return false;
|
|
}
|
|
|
|
TeamShareSnapshot parsed = new TeamShareSnapshot();
|
|
int readIndex = 0;
|
|
|
|
if (!TryReadVarInt(payloadBytes, ref readIndex, out parsed.leaderHeroId))
|
|
{
|
|
errorMessage = "Leader hero id is invalid.";
|
|
return false;
|
|
}
|
|
|
|
for (int slotIndex = 0; slotIndex < TeamShareSnapshot.SlotCount; slotIndex++)
|
|
{
|
|
TeamShareSlotData slot = new TeamShareSlotData();
|
|
int heroId;
|
|
if (!TryReadVarInt(payloadBytes, ref readIndex, out heroId))
|
|
{
|
|
errorMessage = "Invalid slot hero id.";
|
|
return false;
|
|
}
|
|
|
|
slot.heroId = heroId;
|
|
|
|
int skillCount;
|
|
if (!TryReadVarInt(payloadBytes, ref readIndex, out skillCount) || skillCount < 0)
|
|
{
|
|
errorMessage = "Invalid skill count.";
|
|
return false;
|
|
}
|
|
|
|
for (int skillIndex = 0; skillIndex < skillCount; skillIndex++)
|
|
{
|
|
int skillId;
|
|
if (!TryReadVarInt(payloadBytes, ref readIndex, out skillId))
|
|
{
|
|
errorMessage = "Invalid skill id.";
|
|
return false;
|
|
}
|
|
|
|
slot.skillGroupIds.Add(skillId);
|
|
}
|
|
|
|
parsed.slots[slotIndex] = slot;
|
|
}
|
|
|
|
int schemeNameLength;
|
|
if (!TryReadVarInt(payloadBytes, ref readIndex, out schemeNameLength) || schemeNameLength < 0)
|
|
{
|
|
errorMessage = "Invalid scheme name.";
|
|
return false;
|
|
}
|
|
|
|
if (readIndex + schemeNameLength != payloadBytes.Count)
|
|
{
|
|
errorMessage = "Invalid share code payload.";
|
|
return false;
|
|
}
|
|
|
|
string embeddedSchemeName = Encoding.UTF8.GetString(payloadBytes.GetRange(readIndex, schemeNameLength).ToArray());
|
|
parsed.schemeName = SanitizeSchemeName(embeddedSchemeName);
|
|
if (string.IsNullOrWhiteSpace(parsed.schemeName))
|
|
{
|
|
parsed.schemeName = SanitizeSchemeName(visibleSchemeName);
|
|
}
|
|
|
|
NormalizeSnapshot(parsed);
|
|
snapshot = parsed;
|
|
return true;
|
|
}
|
|
|
|
private static bool TryParseLegacyPayload(string payload, string visibleSchemeName, out TeamShareSnapshot snapshot, out string errorMessage)
|
|
{
|
|
snapshot = null;
|
|
errorMessage = string.Empty;
|
|
|
|
string[] segments = payload.Split(new[] { "|" }, StringSplitOptions.None);
|
|
int expectedSegmentCount = 1 + TeamShareSnapshot.SlotCount + 2;
|
|
if (segments.Length != expectedSegmentCount)
|
|
{
|
|
errorMessage = "Invalid legacy share code format.";
|
|
return false;
|
|
}
|
|
|
|
string prefix = segments[0];
|
|
if (!string.Equals(prefix, LegacyPrefixV1, StringComparison.Ordinal) && !string.Equals(prefix, LegacyPrefixV2, StringComparison.Ordinal))
|
|
{
|
|
errorMessage = "Unsupported legacy share code version.";
|
|
return false;
|
|
}
|
|
|
|
string checksumSegment = segments[segments.Length - 1];
|
|
string checksumToken = checksumSegment;
|
|
if (checksumSegment.StartsWith("H=", StringComparison.Ordinal))
|
|
{
|
|
checksumToken = checksumSegment.Substring(2);
|
|
}
|
|
|
|
string expectedPayload = string.Join("|", segments, 0, segments.Length - 1);
|
|
string expectedHash = ComputeShortHash(expectedPayload);
|
|
if (!string.Equals(expectedHash, checksumToken, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
errorMessage = "Share code checksum failed.";
|
|
return false;
|
|
}
|
|
|
|
TeamShareSnapshot parsed = new TeamShareSnapshot();
|
|
parsed.schemeName = SanitizeSchemeName(visibleSchemeName);
|
|
|
|
string leaderSegment = segments[segments.Length - 2];
|
|
if (!leaderSegment.StartsWith("L=", StringComparison.Ordinal))
|
|
{
|
|
errorMessage = "Leader segment is missing.";
|
|
return false;
|
|
}
|
|
|
|
int leaderHeroId;
|
|
if (!int.TryParse(leaderSegment.Substring(2), out leaderHeroId))
|
|
{
|
|
errorMessage = "Leader hero id is invalid.";
|
|
return false;
|
|
}
|
|
|
|
parsed.leaderHeroId = leaderHeroId;
|
|
|
|
for (int i = 0; i < TeamShareSnapshot.SlotCount; i++)
|
|
{
|
|
TeamShareSlotData slotData;
|
|
if (!TryParseLegacySlot(segments[i + 1], out slotData, out errorMessage))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
parsed.slots[i] = slotData;
|
|
}
|
|
|
|
NormalizeSnapshot(parsed);
|
|
snapshot = parsed;
|
|
return true;
|
|
}
|
|
|
|
private static bool TryParseLegacySlot(string token, out TeamShareSlotData slotData, out string errorMessage)
|
|
{
|
|
slotData = new TeamShareSlotData();
|
|
errorMessage = string.Empty;
|
|
|
|
string safeToken = string.IsNullOrWhiteSpace(token) ? EmptySlotToken : token.Trim();
|
|
if (string.Equals(safeToken, EmptySlotToken, StringComparison.Ordinal))
|
|
{
|
|
return true;
|
|
}
|
|
|
|
int splitIndex = safeToken.IndexOf('@');
|
|
string heroPart = splitIndex >= 0 ? safeToken.Substring(0, splitIndex) : safeToken;
|
|
string skillsPart = splitIndex >= 0 && splitIndex + 1 < safeToken.Length ? safeToken.Substring(splitIndex + 1) : string.Empty;
|
|
|
|
int heroId;
|
|
if (!int.TryParse(heroPart, out heroId) || heroId <= 0)
|
|
{
|
|
errorMessage = "Invalid hero id: " + heroPart;
|
|
return false;
|
|
}
|
|
|
|
slotData.heroId = heroId;
|
|
slotData.skillGroupIds = new List<int>();
|
|
|
|
if (string.IsNullOrWhiteSpace(skillsPart))
|
|
{
|
|
return true;
|
|
}
|
|
|
|
string[] skillTokens = skillsPart.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
|
|
for (int i = 0; i < skillTokens.Length; i++)
|
|
{
|
|
int skillId;
|
|
if (!int.TryParse(skillTokens[i], out skillId) || skillId <= 0)
|
|
{
|
|
errorMessage = "Invalid skill id: " + skillTokens[i];
|
|
return false;
|
|
}
|
|
|
|
slotData.skillGroupIds.Add(skillId);
|
|
}
|
|
|
|
slotData.skillGroupIds = SanitizeSkillIds(slotData.skillGroupIds);
|
|
return true;
|
|
}
|
|
|
|
private static void NormalizeSnapshot(TeamShareSnapshot snapshot)
|
|
{
|
|
if (snapshot == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
snapshot.version = TeamShareSnapshot.CurrentVersion;
|
|
snapshot.schemeName = SanitizeSchemeName(snapshot.schemeName);
|
|
|
|
if (snapshot.slots == null || snapshot.slots.Length != TeamShareSnapshot.SlotCount)
|
|
{
|
|
TeamShareSlotData[] normalizedSlots = TeamShareSnapshot.CreateDefaultSlots();
|
|
if (snapshot.slots != null)
|
|
{
|
|
int copyLength = Mathf.Min(snapshot.slots.Length, normalizedSlots.Length);
|
|
for (int i = 0; i < copyLength; i++)
|
|
{
|
|
normalizedSlots[i] = snapshot.slots[i] ?? new TeamShareSlotData();
|
|
}
|
|
}
|
|
|
|
snapshot.slots = normalizedSlots;
|
|
}
|
|
|
|
for (int i = 0; i < snapshot.slots.Length; i++)
|
|
{
|
|
if (snapshot.slots[i] == null)
|
|
{
|
|
snapshot.slots[i] = new TeamShareSlotData();
|
|
}
|
|
|
|
if (snapshot.slots[i].heroId <= 0)
|
|
{
|
|
snapshot.slots[i].heroId = 0;
|
|
snapshot.slots[i].skillGroupIds = new List<int>();
|
|
continue;
|
|
}
|
|
|
|
snapshot.slots[i].skillGroupIds = SanitizeSkillIds(snapshot.slots[i].skillGroupIds);
|
|
}
|
|
}
|
|
|
|
private static List<int> SanitizeSkillIds(IEnumerable<int> source)
|
|
{
|
|
List<int> result = new List<int>();
|
|
if (source == null)
|
|
{
|
|
return result;
|
|
}
|
|
|
|
HashSet<int> dedupe = new HashSet<int>();
|
|
foreach (int skillId in source)
|
|
{
|
|
if (skillId <= 0 || !dedupe.Add(skillId))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
result.Add(skillId);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
private static string SanitizeSchemeName(string schemeName)
|
|
{
|
|
string safeName = string.IsNullOrWhiteSpace(schemeName) ? TeamShareSnapshot.DefaultSchemeName : schemeName.Trim();
|
|
safeName = safeName.Replace("\r", string.Empty).Replace("\n", string.Empty).Replace(OuterSeparator.ToString(), string.Empty);
|
|
return safeName.Length > 24 ? safeName.Substring(0, 24) : safeName;
|
|
}
|
|
|
|
private static int[] ReadCurrentSelectedHeroIds()
|
|
{
|
|
int[] ids = new int[TeamShareSnapshot.SlotCount];
|
|
newTeamSelector selector = newTeamSelector.Instance;
|
|
if (selector != null)
|
|
{
|
|
List<slots_heroSlots> createdSlots = selector.GetCreatedSlots();
|
|
if (createdSlots != null && createdSlots.Count > 0)
|
|
{
|
|
for (int i = 0; i < ids.Length && i < createdSlots.Count; i++)
|
|
{
|
|
ids[i] = createdSlots[i] != null ? Mathf.Max(0, createdSlots[i].heroSlot_heroID) : 0;
|
|
}
|
|
|
|
return ids;
|
|
}
|
|
|
|
ids[0] = Mathf.Max(0, selector.selected_heroSlot01_heroID);
|
|
ids[1] = Mathf.Max(0, selector.selected_heroSlot02_heroID);
|
|
ids[2] = Mathf.Max(0, selector.selected_heroSlot03_heroID);
|
|
ids[3] = Mathf.Max(0, selector.selected_heroSlot04_heroID);
|
|
ids[4] = Mathf.Max(0, selector.selected_heroSlot05_heroID);
|
|
return ids;
|
|
}
|
|
|
|
ids[0] = PlayerPrefs.GetInt(HeroSlot01Key, 0);
|
|
ids[1] = PlayerPrefs.GetInt(HeroSlot02Key, 0);
|
|
ids[2] = PlayerPrefs.GetInt(HeroSlot03Key, 0);
|
|
ids[3] = PlayerPrefs.GetInt(HeroSlot04Key, 0);
|
|
ids[4] = PlayerPrefs.GetInt(HeroSlot05Key, 0);
|
|
return ids;
|
|
}
|
|
|
|
private static int ResolveCurrentLeaderHeroId(TeamShareSnapshot snapshot)
|
|
{
|
|
TeamManager teamManager = TeamManager.Instance;
|
|
if (teamManager != null)
|
|
{
|
|
TeamSetting selectedTeam = null;
|
|
try
|
|
{
|
|
selectedTeam = teamManager.getCurrentSelectedTeam();
|
|
}
|
|
catch
|
|
{
|
|
selectedTeam = null;
|
|
}
|
|
|
|
if (selectedTeam != null && selectedTeam.LeaderId > 0)
|
|
{
|
|
return selectedTeam.LeaderId;
|
|
}
|
|
}
|
|
|
|
if (snapshot != null && snapshot.slots != null)
|
|
{
|
|
for (int i = 0; i < snapshot.slots.Length; i++)
|
|
{
|
|
if (snapshot.slots[i] != null && snapshot.slots[i].heroId > 0)
|
|
{
|
|
return snapshot.slots[i].heroId;
|
|
}
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
private static bool TryApplySnapshotPermissive(TeamShareSnapshot snapshot, TeamShareApplyReport report, out string errorMessage)
|
|
{
|
|
errorMessage = string.Empty;
|
|
if (snapshot == null || snapshot.slots == null)
|
|
{
|
|
errorMessage = "Team snapshot is null.";
|
|
return false;
|
|
}
|
|
|
|
HashSet<int> seenHeroIds = new HashSet<int>();
|
|
List<int> appliedHeroIds = new List<int>();
|
|
|
|
for (int i = 0; i < snapshot.slots.Length; i++)
|
|
{
|
|
TeamShareSlotData slot = snapshot.slots[i];
|
|
if (slot == null)
|
|
{
|
|
snapshot.slots[i] = new TeamShareSlotData();
|
|
continue;
|
|
}
|
|
|
|
if (slot.heroId <= 0)
|
|
{
|
|
slot.skillGroupIds = new List<int>();
|
|
continue;
|
|
}
|
|
|
|
if (!seenHeroIds.Add(slot.heroId))
|
|
{
|
|
report.skippedHeroCount++;
|
|
report.messages.Add("\u69fd\u4f4d " + (i + 1) + " \u7684\u91cd\u590d\u89d2\u8272\u5df2\u8df3\u8fc7");
|
|
snapshot.slots[i] = new TeamShareSlotData();
|
|
continue;
|
|
}
|
|
|
|
AllyHero_SO hero = FindHeroById(slot.heroId);
|
|
if (hero == null)
|
|
{
|
|
report.skippedHeroCount++;
|
|
report.messages.Add("\u69fd\u4f4d " + (i + 1) + " \u7684\u89d2\u8272\u4e0d\u5b58\u5728\uff0c\u5df2\u8df3\u8fc7");
|
|
snapshot.slots[i] = new TeamShareSlotData();
|
|
continue;
|
|
}
|
|
|
|
if (!hero.isUnlocked)
|
|
{
|
|
report.skippedHeroCount++;
|
|
report.messages.Add("\u69fd\u4f4d " + (i + 1) + " \u7684\u89d2\u8272\u672a\u89e3\u9501\uff0c\u5df2\u8df3\u8fc7");
|
|
snapshot.slots[i] = new TeamShareSlotData();
|
|
continue;
|
|
}
|
|
|
|
hero.LoadEquippedEquipmentFromLocal();
|
|
|
|
List<int> sourceSkillIds = SanitizeSkillIds(slot.skillGroupIds);
|
|
List<int> equippedSkillIds = new List<int>();
|
|
AllyHero_SO.AllyLevelInfo levelInfo = hero.GetEffectiveLevelForCurrentEXP();
|
|
int currentLevelId = levelInfo != null ? levelInfo.levelID : 0;
|
|
int maxSkillSlots = Mathf.Max(0, hero.GetEffectiveSkillSlotLimit());
|
|
|
|
for (int skillIndex = 0; skillIndex < sourceSkillIds.Count; skillIndex++)
|
|
{
|
|
int skillGroupId = sourceSkillIds[skillIndex];
|
|
SkillGroup group = hero.GetSkillGroupByID(skillGroupId);
|
|
if (group == null)
|
|
{
|
|
report.skippedSkillCount++;
|
|
report.messages.Add("\u89d2\u8272 " + hero.ally_heroName + " \u7684\u6280\u80fd\u4e0d\u5b58\u5728\uff0c\u5df2\u8df3\u8fc7");
|
|
continue;
|
|
}
|
|
|
|
if (currentLevelId < Mathf.Max(0, group.thisSkill_levelLimit))
|
|
{
|
|
report.skippedSkillCount++;
|
|
report.messages.Add("\u89d2\u8272 " + hero.ally_heroName + " \u7684\u6280\u80fd " + (group.groupName ?? skillGroupId.ToString()) + " \u672a\u89e3\u9501\uff0c\u5df2\u8df3\u8fc7");
|
|
continue;
|
|
}
|
|
|
|
if (equippedSkillIds.Count >= maxSkillSlots)
|
|
{
|
|
report.skippedSkillCount += sourceSkillIds.Count - skillIndex;
|
|
report.messages.Add("\u89d2\u8272 " + hero.ally_heroName + " \u5df2\u8fbe\u5230\u6280\u80fd\u69fd\u4e0a\u9650");
|
|
break;
|
|
}
|
|
|
|
equippedSkillIds.Add(skillGroupId);
|
|
}
|
|
|
|
hero.equippedSkillGroupIDs = equippedSkillIds.ToArray();
|
|
hero.SaveEquippedSkillsToLocal();
|
|
slot.heroId = hero.ally_heroID;
|
|
slot.skillGroupIds = equippedSkillIds;
|
|
appliedHeroIds.Add(hero.ally_heroID);
|
|
report.appliedHeroCount++;
|
|
report.appliedSkillCount += equippedSkillIds.Count;
|
|
|
|
#if UNITY_EDITOR
|
|
if (!Application.isPlaying)
|
|
{
|
|
EditorUtility.SetDirty(hero);
|
|
}
|
|
#endif
|
|
}
|
|
|
|
#if UNITY_EDITOR
|
|
if (!Application.isPlaying)
|
|
{
|
|
AssetDatabase.SaveAssets();
|
|
}
|
|
#endif
|
|
|
|
if (appliedHeroIds.Count == 0)
|
|
{
|
|
errorMessage = "No unlocked heroes could be applied.";
|
|
return false;
|
|
}
|
|
|
|
if (snapshot.leaderHeroId <= 0 || !appliedHeroIds.Contains(snapshot.leaderHeroId))
|
|
{
|
|
snapshot.leaderHeroId = appliedHeroIds[0];
|
|
}
|
|
|
|
report.schemeName = snapshot.schemeName;
|
|
return true;
|
|
}
|
|
|
|
private static void ApplySnapshotToTeamManager(TeamShareSnapshot snapshot)
|
|
{
|
|
TeamManager teamManager = TeamManager.Instance;
|
|
if (teamManager == null || snapshot == null || snapshot.slots == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
TeamSetting selectedTeam = null;
|
|
try
|
|
{
|
|
selectedTeam = teamManager.getCurrentSelectedTeam();
|
|
}
|
|
catch
|
|
{
|
|
selectedTeam = null;
|
|
}
|
|
|
|
if (selectedTeam == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (selectedTeam.teamIdList == null)
|
|
{
|
|
selectedTeam.teamIdList = new List<CharacterView>();
|
|
}
|
|
|
|
while (selectedTeam.teamIdList.Count < TeamShareSnapshot.SlotCount)
|
|
{
|
|
selectedTeam.teamIdList.Add(new CharacterView(0, null));
|
|
}
|
|
|
|
for (int i = 0; i < TeamShareSnapshot.SlotCount; i++)
|
|
{
|
|
TeamShareSlotData slot = snapshot.slots[i] ?? new TeamShareSlotData();
|
|
selectedTeam.teamIdList[i] = new CharacterView(slot.heroId, null);
|
|
}
|
|
|
|
selectedTeam.LeaderId = snapshot.leaderHeroId > 0 ? snapshot.leaderHeroId : ResolveCurrentLeaderHeroId(snapshot);
|
|
teamManager.setCurrentSelectedTeam(selectedTeam, teamManager.currentSelectedTeam);
|
|
}
|
|
|
|
private static void ApplySnapshotToSelectionPrefs(TeamShareSnapshot snapshot)
|
|
{
|
|
if (snapshot == null || snapshot.slots == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
PlayerPrefs.SetInt(HeroSlot01Key, snapshot.slots[0] != null ? snapshot.slots[0].heroId : 0);
|
|
PlayerPrefs.SetInt(HeroSlot02Key, snapshot.slots[1] != null ? snapshot.slots[1].heroId : 0);
|
|
PlayerPrefs.SetInt(HeroSlot03Key, snapshot.slots[2] != null ? snapshot.slots[2].heroId : 0);
|
|
PlayerPrefs.SetInt(HeroSlot04Key, snapshot.slots[3] != null ? snapshot.slots[3].heroId : 0);
|
|
PlayerPrefs.SetInt(HeroSlot05Key, snapshot.slots[4] != null ? snapshot.slots[4].heroId : 0);
|
|
PlayerPrefs.Save();
|
|
}
|
|
|
|
private static void RefreshSelectionViews()
|
|
{
|
|
newTeamSelector selector = newTeamSelector.Instance;
|
|
if (selector != null)
|
|
{
|
|
selector.LoadSelectedHeroes();
|
|
}
|
|
|
|
if (teamSettingPanel.Instance != null)
|
|
{
|
|
teamSettingPanel.Instance.updateTeamSettingElements();
|
|
teamSettingPanel.Instance.updateSelectableCharacterViewList();
|
|
}
|
|
}
|
|
|
|
private static AllyHero_SO FindHeroById(int heroId)
|
|
{
|
|
EnsureHeroCache();
|
|
AllyHero_SO hero;
|
|
return cachedHeroById != null && cachedHeroById.TryGetValue(heroId, out hero) ? hero : null;
|
|
}
|
|
|
|
private static void EnsureHeroCache()
|
|
{
|
|
if (cachedHeroById != null && cachedHeroById.Count > 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
cachedHeroes = Resources.LoadAll<AllyHero_SO>(string.Empty);
|
|
cachedHeroById = new Dictionary<int, AllyHero_SO>();
|
|
if (cachedHeroes == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
for (int i = 0; i < cachedHeroes.Length; i++)
|
|
{
|
|
AllyHero_SO hero = cachedHeroes[i];
|
|
if (hero == null || hero.ally_heroID <= 0)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
cachedHeroById[hero.ally_heroID] = hero;
|
|
}
|
|
}
|
|
|
|
private static void SplitVisibleNameAndPayload(string rawShareCode, out string visibleSchemeName, out string payload)
|
|
{
|
|
visibleSchemeName = TeamShareSnapshot.DefaultSchemeName;
|
|
payload = rawShareCode;
|
|
|
|
int t3Index = rawShareCode.IndexOf(OuterSeparator + Prefix, StringComparison.Ordinal);
|
|
if (t3Index >= 0)
|
|
{
|
|
visibleSchemeName = SanitizeSchemeName(rawShareCode.Substring(0, t3Index));
|
|
payload = rawShareCode.Substring(t3Index + 1);
|
|
return;
|
|
}
|
|
|
|
int ts3Index = rawShareCode.IndexOf(OuterSeparator + LegacyPrefixV3, StringComparison.Ordinal);
|
|
if (ts3Index >= 0)
|
|
{
|
|
visibleSchemeName = SanitizeSchemeName(rawShareCode.Substring(0, ts3Index));
|
|
payload = rawShareCode.Substring(ts3Index + 1);
|
|
return;
|
|
}
|
|
|
|
int btIndex = rawShareCode.IndexOf(OuterSeparator + LegacyPrefixV2, StringComparison.Ordinal);
|
|
if (btIndex < 0)
|
|
{
|
|
btIndex = rawShareCode.IndexOf(OuterSeparator + LegacyPrefixV1, StringComparison.Ordinal);
|
|
}
|
|
|
|
if (btIndex >= 0)
|
|
{
|
|
visibleSchemeName = SanitizeSchemeName(rawShareCode.Substring(0, btIndex));
|
|
payload = rawShareCode.Substring(btIndex + 1);
|
|
}
|
|
}
|
|
|
|
private static void WriteVarInt(List<byte> bytes, int value)
|
|
{
|
|
uint safeValue = value <= 0 ? 0u : (uint)value;
|
|
while (safeValue >= 0x80u)
|
|
{
|
|
bytes.Add((byte)(safeValue | 0x80u));
|
|
safeValue >>= 7;
|
|
}
|
|
|
|
bytes.Add((byte)safeValue);
|
|
}
|
|
|
|
private static bool TryReadVarInt(List<byte> bytes, ref int index, out int value)
|
|
{
|
|
value = 0;
|
|
if (bytes == null || index < 0 || index >= bytes.Count)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
int shift = 0;
|
|
uint result = 0u;
|
|
while (index < bytes.Count && shift < 35)
|
|
{
|
|
byte b = bytes[index++];
|
|
result |= (uint)(b & 0x7F) << shift;
|
|
if ((b & 0x80) == 0)
|
|
{
|
|
if (result > int.MaxValue)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
value = (int)result;
|
|
return true;
|
|
}
|
|
|
|
shift += 7;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private static string ToBase64Url(byte[] bytes)
|
|
{
|
|
string text = Convert.ToBase64String(bytes ?? Array.Empty<byte>());
|
|
return text.Replace('+', '-').Replace('/', '_').TrimEnd('=');
|
|
}
|
|
|
|
private static bool TryFromBase64Url(string text, out byte[] bytes)
|
|
{
|
|
bytes = null;
|
|
if (string.IsNullOrWhiteSpace(text))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
string safeText = text.Replace('-', '+').Replace('_', '/');
|
|
switch (safeText.Length % 4)
|
|
{
|
|
case 2:
|
|
safeText += "==";
|
|
break;
|
|
case 3:
|
|
safeText += "=";
|
|
break;
|
|
}
|
|
|
|
try
|
|
{
|
|
bytes = Convert.FromBase64String(safeText);
|
|
return true;
|
|
}
|
|
catch
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
private static uint ComputeShortHashUInt(List<byte> bytes)
|
|
{
|
|
const uint fnvOffset = 2166136261u;
|
|
const uint fnvPrime = 16777619u;
|
|
|
|
uint hash = fnvOffset;
|
|
if (bytes == null)
|
|
{
|
|
return hash;
|
|
}
|
|
|
|
for (int i = 0; i < bytes.Count; i++)
|
|
{
|
|
hash ^= bytes[i];
|
|
hash *= fnvPrime;
|
|
}
|
|
|
|
return hash;
|
|
}
|
|
|
|
private static ushort ComputeShortHashUShort(List<byte> bytes)
|
|
{
|
|
uint hash = ComputeShortHashUInt(bytes);
|
|
return (ushort)((hash ^ (hash >> 16)) & 0xFFFFu);
|
|
}
|
|
|
|
private static string ComputeShortHash(string payload)
|
|
{
|
|
byte[] bytes = Encoding.UTF8.GetBytes(payload ?? string.Empty);
|
|
return ToBase36(ComputeShortHashUInt(new List<byte>(bytes))).ToUpperInvariant();
|
|
}
|
|
|
|
private static string ToBase36(uint value)
|
|
{
|
|
const string alphabet = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
|
if (value == 0u)
|
|
{
|
|
return "0";
|
|
}
|
|
|
|
StringBuilder builder = new StringBuilder();
|
|
uint current = value;
|
|
while (current > 0u)
|
|
{
|
|
uint remainder = current % 36u;
|
|
builder.Insert(0, alphabet[(int)remainder]);
|
|
current /= 36u;
|
|
}
|
|
|
|
return builder.ToString();
|
|
}
|
|
}
|