修了不少东西

This commit is contained in:
FloatGaming
2026-07-16 23:04:59 +08:00
parent 29972d0705
commit 73fe474cc6
134 changed files with 7673 additions and 741 deletions
+27 -7
View File
@@ -1140,17 +1140,37 @@ public class AllyCombatant : MonoBehaviour, ICombatant
private static AllyHero_SO.AllyLevelInfo GetEffectiveLevelForExp(AllyHero_SO so, int exp)
{
if (so == null || so.levelStats == null || so.levelStats.Count == 0) return null;
AllyHero_SO.AllyLevelInfo best = null;
foreach (var lvl in so.levelStats)
List<AllyHero_SO.AllyLevelInfo> sorted = new List<AllyHero_SO.AllyLevelInfo>();
foreach (var levelInfo in so.levelStats)
{
if (lvl == null) continue;
if (exp >= lvl.requiredEXP)
if (levelInfo != null)
{
if (best == null || lvl.requiredEXP >= best.requiredEXP)
best = lvl;
sorted.Add(levelInfo);
}
}
return best ?? so.levelStats[0];
if (sorted.Count == 0) return null;
sorted.Sort((left, right) => left.requiredEXP.CompareTo(right.requiredEXP));
int expQualifiedIndex = 0;
int safeExp = Mathf.Max(0, exp);
for (int i = 0; i < sorted.Count; i++)
{
if (safeExp >= Mathf.Max(0, sorted[i].requiredEXP))
{
expQualifiedIndex = i;
}
else
{
break;
}
}
int unlockedIndex = Mathf.Clamp(so.ally_growthUnlockedTierIndex, 0, sorted.Count - 1);
int effectiveIndex = Mathf.Min(expQualifiedIndex, unlockedIndex);
return sorted[Mathf.Clamp(effectiveIndex, 0, sorted.Count - 1)];
}
// Scoring API
+68 -4
View File
@@ -9,6 +9,9 @@ using UnityEditor;
[CreateAssetMenu(fileName = "NewAllyHero", menuName = "SO_Data/AllyHero")]
public class AllyHero_SO : ScriptableObject
{
private const string EquippedEquipmentSaveCategory = "ally_equipped_equipment";
private const string EquippedEquipmentRecoverySlotPrefix = "ally_equipped_equipment_";
public static readonly string[] BehaviourAxisNames =
{
"题海战术",
@@ -154,6 +157,13 @@ public class AllyHero_SO : ScriptableObject
public equipmentSO equippedEquipment;
public string equippedEquipmentId;
// 【性能】装备存档已从磁盘水合过一次的标记。水合后内存即权威来源:
// SetEquippedEquipment / ApplyEquippedEquipmentPayload / ClearEquippedEquipment 都会同步更新内存与磁盘,
// 因此无需反复解密读盘。idols 界面每次切换角色时 RebuildBag 会对"每件装备 × 每个英雄"调用
// LoadEquippedEquipmentFromLocal 做占用检查,若每次都真正解密 SecureSaveVault 会造成严重卡顿。
// 该标记确保每个英雄整个会话只读盘一次,之后走内存,行为完全不变。
[System.NonSerialized] private bool equippedEquipmentHydrated;
public SkillDefinition GetPrimarySkill()
{
if (availableSkills == null || primarySkillIndex < 0 || primarySkillIndex >= availableSkills.Length) return null;
@@ -506,12 +516,14 @@ public class AllyHero_SO : ScriptableObject
{
List<AllyLevelInfo> sorted = BuildSortedLevelStats();
int unlockedIndex = ResolveUnlockedLevelIndex(sorted);
if (unlockedIndex < 0 || unlockedIndex >= sorted.Count)
int expQualifiedIndex = ResolveExpQualifiedLevelIndex(sorted);
if (unlockedIndex < 0 || expQualifiedIndex < 0 || sorted == null || sorted.Count == 0)
{
return null;
}
return sorted[unlockedIndex];
int effectiveIndex = Mathf.Clamp(Mathf.Min(expQualifiedIndex, unlockedIndex), 0, sorted.Count - 1);
return sorted[effectiveIndex];
}
private List<AllyLevelInfo> BuildSortedLevelStats()
@@ -702,6 +714,16 @@ public class AllyHero_SO : ScriptableObject
return "ally_equippedEquipment_" + ally_heroID;
}
private string GetEquippedEquipmentSaveKey()
{
return ally_heroID.ToString();
}
private string GetEquippedEquipmentRecoverySlotKey()
{
return EquippedEquipmentRecoverySlotPrefix + ally_heroID;
}
private string GetSelectedSkinPrefsKey()
{
return "ally_selectedSkin_" + ally_heroID;
@@ -731,13 +753,39 @@ public class AllyHero_SO : ScriptableObject
equippedEquipmentId = equippedEquipment != null ? equippedEquipment.name : string.Empty;
var payload = new EquippedEquipmentPayload { equippedEquipmentId = equippedEquipmentId ?? string.Empty };
string json = JsonUtility.ToJson(payload);
SecureSaveVault.SaveJson(EquippedEquipmentSaveCategory, GetEquippedEquipmentSaveKey(), payload);
LocalRecoveryMirror.SaveJson(GetEquippedEquipmentRecoverySlotKey(), payload);
PlayerPrefs.SetString(GetEquippedEquipmentPrefsKey(), json);
PlayerPrefs.Save();
}
public void LoadEquippedEquipmentFromLocal()
public void LoadEquippedEquipmentFromLocal(bool forceReload = false)
{
// 已水合且非强制刷新时直接返回,避免重复解密读盘(见 equippedEquipmentHydrated 说明)。
if (equippedEquipmentHydrated && !forceReload)
{
return;
}
// 无论后续走哪条分支,本次调用都视为已完成水合:
// 命中存档→内存已填充;无存档→内存保持当前值,也不必反复重试读盘。
equippedEquipmentHydrated = true;
string key = GetEquippedEquipmentPrefsKey();
EquippedEquipmentPayload payload;
if (SecureSaveVault.TryLoadJson(EquippedEquipmentSaveCategory, GetEquippedEquipmentSaveKey(), out payload) && payload != null)
{
ApplyEquippedEquipmentPayload(payload);
return;
}
if (LocalRecoveryMirror.TryLoadJson(GetEquippedEquipmentRecoverySlotKey(), out payload) && payload != null)
{
ApplyEquippedEquipmentPayload(payload);
SecureSaveVault.SaveJson(EquippedEquipmentSaveCategory, GetEquippedEquipmentSaveKey(), payload);
return;
}
if (!PlayerPrefs.HasKey(key))
{
return;
@@ -749,7 +797,19 @@ public class AllyHero_SO : ScriptableObject
return;
}
EquippedEquipmentPayload payload = JsonUtility.FromJson<EquippedEquipmentPayload>(json);
payload = JsonUtility.FromJson<EquippedEquipmentPayload>(json);
if (payload == null)
{
return;
}
ApplyEquippedEquipmentPayload(payload);
SecureSaveVault.SaveJson(EquippedEquipmentSaveCategory, GetEquippedEquipmentSaveKey(), payload);
LocalRecoveryMirror.SaveJson(GetEquippedEquipmentRecoverySlotKey(), payload);
}
private void ApplyEquippedEquipmentPayload(EquippedEquipmentPayload payload)
{
if (payload == null)
{
return;
@@ -763,6 +823,7 @@ public class AllyHero_SO : ScriptableObject
{
equippedEquipment = equipment;
equippedEquipmentId = equipment != null ? equipment.name : string.Empty;
equippedEquipmentHydrated = true;
#if UNITY_EDITOR
if (!Application.isPlaying)
@@ -801,6 +862,9 @@ public class AllyHero_SO : ScriptableObject
{
equippedEquipment = null;
equippedEquipmentId = string.Empty;
equippedEquipmentHydrated = true;
SecureSaveVault.Delete(EquippedEquipmentSaveCategory, GetEquippedEquipmentSaveKey());
LocalRecoveryMirror.DeleteSlot(GetEquippedEquipmentRecoverySlotKey());
PlayerPrefs.DeleteKey(GetEquippedEquipmentPrefsKey());
PlayerPrefs.Save();
#if UNITY_EDITOR
+14
View File
@@ -50,6 +50,10 @@ public class pressStart : MonoBehaviour
public Image fadeOutImage;
public float startButtonDelay = 2f; // UI starts showing after 2 seconds; allow clicking then.
[Header("UI_UI Jump Blocker")]
public bool blockUIUISceneJump = false;
public string blockUIUISceneJumpMessage = "暂不可进入。";
// polling
private Coroutine pollCoroutine;
private float pollInterval = 3f;
@@ -252,6 +256,16 @@ public class pressStart : MonoBehaviour
// 修改为点击一次即可开始进入游戏序列
Debug.Log("[pressStart] Start requested, initiating transition sequence.");
GameConfig.testMode = false;
if (blockUIUISceneJump)
{
string message = string.IsNullOrWhiteSpace(blockUIUISceneJumpMessage)
? "暂不可进入。"
: blockUIUISceneJumpMessage;
gNotice.error.display(message);
Debug.LogWarning("[pressStart] UI_UI scene jump blocked: " + message);
return;
}
if (warningText != null)
{
@@ -0,0 +1,51 @@
using UnityEngine;
public static class TeamSelectionDefaults
{
public const int SlotCount = 5;
private static readonly int[] DefaultHeroIds = { 30201, 30202, 30203, 30204, 30205 };
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
private static void Bootstrap()
{
EnsureDefaultTeamIfMissing();
}
public static void EnsureDefaultTeamIfMissing()
{
for (int slot = 1; slot <= SlotCount; slot++)
{
if (PlayerPrefs.HasKey(GetHeroSlotKey(slot)))
{
return;
}
}
for (int slot = 1; slot <= SlotCount; slot++)
{
PlayerPrefs.SetInt(GetHeroSlotKey(slot), GetDefaultHeroId(slot));
}
PlayerPrefs.SetInt("SelectedMainHeroID", DefaultHeroIds[0]);
PlayerPrefs.Save();
}
public static int ReadHeroId(int slot)
{
EnsureDefaultTeamIfMissing();
return Mathf.Max(0, PlayerPrefs.GetInt(GetHeroSlotKey(slot), GetDefaultHeroId(slot)));
}
public static int GetDefaultHeroId(int slot)
{
int index = Mathf.Clamp(slot - 1, 0, DefaultHeroIds.Length - 1);
return DefaultHeroIds[index];
}
private static string GetHeroSlotKey(int slot)
{
int safeSlot = Mathf.Clamp(slot, 1, SlotCount);
return $"selected_heroSlot0{safeSlot}_heroID";
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 7dfd23d5af82d6045b465356990ca8b9
@@ -147,21 +147,21 @@ public class TeamSelectorAnimController : MonoBehaviour
void CacheTeamSnapshot()
{
cachedTeam[0] = PlayerPrefs.GetInt("selected_heroSlot01_heroID", 0);
cachedTeam[1] = PlayerPrefs.GetInt("selected_heroSlot02_heroID", 0);
cachedTeam[2] = PlayerPrefs.GetInt("selected_heroSlot03_heroID", 0);
cachedTeam[3] = PlayerPrefs.GetInt("selected_heroSlot04_heroID", 0);
cachedTeam[4] = PlayerPrefs.GetInt("selected_heroSlot05_heroID", 0);
cachedTeam[0] = TeamSelectionDefaults.ReadHeroId(1);
cachedTeam[1] = TeamSelectionDefaults.ReadHeroId(2);
cachedTeam[2] = TeamSelectionDefaults.ReadHeroId(3);
cachedTeam[3] = TeamSelectionDefaults.ReadHeroId(4);
cachedTeam[4] = TeamSelectionDefaults.ReadHeroId(5);
}
bool HasTeamChanged()
{
if (teamChanged) return true;
if (cachedTeam[0] != PlayerPrefs.GetInt("selected_heroSlot01_heroID", 0)) return true;
if (cachedTeam[1] != PlayerPrefs.GetInt("selected_heroSlot02_heroID", 0)) return true;
if (cachedTeam[2] != PlayerPrefs.GetInt("selected_heroSlot03_heroID", 0)) return true;
if (cachedTeam[3] != PlayerPrefs.GetInt("selected_heroSlot04_heroID", 0)) return true;
if (cachedTeam[4] != PlayerPrefs.GetInt("selected_heroSlot05_heroID", 0)) return true;
if (cachedTeam[0] != TeamSelectionDefaults.ReadHeroId(1)) return true;
if (cachedTeam[1] != TeamSelectionDefaults.ReadHeroId(2)) return true;
if (cachedTeam[2] != TeamSelectionDefaults.ReadHeroId(3)) return true;
if (cachedTeam[3] != TeamSelectionDefaults.ReadHeroId(4)) return true;
if (cachedTeam[4] != TeamSelectionDefaults.ReadHeroId(5)) return true;
return false;
}
+6 -5
View File
@@ -289,12 +289,13 @@ public class newTeamSelector : MonoBehaviour
private int[] ReadSelectedHeroIdsFromPlayerPrefs()
{
TeamSelectionDefaults.EnsureDefaultTeamIfMissing();
int[] ids = new int[Mathf.Max(5, slotCount)];
if (ids.Length > 0) ids[0] = Mathf.Max(0, PlayerPrefs.GetInt("selected_heroSlot01_heroID", 0));
if (ids.Length > 1) ids[1] = Mathf.Max(0, PlayerPrefs.GetInt("selected_heroSlot02_heroID", 0));
if (ids.Length > 2) ids[2] = Mathf.Max(0, PlayerPrefs.GetInt("selected_heroSlot03_heroID", 0));
if (ids.Length > 3) ids[3] = Mathf.Max(0, PlayerPrefs.GetInt("selected_heroSlot04_heroID", 0));
if (ids.Length > 4) ids[4] = Mathf.Max(0, PlayerPrefs.GetInt("selected_heroSlot05_heroID", 0));
if (ids.Length > 0) ids[0] = TeamSelectionDefaults.ReadHeroId(1);
if (ids.Length > 1) ids[1] = TeamSelectionDefaults.ReadHeroId(2);
if (ids.Length > 2) ids[2] = TeamSelectionDefaults.ReadHeroId(3);
if (ids.Length > 3) ids[3] = TeamSelectionDefaults.ReadHeroId(4);
if (ids.Length > 4) ids[4] = TeamSelectionDefaults.ReadHeroId(5);
return ids;
}
+1 -1
View File
@@ -1679,7 +1679,7 @@ public static class MailRewardGrantService
resolved = CreateBasicResolved(ResolvedKind.PlayerExp, amount, rewardName, reward.reward_image != null ? reward.reward_image : _playerExpRewardIcon, reward.reward_description, reward, "\u73a9\u5bb6\u7ecf\u9a8c");
return true;
case mail_so.reward_type.money:
resolved = CreateBasicResolved(ResolvedKind.Coins, amount, rewardName, reward.reward_image != null ? reward.reward_image : _coinRewardIcon, reward.reward_description, reward, "\u91d1\u5e01");
resolved = CreateBasicResolved(ResolvedKind.Coins, amount, rewardName, reward.reward_image != null ? reward.reward_image : _coinRewardIcon, reward.reward_description, reward, "算力");
return true;
case mail_so.reward_type.metarial:
resolved = CreateBasicResolved(ResolvedKind.Material, amount, rewardName, reward.reward_image != null ? reward.reward_image : _materialRewardIcon, reward.reward_description, reward, "\u8bb0\u5fc6\u788e\u7247");
@@ -7268,6 +7268,7 @@ GameObject:
- component: {fileID: 1348388220439716234}
- component: {fileID: 6137481741208887361}
- component: {fileID: 1863233853179500241}
- component: {fileID: 1338150670383926109}
m_Layer: 5
m_Name: Text_PLAYER
m_TagString: Untagged
@@ -7291,9 +7292,9 @@ RectTransform:
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0.5, y: 0.5}
m_AnchorMax: {x: 0.5, y: 0.5}
m_AnchoredPosition: {x: -80.7, y: -31.391098}
m_SizeDelta: {x: 273.4951, y: 26.8889}
m_Pivot: {x: 0.5, y: 0.5}
m_AnchoredPosition: {x: 56.047485, y: -31.391068}
m_SizeDelta: {x: 0, y: 26.8889}
m_Pivot: {x: 1, y: 0.5}
--- !u!222 &6137481741208887361
CanvasRenderer:
m_ObjectHideFlags: 0
@@ -7315,19 +7316,19 @@ MonoBehaviour:
m_Name:
m_EditorClassIdentifier:
m_Material: {fileID: 0}
m_Color: {r: 0.23921569, g: 0.21960784, b: 0.40392157, a: 1}
m_RaycastTarget: 1
m_Color: {r: 1, g: 1, b: 1, a: 1}
m_RaycastTarget: 0
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
m_Maskable: 1
m_OnCullStateChanged:
m_PersistentCalls:
m_Calls: []
m_FontData:
m_Font: {fileID: 12800000, guid: 2dfc162c344875b4da01e6a15073dce5, type: 3}
m_FontSize: 26
m_Font: {fileID: 12800000, guid: dfa10de3e2846524089b5073175cd151, type: 3}
m_FontSize: 20
m_FontStyle: 0
m_BestFit: 1
m_MinSize: 2
m_BestFit: 0
m_MinSize: 0
m_MaxSize: 26
m_Alignment: 5
m_AlignByGeometry: 0
@@ -7335,7 +7336,21 @@ MonoBehaviour:
m_HorizontalOverflow: 0
m_VerticalOverflow: 0
m_LineSpacing: 1
m_Text: USERNAME
m_Text: "USERNAME\u8FD9\u662F\u7528\u6237\u7684\u795E\u79D8\u7528\u6237\u540D\u662F12123"
--- !u!114 &1338150670383926109
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 5745207992015071035}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 3245ec927659c4140ac4f8d17403cc18, type: 3}
m_Name:
m_EditorClassIdentifier:
m_HorizontalFit: 2
m_VerticalFit: 0
--- !u!1 &5751219486557543197
GameObject:
m_ObjectHideFlags: 0
@@ -313,6 +313,12 @@ public sealed class AllyHeroDeployLedger : MonoBehaviour
return;
}
if (!loadedFromSave && hasAnyRecoverableLocalState)
{
Debug.LogWarning("[AllyHeroDeployLedger] Recoverable save state exists but could not be loaded; skip saving to avoid overwriting older idol growth data.");
return;
}
SyncAllMirrorFlags();
SyncAllLegacySelectedSlotExpKeys();
AllyHeroDeployLedgerStorage.TrySave(BuildPayload());
@@ -128,20 +128,24 @@ public static class AllyHeroDeployLedgerStorage
byte[] encryptedBytes = Convert.FromBase64String(envelope.payload);
AllyHeroDeployLedgerPayload loadedPayload = null;
var identifierVariants = SaveIdentityUtility.GetApplicationIdentifierVariants();
for (int i = 0; i < identifierVariants.Count; i++)
var deviceBindingVariants = SaveIdentityUtility.GetDeviceBindingVariants();
for (int i = 0; i < identifierVariants.Count && loadedPayload == null; i++)
{
try
for (int d = 0; d < deviceBindingVariants.Count; d++)
{
byte[] plainBytes = XorTransform(encryptedBytes, BuildKeyBytes(identifierVariants[i]));
string payloadJson = Encoding.UTF8.GetString(plainBytes);
loadedPayload = JsonUtility.FromJson<AllyHeroDeployLedgerPayload>(payloadJson);
if (loadedPayload != null)
try
{
byte[] plainBytes = XorTransform(encryptedBytes, BuildKeyBytes(identifierVariants[i], deviceBindingVariants[d]));
string payloadJson = Encoding.UTF8.GetString(plainBytes);
loadedPayload = JsonUtility.FromJson<AllyHeroDeployLedgerPayload>(payloadJson);
if (loadedPayload != null)
{
break;
}
}
catch
{
break;
}
}
catch
{
}
}
@@ -208,25 +212,19 @@ public static class AllyHeroDeployLedgerStorage
private static string ComputeSignature(string payloadBase64)
{
string signText = payloadBase64 + "|" + SaveIdentityUtility.GetPrimaryApplicationIdentifier() + "|" + SecretSeed + "|" + SystemInfo.deviceUniqueIdentifier;
using (SHA256 sha = SHA256.Create())
{
byte[] bytes = Encoding.UTF8.GetBytes(signText);
byte[] hash = sha.ComputeHash(bytes);
return Convert.ToBase64String(hash);
}
return ComputeSignature(payloadBase64, SaveIdentityUtility.GetPrimaryApplicationIdentifier(), SaveIdentityUtility.GetPrimaryDeviceBinding());
}
private static byte[] BuildKeyBytes()
{
return BuildKeyBytes(SaveIdentityUtility.GetPrimaryApplicationIdentifier());
return BuildKeyBytes(SaveIdentityUtility.GetPrimaryApplicationIdentifier(), SaveIdentityUtility.GetPrimaryDeviceBinding());
}
private static byte[] BuildKeyBytes(string applicationIdentifier)
private static byte[] BuildKeyBytes(string applicationIdentifier, string deviceBinding)
{
using (SHA256 sha = SHA256.Create())
{
string seed = applicationIdentifier + "|" + SystemInfo.deviceUniqueIdentifier + "|" + SecretSeed;
string seed = applicationIdentifier + "|" + deviceBinding + "|" + SecretSeed;
return sha.ComputeHash(Encoding.UTF8.GetBytes(seed));
}
}
@@ -234,21 +232,25 @@ public static class AllyHeroDeployLedgerStorage
private static bool TryValidateSignature(string payloadBase64, string signature)
{
var identifierVariants = SaveIdentityUtility.GetApplicationIdentifierVariants();
var deviceBindingVariants = SaveIdentityUtility.GetDeviceBindingVariants();
for (int i = 0; i < identifierVariants.Count; i++)
{
string expectedSignature = ComputeSignature(payloadBase64, identifierVariants[i]);
if (string.Equals(expectedSignature, signature, StringComparison.Ordinal))
for (int d = 0; d < deviceBindingVariants.Count; d++)
{
return true;
string expectedSignature = ComputeSignature(payloadBase64, identifierVariants[i], deviceBindingVariants[d]);
if (string.Equals(expectedSignature, signature, StringComparison.Ordinal))
{
return true;
}
}
}
return false;
}
private static string ComputeSignature(string payloadBase64, string applicationIdentifier)
private static string ComputeSignature(string payloadBase64, string applicationIdentifier, string deviceBinding)
{
string signText = payloadBase64 + "|" + applicationIdentifier + "|" + SecretSeed + "|" + SystemInfo.deviceUniqueIdentifier;
string signText = payloadBase64 + "|" + applicationIdentifier + "|" + SecretSeed + "|" + deviceBinding;
using (SHA256 sha = SHA256.Create())
{
byte[] bytes = Encoding.UTF8.GetBytes(signText);
@@ -197,6 +197,12 @@ public sealed class DushMaterialLedger : MonoBehaviour
InitializeIfNeeded();
}
if (!loadedFromSave && hasAnyRecoverableLocalState)
{
Debug.LogWarning("[DushMaterialLedger] Recoverable save state exists but could not be loaded; skip saving to avoid overwriting older material data.");
return;
}
SyncMirrorCounts();
DushMaterialLedgerStorage.TrySave(BuildPayload());
SyncToPlayerData();
@@ -138,20 +138,24 @@ public static class DushMaterialLedgerStorage
var encryptedBytes = Convert.FromBase64String(envelope.payload);
DushMaterialLedgerPayload loadedPayload = null;
var identifierVariants = SaveIdentityUtility.GetApplicationIdentifierVariants();
for (int i = 0; i < identifierVariants.Count; i++)
var deviceBindingVariants = SaveIdentityUtility.GetDeviceBindingVariants();
for (int i = 0; i < identifierVariants.Count && loadedPayload == null; i++)
{
try
for (int d = 0; d < deviceBindingVariants.Count; d++)
{
var plainBytes = XorTransform(encryptedBytes, BuildKeyBytes(identifierVariants[i]));
var payloadJson = Encoding.UTF8.GetString(plainBytes);
loadedPayload = JsonUtility.FromJson<DushMaterialLedgerPayload>(payloadJson);
if (loadedPayload != null)
try
{
var plainBytes = XorTransform(encryptedBytes, BuildKeyBytes(identifierVariants[i], deviceBindingVariants[d]));
var payloadJson = Encoding.UTF8.GetString(plainBytes);
loadedPayload = JsonUtility.FromJson<DushMaterialLedgerPayload>(payloadJson);
if (loadedPayload != null)
{
break;
}
}
catch
{
break;
}
}
catch
{
}
}
@@ -218,25 +222,19 @@ public static class DushMaterialLedgerStorage
private static string ComputeSignature(string payloadBase64)
{
var signText = payloadBase64 + "|" + SaveIdentityUtility.GetPrimaryApplicationIdentifier() + "|" + SecretSeed + "|" + SystemInfo.deviceUniqueIdentifier;
using (var sha = SHA256.Create())
{
var bytes = Encoding.UTF8.GetBytes(signText);
var hash = sha.ComputeHash(bytes);
return Convert.ToBase64String(hash);
}
return ComputeSignature(payloadBase64, SaveIdentityUtility.GetPrimaryApplicationIdentifier(), SaveIdentityUtility.GetPrimaryDeviceBinding());
}
private static byte[] BuildKeyBytes()
{
return BuildKeyBytes(SaveIdentityUtility.GetPrimaryApplicationIdentifier());
return BuildKeyBytes(SaveIdentityUtility.GetPrimaryApplicationIdentifier(), SaveIdentityUtility.GetPrimaryDeviceBinding());
}
private static byte[] BuildKeyBytes(string applicationIdentifier)
private static byte[] BuildKeyBytes(string applicationIdentifier, string deviceBinding)
{
using (var sha = SHA256.Create())
{
var seed = applicationIdentifier + "|" + SystemInfo.deviceUniqueIdentifier + "|" + SecretSeed;
var seed = applicationIdentifier + "|" + deviceBinding + "|" + SecretSeed;
return sha.ComputeHash(Encoding.UTF8.GetBytes(seed));
}
}
@@ -244,21 +242,25 @@ public static class DushMaterialLedgerStorage
private static bool TryValidateSignature(string payloadBase64, string signature)
{
var identifierVariants = SaveIdentityUtility.GetApplicationIdentifierVariants();
var deviceBindingVariants = SaveIdentityUtility.GetDeviceBindingVariants();
for (int i = 0; i < identifierVariants.Count; i++)
{
var expectedSignature = ComputeSignature(payloadBase64, identifierVariants[i]);
if (string.Equals(expectedSignature, signature, StringComparison.Ordinal))
for (int d = 0; d < deviceBindingVariants.Count; d++)
{
return true;
var expectedSignature = ComputeSignature(payloadBase64, identifierVariants[i], deviceBindingVariants[d]);
if (string.Equals(expectedSignature, signature, StringComparison.Ordinal))
{
return true;
}
}
}
return false;
}
private static string ComputeSignature(string payloadBase64, string applicationIdentifier)
private static string ComputeSignature(string payloadBase64, string applicationIdentifier, string deviceBinding)
{
var signText = payloadBase64 + "|" + applicationIdentifier + "|" + SecretSeed + "|" + SystemInfo.deviceUniqueIdentifier;
var signText = payloadBase64 + "|" + applicationIdentifier + "|" + SecretSeed + "|" + deviceBinding;
using (var sha = SHA256.Create())
{
var bytes = Encoding.UTF8.GetBytes(signText);
@@ -10,6 +10,8 @@ public sealed class EquipmentConsumableLedger : MonoBehaviour
private readonly Dictionary<string, int> countsByKey = new Dictionary<string, int>(StringComparer.Ordinal);
private bool initialized;
private bool loadedFromSave;
private bool hasAnyRecoverableLocalState;
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
private static void Bootstrap()
@@ -65,11 +67,18 @@ public sealed class EquipmentConsumableLedger : MonoBehaviour
}
EquipmentConsumableLedgerPayload payload;
EquipmentConsumableLedgerStorage.TryLoad(out payload);
loadedFromSave = EquipmentConsumableLedgerStorage.TryLoad(out payload);
hasAnyRecoverableLocalState = loadedFromSave || EquipmentConsumableLedgerStorage.HasAnyRecoverableState();
RebuildFromPayload(payload);
initialized = true;
SyncMirrorCounts();
SaveNow();
// If an older save exists but cannot be read right now, do not overwrite it
// with an empty default payload during startup.
if (loadedFromSave || !hasAnyRecoverableLocalState)
{
SaveNow();
}
}
public int GetCount(EquipmentConsumableKind kind)
@@ -126,6 +135,12 @@ public sealed class EquipmentConsumableLedger : MonoBehaviour
InitializeIfNeeded();
}
if (!loadedFromSave && hasAnyRecoverableLocalState)
{
Debug.LogWarning("[EquipmentConsumableLedger] Recoverable save state exists but could not be loaded; skip saving to avoid overwriting older material data.");
return;
}
SyncMirrorCounts();
EquipmentConsumableLedgerStorage.TrySave(BuildPayload());
}
@@ -52,6 +52,7 @@ public static class EquipmentConsumableLedgerStorage
{
return HasAnyVaultFile(MainFileName)
|| HasAnyVaultFile(BackupFileName)
|| PlayerProgressBackupService.HasEquipmentConsumableBackup()
|| LocalRecoveryMirror.HasSlotData(RecoverySlotKey);
}
@@ -136,20 +137,24 @@ public static class EquipmentConsumableLedgerStorage
var encryptedBytes = Convert.FromBase64String(envelope.payload);
EquipmentConsumableLedgerPayload loadedPayload = null;
var identifierVariants = SaveIdentityUtility.GetApplicationIdentifierVariants();
for (int i = 0; i < identifierVariants.Count; i++)
var deviceBindingVariants = SaveIdentityUtility.GetDeviceBindingVariants();
for (int i = 0; i < identifierVariants.Count && loadedPayload == null; i++)
{
try
for (int d = 0; d < deviceBindingVariants.Count; d++)
{
var plainBytes = XorTransform(encryptedBytes, BuildKeyBytes(identifierVariants[i]));
var payloadJson = Encoding.UTF8.GetString(plainBytes);
loadedPayload = JsonUtility.FromJson<EquipmentConsumableLedgerPayload>(payloadJson);
if (loadedPayload != null)
try
{
var plainBytes = XorTransform(encryptedBytes, BuildKeyBytes(identifierVariants[i], deviceBindingVariants[d]));
var payloadJson = Encoding.UTF8.GetString(plainBytes);
loadedPayload = JsonUtility.FromJson<EquipmentConsumableLedgerPayload>(payloadJson);
if (loadedPayload != null)
{
break;
}
}
catch
{
break;
}
}
catch
{
}
}
@@ -202,25 +207,19 @@ public static class EquipmentConsumableLedgerStorage
private static string ComputeSignature(string payloadBase64)
{
var signText = payloadBase64 + "|" + SaveIdentityUtility.GetPrimaryApplicationIdentifier() + "|" + SecretSeed + "|" + SystemInfo.deviceUniqueIdentifier;
using (var sha = SHA256.Create())
{
var bytes = Encoding.UTF8.GetBytes(signText);
var hash = sha.ComputeHash(bytes);
return Convert.ToBase64String(hash);
}
return ComputeSignature(payloadBase64, SaveIdentityUtility.GetPrimaryApplicationIdentifier(), SaveIdentityUtility.GetPrimaryDeviceBinding());
}
private static byte[] BuildKeyBytes()
{
return BuildKeyBytes(SaveIdentityUtility.GetPrimaryApplicationIdentifier());
return BuildKeyBytes(SaveIdentityUtility.GetPrimaryApplicationIdentifier(), SaveIdentityUtility.GetPrimaryDeviceBinding());
}
private static byte[] BuildKeyBytes(string applicationIdentifier)
private static byte[] BuildKeyBytes(string applicationIdentifier, string deviceBinding)
{
using (var sha = SHA256.Create())
{
var seed = applicationIdentifier + "|" + SystemInfo.deviceUniqueIdentifier + "|" + SecretSeed;
var seed = applicationIdentifier + "|" + deviceBinding + "|" + SecretSeed;
return sha.ComputeHash(Encoding.UTF8.GetBytes(seed));
}
}
@@ -228,21 +227,25 @@ public static class EquipmentConsumableLedgerStorage
private static bool TryValidateSignature(string payloadBase64, string signature)
{
var identifierVariants = SaveIdentityUtility.GetApplicationIdentifierVariants();
var deviceBindingVariants = SaveIdentityUtility.GetDeviceBindingVariants();
for (int i = 0; i < identifierVariants.Count; i++)
{
var expectedSignature = ComputeSignature(payloadBase64, identifierVariants[i]);
if (string.Equals(expectedSignature, signature, StringComparison.Ordinal))
for (int d = 0; d < deviceBindingVariants.Count; d++)
{
return true;
var expectedSignature = ComputeSignature(payloadBase64, identifierVariants[i], deviceBindingVariants[d]);
if (string.Equals(expectedSignature, signature, StringComparison.Ordinal))
{
return true;
}
}
}
return false;
}
private static string ComputeSignature(string payloadBase64, string applicationIdentifier)
private static string ComputeSignature(string payloadBase64, string applicationIdentifier, string deviceBinding)
{
var signText = payloadBase64 + "|" + applicationIdentifier + "|" + SecretSeed + "|" + SystemInfo.deviceUniqueIdentifier;
var signText = payloadBase64 + "|" + applicationIdentifier + "|" + SecretSeed + "|" + deviceBinding;
using (var sha = SHA256.Create())
{
var bytes = Encoding.UTF8.GetBytes(signText);
@@ -295,6 +295,13 @@ public sealed class ExpBottleLedger : MonoBehaviour
public void SaveNow()
{
InitializeIfNeededForSave();
if (!loadedFromSave && hasAnyRecoverableLocalState)
{
Debug.LogWarning("[ExpBottleLedger] Recoverable save state exists but could not be loaded; skip saving to avoid overwriting older material data.");
return;
}
SyncMirrorCounts();
ExpBottleLedgerStorage.TrySave(BuildPayload());
SyncToPlayerData();
@@ -138,20 +138,24 @@ public static class ExpBottleLedgerStorage
var encryptedBytes = Convert.FromBase64String(envelope.payload);
ExpBottleLedgerPayload loadedPayload = null;
var identifierVariants = SaveIdentityUtility.GetApplicationIdentifierVariants();
for (int i = 0; i < identifierVariants.Count; i++)
var deviceBindingVariants = SaveIdentityUtility.GetDeviceBindingVariants();
for (int i = 0; i < identifierVariants.Count && loadedPayload == null; i++)
{
try
for (int d = 0; d < deviceBindingVariants.Count; d++)
{
var plainBytes = XorTransform(encryptedBytes, BuildKeyBytes(identifierVariants[i]));
var payloadJson = Encoding.UTF8.GetString(plainBytes);
loadedPayload = JsonUtility.FromJson<ExpBottleLedgerPayload>(payloadJson);
if (loadedPayload != null)
try
{
var plainBytes = XorTransform(encryptedBytes, BuildKeyBytes(identifierVariants[i], deviceBindingVariants[d]));
var payloadJson = Encoding.UTF8.GetString(plainBytes);
loadedPayload = JsonUtility.FromJson<ExpBottleLedgerPayload>(payloadJson);
if (loadedPayload != null)
{
break;
}
}
catch
{
break;
}
}
catch
{
}
}
@@ -218,25 +222,19 @@ public static class ExpBottleLedgerStorage
private static string ComputeSignature(string payloadBase64)
{
var signText = payloadBase64 + "|" + SaveIdentityUtility.GetPrimaryApplicationIdentifier() + "|" + SecretSeed + "|" + SystemInfo.deviceUniqueIdentifier;
using (var sha = SHA256.Create())
{
var bytes = Encoding.UTF8.GetBytes(signText);
var hash = sha.ComputeHash(bytes);
return Convert.ToBase64String(hash);
}
return ComputeSignature(payloadBase64, SaveIdentityUtility.GetPrimaryApplicationIdentifier(), SaveIdentityUtility.GetPrimaryDeviceBinding());
}
private static byte[] BuildKeyBytes()
{
return BuildKeyBytes(SaveIdentityUtility.GetPrimaryApplicationIdentifier());
return BuildKeyBytes(SaveIdentityUtility.GetPrimaryApplicationIdentifier(), SaveIdentityUtility.GetPrimaryDeviceBinding());
}
private static byte[] BuildKeyBytes(string applicationIdentifier)
private static byte[] BuildKeyBytes(string applicationIdentifier, string deviceBinding)
{
using (var sha = SHA256.Create())
{
var seed = applicationIdentifier + "|" + SystemInfo.deviceUniqueIdentifier + "|" + SecretSeed;
var seed = applicationIdentifier + "|" + deviceBinding + "|" + SecretSeed;
return sha.ComputeHash(Encoding.UTF8.GetBytes(seed));
}
}
@@ -244,21 +242,25 @@ public static class ExpBottleLedgerStorage
private static bool TryValidateSignature(string payloadBase64, string signature)
{
var identifierVariants = SaveIdentityUtility.GetApplicationIdentifierVariants();
var deviceBindingVariants = SaveIdentityUtility.GetDeviceBindingVariants();
for (int i = 0; i < identifierVariants.Count; i++)
{
var expectedSignature = ComputeSignature(payloadBase64, identifierVariants[i]);
if (string.Equals(expectedSignature, signature, StringComparison.Ordinal))
for (int d = 0; d < deviceBindingVariants.Count; d++)
{
return true;
var expectedSignature = ComputeSignature(payloadBase64, identifierVariants[i], deviceBindingVariants[d]);
if (string.Equals(expectedSignature, signature, StringComparison.Ordinal))
{
return true;
}
}
}
return false;
}
private static string ComputeSignature(string payloadBase64, string applicationIdentifier)
private static string ComputeSignature(string payloadBase64, string applicationIdentifier, string deviceBinding)
{
var signText = payloadBase64 + "|" + applicationIdentifier + "|" + SecretSeed + "|" + SystemInfo.deviceUniqueIdentifier;
var signText = payloadBase64 + "|" + applicationIdentifier + "|" + SecretSeed + "|" + deviceBinding;
using (var sha = SHA256.Create())
{
var bytes = Encoding.UTF8.GetBytes(signText);
@@ -48,11 +48,99 @@ public static class FirstRunFactoryResetService
return;
}
// 物理文件兜底探测(最后一道防线):
// HasAnyRecoverableLocalState() 依赖各 Storage 的解密/结构判断,若因根目录别名未覆盖、
// 或存档结构异常而误判为"无存档",仍可能走到出厂重置。这里直接扫描磁盘上是否存在
// 任何存档物理文件(不解密、只看存在),只要有就绝不重置,宁可等待其它恢复路径。
if (HasAnyPhysicalSaveFileOnDisk())
{
Debug.LogWarning("[FactoryReset] 磁盘上存在存档物理文件但未能读出," +
"为避免误删玩家数据,跳过出厂重置并补写初始化标记。");
EnsureInitializedMarker();
return;
}
Debug.LogWarning("[FactoryReset] 首次运行且无可恢复存档,应用出厂默认设置。");
ApplyFactoryReset();
EnsureInitializedMarker();
}
// 扫描所有可能的存档根目录(含新旧包名/公司名别名),只要磁盘上存在任意一个存档物理文件
// 就返回 true。只判存在、不解密——因此不受 deviceUniqueIdentifier 变化影响。
private static bool HasAnyPhysicalSaveFileOnDisk()
{
try
{
IReadOnlyList<string> roots = SaveIdentityUtility.GetPersistentRootVariants();
for (int i = 0; i < roots.Count; i++)
{
string root = roots[i];
if (string.IsNullOrWhiteSpace(root))
{
continue;
}
// 1) 加密存档目录 .cache_bridge:递归找任意 .dat / .bak
string vaultDir = Path.Combine(root, ".cache_bridge");
if (DirectoryHasFileWithAnyExtension(vaultDir, new[] { ".dat", ".bak" }))
{
return true;
}
// 2) 设备无关明文备份 player_progress.bbackup
if (File.Exists(Path.Combine(root, "player_progress.bbackup")))
{
return true;
}
// 3) 恢复镜像目录 .save_recovery:存在任意文件即视为有存档
string recoveryDir = Path.Combine(root, ".save_recovery");
if (DirectoryHasAnyFile(recoveryDir))
{
return true;
}
}
}
catch (Exception ex)
{
// 探测失败时采取保守策略:报告"存在存档",宁可跳过重置也不误删。
Debug.LogWarning("[FactoryReset] 物理存档探测异常,保守跳过出厂重置: " + ex.Message);
return true;
}
return false;
}
private static bool DirectoryHasFileWithAnyExtension(string directory, string[] extensions)
{
if (string.IsNullOrWhiteSpace(directory) || !Directory.Exists(directory))
{
return false;
}
for (int i = 0; i < extensions.Length; i++)
{
string[] matches = Directory.GetFiles(directory, "*" + extensions[i], SearchOption.AllDirectories);
if (matches != null && matches.Length > 0)
{
return true;
}
}
return false;
}
private static bool DirectoryHasAnyFile(string directory)
{
if (string.IsNullOrWhiteSpace(directory) || !Directory.Exists(directory))
{
return false;
}
string[] matches = Directory.GetFiles(directory, "*", SearchOption.AllDirectories);
return matches != null && matches.Length > 0;
}
private static bool HasInitializedMarker()
{
try
@@ -238,6 +238,13 @@ public sealed class PlayerEconomyLedger : MonoBehaviour
public void SaveNow()
{
InitializeIfNeededForSave();
if (!loadedFromSave && hasAnyRecoverableLocalState)
{
Debug.LogWarning("[PlayerEconomyLedger] Recoverable save state exists but could not be loaded; skip saving to avoid overwriting older economy data.");
return;
}
PlayerEconomyStorage.TrySave(payload);
SyncToPlayerData();
GlobalAchievementService.EnsureInstance().ReportCurrentCoins(payload.coins);
@@ -138,21 +138,25 @@ public static class PlayerEconomyStorage
var encryptedBytes = Convert.FromBase64String(envelope.payload);
var identifierVariants = SaveIdentityUtility.GetApplicationIdentifierVariants();
var deviceBindingVariants = SaveIdentityUtility.GetDeviceBindingVariants();
PlayerEconomyPayload loadedPayload = null;
for (int i = 0; i < identifierVariants.Count; i++)
for (int i = 0; i < identifierVariants.Count && loadedPayload == null; i++)
{
try
for (int d = 0; d < deviceBindingVariants.Count; d++)
{
var plainBytes = XorTransform(encryptedBytes, BuildKeyBytes(identifierVariants[i]));
var payloadJson = Encoding.UTF8.GetString(plainBytes);
loadedPayload = JsonUtility.FromJson<PlayerEconomyPayload>(payloadJson);
if (loadedPayload != null)
try
{
var plainBytes = XorTransform(encryptedBytes, BuildKeyBytes(identifierVariants[i], deviceBindingVariants[d]));
var payloadJson = Encoding.UTF8.GetString(plainBytes);
loadedPayload = JsonUtility.FromJson<PlayerEconomyPayload>(payloadJson);
if (loadedPayload != null)
{
break;
}
}
catch
{
break;
}
}
catch
{
}
}
@@ -219,25 +223,19 @@ public static class PlayerEconomyStorage
private static string ComputeSignature(string payloadBase64)
{
var signText = payloadBase64 + "|" + SaveIdentityUtility.GetPrimaryApplicationIdentifier() + "|" + SecretSeed + "|" + SystemInfo.deviceUniqueIdentifier;
using (var sha = SHA256.Create())
{
var bytes = Encoding.UTF8.GetBytes(signText);
var hash = sha.ComputeHash(bytes);
return Convert.ToBase64String(hash);
}
return ComputeSignature(payloadBase64, SaveIdentityUtility.GetPrimaryApplicationIdentifier(), SaveIdentityUtility.GetPrimaryDeviceBinding());
}
private static byte[] BuildKeyBytes()
{
return BuildKeyBytes(SaveIdentityUtility.GetPrimaryApplicationIdentifier());
return BuildKeyBytes(SaveIdentityUtility.GetPrimaryApplicationIdentifier(), SaveIdentityUtility.GetPrimaryDeviceBinding());
}
private static byte[] BuildKeyBytes(string applicationIdentifier)
private static byte[] BuildKeyBytes(string applicationIdentifier, string deviceBinding)
{
using (var sha = SHA256.Create())
{
var seed = applicationIdentifier + "|" + SystemInfo.deviceUniqueIdentifier + "|" + SecretSeed;
var seed = applicationIdentifier + "|" + deviceBinding + "|" + SecretSeed;
return sha.ComputeHash(Encoding.UTF8.GetBytes(seed));
}
}
@@ -245,21 +243,25 @@ public static class PlayerEconomyStorage
private static bool TryValidateSignature(string payloadBase64, string signature)
{
var identifierVariants = SaveIdentityUtility.GetApplicationIdentifierVariants();
var deviceBindingVariants = SaveIdentityUtility.GetDeviceBindingVariants();
for (int i = 0; i < identifierVariants.Count; i++)
{
var expectedSignature = ComputeSignature(payloadBase64, identifierVariants[i]);
if (string.Equals(expectedSignature, signature, StringComparison.Ordinal))
for (int d = 0; d < deviceBindingVariants.Count; d++)
{
return true;
var expectedSignature = ComputeSignature(payloadBase64, identifierVariants[i], deviceBindingVariants[d]);
if (string.Equals(expectedSignature, signature, StringComparison.Ordinal))
{
return true;
}
}
}
return false;
}
private static string ComputeSignature(string payloadBase64, string applicationIdentifier)
private static string ComputeSignature(string payloadBase64, string applicationIdentifier, string deviceBinding)
{
var signText = payloadBase64 + "|" + applicationIdentifier + "|" + SecretSeed + "|" + SystemInfo.deviceUniqueIdentifier;
var signText = payloadBase64 + "|" + applicationIdentifier + "|" + SecretSeed + "|" + deviceBinding;
using (var sha = SHA256.Create())
{
var bytes = Encoding.UTF8.GetBytes(signText);
@@ -191,6 +191,12 @@ public sealed class PlayerExperienceLedger : MonoBehaviour
payload = CreateDefaultPayload();
}
if (!loadedFromSave && hasAnyRecoverableLocalState)
{
Debug.LogWarning("[PlayerExperienceLedger] Recoverable save state exists but could not be loaded; skip saving to avoid overwriting older player experience data.");
return;
}
payload.lastUpdatedUtcTicks = DateTime.UtcNow.Ticks;
SecureSaveVault.SaveJson("player_experience", "runtime", payload, GetLegacySavePath());
PlayerProgressBackupService.SavePlayerExperience(payload.playerExp);
@@ -78,6 +78,12 @@ public static class PlayerProgressBackupService
return TryLoadBundle(out bundle) && bundle != null && bundle.dushMaterial != null;
}
public static bool HasEquipmentConsumableBackup()
{
PlayerProgressBackupBundle bundle;
return TryLoadBundle(out bundle) && bundle != null && bundle.equipmentConsumable != null;
}
public static bool HasStoreOwnershipBackup()
{
PlayerProgressBackupBundle bundle;
@@ -301,7 +301,7 @@ public sealed class PlayerSkillService : MonoBehaviour
if (grantedCoins > 0)
{
PlayerEconomyLedger.EnsureInstance().AddCoins(grantedCoins);
grantedEntries.Add(gItemGet.Create("\u91d1\u5e01", null, grantedCoins, new Color32(240, 196, 74, 255)));
grantedEntries.Add(gItemGet.Create("算力", null, grantedCoins, new Color32(240, 196, 74, 255)));
}
}
@@ -376,7 +376,7 @@ public sealed class PlayerSkillService : MonoBehaviour
int grantedCoins = purchaseAmount * 4;
PlayerEconomyLedger.EnsureInstance().AddCoins(grantedCoins);
rewardEntry = gItemGet.Create("\u91d1\u5e01", item.itemIcon, grantedCoins, new Color32(240, 196, 74, 255));
rewardEntry = gItemGet.Create("算力", item.itemIcon, grantedCoins, new Color32(240, 196, 74, 255));
return true;
}
@@ -31,11 +31,69 @@ public static class SaveIdentityUtility
"com.DefaultCompany.EaseOut_Main_FreshNew"
};
// 设备无关绑定令牌:新版存档的加密/签名统一改用它,取代 SystemInfo.deviceUniqueIdentifier。
// 根因修复——deviceUniqueIdentifier 在安卓更新/换签名/换渠道后会变化,导致旧密钥永远解不出存档。
// 用固定令牌后,更新或换机都不再影响解密。读取时仍会尝试真实设备 ID 变体以兼容本机旧存档。
public const string DeviceIndependentBindingToken = "device_independent_binding_v1";
// 【性能】这些值在进程生命周期内恒定:SystemInfo.deviceUniqueIdentifier、Application.identifier/
// companyName/productName 在安卓上开销较大(尤其 deviceUniqueIdentifier 涉及系统调用与哈希)。
// 存档每次读取/验签都会取用它们,若每次重算并分配 List,会在 UI 大量刷新(如 idols 角色切换、
// 场景切换批量重建)时造成明显卡顿。这里缓存一次,之后直接复用,行为完全不变。
private static IReadOnlyList<string> s_cachedApplicationIdentifierVariants;
private static IReadOnlyList<string> s_cachedDeviceBindingVariants;
private static string s_cachedDeviceUniqueIdentifier;
private static bool s_deviceUniqueIdentifierResolved;
public static string GetPrimaryApplicationIdentifier()
{
return StableApplicationIdentifier;
}
// 写入存档时使用的设备绑定:固定为设备无关令牌。
public static string GetPrimaryDeviceBinding()
{
return DeviceIndependentBindingToken;
}
// 读取/验签时尝试的设备绑定变体,按命中概率排序:
// 1) 设备无关令牌(新版写入所用,最常命中)
// 2) 当前设备真实 deviceUniqueIdentifier(兼容本机旧版写入的设备绑定存档,首次读到后会被重写为设备无关,实现无缝迁移)
// 结果缓存,避免每次读取都触发一次昂贵的 deviceUniqueIdentifier 查询与 List 分配。
public static IReadOnlyList<string> GetDeviceBindingVariants()
{
if (s_cachedDeviceBindingVariants != null)
{
return s_cachedDeviceBindingVariants;
}
var result = new List<string>();
AddDistinct(result, DeviceIndependentBindingToken);
AddDistinct(result, SafeDeviceUniqueIdentifier());
s_cachedDeviceBindingVariants = result;
return s_cachedDeviceBindingVariants;
}
private static string SafeDeviceUniqueIdentifier()
{
if (s_deviceUniqueIdentifierResolved)
{
return s_cachedDeviceUniqueIdentifier;
}
try
{
s_cachedDeviceUniqueIdentifier = SystemInfo.deviceUniqueIdentifier;
}
catch (Exception)
{
s_cachedDeviceUniqueIdentifier = null;
}
s_deviceUniqueIdentifierResolved = true;
return s_cachedDeviceUniqueIdentifier;
}
public static string GetCanonicalPersistentRoot()
{
if (Application.platform == RuntimePlatform.WindowsEditor || Application.platform == RuntimePlatform.WindowsPlayer)
@@ -50,6 +108,11 @@ public static class SaveIdentityUtility
public static IReadOnlyList<string> GetApplicationIdentifierVariants()
{
if (s_cachedApplicationIdentifierVariants != null)
{
return s_cachedApplicationIdentifierVariants;
}
var result = new List<string>();
AddDistinct(result, StableApplicationIdentifier);
AddDistinct(result, Application.identifier);
@@ -74,7 +137,8 @@ public static class SaveIdentityUtility
}
}
return result;
s_cachedApplicationIdentifierVariants = result;
return s_cachedApplicationIdentifierVariants;
}
public static IReadOnlyList<string> GetPersistentRootVariants(string extraLegacyPath = null)
+54 -47
View File
@@ -373,12 +373,7 @@ public static class SecureSaveVault
private static string ComputeSignature(string category, string payloadBase64)
{
string signText = payloadBase64 + "|" + category + "|" + SaveIdentityUtility.GetPrimaryApplicationIdentifier() + "|" + SecretSeed + "|" + SystemInfo.deviceUniqueIdentifier;
using (var sha = SHA256.Create())
{
byte[] hash = sha.ComputeHash(Encoding.UTF8.GetBytes(signText));
return Convert.ToBase64String(hash);
}
return ComputeSignature(category, payloadBase64, SaveIdentityUtility.GetPrimaryApplicationIdentifier(), SaveIdentityUtility.GetPrimaryDeviceBinding());
}
private static byte[] ProtectBytes(string category, string key, byte[] plainBytes)
@@ -394,7 +389,7 @@ public static class SecureSaveVault
{
aes.Mode = CipherMode.CBC;
aes.Padding = PaddingMode.PKCS7;
aes.Key = BuildAesKey(category, key, SaveIdentityUtility.GetPrimaryApplicationIdentifier());
aes.Key = BuildAesKey(category, key, SaveIdentityUtility.GetPrimaryApplicationIdentifier(), SaveIdentityUtility.GetPrimaryDeviceBinding());
aes.GenerateIV();
using (var encryptor = aes.CreateEncryptor())
{
@@ -419,37 +414,41 @@ public static class SecureSaveVault
}
#endif
IReadOnlyList<string> identifierVariants = SaveIdentityUtility.GetApplicationIdentifierVariants();
IReadOnlyList<string> deviceBindingVariants = SaveIdentityUtility.GetDeviceBindingVariants();
for (int i = 0; i < identifierVariants.Count; i++)
{
using (var aes = Aes.Create())
for (int dv = 0; dv < deviceBindingVariants.Count; dv++)
{
aes.Mode = CipherMode.CBC;
aes.Padding = PaddingMode.PKCS7;
aes.Key = BuildAesKey(category, key, identifierVariants[i]);
int ivLength = aes.BlockSize / 8;
if (protectedBytes == null || protectedBytes.Length <= ivLength)
using (var aes = Aes.Create())
{
return false;
}
byte[] iv = new byte[ivLength];
byte[] cipher = new byte[protectedBytes.Length - ivLength];
Buffer.BlockCopy(protectedBytes, 0, iv, 0, ivLength);
Buffer.BlockCopy(protectedBytes, ivLength, cipher, 0, cipher.Length);
aes.IV = iv;
try
{
using (var decryptor = aes.CreateDecryptor())
aes.Mode = CipherMode.CBC;
aes.Padding = PaddingMode.PKCS7;
aes.Key = BuildAesKey(category, key, identifierVariants[i], deviceBindingVariants[dv]);
int ivLength = aes.BlockSize / 8;
if (protectedBytes == null || protectedBytes.Length <= ivLength)
{
plainBytes = decryptor.TransformFinalBlock(cipher, 0, cipher.Length);
if (plainBytes != null)
return false;
}
byte[] iv = new byte[ivLength];
byte[] cipher = new byte[protectedBytes.Length - ivLength];
Buffer.BlockCopy(protectedBytes, 0, iv, 0, ivLength);
Buffer.BlockCopy(protectedBytes, ivLength, cipher, 0, cipher.Length);
aes.IV = iv;
try
{
using (var decryptor = aes.CreateDecryptor())
{
return true;
plainBytes = decryptor.TransformFinalBlock(cipher, 0, cipher.Length);
if (plainBytes != null)
{
return true;
}
}
}
}
catch
{
catch
{
}
}
}
}
@@ -476,7 +475,7 @@ public static class SecureSaveVault
try
{
protectedBytes = s_dpapiProtectMethod.Invoke(null, new object[] { plainBytes, BuildEntropy(category, SaveIdentityUtility.GetPrimaryApplicationIdentifier()), s_dpapiCurrentUserScope }) as byte[];
protectedBytes = s_dpapiProtectMethod.Invoke(null, new object[] { plainBytes, BuildEntropy(category, SaveIdentityUtility.GetPrimaryApplicationIdentifier(), SaveIdentityUtility.GetPrimaryDeviceBinding()), s_dpapiCurrentUserScope }) as byte[];
return protectedBytes != null && protectedBytes.Length > 0;
}
catch (Exception ex)
@@ -496,18 +495,22 @@ public static class SecureSaveVault
}
IReadOnlyList<string> identifierVariants = SaveIdentityUtility.GetApplicationIdentifierVariants();
IReadOnlyList<string> deviceBindingVariants = SaveIdentityUtility.GetDeviceBindingVariants();
for (int i = 0; i < identifierVariants.Count; i++)
{
try
for (int d = 0; d < deviceBindingVariants.Count; d++)
{
plainBytes = s_dpapiUnprotectMethod.Invoke(null, new object[] { protectedBytes, BuildEntropy(category, identifierVariants[i]), s_dpapiCurrentUserScope }) as byte[];
if (plainBytes != null && plainBytes.Length > 0)
try
{
plainBytes = s_dpapiUnprotectMethod.Invoke(null, new object[] { protectedBytes, BuildEntropy(category, identifierVariants[i], deviceBindingVariants[d]), s_dpapiCurrentUserScope }) as byte[];
if (plainBytes != null && plainBytes.Length > 0)
{
return true;
}
}
catch
{
return true;
}
}
catch
{
}
}
@@ -560,20 +563,20 @@ public static class SecureSaveVault
}
#endif
private static byte[] BuildEntropy(string category, string applicationIdentifier)
private static byte[] BuildEntropy(string category, string applicationIdentifier, string deviceBinding)
{
string seed = applicationIdentifier + "|" + SystemInfo.deviceUniqueIdentifier + "|" + SecretSeed + "|" + category;
string seed = applicationIdentifier + "|" + deviceBinding + "|" + SecretSeed + "|" + category;
using (var sha = SHA256.Create())
{
return sha.ComputeHash(Encoding.UTF8.GetBytes(seed));
}
}
private static byte[] BuildAesKey(string category, string key, string applicationIdentifier)
private static byte[] BuildAesKey(string category, string key, string applicationIdentifier, string deviceBinding)
{
using (var sha = SHA256.Create())
{
string seed = applicationIdentifier + "|" + SystemInfo.deviceUniqueIdentifier + "|" + SecretSeed + "|" + category + "|" + key;
string seed = applicationIdentifier + "|" + deviceBinding + "|" + SecretSeed + "|" + category + "|" + key;
return sha.ComputeHash(Encoding.UTF8.GetBytes(seed));
}
}
@@ -635,21 +638,25 @@ public static class SecureSaveVault
private static bool TryValidateSignature(string category, string payloadBase64, string signature)
{
IReadOnlyList<string> identifierVariants = SaveIdentityUtility.GetApplicationIdentifierVariants();
IReadOnlyList<string> deviceBindingVariants = SaveIdentityUtility.GetDeviceBindingVariants();
for (int i = 0; i < identifierVariants.Count; i++)
{
string expectedSignature = ComputeSignature(category, payloadBase64, identifierVariants[i]);
if (string.Equals(expectedSignature, signature, StringComparison.Ordinal))
for (int d = 0; d < deviceBindingVariants.Count; d++)
{
return true;
string expectedSignature = ComputeSignature(category, payloadBase64, identifierVariants[i], deviceBindingVariants[d]);
if (string.Equals(expectedSignature, signature, StringComparison.Ordinal))
{
return true;
}
}
}
return false;
}
private static string ComputeSignature(string category, string payloadBase64, string applicationIdentifier)
private static string ComputeSignature(string category, string payloadBase64, string applicationIdentifier, string deviceBinding)
{
string signText = payloadBase64 + "|" + category + "|" + applicationIdentifier + "|" + SecretSeed + "|" + SystemInfo.deviceUniqueIdentifier;
string signText = payloadBase64 + "|" + category + "|" + applicationIdentifier + "|" + SecretSeed + "|" + deviceBinding;
using (var sha = SHA256.Create())
{
byte[] hash = sha.ComputeHash(Encoding.UTF8.GetBytes(signText));
@@ -97,13 +97,13 @@ public static class StoreExpBottlePurchaseService
if (!HasEnoughCurrency(primaryCost.currencyType, totalCost))
{
failureMessage = LocalizationService.Get("store.error.insufficient_currency", "货币不足");
failureMessage = GetInsufficientCurrencyMessage(primaryCost.currencyType);
return false;
}
if (!TrySpendCurrency(primaryCost.currencyType, totalCost))
{
failureMessage = LocalizationService.Get("store.error.insufficient_currency", "货币不足");
failureMessage = GetInsufficientCurrencyMessage(primaryCost.currencyType);
return false;
}
@@ -224,7 +224,7 @@ public static class StoreExpBottlePurchaseService
var builder = new StringBuilder();
builder.Append("[StorePurchase] 已成功购买:");
builder.Append(itemSO != null ? itemSO.itemName : LocalizationService.Get("item.unknown.store_item", "Unknown Item"));
builder.Append(" | 花费Coins=");
builder.Append(" | 花费算力=");
builder.Append(totalCost);
builder.Append(" | 发放数量=");
builder.Append(grantedCount);
@@ -341,6 +341,11 @@ public static class StoreExpBottlePurchaseService
|| currencyType == storeItemSO.CurrencyType.material;
}
private static string GetInsufficientCurrencyMessage(storeItemSO.CurrencyType currencyType)
{
return currencyType == storeItemSO.CurrencyType.material ? "记忆碎片不足" : "算力不足";
}
private static bool HasEnoughCurrency(storeItemSO.CurrencyType currencyType, int amount)
{
switch (currencyType)
@@ -16,6 +16,8 @@ public sealed class StoreOwnershipLedger : MonoBehaviour
private readonly Dictionary<int, StoreOwnershipEntry> entriesByItemId = new Dictionary<int, StoreOwnershipEntry>();
private readonly List<storeItemSO> cachedStoreItems = new List<storeItemSO>();
private bool initialized;
private bool loadedFromSave;
private bool hasAnyRecoverableLocalState;
private int lastSaveFrame = -1;
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
@@ -73,8 +75,8 @@ public sealed class StoreOwnershipLedger : MonoBehaviour
initialized = true;
StoreOwnershipPayload payload;
bool loadedFromSave = StoreOwnershipStorage.TryLoad(out payload);
bool hasAnyRecoverableLocalState = loadedFromSave || StoreOwnershipStorage.HasAnyRecoverableState();
loadedFromSave = StoreOwnershipStorage.TryLoad(out payload);
hasAnyRecoverableLocalState = loadedFromSave || StoreOwnershipStorage.HasAnyRecoverableState();
RebuildFromPayload(payload);
LoadStoreItems();
@@ -208,6 +210,12 @@ public sealed class StoreOwnershipLedger : MonoBehaviour
return;
}
if (!loadedFromSave && hasAnyRecoverableLocalState)
{
Debug.LogWarning("[StoreOwnershipLedger] Recoverable save state exists but could not be loaded; skip saving to avoid overwriting older store ownership data.");
return;
}
if (Application.isPlaying && lastSaveFrame == Time.frameCount)
{
return;
@@ -138,20 +138,24 @@ public static class StoreOwnershipStorage
var encryptedBytes = Convert.FromBase64String(envelope.payload);
StoreOwnershipPayload loadedPayload = null;
var identifierVariants = SaveIdentityUtility.GetApplicationIdentifierVariants();
for (int i = 0; i < identifierVariants.Count; i++)
var deviceBindingVariants = SaveIdentityUtility.GetDeviceBindingVariants();
for (int i = 0; i < identifierVariants.Count && loadedPayload == null; i++)
{
try
for (int d = 0; d < deviceBindingVariants.Count; d++)
{
var plainBytes = XorTransform(encryptedBytes, BuildKeyBytes(identifierVariants[i]));
var payloadJson = Encoding.UTF8.GetString(plainBytes);
loadedPayload = JsonUtility.FromJson<StoreOwnershipPayload>(payloadJson);
if (loadedPayload != null)
try
{
var plainBytes = XorTransform(encryptedBytes, BuildKeyBytes(identifierVariants[i], deviceBindingVariants[d]));
var payloadJson = Encoding.UTF8.GetString(plainBytes);
loadedPayload = JsonUtility.FromJson<StoreOwnershipPayload>(payloadJson);
if (loadedPayload != null)
{
break;
}
}
catch
{
break;
}
}
catch
{
}
}
@@ -218,25 +222,19 @@ public static class StoreOwnershipStorage
private static string ComputeSignature(string payloadBase64)
{
var signText = payloadBase64 + "|" + SaveIdentityUtility.GetPrimaryApplicationIdentifier() + "|" + SecretSeed + "|" + SystemInfo.deviceUniqueIdentifier;
using (var sha = SHA256.Create())
{
var bytes = Encoding.UTF8.GetBytes(signText);
var hash = sha.ComputeHash(bytes);
return Convert.ToBase64String(hash);
}
return ComputeSignature(payloadBase64, SaveIdentityUtility.GetPrimaryApplicationIdentifier(), SaveIdentityUtility.GetPrimaryDeviceBinding());
}
private static byte[] BuildKeyBytes()
{
return BuildKeyBytes(SaveIdentityUtility.GetPrimaryApplicationIdentifier());
return BuildKeyBytes(SaveIdentityUtility.GetPrimaryApplicationIdentifier(), SaveIdentityUtility.GetPrimaryDeviceBinding());
}
private static byte[] BuildKeyBytes(string applicationIdentifier)
private static byte[] BuildKeyBytes(string applicationIdentifier, string deviceBinding)
{
using (var sha = SHA256.Create())
{
var seed = applicationIdentifier + "|" + SystemInfo.deviceUniqueIdentifier + "|" + SecretSeed;
var seed = applicationIdentifier + "|" + deviceBinding + "|" + SecretSeed;
return sha.ComputeHash(Encoding.UTF8.GetBytes(seed));
}
}
@@ -244,21 +242,25 @@ public static class StoreOwnershipStorage
private static bool TryValidateSignature(string payloadBase64, string signature)
{
var identifierVariants = SaveIdentityUtility.GetApplicationIdentifierVariants();
var deviceBindingVariants = SaveIdentityUtility.GetDeviceBindingVariants();
for (int i = 0; i < identifierVariants.Count; i++)
{
var expectedSignature = ComputeSignature(payloadBase64, identifierVariants[i]);
if (string.Equals(expectedSignature, signature, StringComparison.Ordinal))
for (int d = 0; d < deviceBindingVariants.Count; d++)
{
return true;
var expectedSignature = ComputeSignature(payloadBase64, identifierVariants[i], deviceBindingVariants[d]);
if (string.Equals(expectedSignature, signature, StringComparison.Ordinal))
{
return true;
}
}
}
return false;
}
private static string ComputeSignature(string payloadBase64, string applicationIdentifier)
private static string ComputeSignature(string payloadBase64, string applicationIdentifier, string deviceBinding)
{
var signText = payloadBase64 + "|" + applicationIdentifier + "|" + SecretSeed + "|" + SystemInfo.deviceUniqueIdentifier;
var signText = payloadBase64 + "|" + applicationIdentifier + "|" + SecretSeed + "|" + deviceBinding;
using (var sha = SHA256.Create())
{
var bytes = Encoding.UTF8.GetBytes(signText);
+1 -1
View File
@@ -398,7 +398,7 @@ public class dailyTaskManager : MonoBehaviour
switch (definition.rewardType)
{
case userTasksPool.RewardType.coins:
return gItemGet.Create("Coins", coinRewardSprite, Mathf.Max(1, definition.rewardAmount), ItemRarity.Common);
return gItemGet.Create("算力", coinRewardSprite, Mathf.Max(1, definition.rewardAmount), ItemRarity.Common);
case userTasksPool.RewardType.expBottle:
{
@@ -19,7 +19,7 @@ MonoBehaviour:
refreshFrequency: 1
targetValue: 1
rewardType: 0
rewardAmount: 1
rewardAmount: 50
rewardExpBottleKind: 0
rewardDushMaterialKind: 0
addToTaskPool: 1
@@ -30,7 +30,7 @@ MonoBehaviour:
refreshFrequency: 1
targetValue: 3
rewardType: 0
rewardAmount: 1
rewardAmount: 150
rewardExpBottleKind: 0
rewardDushMaterialKind: 0
addToTaskPool: 1
@@ -41,7 +41,7 @@ MonoBehaviour:
refreshFrequency: 1
targetValue: 99
rewardType: 0
rewardAmount: 1
rewardAmount: 120
rewardExpBottleKind: 0
rewardDushMaterialKind: 0
addToTaskPool: 1
@@ -52,7 +52,7 @@ MonoBehaviour:
refreshFrequency: 1
targetValue: 1000000
rewardType: 0
rewardAmount: 1
rewardAmount: 120
rewardExpBottleKind: 0
rewardDushMaterialKind: 0
addToTaskPool: 1
@@ -63,7 +63,7 @@ MonoBehaviour:
refreshFrequency: 1
targetValue: 10000000
rewardType: 0
rewardAmount: 1
rewardAmount: 300
rewardExpBottleKind: 0
rewardDushMaterialKind: 0
addToTaskPool: 1
@@ -74,7 +74,7 @@ MonoBehaviour:
refreshFrequency: 1
targetValue: 1
rewardType: 0
rewardAmount: 1
rewardAmount: 50
rewardExpBottleKind: 0
rewardDushMaterialKind: 0
addToTaskPool: 1
@@ -86,7 +86,7 @@ MonoBehaviour:
refreshFrequency: 1
targetValue: 1
rewardType: 0
rewardAmount: 1
rewardAmount: 100
rewardExpBottleKind: 0
rewardDushMaterialKind: 0
addToTaskPool: 1
@@ -97,18 +97,18 @@ MonoBehaviour:
refreshFrequency: 1
targetValue: 900
rewardType: 0
rewardAmount: 1
rewardAmount: 100
rewardExpBottleKind: 0
rewardDushMaterialKind: 0
addToTaskPool: 1
selectionWeight: 1
- taskID: 10109
description: "\u6D88\u8D39 2000 coins"
description: "\u6D88\u8D39 2000 \u7B97\u529B"
taskType: 7
refreshFrequency: 1
targetValue: 2000
rewardType: 0
rewardAmount: 1
rewardAmount: 200
rewardExpBottleKind: 0
rewardDushMaterialKind: 0
addToTaskPool: 1
@@ -119,7 +119,7 @@ MonoBehaviour:
refreshFrequency: 1
targetValue: 3
rewardType: 0
rewardAmount: 1
rewardAmount: 180
rewardExpBottleKind: 0
rewardDushMaterialKind: 0
addToTaskPool: 1
@@ -1289,7 +1289,7 @@ public class GameManager : MonoBehaviour
yield return null;
}
}
gNotice.recommendation.display(LocalizationService.Get("gameplay.notice.game_start", "游戏开始!"));
// gNotice.recommendation.display(LocalizationService.Get("gameplay.notice.game_start", "游戏开始!"));
// Resume using PauseManager
PauseManager.Instance?.Pause(false);
@@ -1349,7 +1349,7 @@ public class GameManager : MonoBehaviour
yield return null;
}
}
gNotice.recommendation.display(LocalizationService.Get("gameplay.notice.game_start", "游戏开始!"));
// gNotice.recommendation.display(LocalizationService.Get("gameplay.notice.game_start", "游戏开始!"));
// Resume
PauseManager.Instance?.Pause(false);
@@ -1441,7 +1441,7 @@ public class GameManager : MonoBehaviour
yield return null;
}
}
gNotice.recommendation.display(LocalizationService.Get("gameplay.notice.game_start", "游戏开始!"));
// gNotice.recommendation.display(LocalizationService.Get("gameplay.notice.game_start", "游戏开始!"));
// Resume
PauseManager.Instance?.Pause(false);
@@ -1,15 +1,25 @@
using System.Collections.Generic;
using UnityEngine;
#if ENABLE_INPUT_SYSTEM
using UnityEngine.InputSystem;
using UnityEngine.InputSystem.EnhancedTouch;
using ETouch = UnityEngine.InputSystem.EnhancedTouch.Touch;
#endif
/// <summary>
/// 手游触屏输入分发器:每帧遍历所有触摸点(及编辑器鼠标),用 Physics2D.OverlapPoint
/// 命中带 TrackTouchZone 的透明 Collider2D,把按下/抬起转发给 InputManager。
///
/// 为什么用这种方式而不是 UI 的 IPointerDownHandler
/// 【多点触控根因】项目 activeInputHandler = Both(同时启用新旧输入系统)。
/// 在这种配置下,旧版 UnityEngine.Input.touches 走的是兼容 shim,同时按多指时
/// 常常只上报一个触点 → 只能触发一个轨道。所以这里优先用新输入系统的
/// EnhancedTouchTouch.activeTouches),它能可靠上报所有并发手指;
/// 仅在没有新输入系统时回退到旧版 Input.touches。判定逻辑完全不变。
///
/// 为什么用物理命中而不是 UI 的 IPointerDownHandler
/// - 轨道会平移+旋转晃动,点击区必须跟随晃动的 ColliderOverlapPoint 支持旋转过的碰撞体)。
/// - 需要多点触控:每根手指独立按住各自的轨道(靠 fingerId 配对按下/抬起)。
/// - 需要多点触控:每根手指独立按住各自的轨道(靠 touchId 配对按下/抬起)。
///
/// 判定逻辑完全不变:这里只调用现成的 PressTrack/ReleaseTrack
/// 判定时间戳仍由 Note.HandlePress 读取 GameplayClock.NowSongTimedspTime)。
///
/// 用法:场景里放一个空物体挂本脚本,把主相机拖到 gameplayCamera(留空则自动取 Camera.main)。
@@ -35,10 +45,8 @@ public class TrackTouchInput : MonoBehaviour
"留空自动按相机 orthographic 判断;一般不用手动改。")]
[SerializeField] private bool forceOrthographicMode = false;
// fingerId -> 当前按住的轨道索引。用于在抬起/取消时精确释放对应轨道。
// 触点 id -> 当前按住的轨道索引。用于在抬起/取消时精确释放对应轨道。
private readonly Dictionary<int, int> _activeTouches = new Dictionary<int, int>();
// 鼠标模拟用的"手指 id",取一个不会和真实 fingerId 冲突的值。
private const int MouseFingerId = -100;
private int _mouseHeldTrack = -1;
private Camera Cam
@@ -50,14 +58,13 @@ public class TrackTouchInput : MonoBehaviour
}
}
private void Awake()
#if ENABLE_INPUT_SYSTEM
private void OnEnable()
{
// 【多点触控根因修复】若 multiTouchEnabled 为 falseInput.touchCount 永远 ≤1
// 同时按多条轨道时只会报告一个触点 → 只能触发一个轨道。强制开启多点触控。
// simulateMouseWithTouches 关掉:避免触摸被合成成鼠标事件,与真实多指冲突。
Input.multiTouchEnabled = true;
Input.simulateMouseWithTouches = false;
// 开启增强触摸,Touch.activeTouches 才会被填充。
EnhancedTouchSupport.Enable();
}
#endif
private void Update()
{
@@ -67,50 +74,101 @@ public class TrackTouchInput : MonoBehaviour
Camera cam = Cam;
if (cam == null) return;
ProcessTouches(im, cam);
int touchCount = ProcessTouches(im, cam);
if (enableMouseFallback && Input.touchCount == 0)
if (enableMouseFallback && touchCount == 0)
{
ProcessMouse(im, cam);
}
}
private void ProcessTouches(InputManager im, Camera cam)
/// <summary>处理所有并发触点,返回本帧触点数量。</summary>
private int ProcessTouches(InputManager im, Camera cam)
{
for (int i = 0; i < Input.touchCount; i++)
#if ENABLE_INPUT_SYSTEM
// 优先走新输入系统的增强触摸:可靠上报所有并发手指。
var touches = ETouch.activeTouches;
int count = touches.Count;
for (int i = 0; i < count; i++)
{
ETouch t = touches[i];
switch (t.phase)
{
case UnityEngine.InputSystem.TouchPhase.Began:
HandleTouchBegan(im, cam, t.touchId, t.screenPosition);
break;
case UnityEngine.InputSystem.TouchPhase.Ended:
case UnityEngine.InputSystem.TouchPhase.Canceled:
HandleTouchEnded(im, t.touchId);
break;
// Moved / Stationary:手指在轨道内保持按住即可,不重新判定归属。
}
}
return count;
#else
// 回退:旧版 Input.touches(仅在未启用新输入系统时使用)。
int count = Input.touchCount;
for (int i = 0; i < count; i++)
{
Touch t = Input.GetTouch(i);
switch (t.phase)
{
case TouchPhase.Began:
{
int track = ResolveTrack(cam, t.position);
if (track >= 0)
{
_activeTouches[t.fingerId] = track;
im.PressTrack(track);
}
HandleTouchBegan(im, cam, t.fingerId, t.position);
break;
}
case TouchPhase.Ended:
case TouchPhase.Canceled:
{
if (_activeTouches.TryGetValue(t.fingerId, out int track))
{
_activeTouches.Remove(t.fingerId);
im.ReleaseTrack(track);
}
HandleTouchEnded(im, t.fingerId);
break;
}
// Moved / Stationary:手指在轨道内保持按住即可,不重新判定归属,
// 避免手指轻微滑动跨到相邻轨道时反复 Press/Release 造成误判。
}
}
return count;
#endif
}
private void HandleTouchBegan(InputManager im, Camera cam, int touchId, Vector2 screenPos)
{
int track = ResolveTrack(cam, screenPos);
if (track >= 0)
{
_activeTouches[touchId] = track;
im.PressTrack(track);
}
}
private void HandleTouchEnded(InputManager im, int touchId)
{
if (_activeTouches.TryGetValue(touchId, out int track))
{
_activeTouches.Remove(touchId);
im.ReleaseTrack(track);
}
}
private void ProcessMouse(InputManager im, Camera cam)
{
#if ENABLE_INPUT_SYSTEM
var mouse = Mouse.current;
if (mouse == null) return;
if (mouse.leftButton.wasPressedThisFrame)
{
int track = ResolveTrack(cam, mouse.position.ReadValue());
if (track >= 0)
{
_mouseHeldTrack = track;
im.PressTrack(track);
}
}
else if (mouse.leftButton.wasReleasedThisFrame)
{
if (_mouseHeldTrack >= 0)
{
im.ReleaseTrack(_mouseHeldTrack);
_mouseHeldTrack = -1;
}
}
#else
if (Input.GetMouseButtonDown(0))
{
int track = ResolveTrack(cam, Input.mousePosition);
@@ -128,6 +186,7 @@ public class TrackTouchInput : MonoBehaviour
_mouseHeldTrack = -1;
}
}
#endif
}
/// <summary>
@@ -135,8 +194,7 @@ public class TrackTouchInput : MonoBehaviour
///
/// 相机移动/晃动/推拉时,每帧都用当前相机状态转换,所以命中自动跟随相机。
/// - 正交相机:ScreenToWorldPoint 忽略深度,直接取 x/y。
/// - 透视相机:从相机发射线,求与轨道平面(Z = trackPlaneWorldZ)的交点
/// 避免因相机 Z 变化(如 cameraDash)导致命中点偏移。
/// - 透视相机:从相机发射线,求与轨道平面(Z = trackPlaneWorldZ)的交点
/// </summary>
private int ResolveTrack(Camera cam, Vector2 screenPos)
{
@@ -149,7 +207,6 @@ public class TrackTouchInput : MonoBehaviour
}
else
{
// 透视:射线与轨道平面(法线 +Z,过 z = trackPlaneWorldZ)求交。
Ray ray = cam.ScreenPointToRay(screenPos);
float denom = ray.direction.z;
if (Mathf.Abs(denom) < 1e-6f)
@@ -187,5 +244,9 @@ public class TrackTouchInput : MonoBehaviour
}
_activeTouches.Clear();
_mouseHeldTrack = -1;
#if ENABLE_INPUT_SYSTEM
EnhancedTouchSupport.Disable();
#endif
}
}
@@ -201,30 +201,51 @@ namespace GameServer.Client
public async Task<ArenaRoomSnapshot> CreateRoom(string songId, string songName, string difficulty, string password = null)
{
await EnsureConnected();
JObject resp = await SendAction("create_room", new
try
{
song_id = songId,
song_name = songName,
difficulty = difficulty,
password = string.IsNullOrWhiteSpace(password) ? null : password
});
ArenaRoomSnapshot snapshot = NormalizeSnapshotFromActionData("create_room", resp);
SetCurrentRoom(snapshot);
return snapshot;
await EnsureConnected();
JObject resp = await SendAction("create_room", new
{
song_id = songId,
song_name = songName,
difficulty = difficulty,
password = string.IsNullOrWhiteSpace(password) ? null : password
});
ArenaRoomSnapshot snapshot = NormalizeSnapshotFromActionData("create_room", resp);
SetCurrentRoom(snapshot);
return snapshot;
}
catch (Exception ex)
{
NetworkManager.ShowUserFacingNetworkError(ex.Message);
throw;
}
}
public async Task<ArenaRoomSnapshot> JoinRoom(string roomCode, string password = null)
{
await EnsureConnected();
JObject resp = await SendAction("join_room", new
try
{
room_code = roomCode,
password = string.IsNullOrWhiteSpace(password) ? null : password
});
ArenaRoomSnapshot snapshot = NormalizeSnapshotFromActionData("join_room", resp);
SetCurrentRoom(snapshot);
return snapshot;
if (string.IsNullOrWhiteSpace(roomCode))
{
throw new Exception("MISSING_ROOM_CODE: room_code is empty");
}
await EnsureConnected();
JObject resp = await SendAction("join_room", new
{
room_code = roomCode.Trim(),
password = string.IsNullOrWhiteSpace(password) ? null : password
});
ArenaRoomSnapshot snapshot = NormalizeSnapshotFromActionData("join_room", resp);
SetCurrentRoom(snapshot);
return snapshot;
}
catch (Exception ex)
{
NetworkManager.ShowUserFacingNetworkError(ex.Message);
throw;
}
}
public async Task<bool> LeaveRoom()
@@ -385,6 +406,31 @@ namespace GameServer.Client
return snapshot;
}
public async Task<ArenaRoomSnapshot> ChangeRoomSong(string songId, string songName, string difficulty)
{
try
{
await EnsureConnected();
JObject resp = await SendAction("change_room_song", new
{
song_id = songId,
song_name = songName,
difficulty = difficulty
});
ArenaRoomSnapshot snapshot = NormalizeSnapshotFromActionData("change_room_song", resp);
if (snapshot != null)
{
SetCurrentRoom(snapshot);
}
return snapshot;
}
catch (Exception ex)
{
NetworkManager.ShowUserFacingNetworkError(ex.Message);
throw;
}
}
public async Task<ArenaRoomSnapshot> StartGame()
{
await EnsureConnected();
@@ -804,11 +850,11 @@ namespace GameServer.Client
NetworkManager network = NetworkManager.Instance;
if (network == null)
{
throw new Exception("NetworkManager is not initialized");
throw new Exception("NETWORK_NOT_READY: NetworkManager is not initialized");
}
if (string.IsNullOrWhiteSpace(network.SteamId))
{
throw new Exception("steam_id is empty");
throw new Exception("MISSING_STEAM_ID: steam_id is empty");
}
_isConnecting = true;
@@ -864,11 +910,11 @@ namespace GameServer.Client
NetworkManager network = NetworkManager.Instance;
if (network == null)
{
throw new Exception("NetworkManager is not initialized");
throw new Exception("NETWORK_NOT_READY: NetworkManager is not initialized");
}
if (string.IsNullOrWhiteSpace(network.SteamId))
{
throw new Exception("steam_id is empty");
throw new Exception("MISSING_STEAM_ID: steam_id is empty");
}
_isSocialConnecting = true;
@@ -1213,6 +1259,15 @@ namespace GameServer.Client
}
return;
}
case "room_song_changed":
{
ArenaRoomSnapshot snapshot = ReadRoomSnapshot(message);
if (snapshot != null)
{
Enqueue(() => SetCurrentRoom(snapshot));
}
return;
}
case "room_dismissed":
{
string roomCode = ReadString(message, "room_code");
@@ -1734,13 +1789,19 @@ namespace GameServer.Client
{
_shutdownLeaveAttempted = false;
string previousRoomCode = CurrentRoom != null ? CurrentRoom.room_code : null;
string previousSongId = CurrentRoom != null ? CurrentRoom.song_id : null;
string previousDifficulty = CurrentRoom != null ? CurrentRoom.difficulty : null;
if (snapshot == null || !IsRoomStartedState(snapshot.status))
{
_isLaunchingArenaGameplay = false;
}
if (snapshot == null || (!string.IsNullOrWhiteSpace(previousRoomCode)
&& !string.Equals(previousRoomCode, snapshot.room_code, StringComparison.Ordinal)))
&& !string.Equals(previousRoomCode, snapshot.room_code, StringComparison.Ordinal))
|| (snapshot != null
&& string.Equals(previousRoomCode, snapshot.room_code, StringComparison.Ordinal)
&& (!string.Equals(previousSongId, snapshot.song_id, StringComparison.Ordinal)
|| !string.Equals(previousDifficulty, snapshot.difficulty, StringComparison.OrdinalIgnoreCase))))
{
ClearRoomRankingScores();
}
@@ -2353,7 +2414,7 @@ namespace GameServer.Client
{
string finalCode = code ?? "ERROR";
string finalMessage = message ?? "Unknown arena error";
gNotice.error.display($"{finalCode}: {finalMessage}");
gNotice.error.display(NetworkManager.DescribeUserFacingNetworkError($"{finalCode}: {finalMessage}"));
OnErrorReceived?.Invoke(finalCode, finalMessage);
});
}
@@ -3890,4 +3951,3 @@ namespace GameServer.Client
}
}
}
@@ -337,6 +337,7 @@ namespace GameServer.Client
[JsonProperty("receiver_id")] public string receiver_id;
[JsonProperty("uid")] public int uid;
[JsonProperty("display_name")] public string display_name;
[JsonProperty("player_title")] public string player_title;
[JsonProperty("avatar_url")] public string avatar_url;
[JsonProperty("content")] public string content;
[JsonProperty("created_at")] public string created_at;
@@ -21,6 +21,8 @@ public class NetworkManager : MonoBehaviour
public static NetworkManager Instance { get; private set; }
private static string _startupSteamId = string.Empty;
private static string _startupSteamDisplayName = string.Empty;
private static string _lastUserFacingNetworkError = string.Empty;
private static float _lastUserFacingNetworkErrorAt;
private static readonly Dictionary<string, CachedRemoteIdentity> _remoteIdentityCache =
new Dictionary<string, CachedRemoteIdentity>(StringComparer.Ordinal);
private static readonly Dictionary<string, Task<ProfileData>> _remoteProfileTasks =
@@ -99,6 +101,8 @@ public class NetworkManager : MonoBehaviour
Instance = null;
_startupSteamId = string.Empty;
_startupSteamDisplayName = string.Empty;
_lastUserFacingNetworkError = string.Empty;
_lastUserFacingNetworkErrorAt = 0f;
_remoteIdentityCache.Clear();
_remoteProfileTasks.Clear();
}
@@ -627,6 +631,7 @@ public class NetworkManager : MonoBehaviour
{
if (OnlineModeSettings.IsLocalOnlyMode)
{
NotifyNetworkError("LOCAL_ONLY_MODE");
return new ArenaCreateResponse
{
success = false,
@@ -635,6 +640,7 @@ public class NetworkManager : MonoBehaviour
};
}
EnsureSteamIdAvailableForOnlineFeature();
var req = new ArenaCreateRequest
{
steam_id = SteamId,
@@ -643,6 +649,13 @@ public class NetworkManager : MonoBehaviour
password = password
};
ArenaCreateResponse resp = await PostJson<ArenaCreateResponse>(BuildApiUrl("/api/arena/create"), req, CancellationToken.None);
if (resp == null || !resp.success)
{
string error = BuildErrorText(resp?.error_code, resp?.message, "CREATE_ROOM_FAILED");
NotifyNetworkError(error);
throw new Exception(error);
}
OnArenaCreated?.Invoke(resp);
return resp;
}
@@ -651,6 +664,7 @@ public class NetworkManager : MonoBehaviour
{
if (OnlineModeSettings.IsLocalOnlyMode)
{
NotifyNetworkError("LOCAL_ONLY_MODE");
return new ArenaJoinResponse
{
success = false,
@@ -660,6 +674,7 @@ public class NetworkManager : MonoBehaviour
};
}
EnsureSteamIdAvailableForOnlineFeature();
var req = new ArenaJoinRequest
{
steam_id = SteamId,
@@ -667,6 +682,13 @@ public class NetworkManager : MonoBehaviour
password = password
};
ArenaJoinResponse resp = await PostJson<ArenaJoinResponse>(BuildApiUrl("/api/arena/join"), req, CancellationToken.None);
if (resp == null || !resp.success)
{
string error = BuildErrorText(resp?.error_code, resp?.message, "JOIN_ROOM_FAILED");
NotifyNetworkError(error);
throw new Exception(error);
}
OnArenaJoined?.Invoke(resp);
return resp;
}
@@ -1682,6 +1704,15 @@ public class NetworkManager : MonoBehaviour
return new InvalidOperationException(message);
}
private void EnsureSteamIdAvailableForOnlineFeature()
{
RefreshSteamIdentity(true);
if (string.IsNullOrWhiteSpace(SteamId))
{
throw new InvalidOperationException("MISSING_STEAM_ID: steam_id is empty");
}
}
private ProfileData BuildLocalOnlyProfile(string targetSteamId)
{
string normalizedSteamId = string.IsNullOrWhiteSpace(targetSteamId) ? string.Empty : targetSteamId.Trim();
@@ -1850,11 +1881,135 @@ public class NetworkManager : MonoBehaviour
private static void NotifyNetworkError(string message)
{
if (!string.IsNullOrWhiteSpace(message))
ShowUserFacingNetworkError(message);
}
public static void ShowUserFacingNetworkError(string message)
{
string userMessage = DescribeUserFacingNetworkError(message);
if (!string.IsNullOrWhiteSpace(userMessage))
{
gNotice.error.display(message);
float now = Time.realtimeSinceStartup;
if (string.Equals(_lastUserFacingNetworkError, userMessage, StringComparison.Ordinal)
&& now - _lastUserFacingNetworkErrorAt < 0.75f)
{
return;
}
_lastUserFacingNetworkError = userMessage;
_lastUserFacingNetworkErrorAt = now;
gNotice.error.display(userMessage);
}
}
public static string DescribeUserFacingNetworkError(string message)
{
if (string.IsNullOrWhiteSpace(message))
{
return "网络错误,请稍后重试";
}
string raw = message.Trim();
string upper = raw.ToUpperInvariant();
if (upper.Contains("LOCAL_ONLY_MODE") || raw.IndexOf("local-only mode", StringComparison.OrdinalIgnoreCase) >= 0)
{
return "当前为离线模式,无法使用联网功能";
}
if (upper.Contains("MISSING_STEAM_ID")
|| upper.Contains("STEAM_ID IS EMPTY")
|| upper.Contains("TARGETSTEAMID IS EMPTY")
|| upper.Contains("STEAMID IS EMPTY")
|| upper.Contains("STEAM ID IS EMPTY"))
{
return "无法得到 SteamID,请确认已启动 Steam、登录账号并保持在线";
}
if (upper.Contains("NETWORKMANAGER IS NOT INITIALIZED") || upper.Contains("NETWORK_NOT_READY"))
{
return "网络模块尚未初始化,请稍后重试";
}
if (upper.Contains("ROOM_NOT_FOUND")
|| raw.IndexOf("room not found", StringComparison.OrdinalIgnoreCase) >= 0
|| raw.IndexOf("房间不存在", StringComparison.OrdinalIgnoreCase) >= 0)
{
return "房间不存在或已解散";
}
if (upper.Contains("ALREADY_IN_ROOM"))
{
return "你已经在房间中";
}
if (upper.Contains("MISSING_ROOM_CODE") || upper.Contains("ROOM_CODE IS EMPTY"))
{
return "房间号不能为空";
}
if (upper.Contains("ROOM_FULL"))
{
return "房间已满";
}
if (upper.Contains("ROOM_STARTED") || upper.Contains("GAME_STARTED") || upper.Contains("ALREADY_STARTED"))
{
return "房间已开始游戏,无法加入";
}
if (upper.Contains("WRONG_PASSWORD") || upper.Contains("INVALID_PASSWORD") || upper.Contains("PASSWORD"))
{
return "房间密码错误";
}
if (upper.Contains("NOT_IN_ROOM"))
{
return "当前不在房间中";
}
if (upper.Contains("TIMED OUT")
|| upper.Contains("TIMEOUT")
|| upper.Contains("CONNECTFAILURE")
|| upper.Contains("CONNECTIONERROR")
|| upper.Contains("CONNECTION FAILED")
|| upper.Contains("CANNOT CONNECT")
|| upper.Contains("COULD NOT RESOLVE")
|| upper.Contains("RESOLVE DESTINATION HOST")
|| upper.Contains("NAMERESOLUTION")
|| upper.Contains("HOST UNREACHABLE")
|| upper.Contains("CONNECTION REFUSED")
|| upper.Contains("CONNECTION RESET")
|| upper.Contains("SOCKET")
|| raw.IndexOf("unable to connect", StringComparison.OrdinalIgnoreCase) >= 0
|| raw.IndexOf("failed to connect", StringComparison.OrdinalIgnoreCase) >= 0)
{
return "未联网或服务器连接失败,请检查网络后重试";
}
if (upper.Contains("HTTP") && (upper.Contains(" 404") || upper.Contains("404:")))
{
return "请求的服务不存在,请稍后重试";
}
if (upper.Contains("HTTP") && (upper.Contains(" 500") || upper.Contains("500:")))
{
return "服务器暂时无法处理请求,请稍后重试";
}
if (raw.Length > 80)
{
return "网络错误,请稍后重试";
}
return raw;
}
private static string BuildErrorText(string code, string message, string fallbackCode)
{
string safeCode = string.IsNullOrWhiteSpace(code) ? fallbackCode : code.Trim();
string safeMessage = string.IsNullOrWhiteSpace(message) ? safeCode : message.Trim();
return $"{safeCode}: {safeMessage}";
}
}
}
@@ -403,10 +403,10 @@ MonoBehaviour:
rkPrefab: {fileID: 4439332187331332072, guid: 5d56483f68516d94e9277bbac63f8525, type: 3}
rkParent: {fileID: 4242620377161803188}
btmSprites:
- {fileID: 21300000, guid: 040be0e474e09894ca320b08d9462cc8, type: 3}
- {fileID: 21300000, guid: 60cfd84ae231a4943b3f0f6cf0dcf409, type: 3}
- {fileID: 21300000, guid: 1258ac417dd158a48b05eeedb739c469, type: 3}
- {fileID: 21300000, guid: 28c51f93b6260224c867f45f411a0e85, type: 3}
- {fileID: 21300000, guid: 9e6a75c290497c4469b9b86d5eab72a1, type: 3}
- {fileID: 21300000, guid: 9e6a75c290497c4469b9b86d5eab72a1, type: 3}
- {fileID: 21300000, guid: 9e6a75c290497c4469b9b86d5eab72a1, type: 3}
- {fileID: 21300000, guid: 7f3e2cfd199d3a844b15751f1eddf15b, type: 3}
--- !u!1 &2889696334540093026
GameObject:
m_ObjectHideFlags: 0
@@ -140,7 +140,7 @@ MonoBehaviour:
m_OnCullStateChanged:
m_PersistentCalls:
m_Calls: []
m_Sprite: {fileID: 21300000, guid: 28c51f93b6260224c867f45f411a0e85, type: 3}
m_Sprite: {fileID: 21300000, guid: 9e6a75c290497c4469b9b86d5eab72a1, type: 3}
m_Type: 1
m_PreserveAspect: 0
m_FillCenter: 1
@@ -208,7 +208,7 @@ MonoBehaviour:
m_Name:
m_EditorClassIdentifier:
m_Material: {fileID: 0}
m_Color: {r: 1, g: 1, b: 1, a: 1}
m_Color: {r: 0, g: 0, b: 0, a: 1}
m_RaycastTarget: 1
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
m_Maskable: 1
@@ -287,7 +287,7 @@ MonoBehaviour:
m_Name:
m_EditorClassIdentifier:
m_Material: {fileID: 0}
m_Color: {r: 1, g: 1, b: 1, a: 1}
m_Color: {r: 0, g: 0, b: 0, a: 1}
m_RaycastTarget: 1
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
m_Maskable: 1
@@ -422,7 +422,7 @@ MonoBehaviour:
m_Name:
m_EditorClassIdentifier:
m_Material: {fileID: 0}
m_Color: {r: 1, g: 1, b: 1, a: 1}
m_Color: {r: 0, g: 0, b: 0, a: 1}
m_RaycastTarget: 1
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
m_Maskable: 1
@@ -501,7 +501,7 @@ MonoBehaviour:
m_Name:
m_EditorClassIdentifier:
m_Material: {fileID: 0}
m_Color: {r: 1, g: 1, b: 1, a: 1}
m_Color: {r: 0, g: 0, b: 0, a: 1}
m_RaycastTarget: 1
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
m_Maskable: 1
@@ -589,7 +589,7 @@ MonoBehaviour:
m_OnCullStateChanged:
m_PersistentCalls:
m_Calls: []
m_Sprite: {fileID: 21300000, guid: 28c51f93b6260224c867f45f411a0e85, type: 3}
m_Sprite: {fileID: 21300000, guid: 9e6a75c290497c4469b9b86d5eab72a1, type: 3}
m_Type: 1
m_PreserveAspect: 0
m_FillCenter: 1
@@ -657,7 +657,7 @@ MonoBehaviour:
m_Name:
m_EditorClassIdentifier:
m_Material: {fileID: 0}
m_Color: {r: 1, g: 1, b: 1, a: 1}
m_Color: {r: 0, g: 0, b: 0, a: 1}
m_RaycastTarget: 1
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
m_Maskable: 1
@@ -58,7 +58,7 @@ public class rewardPrefab : MonoBehaviour
switch (rewardType)
{
case RewardVisualType.Coins:
return "金币";
return "算力";
case RewardVisualType.Material:
return "记忆碎片";
case RewardVisualType.PlayerExp:
@@ -1153,11 +1153,11 @@ public class teamUIController : MonoBehaviour
*/
// Load ally slot IDs from PlayerPrefs
allySlotIds[0] = PlayerPrefs.GetInt("selected_heroSlot01_heroID", 0);
allySlotIds[1] = PlayerPrefs.GetInt("selected_heroSlot02_heroID", 0);
allySlotIds[2] = PlayerPrefs.GetInt("selected_heroSlot03_heroID", 0);
allySlotIds[3] = PlayerPrefs.GetInt("selected_heroSlot04_heroID", 0);
allySlotIds[4] = PlayerPrefs.GetInt("selected_heroSlot05_heroID", 0);
allySlotIds[0] = TeamSelectionDefaults.ReadHeroId(1);
allySlotIds[1] = TeamSelectionDefaults.ReadHeroId(2);
allySlotIds[2] = TeamSelectionDefaults.ReadHeroId(3);
allySlotIds[3] = TeamSelectionDefaults.ReadHeroId(4);
allySlotIds[4] = TeamSelectionDefaults.ReadHeroId(5);
PopulateAllySOsFromIds();
+36 -36
View File
@@ -36,8 +36,8 @@ RectTransform:
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0.5, y: 0.5}
m_AnchorMax: {x: 0.5, y: 0.5}
m_AnchoredPosition: {x: 0, y: -51.2}
m_SizeDelta: {x: 200, y: 32.156}
m_AnchoredPosition: {x: 0, y: -49.6}
m_SizeDelta: {x: 146, y: 56}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!222 &5597810916468765611
CanvasRenderer:
@@ -67,8 +67,8 @@ MonoBehaviour:
m_OnCullStateChanged:
m_PersistentCalls:
m_Calls: []
m_Sprite: {fileID: 7621278097234006379, guid: 59df2390a598c904bab275d1ea743916, type: 3}
m_Type: 1
m_Sprite: {fileID: -2689630615245801869, guid: 2a46b5f36d286d447bd74d1db9a72cd7, type: 3}
m_Type: 0
m_PreserveAspect: 0
m_FillCenter: 1
m_FillMethod: 4
@@ -157,8 +157,8 @@ RectTransform:
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0.5, y: 0.5}
m_AnchorMax: {x: 0.5, y: 0.5}
m_AnchoredPosition: {x: 0, y: -9.196}
m_SizeDelta: {x: 200, y: 110.402}
m_AnchoredPosition: {x: 0, y: -49.6}
m_SizeDelta: {x: 146, y: 56}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!222 &5591616715459838773
CanvasRenderer:
@@ -188,8 +188,8 @@ MonoBehaviour:
m_OnCullStateChanged:
m_PersistentCalls:
m_Calls: []
m_Sprite: {fileID: 7919194353434859991, guid: b35fcaa46fa9f3e49b71c103f69df698, type: 3}
m_Type: 1
m_Sprite: {fileID: -566881566719626471, guid: 56ba1aaa875c6b7489f77555fd212d3e, type: 3}
m_Type: 0
m_PreserveAspect: 0
m_FillCenter: 1
m_FillMethod: 4
@@ -355,8 +355,8 @@ RectTransform:
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 1, y: 1}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 0, y: 0}
m_AnchoredPosition: {x: 0, y: 1.4682007}
m_SizeDelta: {x: 0, y: -2.9365}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!222 &3720672462226780915
CanvasRenderer:
@@ -379,7 +379,7 @@ MonoBehaviour:
m_Name:
m_EditorClassIdentifier:
m_Material: {fileID: 0}
m_Color: {r: 0.19607843, g: 0.19607843, b: 0.19607843, a: 1}
m_Color: {r: 0.63529414, g: 0.3529412, b: 0.16862746, a: 1}
m_RaycastTarget: 1
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
m_Maskable: 1
@@ -387,7 +387,7 @@ MonoBehaviour:
m_PersistentCalls:
m_Calls: []
m_FontData:
m_Font: {fileID: 12800000, guid: bd4be44636ad6c24a9b117a7108a75e9, type: 3}
m_Font: {fileID: 12800000, guid: d254d20d93651ae448ff13d10dff30ab, type: 3}
m_FontSize: 24
m_FontStyle: 0
m_BestFit: 0
@@ -456,7 +456,7 @@ MonoBehaviour:
m_Top: 0
m_Bottom: 0
m_ChildAlignment: 4
m_Spacing: 0
m_Spacing: -100
m_ChildForceExpandWidth: 1
m_ChildForceExpandHeight: 1
m_ChildControlWidth: 0
@@ -500,8 +500,8 @@ RectTransform:
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0.5, y: 0.5}
m_AnchorMax: {x: 0.5, y: 0.5}
m_AnchoredPosition: {x: 0, y: -171.99}
m_SizeDelta: {x: 300, y: 40}
m_AnchoredPosition: {x: 0, y: -171.1}
m_SizeDelta: {x: 194.5543, y: 52.3284}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!222 &6903183453437413057
CanvasRenderer:
@@ -531,8 +531,8 @@ MonoBehaviour:
m_OnCullStateChanged:
m_PersistentCalls:
m_Calls: []
m_Sprite: {fileID: 2561534620446350566, guid: 23a48ca1e983a4c47971fcf7eab26b89, type: 3}
m_Type: 1
m_Sprite: {fileID: -6405685121925378258, guid: 92151af6550eb214aa4daecf4ea48edb, type: 3}
m_Type: 0
m_PreserveAspect: 0
m_FillCenter: 1
m_FillMethod: 4
@@ -622,7 +622,7 @@ RectTransform:
m_AnchorMin: {x: 0.5, y: 0.5}
m_AnchorMax: {x: 0.5, y: 0.5}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 555, y: 264.2}
m_SizeDelta: {x: 450, y: 230}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!222 &7601750316680178786
CanvasRenderer:
@@ -652,7 +652,7 @@ MonoBehaviour:
m_OnCullStateChanged:
m_PersistentCalls:
m_Calls: []
m_Sprite: {fileID: 21300000, guid: 7f3b1220317607e44bbb8f0e1998044a, type: 3}
m_Sprite: {fileID: 21300000, guid: a31ac6012ab10b146878694e7d720b11, type: 3}
m_Type: 1
m_PreserveAspect: 0
m_FillCenter: 1
@@ -698,8 +698,8 @@ RectTransform:
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0.5, y: 0.5}
m_AnchorMax: {x: 0.5, y: 0.5}
m_AnchoredPosition: {x: 0, y: 16}
m_SizeDelta: {x: 200, y: 60}
m_AnchoredPosition: {x: -100.424805, y: 16}
m_SizeDelta: {x: 346.8495, y: 60}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!222 &67614290717671043
CanvasRenderer:
@@ -729,8 +729,8 @@ MonoBehaviour:
m_OnCullStateChanged:
m_PersistentCalls:
m_Calls: []
m_Sprite: {fileID: 4717104774431673990, guid: 2204c5120d209e24b8d8473101c0670f, type: 3}
m_Type: 1
m_Sprite: {fileID: 21300000, guid: 7edca03691f782d46ac361a681ce5685, type: 3}
m_Type: 0
m_PreserveAspect: 0
m_FillCenter: 1
m_FillMethod: 4
@@ -874,8 +874,8 @@ RectTransform:
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 1, y: 1}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 0, y: 0}
m_AnchoredPosition: {x: -2.8545, y: 2.3508}
m_SizeDelta: {x: -5.7091, y: -4.7016}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!222 &316840073610798188
CanvasRenderer:
@@ -898,7 +898,7 @@ MonoBehaviour:
m_Name:
m_EditorClassIdentifier:
m_Material: {fileID: 0}
m_Color: {r: 1, g: 1, b: 1, a: 1}
m_Color: {r: 0.63529414, g: 0.3529412, b: 0.16862746, a: 1}
m_RaycastTarget: 1
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
m_Maskable: 1
@@ -906,8 +906,8 @@ MonoBehaviour:
m_PersistentCalls:
m_Calls: []
m_FontData:
m_Font: {fileID: 12800000, guid: bd4be44636ad6c24a9b117a7108a75e9, type: 3}
m_FontSize: 18
m_Font: {fileID: 12800000, guid: d254d20d93651ae448ff13d10dff30ab, type: 3}
m_FontSize: 24
m_FontStyle: 0
m_BestFit: 0
m_MinSize: 1
@@ -1104,8 +1104,8 @@ RectTransform:
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 1, y: 1}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 0, y: 0}
m_AnchoredPosition: {x: -2.947403, y: 2.0960999}
m_SizeDelta: {x: -5.8948, y: -4.1923}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!222 &2489646231650356367
CanvasRenderer:
@@ -1128,7 +1128,7 @@ MonoBehaviour:
m_Name:
m_EditorClassIdentifier:
m_Material: {fileID: 0}
m_Color: {r: 1, g: 1, b: 1, a: 1}
m_Color: {r: 0.09803922, g: 0.32941177, b: 0.6, a: 1}
m_RaycastTarget: 1
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
m_Maskable: 1
@@ -1136,8 +1136,8 @@ MonoBehaviour:
m_PersistentCalls:
m_Calls: []
m_FontData:
m_Font: {fileID: 12800000, guid: bd4be44636ad6c24a9b117a7108a75e9, type: 3}
m_FontSize: 20
m_Font: {fileID: 12800000, guid: d254d20d93651ae448ff13d10dff30ab, type: 3}
m_FontSize: 24
m_FontStyle: 0
m_BestFit: 0
m_MinSize: 1
@@ -1148,7 +1148,7 @@ MonoBehaviour:
m_HorizontalOverflow: 0
m_VerticalOverflow: 0
m_LineSpacing: 1
m_Text: "\u521B\u5EFA\u623F\u95F4\u5E76\u4F5C\u4E3A\u623F\u4E3B"
m_Text: "\u521B\u5EFA\u623F\u95F4"
--- !u!1 &4714805719805124595
GameObject:
m_ObjectHideFlags: 0
@@ -1373,8 +1373,8 @@ MonoBehaviour:
m_Calls: []
m_text: "\u200B"
m_isRightToLeft: 0
m_fontAsset: {fileID: 11400000, guid: 452fa97492cae71479cf143f29930751, type: 2}
m_sharedMaterial: {fileID: -346136068272202111, guid: 452fa97492cae71479cf143f29930751, type: 2}
m_fontAsset: {fileID: 11400000, guid: 7cad3f54ac7b53d4885f00c16f1a6929, type: 2}
m_sharedMaterial: {fileID: -346136068272202111, guid: 7cad3f54ac7b53d4885f00c16f1a6929, type: 2}
m_fontSharedMaterials: []
m_fontMaterial: {fileID: 0}
m_fontMaterials: []
@@ -942,7 +942,8 @@ public class globalChatSystem : MonoBehaviour
playerMessagePrefab controller = instance.GetComponentInChildren<playerMessagePrefab>();
if (controller != null)
{
controller.Bind(senderSteamId, avatarUrl, displayName, string.Empty, message.content, bubbleColor);
string title = !string.IsNullOrWhiteSpace(message.player_title) ? message.player_title : string.Empty;
controller.Bind(senderSteamId, avatarUrl, displayName, title, message.content, bubbleColor);
}
return instance;
@@ -38,7 +38,7 @@ RectTransform:
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 0, y: 0}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 323.75, y: 30}
m_SizeDelta: {x: 380.56, y: 0}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!114 &2228420748506963945
MonoBehaviour:
@@ -83,7 +83,7 @@ MonoBehaviour:
m_MinWidth: -1
m_MinHeight: -1
m_PreferredWidth: -1
m_PreferredHeight: 30
m_PreferredHeight: 0
m_FlexibleWidth: -1
m_FlexibleHeight: -1
m_LayoutPriority: 2
+2
View File
@@ -100,6 +100,7 @@ public class roomAsk : MonoBehaviour
catch (Exception ex)
{
Debug.LogWarning($"[roomAsk] Create room failed: {ex.Message}");
GameServer.Client.NetworkManager.ShowUserFacingNetworkError(ex.Message);
}
}
@@ -125,6 +126,7 @@ public class roomAsk : MonoBehaviour
catch (Exception ex)
{
Debug.LogWarning($"[roomAsk] Join room failed: {ex.Message}");
GameServer.Client.NetworkManager.ShowUserFacingNetworkError(ex.Message);
}
}
+443 -3
View File
@@ -38,6 +38,10 @@ public class roomDetails : MonoBehaviour
public Text thisSongName;
public Text thisSongDifficulty;
[Header("change songs")]
public Dropdown songDropdown;
public Dropdown difficultyDropdown;
[Header("objs")]
public GameObject roommatePrefab;
public Transform roommateParent;
@@ -82,6 +86,11 @@ public class roomDetails : MonoBehaviour
private readonly List<RectTransform> _pendingMessageLayoutDirtyRoots = new List<RectTransform>();
private Coroutine _refocusChatInputCoroutine;
private bool _refocusChatInputRequested;
private readonly List<SongData> _selectableSongs = new List<SongData>();
private readonly List<int> _selectableDifficulties = new List<int>();
private Coroutine _songLibraryWaitRoutine;
private bool _suppressSongDropdownEvents;
private bool _isChangingRoomSong;
private const float ChatSendCooldownSeconds = 3f;
private const int InitialHistoryLoadLimit = 20;
@@ -162,6 +171,18 @@ public class roomDetails : MonoBehaviour
sendMessageButton.onClick.AddListener(OnSendMessageClicked);
}
if (songDropdown != null)
{
songDropdown.onValueChanged.RemoveListener(OnSongDropdownChanged);
songDropdown.onValueChanged.AddListener(OnSongDropdownChanged);
}
if (difficultyDropdown != null)
{
difficultyDropdown.onValueChanged.RemoveListener(OnDifficultyDropdownChanged);
difficultyDropdown.onValueChanged.AddListener(OnDifficultyDropdownChanged);
}
if (playerMessageInputField != null)
{
playerMessageInputField.onEndEdit.RemoveListener(OnChatInputEndEdit);
@@ -185,6 +206,7 @@ public class roomDetails : MonoBehaviour
_ = InitializeRoomChatAsync();
Refresh(_service.CurrentRoom);
EnsureSongDropdownsReady();
}
private void OnDestroy()
@@ -225,6 +247,16 @@ public class roomDetails : MonoBehaviour
sendMessageButton.onClick.RemoveListener(OnSendMessageClicked);
}
if (songDropdown != null)
{
songDropdown.onValueChanged.RemoveListener(OnSongDropdownChanged);
}
if (difficultyDropdown != null)
{
difficultyDropdown.onValueChanged.RemoveListener(OnDifficultyDropdownChanged);
}
if (playerMessageInputField != null)
{
playerMessageInputField.onEndEdit.RemoveListener(OnChatInputEndEdit);
@@ -259,6 +291,12 @@ public class roomDetails : MonoBehaviour
_refocusChatInputCoroutine = null;
}
if (_songLibraryWaitRoutine != null)
{
StopCoroutine(_songLibraryWaitRoutine);
_songLibraryWaitRoutine = null;
}
RestoreDefaultChatPlaceholder();
if (_spawnedRoomRankingInstance != null)
@@ -396,6 +434,7 @@ public class roomDetails : MonoBehaviour
UpdateQuitButtonTexts(snapshot);
UpdateReadyButton(snapshot);
RenderParticipants(snapshot);
RefreshSongDropdowns(snapshot);
}
private IEnumerator UpdateRemainTime(ArenaRoomSnapshot snapshot)
@@ -760,7 +799,7 @@ public class roomDetails : MonoBehaviour
&& string.Equals(senderSteamId, localSteamId, StringComparison.Ordinal);
Color bubbleColor = ResolveMessageBubbleColor(isSelf);
string displayName = ResolveMessageDisplayName(message, isSelf);
string title = ResolveMessageTitle();
string title = ResolveMessageTitle(message);
GameObject instance = Instantiate(playerMessagePrefab, messageParent, false);
playerMessagePrefab controller = instance.GetComponent<playerMessagePrefab>();
@@ -1026,9 +1065,11 @@ public class roomDetails : MonoBehaviour
return GameServer.Client.NetworkManager.ResolveDisplayName(message != null ? message.display_name : string.Empty, messageSteamId, false);
}
private static string ResolveMessageTitle()
private static string ResolveMessageTitle(ArenaRoomChatMessage message)
{
return string.Empty;
return message != null && !string.IsNullOrWhiteSpace(message.player_title)
? message.player_title
: string.Empty;
}
private static string GetMessageSenderSteamId(ArenaRoomChatMessage message)
@@ -1387,6 +1428,405 @@ public class roomDetails : MonoBehaviour
|| string.Equals(snapshot.status, "PLAYING", StringComparison.OrdinalIgnoreCase);
}
private void EnsureSongDropdownsReady()
{
if (songDropdown == null && difficultyDropdown == null)
{
return;
}
SongDataLibrary library = SongDataLibrary.Instance;
if (library != null && library.IsLoaded)
{
RefreshSongDropdowns(_service != null ? _service.CurrentRoom : null);
return;
}
if (_songLibraryWaitRoutine == null)
{
_songLibraryWaitRoutine = StartCoroutine(WaitForSongLibraryAndRefresh());
}
}
private IEnumerator WaitForSongLibraryAndRefresh()
{
float timeoutAt = Time.realtimeSinceStartup + 8f;
while (Time.realtimeSinceStartup < timeoutAt)
{
SongDataLibrary library = SongDataLibrary.Instance;
if (library != null && library.IsLoaded)
{
_songLibraryWaitRoutine = null;
RefreshSongDropdowns(_service != null ? _service.CurrentRoom : null);
yield break;
}
yield return null;
}
_songLibraryWaitRoutine = null;
Debug.LogWarning("[roomDetails] SongDataLibrary was not ready; song dropdowns were left empty.");
}
private void RefreshSongDropdowns(ArenaRoomSnapshot snapshot)
{
if (songDropdown == null && difficultyDropdown == null)
{
return;
}
SongDataLibrary library = SongDataLibrary.Instance;
if (library == null || !library.IsLoaded)
{
EnsureSongDropdownsReady();
UpdateSongDropdownInteractable(snapshot);
return;
}
RebuildSelectableSongs(library);
SongData selectedSong = ResolveSnapshotSong(snapshot);
if (selectedSong == null && _selectableSongs.Count > 0)
{
selectedSong = _selectableSongs[0];
}
int selectedDifficulty = ConvertDifficultyKeyToId(snapshot != null ? snapshot.difficulty : null);
if (selectedSong != null)
{
RebuildSelectableDifficulties(selectedSong);
if (!_selectableDifficulties.Contains(selectedDifficulty))
{
selectedDifficulty = _selectableDifficulties.Count > 0 ? _selectableDifficulties[0] : selectedDifficulty;
}
}
else
{
_selectableDifficulties.Clear();
}
_suppressSongDropdownEvents = true;
try
{
PopulateSongDropdown(selectedSong);
PopulateDifficultyDropdown(selectedSong, selectedDifficulty);
}
finally
{
_suppressSongDropdownEvents = false;
}
UpdateSongDropdownInteractable(snapshot);
}
private void RebuildSelectableSongs(SongDataLibrary library)
{
_selectableSongs.Clear();
List<SongData> songs = library != null ? library.GetAllSongs() : null;
if (songs == null)
{
return;
}
songs.Sort((left, right) =>
{
int idCompare = (left != null ? left.songID : int.MaxValue).CompareTo(right != null ? right.songID : int.MaxValue);
if (idCompare != 0)
{
return idCompare;
}
return string.Compare(left != null ? left.songName : string.Empty,
right != null ? right.songName : string.Empty,
StringComparison.CurrentCulture);
});
foreach (SongData song in songs)
{
if (song != null && HasPlayableDifficulty(song))
{
_selectableSongs.Add(song);
}
}
}
private SongData ResolveSnapshotSong(ArenaRoomSnapshot snapshot)
{
if (snapshot != null && int.TryParse(snapshot.song_id, out int songId))
{
for (int i = 0; i < _selectableSongs.Count; i++)
{
SongData song = _selectableSongs[i];
if (song != null && song.songID == songId)
{
return song;
}
}
}
return null;
}
private void RebuildSelectableDifficulties(SongData song)
{
_selectableDifficulties.Clear();
if (song == null || song.chartFiles == null)
{
return;
}
foreach (ChartFileEntry entry in song.chartFiles)
{
if (entry == null || entry.chartFile == null || _selectableDifficulties.Contains(entry.difficulty))
{
continue;
}
_selectableDifficulties.Add(entry.difficulty);
}
_selectableDifficulties.Sort();
}
private void PopulateSongDropdown(SongData selectedSong)
{
if (songDropdown == null)
{
return;
}
songDropdown.ClearOptions();
List<string> options = new List<string>();
for (int i = 0; i < _selectableSongs.Count; i++)
{
SongData song = _selectableSongs[i];
options.Add(song != null && !string.IsNullOrWhiteSpace(song.songName) ? song.songName : $"Song {song?.songID ?? 0}");
}
songDropdown.AddOptions(options);
int selectedIndex = selectedSong != null ? _selectableSongs.IndexOf(selectedSong) : -1;
songDropdown.value = Mathf.Clamp(selectedIndex, 0, Mathf.Max(0, _selectableSongs.Count - 1));
songDropdown.RefreshShownValue();
}
private void PopulateDifficultyDropdown(SongData selectedSong, int selectedDifficulty)
{
if (difficultyDropdown == null)
{
return;
}
difficultyDropdown.ClearOptions();
List<string> options = new List<string>();
for (int i = 0; i < _selectableDifficulties.Count; i++)
{
options.Add(FormatDifficultyOption(selectedSong, _selectableDifficulties[i]));
}
difficultyDropdown.AddOptions(options);
int selectedIndex = _selectableDifficulties.IndexOf(selectedDifficulty);
difficultyDropdown.value = Mathf.Clamp(selectedIndex, 0, Mathf.Max(0, _selectableDifficulties.Count - 1));
difficultyDropdown.RefreshShownValue();
}
private void UpdateSongDropdownInteractable(ArenaRoomSnapshot snapshot)
{
string localSteamId = NetworkManager.Instance != null ? NetworkManager.Instance.SteamId : string.Empty;
bool canChange = !_isChangingRoomSong
&& snapshot != null
&& IsLocalHost(snapshot, localSteamId)
&& !IsRoomStarted(snapshot)
&& _selectableSongs.Count > 0;
if (songDropdown != null)
{
songDropdown.interactable = canChange;
}
if (difficultyDropdown != null)
{
difficultyDropdown.interactable = canChange && _selectableDifficulties.Count > 0;
}
}
private void OnSongDropdownChanged(int index)
{
if (_suppressSongDropdownEvents || index < 0 || index >= _selectableSongs.Count)
{
return;
}
SongData song = _selectableSongs[index];
int currentDifficulty = GetSelectedDifficultyId();
RebuildSelectableDifficulties(song);
if (!_selectableDifficulties.Contains(currentDifficulty))
{
currentDifficulty = _selectableDifficulties.Count > 0 ? _selectableDifficulties[0] : 2;
}
_suppressSongDropdownEvents = true;
try
{
PopulateDifficultyDropdown(song, currentDifficulty);
}
finally
{
_suppressSongDropdownEvents = false;
}
RequestChangeRoomSong(song, currentDifficulty);
}
private void OnDifficultyDropdownChanged(int index)
{
if (_suppressSongDropdownEvents || index < 0 || index >= _selectableDifficulties.Count)
{
return;
}
SongData song = GetSelectedSong();
RequestChangeRoomSong(song, _selectableDifficulties[index]);
}
private async void RequestChangeRoomSong(SongData song, int difficultyId)
{
if (_service == null || song == null || _isChangingRoomSong)
{
return;
}
ArenaRoomSnapshot snapshot = _service.CurrentRoom;
string localSteamId = NetworkManager.Instance != null ? NetworkManager.Instance.SteamId : string.Empty;
if (!IsLocalHost(snapshot, localSteamId) || IsRoomStarted(snapshot))
{
RefreshSongDropdowns(snapshot);
return;
}
string difficultyKey = ConvertDifficultyIdToKey(difficultyId);
if (snapshot != null
&& string.Equals(snapshot.song_id, song.songID.ToString(CultureInfo.InvariantCulture), StringComparison.Ordinal)
&& string.Equals((snapshot.difficulty ?? string.Empty).Trim(), difficultyKey, StringComparison.OrdinalIgnoreCase))
{
return;
}
_isChangingRoomSong = true;
UpdateSongDropdownInteractable(snapshot);
try
{
await _service.ChangeRoomSong(
song.songID.ToString(CultureInfo.InvariantCulture),
song.songName,
difficultyKey);
}
catch (Exception ex)
{
Debug.LogWarning($"[roomDetails] Change room song failed: {ex.Message}");
RefreshSongDropdowns(_service.CurrentRoom);
}
finally
{
_isChangingRoomSong = false;
UpdateSongDropdownInteractable(_service.CurrentRoom);
}
}
private SongData GetSelectedSong()
{
if (songDropdown == null)
{
return ResolveSnapshotSong(_service != null ? _service.CurrentRoom : null);
}
int index = songDropdown.value;
return index >= 0 && index < _selectableSongs.Count ? _selectableSongs[index] : null;
}
private int GetSelectedDifficultyId()
{
if (difficultyDropdown != null)
{
int index = difficultyDropdown.value;
if (index >= 0 && index < _selectableDifficulties.Count)
{
return _selectableDifficulties[index];
}
}
return ConvertDifficultyKeyToId(_service != null && _service.CurrentRoom != null ? _service.CurrentRoom.difficulty : null);
}
private static bool HasPlayableDifficulty(SongData song)
{
if (song == null || song.chartFiles == null)
{
return false;
}
foreach (ChartFileEntry entry in song.chartFiles)
{
if (entry != null && entry.chartFile != null)
{
return true;
}
}
return false;
}
private static string FormatDifficultyOption(SongData song, int difficultyId)
{
string shortName = FormatDifficulty(ConvertDifficultyIdToKey(difficultyId));
ChartFileEntry entry = song != null && song.chartFiles != null
? song.chartFiles.Find(item => item != null && item.difficulty == difficultyId)
: null;
if (entry != null && entry.difficultyLEVEL > 0f)
{
return $"{shortName} Lv.{entry.difficultyLEVEL:0.#}";
}
return shortName;
}
private static int ConvertDifficultyKeyToId(string difficultyKey)
{
if (string.IsNullOrWhiteSpace(difficultyKey))
{
return 2;
}
switch (difficultyKey.Trim().ToLowerInvariant())
{
case "ez":
return 0;
case "hd":
return 1;
case "im":
return 3;
case "in":
default:
return 2;
}
}
private static string ConvertDifficultyIdToKey(int difficultyId)
{
switch (difficultyId)
{
case 0:
return "ez";
case 1:
return "hd";
case 3:
return "im";
case 2:
default:
return "in";
}
}
private static void SetButtonText(Button button, string text)
{
if (button == null)
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -129,11 +129,11 @@ public class load_teammatesProfile : MonoBehaviour
return;
}
// Load selected hero IDs from PlayerPrefs
int heroId1 = PlayerPrefs.GetInt("selected_heroSlot01_heroID", 0);
int heroId2 = PlayerPrefs.GetInt("selected_heroSlot02_heroID", 0);
int heroId3 = PlayerPrefs.GetInt("selected_heroSlot03_heroID", 0);
int heroId4 = PlayerPrefs.GetInt("selected_heroSlot04_heroID", 0);
int heroId5 = PlayerPrefs.GetInt("selected_heroSlot05_heroID", 0);
int heroId1 = TeamSelectionDefaults.ReadHeroId(1);
int heroId2 = TeamSelectionDefaults.ReadHeroId(2);
int heroId3 = TeamSelectionDefaults.ReadHeroId(3);
int heroId4 = TeamSelectionDefaults.ReadHeroId(4);
int heroId5 = TeamSelectionDefaults.ReadHeroId(5);
// Set sprites for each profile image
teammate_profile_01.sprite = GetHeroSquareProfile(heroId1);