chore(m2): 保存功能验收及后续清理前检查点
This commit is contained in:
@@ -11,33 +11,49 @@ using game.PlayerDataStorage;
|
||||
using GameCore;
|
||||
using tysdk;
|
||||
using UnityEngine;
|
||||
using Newtonsoft.Json;
|
||||
using YiDun.Protector;
|
||||
#if UNITY_ANDROID
|
||||
using TapjoyUnity.Internal;
|
||||
#endif
|
||||
|
||||
namespace game
|
||||
{
|
||||
// One service belongs to one login lifetime. A revoked instance is never rebound.
|
||||
public sealed class KVUserService : IUserService, IKVUserLoadReceiver, IKVUserLifecycle
|
||||
// Failed login attempts may retry; explicit logout ends this service lifetime.
|
||||
public class KVUserService : IUserService, IKVUserLifecycle
|
||||
{
|
||||
[Inject] public ICustomServerMgr customServerMgr { get; set; }
|
||||
[Inject] public IPlayerSaveStore SaveStore { get; set; }
|
||||
readonly PlayerKVRuntimeSelection selection;
|
||||
readonly Func<CookieContainer, HttpMessageHandler> handlerFactory;
|
||||
readonly CancellationTokenSource lifetime = new CancellationTokenSource();
|
||||
readonly Dictionary<string, PlayInfo> profiles = new Dictionary<string, PlayInfo>();
|
||||
readonly Dictionary<string, int> levels = new Dictionary<string, int>();
|
||||
PlayerKVGameLoadCoordinator coordinator;
|
||||
PlayerKVGameSessionHandle handle;
|
||||
PlayerKVInitializationIntents intents;
|
||||
bool attempted, loading, closing, published, promptsBound;
|
||||
Task closeTask;
|
||||
long epoch;
|
||||
string playerId;
|
||||
uint internalId;
|
||||
[Inject]
|
||||
public IConfig config { get; set; }
|
||||
|
||||
[Inject]
|
||||
public ICustomServerMgr customServerMgr { get; set; }
|
||||
[Inject]
|
||||
public IPlayerSaveStore SaveStore { get; set; }
|
||||
|
||||
//
|
||||
private readonly PlayerKVRuntimeSelection selection;
|
||||
//
|
||||
private readonly Func<CookieContainer, HttpMessageHandler> handlerFactory;
|
||||
|
||||
private readonly CancellationTokenSource lifetime = new();
|
||||
private readonly Dictionary<string, PlayInfo> profiles = new();
|
||||
private readonly Dictionary<string, int> levels = new();
|
||||
|
||||
private PlayerKVGameLoadCoordinator coordinator;
|
||||
private PlayerKVGameSessionHandle handle;
|
||||
private bool loading, closing, published, promptsBound;
|
||||
private PlayerKVGameSessionHandle failedHandle;
|
||||
private int mmTestValue;
|
||||
private Task closeTask;
|
||||
private long epoch;
|
||||
private string playerId;
|
||||
private uint internalId;
|
||||
public string CustomId { get; set; }
|
||||
public string UserId => IsAuthorized ? playerId : null;
|
||||
public string UserId => KVHandoffReady ? playerId : null;
|
||||
public string ApiPlayerId => UserId;
|
||||
public uint InternalId => IsAuthorized ? internalId : 0;
|
||||
public bool IsLogin => IsAuthorized;
|
||||
public bool IsAuthorized => published && KVHandoffReady;
|
||||
public uint InternalId => KVHandoffReady ? internalId : 0;
|
||||
public bool IsLogin => KVHandoffReady && playerId != null;
|
||||
public bool IsAuthorized => published && InternalId != 0 && KVHandoffReady;
|
||||
public CancellationToken LoginLifetime => lifetime.Token;
|
||||
public DateTime LastLoginTime { get; private set; }
|
||||
public DateTime LoginTime { get; private set; }
|
||||
@@ -47,7 +63,6 @@ namespace game
|
||||
public string Language { get; private set; }
|
||||
public string ContinentCode { get; private set; }
|
||||
public Dictionary<string, string> EAccoutUrl { get; private set; } = new Dictionary<string, string>();
|
||||
internal int KVTapJoyBalance { get; private set; }
|
||||
internal PlayerKVGameLoadError KVLastError { get; private set; }
|
||||
internal bool KVHandoffReady => !closing && handle != null && coordinator != null &&
|
||||
coordinator.TryGetCurrentGameHandle(epoch, out var current) && ReferenceEquals(current, handle);
|
||||
@@ -57,88 +72,315 @@ namespace game
|
||||
public KVUserService() : this(SubBoostrap.RuntimeSelection, jar => new HttpClientHandler
|
||||
{ CookieContainer = jar, UseCookies = true, AllowAutoRedirect = false }) { }
|
||||
|
||||
internal KVUserService(PlayerKVRuntimeSelection selection, Func<CookieContainer, HttpMessageHandler> handlerFactory)
|
||||
private KVUserService(PlayerKVRuntimeSelection selection, Func<CookieContainer, HttpMessageHandler> handlerFactory)
|
||||
{
|
||||
this.selection = selection;
|
||||
this.handlerFactory = handlerFactory ?? throw new ArgumentNullException(nameof(handlerFactory));
|
||||
Application.quitting += Logout;
|
||||
Application.quitting += OnApplicationQuitting;
|
||||
}
|
||||
|
||||
public async Task<ELoginStep> Login(Action<int> progress, bool tyrLoginByToken = true)
|
||||
protected virtual string NewPlayerName => $"Angler#{CustomId}";
|
||||
protected virtual string NewPlayerAvatar => string.Empty;
|
||||
|
||||
protected virtual void InitData(IDictionary<string, string> data)
|
||||
{
|
||||
if (attempted || closing || selection == null || !selection.IsKV ||
|
||||
!selection.TryAdmitPlayer(CustomId, out var canonicalId))
|
||||
{ KVLastError = PlayerKVGameLoadError.Input; return ELoginStep.Api; }
|
||||
// Set before the first await: duplicate calls cannot authenticate or subscribe twice.
|
||||
attempted = true;
|
||||
}
|
||||
|
||||
public virtual async Task<ELoginStep> Login(Action<int> progress, bool tyrLoginByToken = true)
|
||||
{
|
||||
if (loading || closing)
|
||||
{
|
||||
return ELoginStep.Api;
|
||||
}
|
||||
if (IsLogin)
|
||||
{
|
||||
progress?.Invoke(2);
|
||||
return ELoginStep.Done;
|
||||
}
|
||||
if (selection == null || !selection.IsKV || !selection.TryAdmitPlayer(CustomId, out var canonicalId))
|
||||
{
|
||||
KVLastError = PlayerKVGameLoadError.Input;
|
||||
return ELoginStep.Api;
|
||||
}
|
||||
|
||||
loading = true;
|
||||
var requestedId = CustomId;
|
||||
bool successEventStarted = false;
|
||||
IChatService chat = null;
|
||||
try
|
||||
{
|
||||
if (customServerMgr is not CustomServerMgr server ||
|
||||
server.ApiBaseUri.AbsoluteUri != PlayerKVRuntimeSelection.FormalApiUrl)
|
||||
throw new InvalidOperationException("Formal KV transport required.");
|
||||
loading = true;
|
||||
var transaction = PlayerKVGameplayBindings.Bind(this);
|
||||
coordinator = new PlayerKVGameLoadCoordinator(transaction,
|
||||
() => loading && !closing && !published && handle == null,
|
||||
() => { }, () => intents?.Count ?? 0, server.BindKVForGame, server.ClearKVForGame);
|
||||
server.KVUnauthorized += OnUnauthorized;
|
||||
var identity = new PlayerKVIdentitySnapshot(null, false, PlayerKVIdentityChannel.none,
|
||||
Array.Empty<PlayerKVIdentityChannel>());
|
||||
var snapshot = PlayerKVGameplayBindings.Snapshot(DateTime.UtcNow, "formal-v1");
|
||||
var capability = PlayerKVGameLoadCapability.CreateForGame(identity, snapshot, selection.Api,
|
||||
Application.persistentDataPath, handlerFactory, handlerFactory, handlerFactory);
|
||||
var result = await coordinator.LoadForGameAsync(canonicalId, capability, LoginLifetime,
|
||||
() => !closing && CustomId == requestedId);
|
||||
KVLastError = result.Error;
|
||||
if (result.State != PlayerKVGameLoadState.GameLoaded || closing || CustomId != requestedId ||
|
||||
result.Account == null || result.Account.PlayerId != canonicalId || result.Account.Epoch != result.Epoch ||
|
||||
!coordinator.TryGetCurrentGameHandle(result.Epoch, out var original))
|
||||
{ Logout(); return ELoginStep.Api; }
|
||||
epoch = result.Epoch;
|
||||
handle = original;
|
||||
if (SaveStore is not KVPlayerSaveStore store) throw new InvalidOperationException("KV save store required.");
|
||||
store.Bind(original, LoginLifetime);
|
||||
PrepareProfile(result.Account);
|
||||
BindPromptServices(original);
|
||||
// Chat subscribes before the unique success event. No SDK or analytics initialization here.
|
||||
GContext.container.Resolve<IChatService>().WaitingForLogin();
|
||||
if (!KVHandoffReady || CustomId != requestedId) throw new InvalidOperationException("Stale KV login.");
|
||||
server.EnableKVBusiness(epoch);
|
||||
CustomServerMgr server;
|
||||
KVPlayerSaveStore store;
|
||||
PlayerKVGameLoadResult result;
|
||||
PlayerKVGameSessionHandle original;
|
||||
try
|
||||
{
|
||||
if (!await ReleaseFailedLoginAsync() || closing)
|
||||
{
|
||||
return ELoginStep.Api;
|
||||
}
|
||||
server = customServerMgr as CustomServerMgr;
|
||||
if (server == null || server.ApiBaseUri.AbsoluteUri != PlayerKVRuntimeSelection.FormalApiUrl)
|
||||
{
|
||||
throw new InvalidOperationException("Formal KV transport required.");
|
||||
}
|
||||
coordinator = new PlayerKVGameLoadCoordinator(
|
||||
() => loading && !closing && !published && handle == null,
|
||||
server.BindKVForGame, server.ClearKVForGame);
|
||||
server.KVUnauthorized += OnUnauthorized;
|
||||
var capability = PlayerKVGameLoadCapability.CreateForGame(selection.Api,
|
||||
Application.persistentDataPath, handlerFactory, handlerFactory, handlerFactory, prepareOriginalComparison: true);
|
||||
result = await coordinator.LoadForGameAsync(canonicalId, capability, LoginLifetime,
|
||||
() => !closing && CustomId == requestedId);
|
||||
KVLastError = result.Error;
|
||||
if (result.State != PlayerKVGameLoadState.PreparedForGame || result.Data == null ||
|
||||
closing || CustomId != requestedId || result.Account == null ||
|
||||
result.Account.PlayerId != canonicalId || result.Account.Epoch != result.Epoch ||
|
||||
!coordinator.TryGetCurrentGameHandle(result.Epoch, out original))
|
||||
{
|
||||
await FailLoginAsync();
|
||||
return ELoginStep.Api;
|
||||
}
|
||||
epoch = result.Epoch;
|
||||
handle = original;
|
||||
store = SaveStore as KVPlayerSaveStore;
|
||||
if (store == null)
|
||||
{
|
||||
throw new InvalidOperationException("KV save store required.");
|
||||
}
|
||||
store.Bind(original, LoginLifetime);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
KVLastError = PlayerKVGameLoadError.Apply;
|
||||
Debug.LogError("KV preparation failed: " + ex.GetType().Name);
|
||||
await FailLoginAsync();
|
||||
return ELoginStep.Api;
|
||||
}
|
||||
await PrepareProfileAsync(result.Account);
|
||||
if (!KVHandoffReady || CustomId != requestedId)
|
||||
{
|
||||
await FailLoginAsync();
|
||||
return ELoginStep.Api;
|
||||
}
|
||||
playerId = result.Account.PlayerId;
|
||||
internalId = result.Account.InternalId;
|
||||
ZZTimeHelper.AdjustTime(LoginTime);
|
||||
ZZTimeHelper.WathForcusChange();
|
||||
if (!original.CompareDatas(ZZTimeHelper.UtcNow().Ticks).Success)
|
||||
{
|
||||
KVLastError = PlayerKVGameLoadError.Apply;
|
||||
await FailLoginAsync();
|
||||
return ELoginStep.Api;
|
||||
}
|
||||
var merged = original.ReadLocalSnapshot();
|
||||
if (!merged.Success)
|
||||
{
|
||||
KVLastError = PlayerKVGameLoadError.Apply;
|
||||
await FailLoginAsync();
|
||||
return ELoginStep.Api;
|
||||
}
|
||||
KVMgr.Bind(store);
|
||||
InitializePlayerData(merged.Data.ToDictionary(pair => pair.Key, pair => pair.Value, StringComparer.Ordinal));
|
||||
if (!KVHandoffReady || CustomId != requestedId)
|
||||
{
|
||||
await FailLoginAsync();
|
||||
return ELoginStep.Api;
|
||||
}
|
||||
SetMMTestValue();
|
||||
try
|
||||
{
|
||||
BindPromptServices(original);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
KVLastError = PlayerKVGameLoadError.Apply;
|
||||
Debug.LogError("KV prompt binding failed: " + ex.GetType().Name);
|
||||
await FailLoginAsync();
|
||||
return ELoginStep.Api;
|
||||
}
|
||||
chat = GContext.container.Resolve<IChatService>();
|
||||
chat.WaitingForLogin();
|
||||
OnYinDunLogin();
|
||||
if (!KVHandoffReady || CustomId != requestedId)
|
||||
{
|
||||
await FailLoginAsync();
|
||||
return ELoginStep.Api;
|
||||
}
|
||||
server.EnableKVBusiness(epoch);
|
||||
published = true;
|
||||
GContext.Publish(new EvtAPISvrLoginSuccess { playerId = playerId, internalId = internalId });
|
||||
if (!IsAuthorized) return ELoginStep.Api;
|
||||
_ = RunSaveScheduleAsync(store);
|
||||
successEventStarted = true;
|
||||
try
|
||||
{
|
||||
GContext.Publish(new EvtAPISvrLoginSuccess { playerId = playerId, internalId = internalId });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Original LoginApiServer catches subscriber errors and removes API authorization.
|
||||
Debug.LogError(ex);
|
||||
internalId = 0;
|
||||
published = false;
|
||||
server.DisableKVBusiness();
|
||||
}
|
||||
if (closing)
|
||||
{
|
||||
return ELoginStep.Api;
|
||||
}
|
||||
InitTapJoy();
|
||||
progress?.Invoke(2);
|
||||
return ELoginStep.Done;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (successEventStarted)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
KVLastError = PlayerKVGameLoadError.Apply;
|
||||
Debug.LogError("KV login failed: " + ex.GetType().Name);
|
||||
Logout();
|
||||
return ELoginStep.Api;
|
||||
try
|
||||
{
|
||||
(chat as IDisposable)?.Dispose();
|
||||
}
|
||||
catch (Exception cleanupError)
|
||||
{
|
||||
Debug.LogError("KV chat cleanup failed: " + cleanupError);
|
||||
}
|
||||
try
|
||||
{
|
||||
await FailLoginAsync();
|
||||
}
|
||||
catch (Exception cleanupError)
|
||||
{
|
||||
Debug.LogError("KV login cleanup failed: " + cleanupError);
|
||||
}
|
||||
throw;
|
||||
}
|
||||
finally
|
||||
{
|
||||
loading = false;
|
||||
}
|
||||
finally { loading = false; }
|
||||
}
|
||||
|
||||
void PrepareProfile(PlayerKVAccountDescriptor account)
|
||||
private void InitializePlayerData(Dictionary<string, string> data)
|
||||
{
|
||||
mmTestValue = 0;
|
||||
if (!data.ContainsKey("MMTest"))
|
||||
{
|
||||
if (config.Get<bool>("NewlyCreated", false))
|
||||
{
|
||||
mmTestValue = -1;
|
||||
}
|
||||
else
|
||||
{
|
||||
SaveStore.Write("MMTest", "MMFailParam#0").RequireAccepted();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var parts = data["MMTest"].Split('#');
|
||||
if (parts.Length > 1 && parts[0] == "MMFailParam")
|
||||
{
|
||||
int.TryParse(parts[1], out var value);
|
||||
mmTestValue = value;
|
||||
}
|
||||
else
|
||||
{
|
||||
mmTestValue = 0;
|
||||
SaveStore.Write("MMTest", "MMFailParam#0").RequireAccepted();
|
||||
}
|
||||
}
|
||||
GContext.container.Resolve<PlayerData>().LoadPlayerData(data);
|
||||
InitData(data);
|
||||
}
|
||||
|
||||
private async Task PrepareProfileAsync(PlayerKVAccountDescriptor account)
|
||||
{
|
||||
if (account.InternalId == 0 || account.CreateTimeUtc == DateTime.MinValue || account.ServerTimeUtc == DateTime.MinValue)
|
||||
{
|
||||
throw new InvalidOperationException("Authenticated account metadata required.");
|
||||
}
|
||||
CreateTime = account.CreateTimeUtc;
|
||||
LoginTime = account.ServerTimeUtc;
|
||||
config.Set("NewlyCreated", account.NewlyCreated);
|
||||
var previous = Read("LastLoginTimeUtc");
|
||||
LastLoginTime = ParseLastLoginTime(previous.Value, previous.Found.Value);
|
||||
DisplayName = Read("DisplayName").Value;
|
||||
if (string.IsNullOrEmpty(DisplayName)) DisplayName = GetDefaultName(account.PlayerId);
|
||||
AvatarUrl = Read("AvatarUrl").Value ?? string.Empty;
|
||||
Language = Read("Language").Value;
|
||||
if (string.IsNullOrEmpty(Language)) Language = GContext.container.Resolve<ISettingService>().Lang;
|
||||
var continent = Read("ContinentCode").Value;
|
||||
ContinentCode = new[] { "AF", "AN", "AS", "EU", "NA", "OC", "SA" }.Contains(continent) ? continent : null;
|
||||
if (string.IsNullOrEmpty(DisplayName))
|
||||
{
|
||||
var name = await ChangeDisplayName(NewPlayerName);
|
||||
LoginLifetime.ThrowIfCancellationRequested();
|
||||
if (!KVHandoffReady)
|
||||
{
|
||||
throw new InvalidOperationException("Stale KV profile.");
|
||||
}
|
||||
DisplayName = name;
|
||||
}
|
||||
if (string.IsNullOrEmpty(AvatarUrl))
|
||||
{
|
||||
await UpdateAvatarUrl(NewPlayerAvatar);
|
||||
}
|
||||
await UpdateLanguage(GContext.container.Resolve<ISettingService>().Lang);
|
||||
LoginLifetime.ThrowIfCancellationRequested();
|
||||
}
|
||||
|
||||
private async Task FailLoginAsync()
|
||||
{
|
||||
if (closing)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (closeTask != null)
|
||||
{
|
||||
await ReleaseFailedLoginAsync();
|
||||
return;
|
||||
}
|
||||
failedHandle = handle;
|
||||
handle = null;
|
||||
playerId = null;
|
||||
internalId = 0;
|
||||
published = false;
|
||||
DisplayName = AvatarUrl = Language = ContinentCode = null;
|
||||
ClearPlayInfos();
|
||||
if (customServerMgr is CustomServerMgr server)
|
||||
{
|
||||
server.KVUnauthorized -= OnUnauthorized;
|
||||
server.DisableKVBusiness();
|
||||
}
|
||||
closeTask = coordinator?.Revoke() ?? Task.CompletedTask;
|
||||
await ReleaseFailedLoginAsync();
|
||||
}
|
||||
|
||||
private async Task<bool> ReleaseFailedLoginAsync()
|
||||
{
|
||||
if (closeTask == null)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
var status = await (coordinator?.ObserveQuitAsync() ?? Task.FromResult(PlayerKVShutdownStatus.Ready));
|
||||
if (closing || status != PlayerKVShutdownStatus.Ready || closeTask.Status != TaskStatus.RanToCompletion)
|
||||
{
|
||||
KVLastError = status == PlayerKVShutdownStatus.Failed || closeTask.IsFaulted ?
|
||||
PlayerKVGameLoadError.PreviousCloseFailed : PlayerKVGameLoadError.PreviousClosePending;
|
||||
return false;
|
||||
}
|
||||
if (failedHandle != null)
|
||||
{
|
||||
KVMgr.Unbind(SaveStore as KVPlayerSaveStore);
|
||||
(SaveStore as KVPlayerSaveStore)?.ReleaseFailedBinding(failedHandle);
|
||||
(GContext.container.Resolve<ISettingService>() as DefaultSettingService)?.ReleaseFailedPromptOwner(failedHandle);
|
||||
GContext.container.Resolve<ClubService>().ReleaseFailedPromptOwner(failedHandle);
|
||||
}
|
||||
promptsBound = false;
|
||||
failedHandle = null;
|
||||
closeTask = null;
|
||||
coordinator = null;
|
||||
epoch = 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
internal static DateTime ParseLastLoginTime(string value, bool found)
|
||||
@@ -185,7 +427,31 @@ namespace game
|
||||
if (version == customServerMgr.SessionVersion) Logout();
|
||||
}
|
||||
|
||||
public void Logout() => ObserveClose(CloseKVForValidationAsync());
|
||||
private void OnApplicationQuitting()
|
||||
{
|
||||
var original = SaveStore as KVPlayerSaveStore;
|
||||
try
|
||||
{
|
||||
KVMgr.Stop(original);
|
||||
if (original != null && original.IsCurrent)
|
||||
{
|
||||
_ = original.SyncDataAsync();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogError("KV quit save failed: " + ex.GetType().Name);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Logout();
|
||||
}
|
||||
}
|
||||
|
||||
public virtual void Logout()
|
||||
{
|
||||
ObserveClose(CloseKVForValidationAsync());
|
||||
}
|
||||
async void ObserveClose(Task task)
|
||||
{
|
||||
try { await task; }
|
||||
@@ -193,14 +459,21 @@ namespace game
|
||||
}
|
||||
internal Task CloseKVForValidationAsync()
|
||||
{
|
||||
if (closeTask != null) return closeTask;
|
||||
if (closing)
|
||||
{
|
||||
return closeTask ?? Task.CompletedTask;
|
||||
}
|
||||
closing = true;
|
||||
published = false;
|
||||
KVMgr.Unbind(SaveStore as KVPlayerSaveStore);
|
||||
(SaveStore as KVPlayerSaveStore)?.Dispose();
|
||||
playerId = null; internalId = 0; handle = null; epoch = 0;
|
||||
playerId = null;
|
||||
internalId = 0;
|
||||
handle = null;
|
||||
epoch = 0;
|
||||
DisplayName = AvatarUrl = Language = ContinentCode = null;
|
||||
ClearPlayInfos();
|
||||
Application.quitting -= Logout;
|
||||
Application.quitting -= OnApplicationQuitting;
|
||||
if (customServerMgr is CustomServerMgr server)
|
||||
{
|
||||
server.KVUnauthorized -= OnUnauthorized;
|
||||
@@ -216,74 +489,244 @@ namespace game
|
||||
coordinator?.ObserveQuitAsync() ?? Task.FromResult(PlayerKVShutdownStatus.Ready);
|
||||
Task<PlayerKVShutdownStatus> IKVUserLifecycle.ObserveKVQuitAsync() => ObserveKVQuitAsync();
|
||||
|
||||
void IKVUserLoadReceiver.ResetForKVLoad()
|
||||
{
|
||||
if ((!loading && !closing) || published || handle != null)
|
||||
throw new InvalidOperationException("KV identity must be unpublished during load/reset.");
|
||||
intents?.Discard(); intents = null;
|
||||
EAccoutUrl = new Dictionary<string, string>(); KVTapJoyBalance = 0;
|
||||
}
|
||||
void IKVUserLoadReceiver.ReplaceFromKVDecoded(PlayerKVLoginState state, long loadEpoch, IReadOnlyDictionary<string, string> raw)
|
||||
{
|
||||
if (!loading || closing || published || handle != null || state == null || raw == null)
|
||||
throw new InvalidOperationException("KV load receiver is not current.");
|
||||
intents = new PlayerKVInitializationIntents(loadEpoch, state.Channel);
|
||||
foreach (var pair in state.Normalizations) intents.Add(loadEpoch, pair.Key, pair.Value);
|
||||
EAccoutUrl = state.AccountUrls.ToDictionary(p => p.Key, p => p.Value, StringComparer.Ordinal);
|
||||
KVTapJoyBalance = state.TapJoyBalance;
|
||||
}
|
||||
void IKVUserLoadReceiver.DiscardKVIntents() => intents?.Discard();
|
||||
|
||||
async Task SaveProfile(string key, string value)
|
||||
private async Task<bool> SaveProfile(string key, string value)
|
||||
{
|
||||
var original = handle;
|
||||
var store = SaveStore;
|
||||
if (!IsAuthorized || store == null) throw new InvalidOperationException("KV identity unavailable.");
|
||||
var result = await store.WriteAndConfirmAsync(key, value, LoginLifetime);
|
||||
if (!IsAuthorized || !ReferenceEquals(store, SaveStore) || !store.IsCurrent || !result.Confirmed)
|
||||
throw new InvalidOperationException("KV profile save was not confirmed.");
|
||||
}
|
||||
|
||||
async Task RunSaveScheduleAsync(IPlayerSaveStore store)
|
||||
{
|
||||
if ((!IsLogin && !loading) || !KVHandoffReady || store == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
try
|
||||
{
|
||||
while (IsAuthorized && store.IsCurrent)
|
||||
{
|
||||
await Task.Delay(TimeSpan.FromSeconds(40), LoginLifetime);
|
||||
if (!IsAuthorized || !store.IsCurrent) return;
|
||||
var result = await store.FlushAsync(LoginLifetime);
|
||||
if (!result.Confirmed && !LoginLifetime.IsCancellationRequested)
|
||||
Debug.LogWarning("KV scheduled save: " + result.Status);
|
||||
}
|
||||
var result = await store.SyncDataImmidiate(key, value);
|
||||
return ReferenceEquals(original, handle) && KVHandoffReady &&
|
||||
ReferenceEquals(store, SaveStore) && store.IsCurrent && result;
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
catch (OperationCanceledException) { }
|
||||
catch (Exception ex) { Debug.LogError("KV save scheduler failed: " + ex.GetType().Name); }
|
||||
}
|
||||
|
||||
public async Task<string> ChangeDisplayName(string newName)
|
||||
{
|
||||
await SaveProfile("DisplayName", newName);
|
||||
DisplayName = newName; GContext.Publish(new ChangeAvatarEvent()); UpdateProfile(); return newName;
|
||||
var originName = string.IsNullOrEmpty(DisplayName) ? NewPlayerName : DisplayName;
|
||||
if (!await SaveProfile("DisplayName", newName))
|
||||
{
|
||||
return originName;
|
||||
}
|
||||
DisplayName = newName;
|
||||
GContext.Publish(new ChangeAvatarEvent());
|
||||
UpdateProfileName();
|
||||
return newName;
|
||||
}
|
||||
public async Task<string> UpdateAvatarUrl(string url)
|
||||
{
|
||||
await SaveProfile("AvatarUrl", url);
|
||||
AvatarUrl = url; GContext.Publish(new ChangeAvatarEvent()); UpdateProfile(); return url;
|
||||
var originUrl = AvatarUrl;
|
||||
|
||||
Debug.Log("UpdateAvatarUrlAsync originUrl:" + originUrl + " url " + url);
|
||||
|
||||
if (string.IsNullOrEmpty(url))
|
||||
{
|
||||
return originUrl;
|
||||
}
|
||||
if (url == originUrl)
|
||||
{
|
||||
return originUrl;
|
||||
}
|
||||
|
||||
if (!await SaveProfile("AvatarUrl", url))
|
||||
{
|
||||
return originUrl;
|
||||
}
|
||||
AvatarUrl = url;
|
||||
GContext.Publish(new ChangeAvatarEvent());
|
||||
UpdateProfileAvatar();
|
||||
return url;
|
||||
}
|
||||
public async Task<string> UpdateLanguage(string code)
|
||||
{ await SaveProfile("Language", code); Language = code; return code; }
|
||||
{
|
||||
if (Language == code)
|
||||
{
|
||||
return Language;
|
||||
}
|
||||
|
||||
if (await SaveProfile("Language", code.Replace('_', '-')))
|
||||
{
|
||||
Language = code;
|
||||
}
|
||||
return Language;
|
||||
}
|
||||
public void GMMMT(int value)
|
||||
{
|
||||
mmTestValue = value;
|
||||
SaveStore.Write("MMTest", $"MMFailParam#{mmTestValue}").RequireAccepted();
|
||||
SetMMTestValue();
|
||||
}
|
||||
|
||||
private void SetMMTestValue()
|
||||
{
|
||||
string ab = GContext.container.Resolve<MMTService>().Init(mmTestValue);
|
||||
GEvent.SetUserProp("MMTestValue", ab, true);
|
||||
Debug.Log($"UserId:{UserId} MMTestValue :0b{Convert.ToString(mmTestValue, 2)} MMTestValueString:{ab}");
|
||||
}
|
||||
|
||||
protected void AvatarToKV(TYSdkFacade.AccountInfo accountInfo)
|
||||
{
|
||||
if (accountInfo != null &&
|
||||
(accountInfo.channel == EAccoutType.hwFacebook || accountInfo.channel == EAccoutType.hwGoogle) &&
|
||||
!string.IsNullOrEmpty(accountInfo.avatar))
|
||||
{
|
||||
if (EAccoutUrl == null)
|
||||
{
|
||||
EAccoutUrl = new Dictionary<string, string>();
|
||||
}
|
||||
EAccoutUrl[accountInfo.channel.ToString()] = accountInfo.avatar;
|
||||
SaveStore.Write("EAccoutUrl", JsonConvert.SerializeObject(EAccoutUrl)).RequireAccepted();
|
||||
}
|
||||
}
|
||||
|
||||
private void InitTapJoy()
|
||||
{
|
||||
#if UNITY_ANDROID
|
||||
var lastBalanceStr = Read("TapJoy_Diamond").Value ?? "";
|
||||
Debug.Log($"InitTapJoy -> {lastBalanceStr}");
|
||||
var lastBalance = int.TryParse(lastBalanceStr, out var temp) ? temp : 0;
|
||||
|
||||
var tapjoyObject = GameObject.FindObjectOfType<TapjoyComponent>();
|
||||
if (tapjoyObject && tapjoyObject.gameObject && !tapjoyObject.GetComponent<TapjoyHelper>())
|
||||
{
|
||||
var tapjoyHelper = tapjoyObject.gameObject.AddComponent<TapjoyHelper>();
|
||||
GameObject.DontDestroyOnLoad(tapjoyHelper);
|
||||
tapjoyHelper.Init(lastBalance, OnEarnCurrencyName, OnBalance);
|
||||
}
|
||||
return;
|
||||
void OnEarnCurrencyName(string currencyName, int amount)
|
||||
{
|
||||
Debug.Log($"OnEarnCurrencyName -> {currencyName} {amount}");
|
||||
// var amount = (balance - lastBalance);
|
||||
var diamonds = new ItemData { id = 1005, count = amount };
|
||||
diamonds.curCount = (ulong)GContext.container.Resolve<PlayerItemData>().GetItemCount(diamonds.id);
|
||||
GContext.container.Resolve<PlayerItemData>().AddItem(diamonds, null);
|
||||
GContext.Publish(new ShowData(new List<ItemData> { diamonds }, RewardType.NormalOne));
|
||||
GContext.Publish(new ShowData());
|
||||
#if AGG
|
||||
using (var e = GEvent.GameEvent("tapjoy"))
|
||||
{
|
||||
e.AddContent("item_id", diamonds.id)
|
||||
.AddContent("item_count", diamonds.count);
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
}
|
||||
void OnBalance(string currencyName, int balance)
|
||||
{
|
||||
Debug.Log($"OnBalance -> {currencyName} {balance}");
|
||||
// const string tapjoyKey = "TapJoy_Diamond";
|
||||
// var lastBalanceStr = PlayFabMgr.Instance.GetLocalData(tapjoyKey) ?? "";
|
||||
// var lastBalance = int.TryParse(lastBalanceStr, out var temp) ? temp : 0;
|
||||
{
|
||||
// PlayFabMgr.Instance.UpdateUserDataValue(tapjoyKey, balance.ToString());
|
||||
// if (balance > lastBalance)
|
||||
// {
|
||||
// var amount = (balance - lastBalance);
|
||||
// var diamonds = new ItemData { id = 1005, count = amount };
|
||||
// diamonds.curCount = (ulong)GContext.container.Resolve<PlayerItemData>().GetItemCount(diamonds.id);
|
||||
// GContext.container.Resolve<PlayerItemData>().AddItem(diamonds);
|
||||
// GContext.Publish(new ShowData(new List<ItemData> { diamonds }, RewardType.NormalOne));
|
||||
// // GContext.Publish(new ShowData(diamonds));
|
||||
// GContext.Publish(new ShowData());
|
||||
// }
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
private async void OnYinDunLogin()
|
||||
{
|
||||
// 开发模式下,先不启用了
|
||||
if (config != null && (config.Get("DEBUG", "0") == "1" || config.Get("GM", "0") == "1"))
|
||||
{
|
||||
return;
|
||||
}
|
||||
var userService = GContext.container.Resolve<IUserService>();
|
||||
var yiDunManager = GContext.container.Resolve<YiDunManager>();
|
||||
var roleName = userService.DisplayName;
|
||||
var roleId = userService.UserId;
|
||||
var roleAccount = userService.CustomId;
|
||||
var versionData = new
|
||||
{
|
||||
GameVersion = Application.version,
|
||||
AssetVersion = config.Get(GConstant.K_AssetVersion, string.Empty),
|
||||
#if UNITY_ANDROID
|
||||
Channel = GConstant.Yidun_GooglePlayChannel
|
||||
#elif UNITY_IOS
|
||||
Channel = GConstant.Yidun_IOSChannel
|
||||
#endif
|
||||
};
|
||||
Debug.Log($"JsonUtility.ToJson(versionData) => {JsonConvert.SerializeObject(versionData)}");
|
||||
|
||||
var businessId = GConstant.Yidun_AndroidBusinessId;
|
||||
#if UNITY_IOS
|
||||
businessId = GConstant.Yidun_IOSBusinessId;
|
||||
#endif
|
||||
|
||||
#if (UNITY_ANDROID || UNITY_IOS || UNITY_IPHONE) && !UNITY_EDITOR
|
||||
yiDunManager.SetRoleInfo(businessId, roleId, roleName, roleAccount, null, -1, JsonConvert.SerializeObject(versionData));
|
||||
yiDunManager.PostRiskCheck();
|
||||
#endif
|
||||
}
|
||||
|
||||
public double CreateTotalSeconds() => (ZZTimeHelper.UtcNow() - CreateTime).TotalSeconds;
|
||||
// The non-AGG channel has no account-link operation; never manufacture a successful binding.
|
||||
public Task<(int code, int userid)> LinkAccount(EAccoutType type) => Task.FromResult((1, 0));
|
||||
public Task<bool> GetLinkState(EAccoutType type) => Task.FromResult(false);
|
||||
public virtual async Task<(int code, int userid)> LinkAccount(EAccoutType type)
|
||||
{
|
||||
await Awaiters.NextFrame;
|
||||
return (1, 0);
|
||||
}
|
||||
public virtual async Task<bool> GetLinkState(EAccoutType type)
|
||||
{
|
||||
await Awaiters.NextFrame;
|
||||
return false;
|
||||
}
|
||||
public string GetDefaultName(string id) => "Angler#" + (string.IsNullOrEmpty(id) ? string.Empty : id.Substring(0, Math.Min(5, id.Length)));
|
||||
public void SetPlayerLv(string id, int level) => levels[id] = id == UserId ? GContext.container.Resolve<PlayerData>().lv : level;
|
||||
public int GetPlayerLv(string id) => levels.TryGetValue(id, out var level) ? level : 1;
|
||||
public Task<(string, int)> GetPlayLv(string id)
|
||||
public async Task<(string, int)> GetPlayLv(string id)
|
||||
{
|
||||
var level = id == UserId && IsAuthorized ? (int)GContext.container.Resolve<PlayerData>().lv :
|
||||
id != null && levels.TryGetValue(id, out var cachedLevel) ? cachedLevel : -1;
|
||||
return level >= 0 ? Task.FromResult((id, level)) :
|
||||
Task.FromException<(string, int)>(new InvalidOperationException("Public player level has not been loaded."));
|
||||
if (level >= 0)
|
||||
{
|
||||
return (id, level);
|
||||
}
|
||||
if (id != UserId && !string.IsNullOrEmpty(id) && !levels.ContainsKey(id))
|
||||
{
|
||||
var json = await customServerMgr.FriendRequest("SearchFriend", new SearchFriendReq { PlayFabId = id });
|
||||
if (!string.IsNullOrEmpty(json))
|
||||
{
|
||||
SearchFriendResp response = null;
|
||||
try
|
||||
{
|
||||
response = Newtonsoft.Json.JsonConvert.DeserializeObject<SearchFriendResp>(json);
|
||||
}
|
||||
catch (Newtonsoft.Json.JsonException ex)
|
||||
{
|
||||
Debug.LogError("KV player level response failed: " + ex.GetType().Name);
|
||||
}
|
||||
if (response != null && response.Code > 0 && response.KvProfile != null)
|
||||
{
|
||||
level = response.KvProfile.Level ?? 1;
|
||||
if (level >= 0)
|
||||
{
|
||||
levels[id] = level;
|
||||
return (id, level);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
throw new InvalidOperationException("Public player level has not been loaded.");
|
||||
}
|
||||
public PlayInfo GetPlayInfo(string id)
|
||||
{
|
||||
@@ -296,7 +739,7 @@ namespace game
|
||||
}
|
||||
return profile;
|
||||
}
|
||||
async void SetPlayInfo(string id, PlayInfo profile)
|
||||
private async void SetPlayInfo(string id, PlayInfo profile)
|
||||
{
|
||||
if (id == UserId)
|
||||
{
|
||||
@@ -306,27 +749,63 @@ namespace game
|
||||
return;
|
||||
}
|
||||
var json = await customServerMgr.FriendRequest("SearchFriend", new SearchFriendReq { PlayFabId = id });
|
||||
if (string.IsNullOrEmpty(json)) return;
|
||||
if (string.IsNullOrEmpty(json))
|
||||
{
|
||||
return;
|
||||
}
|
||||
SearchFriendResp result;
|
||||
try { result = Newtonsoft.Json.JsonConvert.DeserializeObject<SearchFriendResp>(json); }
|
||||
try
|
||||
{
|
||||
result = Newtonsoft.Json.JsonConvert.DeserializeObject<SearchFriendResp>(json);
|
||||
}
|
||||
catch (Newtonsoft.Json.JsonException ex)
|
||||
{
|
||||
Debug.LogError("KV player profile response failed: " + ex.GetType().Name);
|
||||
return;
|
||||
}
|
||||
if (result != null && result.Code > 0 && result.Info != null)
|
||||
if (result != null && result.Code > 0 && result.KvProfile != null)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(result.Info.DisplayName)) profile.name = result.Info.DisplayName;
|
||||
profile.avatarUrl = result.Info.AvatarUrl;
|
||||
if (!string.IsNullOrEmpty(result.KvProfile.DisplayName))
|
||||
{
|
||||
profile.name = result.KvProfile.DisplayName;
|
||||
}
|
||||
profile.avatarUrl = result.KvProfile.AvatarUrl;
|
||||
GContext.Publish(new GetClubPlayInfoEvent { id = id, name = profile.name, avatarUrl = profile.avatarUrl });
|
||||
}
|
||||
}
|
||||
public void SetPlayInfo(string id, string name, string avatarUrl)
|
||||
{
|
||||
if (string.IsNullOrEmpty(id)) return;
|
||||
profiles[id] = new PlayInfo { name = string.IsNullOrEmpty(name) ? GetDefaultName(id) : name, avatarUrl = avatarUrl };
|
||||
profiles[id] = new PlayInfo
|
||||
{
|
||||
name = string.IsNullOrEmpty(name) ? GetDefaultName(id) : name,
|
||||
avatarUrl = avatarUrl
|
||||
};
|
||||
}
|
||||
public void ClearPlayInfos() { profiles.Clear(); levels.Clear(); }
|
||||
private void UpdateProfileName()
|
||||
{
|
||||
if (!IsAuthorized)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
PlayerProfile playerProfile = new PlayerProfile();
|
||||
playerProfile.DisplayName = DisplayName;
|
||||
LoginRequest loginRequest = new LoginRequest() { playerId = ApiPlayerId, playerProfile = playerProfile };
|
||||
customServerMgr.AccountRequest("UpdateProfile", loginRequest);
|
||||
}
|
||||
private void UpdateProfileAvatar()
|
||||
{
|
||||
if (!IsAuthorized)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
PlayerProfile playerProfile = new PlayerProfile();
|
||||
playerProfile.Avatar = AvatarUrl;
|
||||
LoginRequest loginRequest = new LoginRequest() { playerId = ApiPlayerId, playerProfile = playerProfile };
|
||||
customServerMgr.AccountRequest("UpdateProfile", loginRequest);
|
||||
}
|
||||
public void UpdateProfile()
|
||||
{
|
||||
if (IsAuthorized) customServerMgr.AccountRequest("UpdateProfile", new LoginRequest { playerId = ApiPlayerId,
|
||||
|
||||
Reference in New Issue
Block a user